原创 | Filter、Interceptor和Aspect对请求的拦截,有什么不同?
林必昭 码农沉思录
在使用Spring MVC开发RESTful API的时候,我们经常会使用Java的拦截机制来处理请求,Filter是Java本身自带拦过滤器,Interceptor则是Spring自带的拦截器,而Aspect切面是Spring AOP一个概念,主要的使用场景有:日志记录、事务控制和异常处理,该篇文章主要说说它们是如何实现的以及他们之间的差别,在这过程中也会探讨全局异常处理机制的原理以及异常处理过程。
Filter
我对Filter过滤器做了以下总结:
介绍:
java的过滤器,依赖于Sevlet,和框架无关的,是所有过滤组件中最外层的,从粒度来说是最大的,它主要是在过滤器中修改字符编码(CharacterEncodingFilter)、过滤掉没用的参数、简单的安全校验(比如登录不登录之类)
实现和配置方式
1.直接实现Filter接口+@Component
2.@Bean+@Configuration(第三方Filter)
3.web.xml配置方式
Filter的实现方式
@Component
public class TimeFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("初始化TimeFilter...");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
System.out.println("-------TimeFilter Start--------");
long start = new Date().getTime();
filterChain.doFilter(request, response);
System.out.println("TimeFilter执行耗时:" + (new Date().getTime() - start));
System.out.println("-------TimeFilter End--------");
}
@Override
public void destroy() {
System.out.println("销毁TimeFilter...");
}
}
注意:关于filterChain.doFilter(request,response,filterChain),执行filterChain.doFilter的意思是将请求转发给过滤器链上的下一个对象,如果没有filter那就是你请求的资源。一般filter都是一个链,web.xml 里面配置了几个就有几个。一个一个的连在一起这里指的是下一个Filter, request->filter1->filter2->filter3->...->response。
我们定义完Filter之后,如果我们不使用@Component注解注入,可以使用另一种方式将Filter注入到我们的容器中,这里使用@Bean的形式定义,通过自定义配置类WebConfig实现配置,最后返回registrationBean,这个方法主要有两个好处就是第一我们可以通过registrationBean.setUrlPatterns(urls)来指明filter在哪些路径下起作用,第二我们可以使用该方法去注入第三方的filter,原因的很多地方的filter其实并不是以@Component注入方式(也就是没有标注@Component注解),这时候我们就只能使用第二种方式来实现了。
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Autowired
TimeInterceptor timeInterceptor;
@Bean
public FilterRegistrationBean charsetFilter(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
TimeFilter timeFilter = new TimeFilter();
CharsetFilter charsetFilter = new CharsetFilter();
registrationBean.setFilter(charsetFilter);
registrationBean.setFilter(timeFilter);
//相当于@webFilter的@WebInitParam()注解的作用
Map paramMap = new HashMap<>();
paramMap.put("charset","utf-8");
registrationBean.setInitParameters(paramMap);
//相当于@webFilter的 urlPatterns = "/*"的作用
List urls = new ArrayList<>();
urls.add("/*");
//urls.add("/user/*");
registrationBean.setUrlPatterns(urls);
return registrationBean;
}
我们在controller中定义一个getInfo()方法:
//请求路径的{id}回传到方法里也就是传到(@PathVariable String id)的id里
@RequestMapping(value = "/user/{id:\\d+}",method = RequestMethod.GET)
@JsonView(User.UserDetailView.class) //这里因为UserDetailView继承了UserSimpleView所有会返回username和password
@ApiOperation("获取用户信息")
public User getInfo(@PathVariable Integer id) {
// throw new UserNotExistException(id);
System.out.println("进入getInfo()服务");
User user = new User();
user.setId(1);
user.setUsername("jacklin");
user.setPassword("123");
return user;
}
当我们调用controller中的getInfo()方法的时候,看看请求响应是否成以及控制台的输出:
GET请求发送成功,返回200,控制台输出如下:
从上述结果,我们可以分析得出,当客户端发送请求,到达Controller方法之前,先执行Filter初始化操作,接着进入Controller的方法体,最后执行完成,通过分析我们明白了Filter的工作原理和方法的执行顺序!
Interceptor
我对**Interceptor**过滤器做了以下总结(导图中加粗部分是重点):
简介:
spring框架的拦截器,主要依赖于Spring MVC框架,它是在 service 或者一个方法调用前,调用一个方法,或者在方法调用后,调用一个方法。
实现和配置方式:
实现HandlerInterceptor接口看,并重写preHandle、postHandle、afterCompletion方法。
解释说明:
SpringMVC中的Interceptor是链式的调用的,在一个应用中或者是在一个请求中可以同时存在多个Interceptor,每个Inteceptor的调用都会按照它的声明顺序依次执行,而且最先执行的Intecptor的preHandler方法,所以可以在这个方法中进行一些前置初始化操作或者是堆当前请求的一个预处理,也可以在这个方法中进行一些判断是否要继续进行下去。
该方法的返回值是Boolean类型的,当它返回为false时,表示请求结束,后续的Interceptor和Controller都不会再执行;
当返回值为true 时就会继续调用下一个Interceptor的preHandle方法,如果已经是最后一个Interceptor的时候就会是调用当前请求的Controller方法。
Interceptor拦截器的实现方式
/**
* @Author 林必昭
* @Date 2019/7/4 13:15
*/
@Component
public class TimeInterceptor implements HandlerInterceptor {
/**
* preHandle方法的返回值是boolean值,当返回的是false时候,不会进入controller里的方法
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("------->preHandle");
System.out.println("控制器类名:" + ((HandlerMethod) handler).getBean().getClass().getName()); //获取类名
System.out.println("控制器中的对应的方法名:" + ((HandlerMethod) handler).getMethod().getName()); //获取类中方法名
request.setAttribute("startTime", new Date().getTime());
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("------->postHandle");
Long start = (Long) request.getAttribute("startTime");
System.out.println("TimeInterceptor 执行耗时:" + " " + (new Date().getTime() - start));
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception e) throws Exception {
System.out.println("------->afterCompletion");
Long start = (Long) request.getAttribute("startTime");
System.out.println("TimeInterceptor 执行耗时:" + " " + (new Date().getTime() - start));
System.out.println("Exception is " + e);
}
}
注意:我们使用@Component定义Interceptor之后,还不能起作用,好要进行下一步配置,我们在之前定义的WebConfig配置类继承抽象类WebMvcConfigurerAdapter,将Interceptor注入容器中:
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Autowired
TimeInterceptor timeInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(timeInterceptor);
}
}
这样Interceptor就起作用了,同样,我们通过发送请求,观察控制台的输出,来分析结果:
从TimeInterceptor拦截器结果,我们可以分析得出,当客户端发送请求,到达Controller方法之前,先执行Interceptor的preHandler方法,接着进入Controller的方法体,通过Interceptor我们可以获取到对应的Controller和执行的方法名,接着执行postHandler方法,最后执行afterCompletion方法,如何结果出现异常,也会执行afterCompletion,这里没有异常,所以Exception为空。
那么当控制层中抛出异常,如果没有使用全局异常处理,在拦截器上也能捕获到异常信息,我们可以尝试一下,在Controller抛出一个RuntimeException,RuntimeException并没有在全局异常处理中被处理,Controller修改如下:
@RequestMapping(value = "/user/{id:\\d+}",method = RequestMethod.GET)
@JsonView(User.UserDetailView.class) //这里因为UserDetailView继承了UserSimpleView所有会返回username和password
@ApiOperation("获取用户信息")
public User getInfo(@PathVariable Integer id) {
/**
* 当抛出UserNotExistException异常的时候,会跳到ControllerExceptionHandler的handleUserNotExistException方法
* 进行相应的处理
*/
throw new RuntimeException("user not exist!!"); //这里抛出一个RuntimeException
// System.out.println("进入getInfo()服务");
// User user = new User();
// user.setId(1);
// user.setUsername("jacklin");
// user.setPassword("123");
// return user;
}
观察控制台输出:
结果很明显了,当控制层出现异常的时候,异常没有被全局处理器处理,到达拦截器,拦截器会捕获到异常,这时候只执行了preHandle和afterCompletionn方法,并没有执行postHandle方法,控制台也输出了异常信息。
想想,如果抛出我们自定义异常,而且自定义异常被全局处理器拦截处理,异常还会到达我们的拦截器吗,我们来自定义一个异常UserNotExistException,如下:
public class UserNotExistException extends RuntimeException {
private static final long serialVersionUID = -9136501205369741760L;
private String id;
public UserNotExistException(String id){
super("user is not exist...");
this.id = id;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
接着,定义全局异常处理器GlobalExceptionHandler,使用@ControllerAdvice修饰:
/**
* 全局异常处理,负责处理controller抛出的异常
*
* @Author 林必昭
* @Date 2019/7/4 11:31
*/
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotExistException.class)
@ResponseBody
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) //服务器内部错误
public Map handleUserNotExistException(UserNotExistException ex) {
Map resultMap = new HashMap<>();
resultMap.put("id", ex.getId());
resultMap.put("message", ex.getMessage());
return resultMap;
}
}
然后,我们再在UserController中抛出我们的自定义异常UserNotExistException,观察控制台的输出,来分析结果:
public User getInfo(@PathVariable Integer id) {
/**
* 当抛出UserNotExistException异常的时候,会跳到ControllerExceptionHandler的handleUserNotExistException方法
* 进行相应的处理
*/
//throw new RuntimeException("user not exist!!");
throw new UserNotExistException("user not exist!!")
}
从结果看出,异常时空的,证明我们定义的异常处理器已经生效,UserNotExistException在GlobalExceptionHandler已经被处理了,所有异常没有到达我们的拦截器,到这里我们可以得出异常的处理顺相顺序结论了,在文化在那个末尾会给出。
Aspect
我对Aspect过滤器做了以下总结:
在使用Spring AOP切面前,我们需要导入pom依赖:
org.springframework.boot
spring-boot-starter-aop
切面拦截的实现方式
@Aspect
@Component
public class TimeAspect {
/**
* 切入点
*/
@Around("execution(* com.lbz.web.controller.UserController.*(..))") //UserController下的任何方法被调用都会执行这个切片
public Object handleControllerMethod(ProceedingJoinPoint point) throws Throwable {
System.out.println("TimeAspect start");
long start = new Date().getTime();
Object object = point.proceed(); //proceed中文意思是继续的意思,也就是切入,相当于filterChain.doFilter()
Object[] args = point.getArgs(); //与Filter和Interceptor的区别是,可以获取到UserController里方法的参数
for (Object arg : args) {
System.out.println("控制层的方法对应参数是:" + arg);
}
System.out.println("TimeAspect执行耗时:" + (new Date().getTime() - start));
System.out.println("TimeAspect end");
return object;
}
}
这里的point.proceed()是继续的意思,也就是切入,相当于filterChain.doFilter(),与Filter和Interceptor不同的是,我们可以通过point.getArgs();拿到对应方法的参数,我们通过遍历把参数打印看一下。
从结果看出,我们可以看到我们拿到方法对应的参数,为1,也就是我们请求:http://localhost:8060/user/1 传入的id的值;
总结:
1.过滤器可以拿到原始方法的Http的请求和响应信息,拿不到对应方法的详细信息,拦截器既可以拿到原始方法的Http请求和响应信息,也能拿到对应方法的详细信息,但是拿不到被调用方法对应参数的值,而切面可以拿到被调用方法传递过来参数的值,但却拿不到原始的Http请求和响应对象。
2.Controller方法抛出异常之后,最先捕获到异常的是切片,如果你定义了全局异常处理器并声明了ControllerAdvice,切片捕获到异常往外抛,就轮到全局异常处理器处理,接着到拦截器,再到过滤器,也就是:
拦截作用顺序:Aspect->全局处理器->拦截器->过滤器->Tomcat
最后,我完成了对Filter、Interceptor、Aspect三种拦截方式的实现和过程分析,通过本次的学习,我也掌握了很多的知识,包括拦截器的工作原理,异常被处理的顺序,全局异常处理机制,掌握如何实现请求的拦截和处理,我个人觉得多看不如一写,多写写加以思考总会有收获,看了很多文章但还是觉得自己理解不够深刻,所有才决定将他记录下来,加深理解,我觉得这样值得,晚安!