zoukankan      html  css  js  c++  java
  • 补习系列(7)-springboot 实现拦截的五种姿势

    简介

    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方法执行前调用
    postHandle 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篇" ,期待更多精彩内容-

  • 相关阅读:
    增量更新代码步骤记录
    软件缺陷管理基本流程
    数据库语言(三):MySQL、PostgreSQL、JDBC
    eclipse的使用
    数据库语言(二):SQL语法实例整理
    windows下MySql没有setup.exe时的安装方法
    数学:完全独立于实际场景的情况下定义的概念,可以正确的描述世界
    数学语言和程序语言的对比:面向过程与面向集合&命题
    iOS开发之IMP和SEL(方法和类的反射)
    iOS之UIButton的normal和selected状态切换
  • 原文地址:https://www.cnblogs.com/littleatp/p/9496009.html
Copyright © 2011-2022 走看看