簡介
AOP(面向切面程式設計)常用于解決系統中的一些耦合問題,是一種程式設計的模式
通過将一些通用邏輯抽取為公共子產品,由容器來進行調用,以達到子產品間隔離的效果。
其還有一個别名,叫面向關注點程式設計,把系統中的核心業務邏輯稱為核心關注點,而一些通用的非核心邏輯劃分為橫切關注點
AOP常用于...
日志記錄
你需要為你的Web應用程式實作通路日志記錄,卻又不想在所有接口中一個個進行打點。
安全控制
為URL 實作通路權限控制,自動攔截一些非法通路。
事務
某些業務流程需要在一個事務中串行
異常處理
系統發生處理異常,根據不同的異常傳回定制的消息體。
在筆者剛開始接觸程式設計之時,AOP還是個新事物,當時曾認為AOP會大行其道。
果不其然,目前流行的Spring 架構中,AOP已經成為其關鍵的核心能力。
接下來,我們要看看在SpringBoot 架構中,怎麼實作常用的一些攔截操作。
先看看下面的一個Controller方法:
示例
@RestController
@RequestMapping("/intercept")
public class InterceptController {
@PostMapping(value = "/body", consumes = { MediaType.TEXT_PLAIN_VALUE, MediaType.APPLICATION_JSON_UTF8_VALUE })
public String body(@RequestBody MsgBody msg) {
return msg == null ? "<EMPTY>" : msg.getContent();
}
public static class MsgBody {
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
在上述代碼的 body 方法中,會接受一個MsgBody請求消息體,最終簡單的輸出content字段。
下面,我們将介紹如何為這個方法實作攔截動作。算起來,共有五種姿勢。
姿勢一、使用 Filter 接口
Filter 接口由 J2EE 定義,在Servlet執行之前由容器進行調用。
而SpringBoot中聲明 Filter 又有兩種方式:
1. 注冊 FilterRegistrationBean
聲明一個FilterRegistrationBean 執行個體,對Filter 做一系列定義,如下:
@Bean
public FilterRegistrationBean customerFilter() {
FilterRegistrationBean registration = new FilterRegistrationBean();
// 設定過濾器
registration.setFilter(new CustomerFilter());
// 攔截路由規則
registration.addUrlPatterns("/intercept/*");
// 設定初始化參數
registration.addInitParameter("name", "customFilter");
registration.setName("CustomerFilter");
registration.setOrder(1);
return registration;
}
其中 CustomerFilter 實作了Filter接口,如下:
public class CustomerFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(CustomerFilter.class);
private String name;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
name = filterConfig.getInitParameter("name");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
logger.info("Filter {} handle before", name);
chain.doFilter(request, response);
logger.info("Filter {} handle after", name);
}
}
2. @WebFilter 注解
為Filter的實作類添加 @WebFilter注解,由SpringBoot 架構掃描後注入
@WebFilter的啟用需要配合@ServletComponentScan才能生效
@Component
@ServletComponentScan
@WebFilter(urlPatterns = "/intercept/*", filterName = "annotateFilter")
public class AnnotateFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(AnnotateFilter.class);
private final String name = "annotateFilter";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
logger.info("Filter {} handle before", name);
chain.doFilter(request, response);
logger.info("Filter {} handle after", name);
}
}
使用注解是最簡單的,但其缺點是仍然無法支援 order屬性(用于控制Filter的排序)。
而通常的@Order注解隻能用于定義Bean的加載順序,卻真正無法控制Filter排序。
這是一個已知問題,
參考這裡推薦指數
3 顆星,Filter 定義屬于J2EE規範,由Servlet容器排程執行。
由于獨立于架構之外,無法使用 Spring 架構的便捷特性,
目前一些第三方元件內建時會使用該方式。
姿勢二、HanlderInterceptor
HandlerInterceptor 用于攔截 Controller 方法的執行,其聲明了幾個方法:
方法 | 說明 |
---|---|
preHandle | Controller方法執行前調用 |
Controller方法後,視圖渲染前調用 | |
afterCompletion | 整個方法執行後(包括異常抛出捕獲) |
基于 HandlerInterceptor接口 實作的樣例:
public class CustomHandlerInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(CustomHandlerInterceptor.class);
/*
* Controller方法調用前,傳回true表示繼續處理
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
HandlerMethod method = (HandlerMethod) handler;
logger.info("CustomerHandlerInterceptor preHandle, {}", method.getMethod().getName());
return true;
}
/*
* Controller方法調用後,視圖渲染前
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
HandlerMethod method = (HandlerMethod) handler;
logger.info("CustomerHandlerInterceptor postHandle, {}", method.getMethod().getName());
response.getOutputStream().write("append content".getBytes());
}
/*
* 整個請求處理完,視圖已渲染。如果存在異常則Exception不為空
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
HandlerMethod method = (HandlerMethod) handler;
logger.info("CustomerHandlerInterceptor afterCompletion, {}", method.getMethod().getName());
}
}
除了上面的代碼實作,還不要忘了将 Interceptor 實作進行注冊:
@Configuration
public class InterceptConfig extends WebMvcConfigurerAdapter {
// 注冊攔截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new CustomHandlerInterceptor()).addPathPatterns("/intercept/**");
super.addInterceptors(registry);
}
4顆星,HandlerInterceptor 來自SpringMVC架構,基本可代替 Filter 接口使用;
除了可以友善的進行異常處理之外,通過接口參數能獲得Controller方法執行個體,還可以實作更靈活的定制。
姿勢三、@ExceptionHandler 注解
@ExceptionHandler 的用途是捕獲方法執行時抛出的異常,
通常可用于捕獲全局異常,并輸出自定義的結果。
如下面的執行個體:
@ControllerAdvice(assignableTypes = InterceptController.class)
public class CustomInterceptAdvice {
private static final Logger logger = LoggerFactory.getLogger(CustomInterceptAdvice.class);
/**
* 攔截異常
*
* @param e
* @param m
* @return
*/
@ExceptionHandler(value = { Exception.class })
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ResponseBody
public String handle(Exception e, HandlerMethod m) {
logger.info("CustomInterceptAdvice handle exception {}, method: {}", e.getMessage(), m.getMethod().getName());
return e.getMessage();
}
}
需要注意的是,@ExceptionHandler 需要與 @ControllerAdvice配合使用
其中 @ControllerAdvice的 assignableTypes 屬性指定了所攔截類的名稱。
除此之外,該注解還支援指定包掃描範圍、注解範圍等等。
5顆星,@ExceptionHandler 使用非常友善,在異常處理的機制上是首選;
目前也是SpringBoot 架構最為推薦使用的方法。
姿勢四、RequestBodyAdvice/ResponseBodyAdvice
RequestBodyAdvice、ResponseBodyAdvice 相對于讀者可能比較陌生,
而這倆接口也是 Spring 4.x 才開始出現的。
RequestBodyAdvice 的用法
我們都知道,SpringBoot 中可以利用@RequestBody這樣的注解完成請求内容體與對象的轉換。
而RequestBodyAdvice 則可用于在請求内容對象轉換的前後時刻進行攔截處理,其定義了幾個方法:
supports | 判斷是否支援 |
handleEmptyBody | 當請求體為空時調用 |
beforeBodyRead | 在請求體未讀取(轉換)時調用 |
afterBodyRead | 在請求體完成讀取後調用 |
實作代碼如下:
@ControllerAdvice(assignableTypes = InterceptController.class)
public class CustomRequestAdvice extends RequestBodyAdviceAdapter {
private static final Logger logger = LoggerFactory.getLogger(CustomRequestAdvice.class);
@Override
public boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
// 傳回true,表示啟動攔截
return MsgBody.class.getTypeName().equals(targetType.getTypeName());
}
@Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
logger.info("CustomRequestAdvice handleEmptyBody");
// 對于空請求體,傳回對象
return body;
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
logger.info("CustomRequestAdvice beforeBodyRead");
// 可定制消息序列化
return new BodyInputMessage(inputMessage);
}
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
logger.info("CustomRequestAdvice afterBodyRead");
// 可針對讀取後的對象做轉換,此處不做處理
return body;
}
上述代碼實作中,針對前面提到的 MsgBody對象類型進行了攔截處理。
在beforeBodyRead 中,傳回一個BodyInputMessage對象,而這個對象便負責源資料流解析轉換
public static class BodyInputMessage implements HttpInputMessage {
private HttpHeaders headers;
private InputStream body;
public BodyInputMessage(HttpInputMessage inputMessage) throws IOException {
this.headers = inputMessage.getHeaders();
// 讀取原字元串
String content = IOUtils.toString(inputMessage.getBody(), "UTF-8");
MsgBody msg = new MsgBody();
msg.setContent(content);
this.body = new ByteArrayInputStream(JsonUtil.toJson(msg).getBytes());
}
@Override
public InputStream getBody() throws IOException {
return body;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
}
代碼說明
完成資料流的轉換,包括以下步驟:
- 擷取請求内容字元串;
- 建構 MsgBody 對象,将内容字元串作為其 content 字段;
- 将 MsgBody 對象 Json 序列化,再次轉成位元組流供後續環節使用。
ResponseBodyAdvice 用法
ResponseBodyAdvice 的用途在于對傳回内容做攔截處理,如下面的示例:
@ControllerAdvice(assignableTypes = InterceptController.class)
public static class CustomResponseAdvice implements ResponseBodyAdvice<String> {
private static final Logger logger = LoggerFactory.getLogger(CustomRequestAdvice.class);
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// 傳回true,表示啟動攔截
return true;
}
@Override
public String beforeBodyWrite(String body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
ServerHttpResponse response) {
logger.info("CustomResponseAdvice beforeBodyWrite");
// 添加字首
String raw = String.valueOf(body);
return "PREFIX:" + raw;
}
}
看,還是容易了解的,我們在傳回的字元串中添加了一個字首!
2 顆星,這是兩個非常冷門的接口,目前的使用場景也相對有限;
一般在需要對輸入輸出流進行特殊處理(比如加解密)的場景下使用。
姿勢五、@Aspect 注解
這是目前最靈活的做法,直接利用注解可實作任意對象、方法的攔截。
在某個Bean的類上面 @Aspect 注解便可以将一個Bean 聲明為具有AOP能力的對象。
@Aspect
@Component
public class InterceptControllerAspect {
private static final Logger logger = LoggerFactory.getLogger(InterceptControllerAspect.class);
@Pointcut("target(org.zales.dmo.boot.controllers.InterceptController)")
public void interceptController() {
}
@Around("interceptController()")
public Object handle(ProceedingJoinPoint joinPoint) throws Throwable {
logger.info("aspect before.");
try {
return joinPoint.proceed();
} finally {
logger.info("aspect after.");
}
}
}
簡單說明
@Pointcut 用于定義切面點,而使用target關鍵字可以定位到具體的類。
@Around 定義了一個切面處理方法,通過注入ProceedingJoinPoint對象達到控制的目的。
一些常用的切面注解:
注解 | |
---|---|
@Before | 方法執行之前 |
@After | 方法執行之後 |
@Around | 方法執行前後 |
@AfterThrowing | 抛出異常後 |
@AfterReturing | 正常傳回後 |
深入一點
aop的能力來自于spring-boot-starter-aop,進一步依賴于aspectjweaver元件。
有興趣可以進一步了解。
5顆星,aspectj 與 SpringBoot 可以無縫內建,這是一個經典的AOP架構,
可以實作任何你想要的功能,筆者之前曾在多個項目中使用,效果是十分不錯的。
注解的支援及自動包掃描大大簡化了開發,然而,你仍然需要先對 Pointcut 的定義有充分的了解。
思考
到這裡,讀者可能想知道,這些實作攔截器的接口之間有什麼關系呢?
答案是,沒有什麼關系! 每一種接口都會在不同的時機被調用,我們基于上面的代碼示例做了日志輸出:
- Filter customFilter handle before
- Filter annotateFilter handle before
- CustomerHandlerInterceptor preHandle, body
- CustomRequestAdvice beforeBodyRead
- CustomRequestAdvice afterBodyRead
- aspect before.
- aspect after.
- CustomResponseAdvice beforeBodyWrite
- CustomerHandlerInterceptor postHandle, body
- CustomerHandlerInterceptor afterCompletion, body
- Filter annotateFilter handle after
- Filter customFilter handle after
可以看到,各種攔截器接口的執行順序如下圖:

小結
AOP 是實作攔截器的基本思路,本文介紹了SpringBoot 項目中實作攔截功能的五種常用姿勢。
對于每一種方法都給出了真實的代碼樣例,讀者可以根據需要選擇自己适用的方案。
最後,歡迎繼續關注"美碼師的補習系列-springboot篇" ,期待更多精彩内容^-^