1. 程式人生 > >補習系列-springboot 實現攔截的五種姿勢

補習系列-springboot 實現攔截的五種姿勢

最簡 err 可用 序列化 tor 集成 方便 接口 讀取

目錄

  • 簡介
  • 姿勢一、使用 Filter 接口
    • 1. 註冊 FilterRegistrationBean
    • 2. @WebFilter 註解
  • 姿勢二、HanlderInterceptor
  • 姿勢三、@ExceptionHandler 註解
  • 姿勢四、RequestBodyAdvice/ResponseBodyAdvice
    • RequestBodyAdvice 的用法
    • ResponseBodyAdvice 用法
  • 姿勢五、@Aspect 註解
  • 思考
  • 小結

簡介

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方法執行前調用 |
|preHandle | 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;
        }
    }

代碼說明
完成數據流的轉換,包括以下步驟:

  1. 獲取請求內容字符串;
  2. 構建 MsgBody 對象,將內容字符串作為其 content 字段;
  3. 將 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篇" ,期待更多精彩內容^-^

補習系列-springboot 實現攔截的五種姿勢