簡介AOP(面向切面編程)常用于解決系統(tǒng)中的一些耦合問題,是一種編程的模式 通過將一些通用邏輯抽取為公共模塊,由容器來進行調(diào)用,以達到模塊間隔離的效果。 其還有一個別名,叫面向關(guān)注點編程,把系統(tǒng)中的核心業(yè)務(wù)邏輯稱為核心關(guān)注點,而一些通用的非核心邏輯劃分為橫切關(guān)注點OP常用于...日志記錄你需要為你的Web應(yīng)用程序?qū)崿F(xiàn)訪問日志記錄,卻又不想在所有接口中一個個進行打點。 安全控制為URL 實現(xiàn)訪問權(quán)限控制,自動攔截一些非法訪問。 事務(wù)某些業(yè)務(wù)流程需要在一個事務(wù)中串行 異常處理系統(tǒng)發(fā)生處理異常,根據(jù)不同的異常返回定制的消息體。 在筆者剛開始接觸編程之時,AOP還是個新事物,當時曾認為AOP會大行其道。 果不其然,目前流行的Spring 框架中,AOP已經(jīng)成為其關(guān)鍵的核心能力。 先看看下面的一個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字段。 下面,我們將介紹如何為這個方法實現(xiàn)攔截動作。算起來,共有五種姿勢。 姿勢一、使用 Filter 接口Filter 接口由 J2EE 定義,在Servlet執(zhí)行之前由容器進行調(diào)用。 而SpringBoot中聲明 Filter 又有兩種方式: 1. 注冊 FilterRegistrationBean聲明一個FilterRegistrationBean 實例,對Filter 做一系列定義,如下: @Bean public FilterRegistrationBean customerFilter() { FilterRegistrationBean registration = new FilterRegistrationBean(); // 設(shè)置過濾器 registration.setFilter(new CustomerFilter()); // 攔截路由規(guī)則 registration.addUrlPatterns('/intercept/*'); // 設(shè)置初始化參數(shù) registration.addInitParameter('name', 'customFilter'); registration.setName('CustomerFilter'); registration.setOrder(1); return registration; } 其中 CustomerFilter 實現(xiàn)了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的實現(xiàn)類添加 @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排序。 這是一個已知問題,參考這里 推薦指數(shù)3 顆星,F(xiàn)ilter 定義屬于J2EE規(guī)范,由Servlet容器調(diào)度執(zhí)行。 由于獨立于框架之外,無法使用 Spring 框架的便捷特性, 目前一些第三方組件集成時會使用該方式。 姿勢二、HanlderInterceptorHandlerInterceptor 用于攔截 Controller 方法的執(zhí)行,其聲明了幾個方法: |方法 | 說明| |-----|-----| |preHandle | Controller方法執(zhí)行前調(diào)用 | |preHandle | Controller方法后,視圖渲染前調(diào)用 | |afterCompletion| 整個方法執(zhí)行后(包括異常拋出捕獲) | 基于 HandlerInterceptor接口 實現(xiàn)的樣例: public class CustomHandlerInterceptor implements HandlerInterceptor { private static final Logger logger = LoggerFactory.getLogger(CustomHandlerInterceptor.class); /* * Controller方法調(diào)用前,返回true表示繼續(xù)處理 */ @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方法調(diào)用后,視圖渲染前 */ @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()); } } 除了上面的代碼實現(xiàn),還不要忘了將 Interceptor 實現(xiàn)進行注冊: @Configuration public class InterceptConfig extends WebMvcConfigurerAdapter { // 注冊攔截器 @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new CustomHandlerInterceptor()).addPathPatterns('/intercept/**'); super.addInterceptors(registry); } 推薦指數(shù)4顆星,HandlerInterceptor 來自SpringMVC框架,基本可代替 Filter 接口使用; 除了可以方便的進行異常處理之外,通過接口參數(shù)能獲得Controller方法實例,還可以實現(xiàn)更靈活的定制。 姿勢三、@ExceptionHandler 注解@ExceptionHandler 的用途是捕獲方法執(zhí)行時拋出的異常, 通常可用于捕獲全局異常,并輸出自定義的結(jié)果。 如下面的實例: @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 屬性指定了所攔截類的名稱。 除此之外,該注解還支持指定包掃描范圍、注解范圍等等。 推薦指數(shù)5顆星,@ExceptionHandler 使用非常方便,在異常處理的機制上是首選; 目前也是SpringBoot 框架最為推薦使用的方法。 姿勢四、RequestBodyAdvice/ResponseBodyAdviceRequestBodyAdvice、ResponseBodyAdvice 相對于讀者可能比較陌生, 而這倆接口也是 Spring 4.x 才開始出現(xiàn)的。 RequestBodyAdvice 的用法我們都知道,SpringBoot 中可以利用@RequestBody這樣的注解完成請求內(nèi)容體與對象的轉(zhuǎn)換。 而RequestBodyAdvice 則可用于在請求內(nèi)容對象轉(zhuǎn)換的前后時刻進行攔截處理,其定義了幾個方法: 方法說明supports判斷是否支持handleEmptyBody當請求體為空時調(diào)用beforeBodyRead在請求體未讀取(轉(zhuǎn)換)時調(diào)用afterBodyRead在請求體完成讀取后調(diào)用 實現(xiàn)代碼如下: @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'); // 可針對讀取后的對象做轉(zhuǎn)換,此處不做處理 return body; } 上述代碼實現(xiàn)中,針對前面提到的 MsgBody對象類型進行了攔截處理。 在beforeBodyRead 中,返回一個BodyInputMessage對象,而這個對象便負責源數(shù)據(jù)流解析轉(zhuǎn)換 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; } } 代碼說明完成數(shù)據(jù)流的轉(zhuǎn)換,包括以下步驟: ResponseBodyAdvice 用法ResponseBodyAdvice 的用途在于對返回內(nèi)容做攔截處理,如下面的示例: @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; } } 看,還是容易理解的,我們在返回的字符串中添加了一個前綴! 推薦指數(shù)2 顆星,這是兩個非常冷門的接口,目前的使用場景也相對有限; 一般在需要對輸入輸出流進行特殊處理(比如加解密)的場景下使用。 姿勢五、@Aspect 注解這是目前最靈活的做法,直接利用注解可實現(xiàn)任意對象、方法的攔截。 在某個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關(guān)鍵字可以定位到具體的類。 @Around 定義了一個切面處理方法,通過注入ProceedingJoinPoint對象達到控制的目的。 一些常用的切面注解: 注解說明@Before方法執(zhí)行之前@After方法執(zhí)行之后@Around方法執(zhí)行前后@AfterThrowing拋出異常后@AfterReturing正常返回后 深入一點aop的能力來自于spring-boot-starter-aop,進一步依賴于aspectjweaver組件。 有興趣可以進一步了解。 推薦指數(shù)5顆星,aspectj 與 SpringBoot 可以無縫集成,這是一個經(jīng)典的AOP框架, 可以實現(xiàn)任何你想要的功能,筆者之前曾在多個項目中使用,效果是十分不錯的。 注解的支持及自動包掃描大大簡化了開發(fā),然而,你仍然需要先對 Pointcut 的定義有充分的了解。 思考到這里,讀者可能想知道,這些實現(xiàn)攔截器的接口之間有什么關(guān)系呢? 答案是,沒有什么關(guān)系! 每一種接口都會在不同的時機被調(diào)用,我們基于上面的代碼示例做了日志輸出: - 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 可以看到,各種攔截器接口的執(zhí)行順序如下圖: 小結(jié)AOP 是實現(xiàn)攔截器的基本思路,本文介紹了SpringBoot 項目中實現(xiàn)攔截功能的五種常用姿勢。 對于每一種方法都給出了真實的代碼樣例,讀者可以根據(jù)需要選擇自己適用的方案。 |
|