在开发 Web 项目的时候,经常需要过滤器来处理一些请求,包括字符集转换什么的,记录请求日志什么的等等。在之前的 Web 开发中,我们习惯把过滤器配置到 web.xml 中,但是在 SpringBoot 中,兵没有这个配置文件,该如何操作呢?其实在 Spingboot 中存在3种形式进行过滤操作。
首先构建一个包,该包需要在项目启动下面,如下图
image
其中1代表是微服务启动类,2代表在启动类下面构建一个包,再在堡垒新建一个过滤器类,并且实现 Filter 接口
image
接下来实现里面的方法,这里我们仅仅是记录方法调用锁花费的时间。当然为了 SpringBoot 能够识别这个组件,需要注解@Component
@Componentpublic 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,方便测试
@RestControllerpublic 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 ,具体代码如下
@Configurationpublic class ProjectConfig { @Bean
public FilterRegistrationBean timerFilter(){
FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean();
filterRegistrationBean.setFilter(new TimerFilter());
List<String>urls=Lists.newArrayList();
urls.add("/*");
filterRegistrationBean.setUrlPatterns(urls);
return filterRegistrationBean;
}
}
这里可以添加过滤器锁拦截的 URL,拦截更加精准。
重新运行项目,不出意外,将会得到同样的结论。
由于上面的过滤器的过来方法里面是使用的ServletRequest request, ServletResponse response
,所以和 Spring 相关的上下文就很难获得,也不知道是从哪个 Controller 来的,所以,就出现了 SpringBoot 框架自带的过滤器interceptor
.
首先构建包cn.ts.demo.interceptor
,并且新建TimerInterceptor
类,该类需要实现HandlerInterceptor
image
可以看到有三个需要实现的方法,从方法名称可以得知每个方法的具体作用。
preHandle
:表示在调用某个方法之前,会调用
postHandle
:表示控制器的方法调用之后,该方法用调用;如果控制器的方法跑出异常,那么这个方法不会被执行。
afterCompletion
:表示无论控制器方法如何处理,该方法都会调用。
现在来完善方法里面的内容,以及对象 Object 是什么。当然也是需要标注为 @Component
.
@Componentpublic 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
捕获到异常。
虽然 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@Componentpublic 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,实际开发可能要综合使用,以便达到我们需要的效果。