Spring Boot 之 Filter, Interceptor, Aspect

July . 06 . 2019
  • 前言

接触 Spring 也有一段时间了, 之所以一直没有写相关的博客, 一是因为框架的东西相对比较杂, 不好下手, 其次是因为懒(^_^), 接下来的几篇我会挑一些框架中比较常见的特性, 来谈谈其用法, 总结一下近期的所学。

  • 环境配置
JDK: 1.8
spring boot: v2.1.6.RELEASE

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.springboot</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  • 先提一个需求

为了方便介绍这三种特性, 先来提一个贯穿全文的需求, 我会使用这三种不同的特性来实现这个需求

统计一下每个请求所消耗的时间
  • 准备一个DEMO

        项目的包结构如下图所示

      projectStructure.JPG

Mycontroller

package com.springboot.demo.web.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/my")
public class MyController {

    @GetMapping("/test")
    public Map test(String name) {

        Map res = new HashMap<>();
        res.put("name", name);
        return res;
    }
}

这样一个简单的测试Demo就写好了, 访问 http://localhost:8080/my/test?name=bob

效果如图

url.JPG

  1. Filter 实现

相信熟悉 javaEE 的同学应该对这个名词不会太陌生, 它的主要作用就是, 能对用户的请求进行预先处理,然后再将请求转发给其他web组件, 实现这个需求的大体思路就是, 在请求进入控制器方法之前记录一个时间, 在请求结束之后记录一个时间, 两个时间的差值就是所消耗的时间。

新建类 TimeFilter 继承了 Filter 类并重写相应的方法

TimeFilter

package com.springboot.demo.web.filter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

/**
 * 耗时计算过滤器
 */
@WebFilter(urlPatterns = {"/my/*"})
public class TimeFilter implements Filter {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        logger.info("过滤器: 初始化");
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
        logger.info("过滤器: 开始\n");

        long start = System.currentTimeMillis();
        chain.doFilter(req, resp);                // 调用控制器方法
        long end = System.currentTimeMillis();

        logger.info("过滤器拦截耗时: " + (end - start) + "ms\n");
    }

    @Override
    public void destroy() {
        logger.info("过滤器: 销毁");
    }
}

由于这里使用了 @WebFilter 注解所以在Spring Boot 的启动项里需要加上@ServletComponentScan注解, 这样Spring Boot才会扫描到该类如下:

DemoApplication

package com.springboot.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;

@SpringBootApplication
@ServletComponentScan
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

当然在 Spring Boot 中配置 Filter 的方式并不只有这一种, 下面我再列出一种配置方法, 二选一即可

新建类 WebConfig 作为配置类

WebConfig

package com.springboot.demo.web.config;

import com.springboot.demo.web.filter.TimeFilter;
import com.springboot.demo.web.intercepter.TimeIntercepter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.ArrayList;
import java.util.List;

// 这是一个配置类
@Configuration
public class WebConfig implements WebMvcConfigurer {

//     添加自定义 filter
    @Bean
    public FilterRegistrationBean timeFilter() {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(new TimeFilter());       // 加入自定义 filter

        List urls = new ArrayList<>();              // 指定拦截url
        urls.add("/my/*");
        registrationBean.setUrlPatterns(urls);

        return registrationBean;
    }
}

这样也可以将 Filter 注入到 Spring 的容器当中, 看个人喜好, 二选一即可。到此, 实现该功能的代码已经完成, 让我们来看看运行效果, 运行项目, 并访问测试地址。

Filter.JPG

可以看到我们的过滤器发挥了作用, 该次请求耗时 65ms


 2. Interceptor 实现

先来看一下Interceptor的定义

SpringMVC拦截器(Interceptor)实现对每一个请求处理前后进行相关的业务处理,类似与servlet中的Filter。

SpringMVC 中的Interceptor 拦截请求是通过HandlerInterceptor来实现的。

实现需求的逻辑和 Filter 实现的方式大致相同, 我们直接来看代码。

新建 TimeIntercepter 类 并且继承 Spring 的 HandlerInterceptor, 重写其方法

TimeIntercepter

package com.springboot.demo.web.intercepter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 耗时计算拦截器
 */
@Component
public class TimeIntercepter implements HandlerInterceptor {

    private Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * 控制器方法执行之前
     * @param req       请求
     * @param resp      响应
     * @param handler   对应控制器的方法
     * @return boolean  返回 true 进入控制器
     * @throws Exception 异常
     */
    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
        logger.info("拦截器: preHandle");
        req.setAttribute("startTime", System.currentTimeMillis());

        HandlerMethod method = (HandlerMethod) handler;
        logger.info("请求控制器类名: " + method.getBean().getClass().getSimpleName());
        logger.info("请求控制器方法名: " + method.getMethod().getName() + "\n");
        return true;
    }

    /**
     * 控制器方法执行时 (当控制器发生异常后就不会执行了)
     * @param req       请求
     * @param resp      响应
     * @param handler   对应控制器的方法
     * @param mav       模型和视图
     * @throws Exception 异常
     */
    @Override
    public void postHandle(HttpServletRequest req, HttpServletResponse resp, Object handler, ModelAndView mav) throws Exception {
        logger.info("拦截器: postHandle");

        long startTime = (long) req.getAttribute("startTime");
        long endTime = System.currentTimeMillis();

        logger.info("耗时: " + (endTime - startTime) + "ms");
    }

    /**
     * 控制器方法完成后 (总是会执行)
     * @param req         请求
     * @param resp        响应
     * @param handler     对应控制器的方法
     * @param ex          控制器执行时发生的异常
     * @throws Exception  异常
     */
    @Override
    public void afterCompletion(HttpServletRequest req, HttpServletResponse resp, Object handler, Exception ex) throws Exception {
        logger.info("拦截器: afterCompletion");

        if (ex != null) {
            logger.info("发生了异常: " + ex.getMessage());
        }
        long startTime = (long) req.getAttribute("startTime");
        long endTime = System.currentTimeMillis();

        logger.info("拦截器拦截耗时: " + (endTime - startTime) + "ms\n");
    }
}

最后在 WebConfig 中注册我们自定义的 Interceptor

WebConfig

package com.springboot.demo.web.config;

import com.springboot.demo.web.intercepter.TimeIntercepter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

// 这是一个配置类
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private TimeIntercepter timeIntercepter = null;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册拦截器, 拦截指定url
        registry.addInterceptor(timeIntercepter)
                .addPathPatterns("/my/*");
    }
}

代码已全部写完, 来看看执行效果, 访问测试 url, 运行效果如下图

interceptor.JPG

可以看到在 Filter 的拦截基础上, 又多了几条Interceptor的日志输出, 它是晚于 Filter 拦截的。


2. Aspcet 实现

Apsect 即 Spring Aop 中的切面, 概念有些抽象我们直接来看代码。

新建类 TimeAspect

TimeAspect

package com.springboot.demo.web.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

/**
 * 切片拦截请求
 */
@Aspect
@Component
public class TimeAspect {

    private Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * 切点 (环绕)
     * @param pjp 被拦截的方法
     * @return    被拦截的方法的返回值
     */
    @Around("execution(* com.springboot.demo.web.controller.MyController.*(..))")  // 拦截 UserController 里的所有方法
    public Object handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable {
        logger.info("切片拦截: 开始");

        long start = System.currentTimeMillis();

        Object[] args = pjp.getArgs();    // 获取请求方法的参数
        for (Object arg : args) {
            logger.info("拦截方法:" + pjp.getSignature().getName() + " 参数: " + arg);
        }

        Object res = pjp.proceed();      // 调用被拦截的方法, 返回拦截方法的返回值

        long end = System.currentTimeMillis();
        logger.info("切片拦截耗时: " + (end - start) + "ms");

        logger.info("切片拦截结束\n");
        return res;
    }
}

切面的实现很简单, 只需要这一个类即可, 原理是使用环绕通知将被调用的方法包裹起来, 注释有详细的解释这里不再赘述。在此启动程序, 来看看运行效果。

aspect.JPG

可以看到在Filter, Interceptor的基础上又多了一层切片的的拦截, 它是晚于Filter 与 Interceptor的。

  • 总结

上面我们通过spring的三种特性, 对我们的需求做了三个版本的实现, 虽然是同一个功能, 但是这三种实现还是有所区别的, 相信细心的同学已经从我们打印出的日志中发现了不通点, 下面我们来做一个总结:

  1.  拦截顺序: Filter -> Interceptor -> Aspect -> Interceptor -> Filter
  2. 时间快慢: Filter > Interceptor > Aspect
  3. Filter 只能拦截请求, 不能获取到请求对应的方法
  4. Interceptor 可以获取到请求的方法, 但不能获取到方法的参数
  5. Aspect 既可以获取到请求的方法, 也可以获取到方法的参数, 并且可以织入到整个方法的流程中, 改变方法的原有逻辑。

end~