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

目录

  • 简介
  • 姿势一、使用 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篇" ,期待更多精彩内容^-^

原文地址:https://www.cnblogs.com/littleatp/p/9496009.html

时间: 2024-11-08 12:40:10

补习系列-springboot 实现拦截的五种姿势的相关文章

补习系列-springboot 单元测试之道

目录 目标 一.About 单元测试 二.About Junit 三.SpringBoot-单元测试 项目依赖 测试样例 四.Mock测试 五.最后 目标 了解 单元测试的背景 了解如何 利用 springboot 实现接口的测试 了解如何 利用 mokito 做代码的 mock 一.About 单元测试 单元测试其实是一种廉价的技术,是由开发者创建运行测试代码,用于对程序模块(软件设计的最小单位)进行正确性检验的一种做法. 而所谓的最小单元,就是指应用的最小可测试部件. 在面向对象领域,最小单

【线程系列二】线程的五种状态

线程有5种状态,分别是新建.受阻塞.运行.死亡.休眠.等待. 在api中的解释如下图1所示 图1 图2 解释一下上述图2的过程. 1.new一个线程对象,该对象的状态为"新建状态". 2.执行start(),如果cpu现在空闲,则切换到运行状态,否则切换到阻塞状态. 3.线程执行sleep(),切换到休眠状态,在休眠时间到期后,自动切换到(运行或阻塞) 4.执行wait(),切换到等待状态,如果不执行notifiy(),则一直处于等待状态. 5.在线程中的run()执行完毕后,线程切换

BZOJ 题目3172: [Tjoi2013]单词(AC自动机||AC自动机+fail树||后缀数组暴力||后缀数组+RMQ+二分等五种姿势水过)

3172: [Tjoi2013]单词 Time Limit: 10 Sec  Memory Limit: 512 MB Submit: 1890  Solved: 877 [Submit][Status][Discuss] Description 某人读论文,一篇论文是由许多单词组成.但他发现一个单词会在论文中出现很多次,现在想知道每个单词分别在论文中出现多少次. Input 第一个一个整数N,表示有多少个单词,接下来N行每行一个单词.每个单词由小写字母组成,N<=200,单词长度不超过10^6

补习系列(9)-springboot 定时器,你用对了吗

目录 简介 一.应用启动任务 二.JDK 自带调度线程池 三.@Scheduled 定制 @Scheduled 线程池 四.@Async 定制 @Async 线程池 小结 简介 大多数的应用程序都离不开定时器,通常在程序启动时.运行期间会需要执行一些特殊的处理任务. 比如资源初始化.数据统计等等,SpringBoot 作为一个灵活的框架,有许多方式可以实现定时器或异步任务. 我总结了下,大致有以下几种: 使用 JDK 的 TimerTask 使用 JDK 自带调度线程池 使用 Quartz 调度

补习系列(15)-springboot 分布式会话原理

目录 一.背景 二.SpringBoot 分布式会话 三.样例程序 四.原理进阶 A. 序列化 B. 会话代理 C. 数据老化 小结 一.背景 在 补习系列(3)-springboot 几种scope 一文中,笔者介绍过 Session的部分,如下: 对于服务器而言,Session 通常是存储在本地的,比如Tomcat 默认将Session 存储在内存(ConcurrentHashMap)中. 但随着网站的用户越来越多,Session所需的空间会越来越大,同时单机部署的 Web应用会出现性能瓶颈

补习系列(10)-springboot 之配置读取

目录 简介 一.配置样例 二.如何注入配置 1. 缺省配置文件 2. 使用注解 3. 启动参数 还有.. 三.如何读取配置 @Value 注解 Environment 接口 @ConfigurationProperties 注解 四.不同环境中的配置 1. 区别开发.测试.发布环境 2. 声明多配置文件 参考文档 简介 在早前的博客中曾经写过 Spring 程序通过 Bean 映射实现配置信息的读取. 在SpringBoot 框架中读取配置的方式变得非常多样,这导致读者在搜寻资料时反而容易迷糊.

补习系列-springboot-restful应用

一.目标 了解 Restful 是什么,基本概念及风格: 能使用SpringBoot 实现一套基础的 Restful 风格接口: 利用Swagger 生成清晰的接口文档. 二.Restful 入门 什么是REST 摘自百科的定义:REST即表述性状态转移(英文:Representational State Transfer,简称REST) 是Roy Fielding博士(HTTP规范主要贡献者)在2000年的论文中提出来的一种软件架构风格. 是一种针对网络应用的设计和开发方式,可以降低开发的复杂

补习系列(20)-大话 WebSocket 与 &quot;尬聊&quot;的实现

目录 一.聊聊 WebSocket 二.Stomp 是个什么鬼 三.SpringBoot 整合 WebSocket A. 引入依赖 B. WebSocket 配置 C. 控制器 D. 前端实现 四.参考文档 一.聊聊 WebSocket 从HTML5技术流行至今,WebSocket已经有非常广泛的应用: 在线游戏,提供实时的操作交互体验 社交平台,与好友实时的私信对话 新闻动态,获得感兴趣的主题信息推送 ... 这些场景,都需要服务器能主动实时的给浏览器或客户端推送消息,注意关键词是主动,还有实

补习系列(21)-SpringBoot初始化之7招式

目录 背景 1. @PostConstruct 注解 2. InitializingBean 接口 3. @Bean initMethod方法 4. 构造器注入 5. ApplicationListener 6. CommandLineRunner 7. ApplicationRunner 测试代码 参考文档 背景 在日常开发时,我们常常需要 在SpringBoot 应用启动时执行某一段逻辑,如下面的场景: 获取一些当前环境的配置或变量 向数据库写入一些初始数据 连接某些第三方系统,确认对方可以