Spring Boot的过滤器

在开发 Web 项目的时候,经常需要过滤器来处理一些请求,包括字符集转换什么的,记录请求日志什么的等等。在之前的 Web 开发中,我们习惯把过滤器配置到 web.xml 中,但是在 SpringBoot 中,兵没有这个配置文件,该如何操作呢?其实在 Spingboot 中存在3种形式进行过滤操作。

1、使用传统的过滤器

首先构建一个包,该包需要在项目启动下面,如下图


image

其中1代表是微服务启动类,2代表在启动类下面构建一个包,再在堡垒新建一个过滤器类,并且实现 Filter 接口


image

接下来实现里面的方法,这里我们仅仅是记录方法调用锁花费的时间。当然为了 SpringBoot 能够识别这个组件,需要注解@Component

@Component
public class TimerFilter implements Filter{

    @Override
    public void destroy() {
        System.out.println("timer Filter is destoried");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        System.out.println("timer Filter begin");
        long start=new Date().getTime();
        chain.doFilter(request, response);
        long end=new Date().getTime();
        System.out.println("timer Filter end,cost time:"+(end-start));
    }

    @Override
    public void init(FilterConfig arg0) throws ServletException {
        System.out.println("timer Filter is inited");
    }

}

构建一个测试 Controller,方便测试

@RestController
public class UserController {
    
    @GetMapping("/filter")
    public String testFilter(){
        return "filter is ok";
    }
}

启动项目,进行测试。
项目启动后,首先看到过滤器里面的初始化方法被执行了


image

接着访问http://localhost:8888/filter,控制台打印出如下内容,表示过滤器正常调用

image

第三方过滤器的使用

有时候,我们使用的是第三方的过滤器,并不是在我们项目启动类注解可扫描的部分,也没法配置到 web.xml 里面,这个时候该怎么办?

我们可以使用 SpringBoot 的配置类进行配置。
首先构建一个包,再新建一个配置类,然后添加注解为@Configuration

image

接下来,我们就开始注入 bean,这个 bean 是FilterRegistrationBean
,具体代码如下

@Configuration
public class ProjectConfig {

    @Bean
    public FilterRegistrationBean timerFilter(){
        FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean();
        
        filterRegistrationBean.setFilter(new TimerFilter());
        Listurls=Lists.newArrayList();
        urls.add("/*");
        filterRegistrationBean.setUrlPatterns(urls);
        
        return filterRegistrationBean;
    }
}

这里可以添加过滤器锁拦截的 URL,拦截更加精准。

重新运行项目,不出意外,将会得到同样的结论。

2、使用Interceptor

由于上面的过滤器的过来方法里面是使用的ServletRequest request, ServletResponse response,所以和 Spring 相关的上下文就很难获得,也不知道是从哪个 Controller 来的,所以,就出现了 SpringBoot 框架自带的过滤器interceptor.

首先构建包cn.ts.demo.interceptor,并且新建TimerInterceptor类,该类需要实现HandlerInterceptor

image

可以看到有三个需要实现的方法,从方法名称可以得知每个方法的具体作用。
preHandle:表示在调用某个方法之前,会调用
postHandle:表示控制器的方法调用之后,该方法用调用;如果控制器的方法跑出异常,那么这个方法不会被执行。
afterCompletion:表示无论控制器方法如何处理,该方法都会调用。

现在来完善方法里面的内容,以及对象 Object 是什么。当然也是需要标注为 @Component.

@Component
public class TimerInterceptor implements HandlerInterceptor{

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse arg1, Object arg2, Exception exception)
            throws Exception {
        System.out.println("afterCompletion exe");
        Long start=(Long)request.getAttribute("startTimer");
        System.out.println("afterCompletion花费时间:"+(new Date().getTime()-start));
        System.out.println("异常:"+exception);
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse arg1, Object arg2, ModelAndView arg3)
            throws Exception {
        System.out.println("postHandle exe");
        Long start=(Long)request.getAttribute("startTimer");
        System.out.println("postHandle花费时间:"+(new Date().getTime()-start));
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse arg1, Object obj) throws Exception {
        System.out.println("preHandle exe");
        //向 request 里面放入一个属性
        request.setAttribute("startTimer", new Date().getTime());
        //查看这里的 obj 是什么
        System.out.println("类名称:"+((HandlerMethod)obj).getBean().getClass().getName());
        System.out.println("方法名称:"+((HandlerMethod)obj).getMethod().getName());
        return true;
    }

}

如此操作之后,并不能直接使用,需要在配置类里面进行配置,同时修改配置继承WebMvcConfigurerAdapter,然后覆盖addInterceptors方法。这个方法需要把刚才做好的TimerInterceptor增加进来。当然需要把TimerInterceptor注入进来,

@Autowired private TimerInterceptor timerInterceptor;
@Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(timerInterceptor);
    }

现在可以重启项目,开始验证。不出意外,就得到如下结果


image

确实能够得到相关的类和方法名称。

如果我们的控制器方法跑出异常,再来看下,修改下控制器的方法。

    @GetMapping("/filter")
    public String testFilter(){
        throw new RuntimeException();
        //return "filter is ok";
    }

继续重启,再运行
得到的结论:

image

postHandle不会执行了,直接跳到afterCompletion。需要注意的是,如果有异常处理机制,也不会再afterCompletion捕获到异常。

3、切片 Aspect

虽然 Interceptor 能够拿到类和方法名称,但是不能够拿到方法的参数和他的值。
查看下 Spring 的源码,找到 DispatcherServlet,这个是用来分发请求的,找 doService方法,再找到doDispatch(request, response);,大概在901行,进入这个方法,找到962-967行

                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }

                // Actually invoke the handler.
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

其中mappedHandler.applyPreHandle就是调用我们拦截器的pre部分。如果为 false,就不再执行下面内容。接下来才是真正执行,也就是ha.handle这个方法,里面包含了参数的拼装,也就是说控制器的参数对象是有这个方法通过 request 里面的参数来构造出来的。所以在 interceptor的 prehandle 方法里面是不知道参数是什么样的。 所以如果需要知道具体的参数,就得使用切片来处理。

Spring AOP 简介
一个切片需要切入点和最强两个部分。

image

大概了解了切片之后,我们需要立马实现他。
首先还是先建立个 aspect 包,然后新建一个切片类TimerAspect。需要增加注解。

image

接下来,需要创建切入点,时间点的说明
Before 雷同 interceptor 的 preHandle;
After 雷同 interceptor 的 postHandle;
AfterThrow 雷同 interceptor 的 afterCompletion;
Around 是包围全部,也就是覆盖上面3个,一般用这个。

哪些方法上执行是一些正则表达式,在上述注解里面,比如
@Around("execution(* cn.ts.demo.controller.UserController.*(..))")
第一个*表示任何的返回值;
UserController后面的点星代表该类里面的所有方法;
括号点点表示方法参数任意。
关羽如何编写这样的表达式,可以参考[AOP参考]https://docs.spring.io/spring/docs/4.3.17.RELEASE/spring-framework-reference/htmlsingle/#aop-pointcuts

接下来继续完善该切片代码

@Aspect
@Component
public class TimerAspect {

    @Around("execution(* cn.ts.demo.controller.UserController.*(..))")
    public Object handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable{
        
        System.out.println("time aspect start");
        //方法参数
        Object[] args = pjp.getArgs();
        for (Object arg:args) {
            System.out.println("参数:"+arg);
        }
        
        Long start=new Date().getTime();
        Object obj=pjp.proceed();
        System.out.println("time aspect花费时间:"+(new Date().getTime()-start));
        System.out.println("time aspect end");
        
        return obj;
    }
}

重启服务,进行测试,


image

由于我们的测试方法没有参数,所以参数打印不存在。

修改下控制器方法的代码

@GetMapping("/filter/{id:\\d+}")
    public String testFilter(@PathVariable String id){
        //throw new RuntimeException();
        return "filter is ok";
    }

然后测试


image

不出意外,参数应该可以正常打印出来。

这样我们把三种过滤器的方法做了说明,也能看得出默认的顺序是过滤器,interceptor,aspect,实际开发可能要综合使用,以便达到我们需要的效果。

你可能感兴趣的:(Spring Boot的过滤器)