补习系列-springboot 实现拦截的五种姿势

https://mp.weixin.qq.com/s/aH1IC7TkLkK16W-R4ZndIw

简介

AOP(面向切面编程)常用于解决系统中的一些耦合问题,是一种编程的模式 通过将一些通用逻辑抽取为公共模块,由容器来进行调用,以达到模块间隔离的效果。

其还有一个别名,叫面向关注点编程,把系统中的核心业务逻辑称为核心关注点,而一些通用的非核心逻辑划分为横切关注点

 

AOP常用于...

日志记录

你需要为你的Web应用程序实现访问日志记录,却又不想在所有接口中一个个进行打点。

安全控制

为URL 实现访问权限控制,自动拦截一些非法访问。

事务

某些业务流程需要在一个事务中串行

异常处理

系统发生处理异常,根据不同的异常返回定制的消息体。

 

在笔者刚开始接触编程之时,AOP还是个新事物,当时曾认为AOP会大行其道。 果不其然,目前流行的Spring 框架中,AOP已经成为其关键的核心能力。

 

接下来,我们要看看在SpringBoot 框架中,怎么实现常用的一些拦截操作。

 

先看看下面的一个Controller方法:

示例

 
  1. @RestController

  2. @RequestMapping("/intercept")

  3. public class InterceptController {

  4.  

  5.    @PostMapping(value = "/body", consumes = { MediaType.TEXT_PLAIN_VALUE, MediaType.APPLICATION_JSON_UTF8_VALUE })

  6.    public String body(@RequestBody MsgBody msg) {

  7.        return msg == null ? "<EMPTY>" : msg.getContent();

  8.    }

  9.  

  10.    public static class MsgBody {

  11.        private String content;

  12.  

  13.        public String getContent() {

  14.            return content;

  15.        }

  16.  

  17.        public void setContent(String content) {

  18.            this.content = content;

  19.        }

  20.  

  21.    }

在上述代码的 body 方法中,会接受一个MsgBody请求消息体,最终简单的输出content字段。

下面,我们将介绍如何为这个方法实现拦截动作。算起来,共有五种姿势。

 

姿势一、使用 Filter 接口

 

Filter 接口由 J2EE 定义,在Servlet执行之前由容器进行调用。 而SpringBoot中声明 Filter 又有两种方式:

 

1. 注册 FilterRegistrationBean

声明一个FilterRegistrationBean 实例,对Filter 做一系列定义,如下:

 
  1.    @Bean

  2.    public FilterRegistrationBean customerFilter() {

  3.        FilterRegistrationBean registration = new FilterRegistrationBean();

  4.  

  5.        // 设置过滤器

  6.        registration.setFilter(new CustomerFilter());

  7.  

  8.        // 拦截路由规则

  9.        registration.addUrlPatterns("/intercept/*");

  10.  

  11.        // 设置初始化参数

  12.        registration.addInitParameter("name", "customFilter");

  13.  

  14.        registration.setName("CustomerFilter");

  15.        registration.setOrder(1);

  16.        return registration;

  17.    }

其中 CustomerFilter 实现了Filter接口,如下:

 
  1. public class CustomerFilter implements Filter {

  2.  

  3.    private static final Logger logger = LoggerFactory.getLogger(CustomerFilter.class);

  4.    private String name;

  5.  

  6.    @Override

  7.    public void init(FilterConfig filterConfig) throws ServletException {

  8.        name = filterConfig.getInitParameter("name");

  9.    }

  10.  

  11.    @Override

  12.    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)

  13.            throws IOException, ServletException {

  14.        logger.info("Filter {} handle before", name);

  15.        chain.doFilter(request, response);

  16.        logger.info("Filter {} handle after", name);

  17.    }

  18. }

 

2. @WebFilter 注解

为Filter的实现类添加 @WebFilter注解,由SpringBoot 框架扫描后注入

@WebFilter的启用需要配合@ServletComponentScan才能生效

 
  1. @Component

  2. @ServletComponentScan

  3. @WebFilter(urlPatterns = "/intercept/*", filterName = "annotateFilter")

  4. public class AnnotateFilter implements Filter {

  5.  

  6.    private static final Logger logger = LoggerFactory.getLogger(AnnotateFilter.class);

  7.    private final String name = "annotateFilter";

  8.  

  9.    @Override

  10.    public void init(FilterConfig filterConfig) throws ServletException {

  11.    }

  12.  

  13.    @Override

  14.    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)

  15.            throws IOException, ServletException {

  16.        logger.info("Filter {} handle before", name);

  17.        chain.doFilter(request, response);

  18.        logger.info("Filter {} handle after", name);

  19.    }

  20. }

使用注解是最简单的,但其缺点是仍然无法支持 order属性(用于控制Filter的排序)。

而通常的@Order注解只能用于定义Bean的加载顺序,却真正无法控制Filter排序。这是一个已知问题,参考这里

 

推荐指数 补习系列-springboot 实现拦截的五种姿势补习系列-springboot 实现拦截的五种姿势补习系列-springboot 实现拦截的五种姿势

Filter 定义属于J2EE规范,由Servlet容器调度执行。

由于独立于框架之外,无法使用 Spring 框架的便捷特性, 目前一些第三方组件集成时会使用该方式。

 

 

姿势二、HanlderInterceptor

 

HandlerInterceptor 用于拦截 Controller 方法的执行,其声明了几个方法:

 

方法 说明
preHandle Controller方法执行前调用
preHandle Controller方法后,视图渲染前调用
afterCompletion 整个方法执行后(包括异常抛出捕获)

 

基于 HandlerInterceptor接口 实现的样例:

 
  1. public class CustomHandlerInterceptor implements HandlerInterceptor {

  2.  

  3.    private static final Logger logger = LoggerFactory.getLogger(CustomHandlerInterceptor.class);

  4.  

  5.    /*

  6.     * Controller方法调用前,返回true表示继续处理

  7.     */

  8.    @Override

  9.    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)

  10.            throws Exception {

  11.        HandlerMethod method = (HandlerMethod) handler;

  12.        logger.info("CustomerHandlerInterceptor preHandle, {}", method.getMethod().getName());

  13.  

  14.        return true;

  15.    }

  16.  

  17.    /*

  18.     * Controller方法调用后,视图渲染前

  19.     */

  20.    @Override

  21.    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,

  22.            ModelAndView modelAndView) throws Exception {

  23.  

  24.        HandlerMethod method = (HandlerMethod) handler;

  25.        logger.info("CustomerHandlerInterceptor postHandle, {}", method.getMethod().getName());

  26.  

  27.        response.getOutputStream().write("append content".getBytes());

  28.    }

  29.  

  30.    /*

  31.     * 整个请求处理完,视图已渲染。如果存在异常则Exception不为空

  32.     */

  33.    @Override

  34.    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)

  35.            throws Exception {

  36.  

  37.        HandlerMethod method = (HandlerMethod) handler;

  38.        logger.info("CustomerHandlerInterceptor afterCompletion, {}", method.getMethod().getName());

  39.    }

  40.  

  41. }

除了上面的代码实现,还不要忘了将 Interceptor 实现进行注册:

 
  1. @Configuration

  2. public class InterceptConfig extends WebMvcConfigurerAdapter {

  3.  

  4.    // 注册拦截器

  5.    @Override

  6.    public void addInterceptors(InterceptorRegistry registry) {

  7.  

  8.        registry.addInterceptor(new CustomHandlerInterceptor()).addPathPatterns("/intercept/**");

  9.        super.addInterceptors(registry);

  10.    }

 

推荐指数 补习系列-springboot 实现拦截的五种姿势补习系列-springboot 实现拦截的五种姿势补习系列-springboot 实现拦截的五种姿势补习系列-springboot 实现拦截的五种姿势

HandlerInterceptor 来自SpringMVC框架,基本可代替 Filter 接口使用。

 除了可以方便的进行异常处理之外,通过接口参数能获得Controller方法实例,还可以实现更灵活的定制。

 

 

姿势三、@ExceptionHandler 注解

 

@ExceptionHandler 的用途是捕获方法执行时抛出的异常, 通常可用于捕获全局异常,并输出自定义的结果。

 

如下面的实例:

 
  1. @ControllerAdvice(assignableTypes = InterceptController.class)

  2. public class CustomInterceptAdvice {

  3.  

  4.    private static final Logger logger = LoggerFactory.getLogger(CustomInterceptAdvice.class);

  5.  

  6.    /**

  7.     * 拦截异常

  8.     *

  9.     * @param e

  10.     * @param m

  11.     * @return

  12.     */

  13.    @ExceptionHandler(value = { Exception.class })

  14.    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)

  15.    @ResponseBody

  16.    public String handle(Exception e, HandlerMethod m) {

  17.  

  18.        logger.info("CustomInterceptAdvice handle exception {}, method: {}", e.getMessage(), m.getMethod().getName());

  19.  

  20.        return e.getMessage();

  21.    }

  22. }

需要注意的是,@ExceptionHandler 需要与 @ControllerAdvice配合使用 其中 @ControllerAdvice的 assignableTypes 属性指定了所拦截类的名称。 除此之外,该注解还支持指定包扫描范围、注解范围等等。

 

推荐指数  补习系列-springboot 实现拦截的五种姿势补习系列-springboot 实现拦截的五种姿势补习系列-springboot 实现拦截的五种姿势补习系列-springboot 实现拦截的五种姿势补习系列-springboot 实现拦截的五种姿势

@ExceptionHandler 使用非常方便,在异常处理的机制上是首选; 目前也是SpringBoot 框架最为推荐使用的方法。

 

 

姿势四、RequestBodyAdvice/ResponseBodyAdvice

 

RequestBodyAdvice、ResponseBodyAdvice 相对于读者可能比较陌生, 而这俩接口也是 Spring 4.x 才开始出现的。

 

RequestBodyAdvice 的用法

我们都知道,SpringBoot 中可以利用@RequestBody这样的注解完成请求内容体与对象的转换。

RequestBodyAdvice *则可用于在请求内容对象转换的前后时刻*进行拦截处理,其定义了几个方法:

方法 说明
supports 判断是否支持
handleEmptyBody 当请求体为空时调用
beforeBodyRead 在请求体未读取(转换)时调用
afterBodyRead 在请求体完成读取后调用

 

实现代码如下:

 
  1. @ControllerAdvice(assignableTypes = InterceptController.class)

  2. public class CustomRequestAdvice extends RequestBodyAdviceAdapter {

  3.  

  4.    private static final Logger logger = LoggerFactory.getLogger(CustomRequestAdvice.class);

  5.  

  6.    @Override

  7.    public boolean supports(MethodParameter methodParameter, Type targetType,

  8.            Class<? extends HttpMessageConverter<?>> converterType) {

  9.        // 返回true,表示启动拦截

  10.        return MsgBody.class.getTypeName().equals(targetType.getTypeName());

  11.    }

  12.  

  13.    @Override

  14.    public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,

  15.            Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {

  16.        logger.info("CustomRequestAdvice handleEmptyBody");

  17.  

  18.        // 对于空请求体,返回对象

  19.        return body;

  20.    }

  21.  

  22.    @Override

  23.    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,

  24.            Class<? extends HttpMessageConverter<?>> converterType) throws IOException {

  25.        logger.info("CustomRequestAdvice beforeBodyRead");

  26.  

  27.        // 可定制消息序列化

  28.        return new BodyInputMessage(inputMessage);

  29.    }

  30.  

  31.    @Override

  32.    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,

  33.            Class<? extends HttpMessageConverter<?>> converterType) {

  34.        logger.info("CustomRequestAdvice afterBodyRead");

  35.  

  36.        // 可针对读取后的对象做转换,此处不做处理

  37.        return body;

  38.    }

上述代码实现中,针对前面提到的 MsgBody对象类型进行了拦截处理。

在beforeBodyRead 中,返回一个BodyInputMessage对象,而这个对象便负责源数据流解析转换

 
  1.    public static class BodyInputMessage implements HttpInputMessage {

  2.        private HttpHeaders headers;

  3.        private InputStream body;

  4.  

  5.        public BodyInputMessage(HttpInputMessage inputMessage) throws IOException {

  6.            this.headers = inputMessage.getHeaders();

  7.  

  8.            // 读取原字符串

  9.            String content = IOUtils.toString(inputMessage.getBody(), "UTF-8");

  10.            MsgBody msg = new MsgBody();

  11.            msg.setContent(content);

  12.  

  13.            this.body = new ByteArrayInputStream(JsonUtil.toJson(msg).getBytes());

  14.        }

  15.  

  16.        @Override

  17.        public InputStream getBody() throws IOException {

  18.            return body;

  19.        }

  20.  

  21.        @Override

  22.        public HttpHeaders getHeaders() {

  23.            return headers;

  24.        }

  25.    }

 

代码说明

完成数据流的转换,包括以下步骤:

  1. 获取请求内容字符串;

  2. 构建 MsgBody 对象,将内容字符串作为其 content 字段;

  3. 将 MsgBody 对象 Json 序列化,再次转成字节流供后续环节使用。

 

ResponseBodyAdvice 用法

 

ResponseBodyAdvice 的用途在于对返回内容做拦截处理,如下面的示例:

 
  1.    @ControllerAdvice(assignableTypes = InterceptController.class)

  2.    public static class CustomResponseAdvice implements ResponseBodyAdvice<String> {

  3.  

  4.        private static final Logger logger = LoggerFactory.getLogger(CustomRequestAdvice.class);

  5.  

  6.        @Override

  7.        public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {

  8.            // 返回true,表示启动拦截

  9.            return true;

  10.        }

  11.  

  12.        @Override

  13.        public String beforeBodyWrite(String body, MethodParameter returnType, MediaType selectedContentType,

  14.                Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,

  15.                ServerHttpResponse response) {

  16.  

  17.            logger.info("CustomResponseAdvice beforeBodyWrite");

  18.  

  19.            // 添加前缀

  20.            String raw = String.valueOf(body);

  21.            return "PREFIX:" + raw;

  22.        }

  23.  

  24.    }

看,还是容易理解的,我们在返回的字符串中添加了一个前缀!

 

推荐指数  补习系列-springboot 实现拦截的五种姿势补习系列-springboot 实现拦截的五种姿势

这是两个非常冷门的接口,目前的使用场景也相对有限; 一般在需要对输入输出流进行特殊处理(比如加解密)的场景下使用。

 

 

姿势五、@Aspect 注解

 

这是目前最灵活的做法,直接利用注解可实现任意对象、方法的拦截。 在某个Bean的类上面* @Aspect* 注解便可以将一个Bean 声明为具有AOP能力的对象。

 

 
  1. @Aspect

  2. @Component

  3. public class InterceptControllerAspect {

  4.  

  5.    private static final Logger logger = LoggerFactory.getLogger(InterceptControllerAspect.class);

  6.  

  7.    @Pointcut("target(org.zales.dmo.boot.controllers.InterceptController)")

  8.    public void interceptController() {

  9.  

  10.    }

  11.  

  12.    @Around("interceptController()")

  13.    public Object handle(ProceedingJoinPoint joinPoint) throws Throwable {

  14.  

  15.        logger.info("aspect before.");

  16.  

  17.        try {

  18.            return joinPoint.proceed();

  19.        } finally {

  20.            logger.info("aspect after.");

  21.        }

  22.    }

  23. }

 

简单说明

@Pointcut 用于定义切面点,而使用target关键字可以定位到具体的类。@Around 定义了一个切面处理方法,通过注入ProceedingJoinPoint对象达到控制的目的。

 

一些常用的切面注解:

注解 说明
@Before 方法执行之前
@After 方法执行之后
@Around 方法执行前后
@AfterThrowing 抛出异常后
@AfterReturing 正常返回后

 

深入一点

aop的能力来自于spring-boot-starter-aop,进一步依赖于aspectjweaver组件。有兴趣可以进一步了解。

 

推荐指数 补习系列-springboot 实现拦截的五种姿势补习系列-springboot 实现拦截的五种姿势补习系列-springboot 实现拦截的五种姿势补习系列-springboot 实现拦截的五种姿势补习系列-springboot 实现拦截的五种姿势

aspectj 与 SpringBoot 可以无缝集成,这是一个经典的AOP框架, 可以实现任何你想要的功能,笔者之前曾在多个项目中使用,效果是十分不错的。

注解的支持及自动包扫描大大简化了开发,然而,你仍然需要先对 Pointcut 的定义有充分的了解。

 

思考

到这里,读者可能想知道,这些实现拦截器的接口之间有什么关系呢? 答案是,没有什么关系! 每一种接口都会在不同的时机被调用,

我们基于上面的代码示例做了日志输出:

 
  1. - Filter customFilter handle before

  2. - Filter annotateFilter handle before

  3. - CustomerHandlerInterceptor preHandle, body

  4. - CustomRequestAdvice beforeBodyRead

  5. - CustomRequestAdvice afterBodyRead

  6. - aspect before.

  7. - aspect after.

  8. - CustomResponseAdvice beforeBodyWrite

  9. - CustomerHandlerInterceptor postHandle, body

  10. - CustomerHandlerInterceptor afterCompletion, body

  11. - Filter annotateFilter handle after

  12. - Filter customFilter handle after

 

可以看到,各种拦截器接口的执行顺序如下图:

补习系列-springboot 实现拦截的五种姿势

 

小结

AOP 是实现拦截器的基本思路,本文介绍了SpringBoot 项目中实现拦截功能的五种常用姿势

对于每一种方法都给出了真实的代码样例,读者可以根据需要选择自己适用的方案。