写在前面
最近在进行系统的日志模块重构工作,在选择技术实现的时候在过滤器和拦截器之间飘忽不定,于是决定抽点时间将过滤器和拦截器进行深度分析,顺便把监听器和AOP等内容也复习一下。
当我们需要基于全局实现某些功能时,在传统的Servlet容器中,可以使用过滤器和监听器,在Java框架中还可以使用拦截器。本篇将从过滤器(Filter)、监听器(Listener)、拦截器(Interceptor)和面向切面编程(AOP)这四个方面入手,分别介绍它们的概念以及如何使用。
为了后续学习需要,新建一个SpringBoot项目,并在pom文件中新增web、lombok依赖。之后新建一个名为controller的包,并在该包内新建一个名为IndexController
的类,该类用于提供一个访问首页的接口:
@RestController
@Slf4j
public class IndexController {
@RequestMapping("/index")
public String index(){
log.info("这是首页");
return "the index page";
}
}
过滤器(Filter)
过滤器简说
过滤器,这里指的是Servlet过滤器,它是在Java Servlet中定义的,能够对Servlet容器中的请求和响应对象进行检查和修改,只起到过滤作用,不会生成Request和Response对象。
过滤器特点:(1)过滤器是基于回调函数实现;(2)过滤器是Servlet规范规定的,只能用于Web程序中;(3)过滤器只在Servlet启动前后起作用,作用范围较窄。
过滤器的配置非常简单,开发者只需自定义类并实现Filter接口,查看一下这个Filter接口的源码:
public interface Filter {
default void init(FilterConfig filterConfig) throws ServletException {
}
void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;
default void destroy() {
}
}
可以看到里面只有三个方法,它们的作用分别如下所示:
(1)init()
,该方法在Servlet容器启动初始化过滤器时被调用,即它在Filter整个的生命周期中只会被调用一次。「此方法必须调用成功,否则Filter无法初始化,那么后续将无法提供服务。」
(2)doFilter()
,Servlet容器中每一次请求都会调用该方法,FilterChain
用于调用下一个过滤器。
(3)destroy()
,当Servlet容器销毁该Filter实例时被调用,通常在该方法中书写销毁或者关闭资源的逻辑。同样在Filter整个的生命周期中只会被调用一次。
SpringBoot实现过滤器的三种方式
在SpringBoot中使用过滤器有三种方式,分别对应不同的业务场景:(1)无路径无顺序;(2)有路径无顺序;(3)有路径有顺序。
无路径无顺序
无路径无顺序,即默认过滤所有的路径,且多个过滤器之间不存在执行顺序。这种方式最简单,只需自定义类并实现Filter
接口,并在自定义类上使用@Component
注解将其交由Spring管理。
第一步,新建filter包,并在该包内新建一个名为IndexFilter
的类,该类需要实现Filter
接口并重写其中的三个方法:
@Component
@Slf4j
public class IndexFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
log.info("进入了Filter的doFilter方法");
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("进入了Filter的init方法");
}
@Override
public void destroy() {
log.info("进入了Filter的destroy方法");
}
}
第二步,启动项目,访问/index
接口,可以看到IDEA输出如下信息:
可以看到每访问该接口,doFilter方法就会被调用,而init和destory方法则在Filter实例的整个生命周期中只执行一次。
有路径无顺序
有路径无顺序,即指定过滤的路径,但是多个过滤器之间不存在执行顺序。这种方式配置稍微复杂些,开发者需要自定义类并实现Filter接口,然后在自定义类上使用@WebFilter
注解,通过@WebFilter
注解来设置过滤器的匹配路径,同时还需要在启动类中添加@ServletComponentScan
注解,@ServletComponentScan
注解用于扫描添加@WebFilter
、@WebServlet
、@WebListener
注解的Bean,并在使用的时候自动注入:
@WebFilter(filterName = "indexFilter2",urlPatterns = "/hello")
@Slf4j
public class IndexFilter2 implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
log.info("进入了Filter2的doFilter方法");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("进入了Filter2的init方法");
}
@Override
public void destroy() {
log.info("进入了Filter2的destroy方法");
}
}
@ServletComponentScan
@SpringBootApplication
public class FilterbookApplication {
public static void main(String[] args) {
SpringApplication.run(FilterbookApplication.class, args);
}
}
可以看到我们在IndexFilter2
的类上添加了@WebFilter(filterName = "IndexFilter2",urlPatterns = "/hello")
配置项,目的就是实现对特定URL的拦截,此处是访问/hello
才会拦截,因此当开发者启动项目访问/index
接口时IndexFilter2
是不会拦截的。
有路径有顺序
有路径无顺序,即指定过滤的路径,同时多个过滤器之间存在执行顺序。这种方式配置比较复杂,只能通过配置类来实现,且只针对实现过滤器的接口,它不需要通过注解来注入Spring容器,只通过配置类实现。
两个自定义过滤器类:
@Slf4j
public class FilterA implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
log.info("进入了FilterA的doFilter方法");
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("进入了FilterA的init方法");
}
@Override
public void destroy() {
log.info("进入了FilterA的destroy方法");
}
}
@Slf4j
public class FilterB implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
log.info("进入了FilterB的doFilter方法");
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("进入了FilterB的init方法");
}
@Override
public void destroy() {
log.info("进入了FilterB的destroy方法");
}
}
IndexFilterConfig
配置类:
@Configuration
public class IndexFilterConfig {
@Bean
public FilterRegistrationBean filterA(){
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
FilterA filterA = new FilterA();
filterRegistrationBean.setFilter(filterA);
filterRegistrationBean.addUrlPatterns("*");
filterRegistrationBean.setName("filterA");
filterRegistrationBean.setOrder(1);
return filterRegistrationBean;
}
@Bean
public FilterRegistrationBean filterB(){
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
FilterB filterB = new FilterB();
filterRegistrationBean.setFilter(filterB);
filterRegistrationBean.addUrlPatterns("*");
filterRegistrationBean.setName("filterB");
filterRegistrationBean.setOrder(2);
return filterRegistrationBean;
}
}
监听器(Listener)
监听器简说
监听器,它也是Servlet层面的内容,可用于监听Web应用中某些对象或者信息的创建、修改和销毁等动作发生,并作出相应的响应处理。
监听器分类
根据监听的对象,监听器可分为三类:
「(1)ServletContext对象监听器。」ServletContext对象监听器。对应于application,需要实现ServletContextListener
接口。在整个Web服务中只有一个,在Web服务关闭时销毁。可用于做数据缓存,如结合redis可在Web服务创建时从数据库加载数据到缓存服务器中,提升系统响应速度。
「(2)HttpSession对象监听器。」HttpSession对象监听器。对应session会话,需要实现HttpSessionListener
接口。在会话起始时创建,当一端关闭会话后销毁。可用于获取在线的用户数量。
「(3)ServletRequest对象监听器。」ServletRequest对象监听器。对应request,需要实现ServletRequestListener
接口。request对象是客户发送请求时创建的,用于封装请求数据,当请求处理完毕时,才销毁。可用于封装用户信息。
SpringBoot实现监听器有两种方式,一种是直接在实现了对应接口的类上添加@Component
注解;另一种则是先在实现类上添加@WebListener
注解,再在项目启动类上添加@ServletComponentScan
注解,此处采用第二种方式。
ServletContext对象监听器
ServletContextListener
接口,用于监控ServletContext
对象的生命周期,实际上就是监听Web应用的生命周期。ServletContextListener
接口主要有两个方法,一个在当Servlet容器启动web应用时调用(contextInitialized
),另一个是在Servlet容器终止web应用时调用(contextDestroyed
):
public interface ServletContextListener extends EventListener {
default void contextInitialized(ServletContextEvent sce) {
}
default void contextDestroyed(ServletContextEvent sce) {
}
}
如果开发者想自定义ServletContext对象监听器,只需实现ServletContextListener
接口,并重写其中的两个方法:
@WebListener
@Slf4j
public class MyServletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("MyServletContextListener contextInitialized has called");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("MyServletContextListener contextInitialized has called");
}
}
HttpSession对象监听器
HttpSessionListener
接口,用于监控HttpSession
对象的生命周期,实际上就是监听Session会话的生命周期。HttpSessionListener
接口主要有两个方法,一个在Session会话创建时调用(sessionCreated
),另一个在Session会话销毁时调用(sessionDestroyed
):
public interface HttpSessionListener extends EventListener {
default void sessionCreated(HttpSessionEvent se) {
}
default void sessionDestroyed(HttpSessionEvent se) {
}
}
如果开发者想自定义HttpSession对象监听器,只需实现HttpSessionListener
接口,并重写其中的两个方法:
@WebListener
@Slf4j
public class MyHttpSessionListener implements HttpSessionListener {
@Override
public void sessionCreated(HttpSessionEvent se) {
log.info("MyHttpSessionListener sessionCreated has called");
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
log.info("MyHttpSessionListener sessionDestroyed has called");
}
}
ServletRequest对象监听器
ServletRequestListener
接口,用于监控ServletRequest
对象的生命周期。ServletRequestListener
接口主要有两个方法,一个在ServletRequest
对象初始化时调用(requestInitialized
),另一个在ServletRequest
对象销毁时调用(requestDestroyed
):
public interface ServletRequestListener extends EventListener {
default void requestDestroyed(ServletRequestEvent sre) {
}
default void requestInitialized(ServletRequestEvent sre) {
}
}
如果开发者想自定义ServletRequest对象监听器,只需实现ServletRequestListener
接口,并重写其中的两个方法:
@WebListener
@Slf4j
public class MyServletRequestListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
log.info("MyServletRequestListener requestDestroyed has called");
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
log.info("MyServletRequestListener requestInitialized has called");
}
}
拦截器(Interceptor)
拦截器简说
拦截器,这里指的是Spring中的拦截器,主要用于拦截用户请求并做相应的处理。拦截器和过滤器、监听器不同,它不依赖于Servlet容器,依赖于Spring框架,是AOP的一种体现,底层基于Java的动态代理实现。
拦截器特点:(1)拦截器基于Java反射机制(动态代理)实现;(2)拦截器是Spring特有的,能使用Spring中的任何资源;(3)拦截器可以用于Web程序,也可以用于Application和Swing程序汇总;(4)拦截器能够深入到方法前后,异常抛出前后,伸缩性较大,作用范围较广。
拦截器是链式调用,一个应用中可以同时存在多个拦截器(Interceptor),一个请求也可以触发多个拦截器,每个拦截器会根据它被声明的顺序依次被调用。拦截器的配置稍微复杂些,用户首先也要自定义一个类并实现HandlerInterceptor
接口,查看一下这个HandlerInterceptor
接口的源码:
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
}
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
}
}
可以看到里面也只有三个方法,它们的作用分别如下所示:
(1)preHandle()
,该方法将在请求处理之前调用。「注意,如果该方法返回值为false,那么就认为当前请求结束,这不仅导致当前的拦截器失效,同时其他的拦截器也不再执行。」
(2)postHandle()
,该方法只有在preHandle()
方法返回true的时候才会执行,且会在Controller中方法调用之后,DispatcherServlet
返回渲染视图之前被调用。「注意,postHandle()
方法被调用的顺序与preHandle()
是相反的,即先声明的拦截器的preHandle()
会先执行,但是它的postHandle()
方法反而会在后执行。」
(3)afterCompletion()
,该方法只有在preHandle()
方法返回true的时候才会执行,且会在整个请求结束之后,DispatcherServlet
渲染了对应的视图之后执行。可以在此方法中进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中。
拦截器实现步骤
SpringBoot实现拦截器分为两个步骤:(1)自定义需要拦截类并实现HandlerInterceptor
接口重写其中的三个方法;(2)通过配置类来注册拦截器,配置类需要实现WebMvcConfigurer
接口,并重写其中的addInterceptors
方法。
第一步,新建interceptor包,并在该包内新建一个名为IndexInterception
的类,该类需要实现HandlerInterceptor
接口并重写其中的三个方法:
@Component
@Slf4j
public class IndexInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("进入了Interceptor的preHandle方法");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("进入了Interceptor的postHandle方法");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("进入了Interceptor的afterCompletion方法");
}
}
第二步,新建config包,并在该包内新建一个名为IndexInterceptorConfig
的类,该类需要实现WebMvcConfigurer
接口并重写其中的addInterceptors
方法:
@Configuration
public class IndexInterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new IndexInterceptor()).addPathPatterns("/**");
}
}
可以看到这里我们将自定义的拦截器处理类注册到InterceptorRegistry
中并通过addPathPatterns("/**")
方法设置了需要拦截的URL。
第三步,启动项目,访问/index
接口,可以看到IDEA输出如下信息:
可以看到每访问该接口,这三个方法就会被调用。一般我们会在preHandle
中获取到拦截的方法:
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("进入了Interceptor的preHandle方法");
HandlerMethod handlerMethod = (HandlerMethod)handler;
Method method = handlerMethod.getMethod();
String methodName = method.getName();
log.info("拦截到的方法名称为:{}",methodName);
return true;
}
AOP
AOP简说
AOP(面向切面编程)是一种编程思想,并不是某种具体实现。一般在提到AOP实现时,会有过滤器和代理模式这两种,过滤器基于函数回调实现,而代理模式基于java反射机制。代理模式分为静态代理和动态代理,动态代理就是拦截器的简单实现。
AOP中的一概念
(1)通知(Advice),AOP框架中的增强处理,用于描述切面何时执行以及如何增强处理。
(2)连接点(Join Point),即应用程序执行过程中能够插入切面的一个点,这个点可以是方法的调用,也可以是异常的抛出。需要注意的是,在Spring AOP中,这个点只能是方法的调用。
(3)切点(Point Cut),可以插入且用于增强处理的连接点。
(4)切面(Aspect),里面通常会定义通知和切点。
(5)引入(Introduction),引入允许开发者向现有的类中添加新方法或者属性。
(6)织入(Weaving),它是一个过程,即将增强处理添加到目标对象中并创建一个被增强的对象的过程。
相比于拦截器,Spring AOP封装性更好,且功能更强大,使用的时候需要单独引入spring-boot-starter-aop
这一jar包。
在使用AOP的时候,由于底层都通过Spring容器来管理,因此可直接通过注解来使用它,以下是经常会使用到的注解:
(1)@Aspect
,使用在类上,用于将普通Java类定义为切面类;
(2)@Pointcut
,用于定义一个切入点,可以是一个规则表达式,也可以是注解;
(3)@Before
,用于在切入点开始处切入内容;
(4)@After
,用于在切入点结尾处切入内容;
(5)@AfterReturning
,用于在切入点返回内容之后处理逻辑;
(6)@Around
,用于在切入点前后切入内容,并控制何时执行切入点的内容。可以理解是@Before
和@After
注解的组合使用;
(7)@AfterThrowing
,用于当切入内容部分抛出异常之后的处理逻辑。(8)@Order(1)
,AOP切面的执行顺序。注意@Before
数值越小越先执行,@After
和@AfterReturning
则是数值越大越先执行。
举个例子,如下所示:
@Component
@Aspect
@Slf4j
public class MyAspect {
@Pointcut("execution(public * com.envy.filterbook.controller.IndexController.*(..))")
public void myPointCut(){}
@Before("myPointCut()")
public void myBefore(){
log.info("myBefore...");
}
@After("myPointCut()")
public void myAfter(){
log.info("myAfter...");
}
@Around("myPointCut()")
public Object myAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("myAround...before...");
Object proceed = joinPoint.proceed();
log.info("myAround...after...");
return proceed;
}
@AfterReturning("myPointCut()")
public void myAfterReturning(){
log.info("myAfterReturning...");
}
}
如何选择
以上介绍的Filter
、ServletContextListener
、Interceptor
和AOP
以及经常用到的@ControllerAdvice
注解,它们的执行顺序如下所示:
ServletContextListener
-->Filter
-->Interceptor
-->AOP
-->具体执行的方法-->AOP
-->@ControllerAdvice
-->Interceptor
-->Filter
-->ServletContextListener
。
上述对象根据实现原理可分为两大类:
(1)Filter
和Listener
:依赖Servlet容器,基于函数回调实现,几乎可拦截所有请求,但无法获取Spring IOC容器中的Bean对象。
(2)Interceptor
和AOP
:依赖Spring框架,基于Java反射和动态代理实现,只能拦截Controller中的请求,可以获取Spring IOC容器中的Bean对象。
可以看到从Filter
-->Interceptor
-->AOP
,拦截功能越来越强大,尤其是Interceptor
和AOP
可以结合Spring框架来进行实现,但是拦截顺序确是越来越往后,所以如果有些请求可以在Servlet容器中过滤,那么这是最好的,毕竟越早的过滤和拦截就消耗越少的系统资源。当然这个需要结合实际业务来进行确定具体选用哪种方式。
过滤器和拦截器区别
其实过滤器和拦截器都是AOP(面向切面编程)的具体实现,都可以实现诸如登录鉴权、日志记录等功能,但是也存在一些不同点,下面将进行分析。
实现原理不同
「(1)过滤器(Filter)基于函数回调。」前面说过自定义的过滤器需要实现Filter
接口并重写其中的三个方法,注意其中的doFilter
方法:
void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;
里面有三个参数,分别是ServletRequest
、ServletResponse
和FilterChain
,前面两个没什么说的,这个FilterChain
其实是一个回调接口:
public interface FilterChain {
void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;
}
可以看到这个类里面也有一个doFilter
方法,这其实就是一个回调方法,该接口常用的实现类是ApplicationFilterChain
,这里以ApplicationFilterChain
为例进行介绍。
查看一下ApplicationFilterChain
类的doFilter
方法逻辑:
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if (Globals.IS_SECURITY_ENABLED) {
ServletRequest req = request;
ServletResponse res = response;
try {
AccessController.doPrivileged(() -> {
this.internalDoFilter(req, res);
return null;
});
} catch (PrivilegedActionException var7) {
//异常处理
}
} else {
this.internalDoFilter(request, response);
}
}
可以看到无论是否开启了全局安全,都会调用internalDoFilter
这个内置的过滤器方法:
private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
//如果当前所在的过滤器不是最后一个,那么获取当前过滤器信息
if (this.pos < this.n) {
ApplicationFilterConfig filterConfig = this.filters[this.pos++];
try {
Filter filter = filterConfig.getFilter();
if (request.isAsyncSupported() && "false".equalsIgnoreCase(filterConfig.getFilterDef().getAsyncSupported())) {
request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", Boolean.FALSE);
}
if (Globals.IS_SECURITY_ENABLED) {
Principal principal = ((HttpServletRequest)request).getUserPrincipal();
Object[] args = new Object[]{request, response, this};
SecurityUtil.doAsPrivilege("doFilter", filter, classType, args, principal);
} else {
filter.doFilter(request, response, this);
}
} catch (ServletException | RuntimeException | IOException var15) {
//异常处理
}
} else {
try {
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(request);
lastServicedResponse.set(response);
}
if (request.isAsyncSupported() && !this.servletSupportsAsync) {
request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", Boolean.FALSE);
}
if (request instanceof HttpServletRequest && response instanceof HttpServletResponse && Globals.IS_SECURITY_ENABLED) {
Principal principal = ((HttpServletRequest)request).getUserPrincipal();
Object[] args = new Object[]{request, response};
SecurityUtil.doAsPrivilege("service", this.servlet, classTypeUsedInService, args, principal);
} else {
this.servlet.service(request, response);
}
} catch (ServletException | RuntimeException | IOException var17) {
//异常处理
} finally {
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set((Object)null);
lastServicedResponse.set((Object)null);
}
}
}
}
可以看到在internalDoFilter
方法中会获取到所有的过滤器,并在内部的doFilter
方法中依次调用这些过滤器,每个过滤器都会先执行内部的doFilter
方法,最后在执行结束前会调用FilterChain.doFilter(request, response)
方法,其实就是调用ApplicationFilterChain.doFilter(request, response)
方法,这样循环执行进而实现函数回调:
「(2)拦截器(Interceptor)基于Java反射机制(动态代理)实现。」
使用范围不同
(1)自定义的过滤器需要实现javax.servlet.Filter
接口,而这个接口是在Servlet规范中定义的,或者说过滤器需要依赖于Servlet容器,因此它只能在Web程序中使用:
(2)自定义的拦截器需要实现org.springframework.web.servlet.HandlerInterceptor
接口,它存在于SpringMVC中,由Spring容器来管理,不依赖于Servlet容器,是可以单独使用的,因此不仅可以用在Web程序中,也可以用在Application、Swing等程序中:
触发时机不同
下图是过滤器和拦截器的触发时机:
「(1)过滤器(Filter)是在请求进入容器后,但在进入Servlet之前进行预处理,请求结束是在Servlet处理完之后。」即过滤器对请求进行预处理,再交给Servlet处理并生成响应,最后再对服务器响应进行后处理。
「(2)拦截器(Interceptor)是请求进入Servlet后,但在进入Controller之前进行预处理,请求结束是在Controller中渲染了对应的视图之后。」拦截器可以在方法执行前调用(preHandle
),方法执行后调用(postHandle
),视图页面渲染后调用(afterCompletion
)。
拦截的请求范围不同
这里我们通过一个实例在进行说明,在前面我们已经分别配置了过滤器和拦截器并提供了/index
这一API,接下来我们尝试同时配置过滤器和拦截器。之后启动项目,可以看到IDEA控制台输出如下信息:
接着访问/index
这一API,可以看到IDEA控制台输出如下信息:
然后再次访问/index
这一API,可以看到IDEA控制台输出如下信息:
也就说执行顺序如下所示:
可以看到过滤器的init和destory方法只会调用一次,且分别在容器初始化和销毁时调用。而拦截器中的三个方法和过滤器中的doFilter方法则会在每次请求的时候都会调用。
实际上过滤器会对几乎所有进入容器的请求都起作用,但是拦截器只会对Controller中的请求或者访问static目录下的静态资源起作用(目标执行方法起作用)。
注入Bean的时机不同
前面我们使用的都是Controller层,但在实际开发过程中经常会遇到Service层,因此接下来我们尝试分别在过滤器和拦截器中都注入Service,看看这两者有什么区别。
第一步,新建service包,并在里面新建IndexService接口,并定义IndexServiceImpl类来实现这个接口:
public interface IndexService {
void test();
}
@Service
public class IndexServiceImpl implements IndexService{
@Override
public void test() {
System.out.println("test for IndexServiceImpl is called");
}
}
第二步,在自定义的过滤器IndexFilter中注入这个IndexService
对象并在doFilter
方法中调用它的test方法:
@Autowired
private IndexService indexService;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
log.info("进入了Filter的doFilter方法");
indexService.test();
filterChain.doFilter(servletRequest,servletResponse);
}
第三步,启动项目,并访问/index
这一API,可以看到IDEA控制台输出如下信息:
进入了Filter的init方法
进入了Filter的doFilter方法
test for IndexServiceImpl is called
进入了Interceptor的preHandle方法
这是首页
进入了Interceptor的postHandle方法
进入了Interceptor的afterCompletion方法
可以看到这个输出顺序与之前介绍的内容完全一致。
第四步,先注释掉第二步中的操作,然后在自定义的拦截器IndexInterceptor
中注入这个IndexService
对象并在postHandle
方法中调用它的test方法:
@Autowired
private IndexService indexService;
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("进入了Interceptor的postHandle方法");
indexService.test();
}
第五步,启动项目,并访问/index
这一API,可以看到IDEA控制台报错了,原因是自定义的拦截器IndexInterceptor
中注入的IndexService
对象是空的:
为什么是这样呢?还记得之前我们在IndexInterceptorConfig#addInterceptors()
中的代码么,我们在调用addInterceptor
方法的时候手动new了一个IndexInterceptor
对象:
@Configuration
public class IndexInterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new IndexInterceptor()).addPathPatterns("/**");
}
}
而我们在IndexInterceptor
类上又添加了@Component
注解,表示让Spring容器来管理并生成IndexInterceptor
对象:
@Component
@Slf4j
public class IndexInterceptor implements HandlerInterceptor {
}
很明显你注册的拦截器和Spring容器中存在的不是同一个,所以才会导致Spring无法管理进而出现空指针。解决办法就是去掉IndexInterceptor
类上的@Component
注解,同时在IndexInterceptorConfig
类中手动定义一个IndexInterceptor
对象,并将该拦截器注册一下:
@Configuration
public class IndexInterceptorConfig implements WebMvcConfigurer {
@Bean
public IndexInterceptor interceptor(){
System.out.println("手动注入了IndexInterceptor");
return new IndexInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor()).addPathPatterns("/**");
}
}
之后再次重新启动项目,并访问/index
这一API,可以看到IDEA控制台输出如下信息:
进入了Filter的init方法
进入了Filter的doFilter方法
进入了Interceptor的preHandle方法
这是首页
进入了Interceptor的postHandle方法
test for IndexServiceImpl is called
进入了Interceptor的afterCompletion方法
执行流程不同
在实际开发过程中经常出现同时存在多个拦截器或者过滤器,此时我们想控制它们执行的先后顺序,这都是可以的。
过滤器控制执行顺序
对于过滤器(Filter)来说,可以在类上使用@Order
注解来控制多个过滤器的执行顺序,其中@Order
注解中的数字越小,那么优先级越高,越先执行:
@Order(1) //过滤器优先级
@Component
@WebFilter(filterName = "IndexFilter",urlPatterns = "/*")
@Slf4j
public class IndexFilter1 implements Filter {
}
@Order(2) //过滤器优先级
@Component
@WebFilter(filterName = "IndexFilter",urlPatterns = "/*")
@Slf4j
public class IndexFilter2 implements Filter {
}
@Order(3) //过滤器优先级
@Component
@WebFilter(filterName = "IndexFilter",urlPatterns = "/*")
@Slf4j
public class IndexFilter3 implements Filter {
}
过滤器执行流程
客户端发起的请求首先是经过了过滤器并处理了Request请求,之后去访问Servlet资源,当Servlet执行完毕后,Response响应也会经过过滤器,即也是经过了过滤器处理:
拦截器控制执行顺序
对于拦截器(Interceptor)来说,它们的注册顺序就是默认的执行顺序,当然也可以通过Order方法来进行控制,Order
方法中的数字越小,那么优先级越高,越先执行:
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor2()).addPathPatterns("/**").order(2);
registry.addInterceptor(interceptor3()).addPathPatterns("/**").order(3);
registry.addInterceptor(interceptor1()).addPathPatterns("/**").order(1);
}
需要注意的是对于多个拦截器来说,先声明的拦截器的preHandle
方法先执行,但是它的postHandle
方法后执行,也就是说postHandle
方法的执行顺序和preHandle
方法的执行顺序是相反的:
进入了Interceptor1的preHandle方法 进入了Interceptor2的preHandle方法 进入了Interceptor3的preHandle方法 这是首页 进入了Interceptor3的postHandle方法 进入了Interceptor2的postHandle方法 进入了Interceptor1的postHandle方法 进入了Interceptor3的afterCompletion方法 进入了Interceptor2的afterCompletion方法 进入了Interceptor1的afterCompletion方法
拦截器执行流程
(1)程序先执行preHandle()
方法,如果该方法返回值为true,那么程序将继续往下执行处理器中的方法,否则认为当前请求结束,不再往下执行;(2)在Controller类(业务处理器)处理完请求后会执行postHandle()
方法,之后通过DispatcherServlet
向客户端返回响应;
(3)DispatcherServlet
处理完请求后执行afterCompletion()
方法。:
接下来通过阅读源码加深印象,我们知道Controller中的所有请求都会经过核心组件DispatcherServlet
并调用它的doDispatch()
方法,查看该方法的源码:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 确定处理当前请求的程序
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 确定处理当前请求的程序适配器
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
if (isGet || HttpMethod.HEAD.matches(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
//调用Interceptor拦截器中的PreHandle方法
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 使用给定的处理程序来处理这个请求,即执行Handle(核心方法业务逻辑也在这个方法中)
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
//进行视图转换
applyDefaultViewName(processedRequest, mv);
//调用Interceptor拦截器中的PostHandle方法
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
//异常处理
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
//异常处理
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
可以看到执行顺序总的来说就是applyPreHandle
-->handle
(实际业务逻辑)-->渲染视图-->applyPostHandle
,查看一下上述提到的applyPreHandle
和applyPostHandle
方法的源码:
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for (int i = 0; i < this.interceptorList.size(); i++) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
if (!interceptor.preHandle(request, response, this.handler)) {
triggerAfterCompletion(request, response, null);
return false;
}
this.interceptorIndex = i;
}
return true;
}
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
throws Exception {
for (int i = this.interceptorList.size() - 1; i >= 0; i--) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
interceptor.postHandle(request, response, this.handler, mv);
}
}
可以发现它们在遍历interceptorList
数组的时候一个是从左到右,另一个是从右到左,所以这两者的调用方式是相反的。
应用场景不同
过滤器(Filter)一般用于:字符编码转换、敏感词过滤、登录权限验证、资源访问控制等;
拦截器(Interceptor)一般用于:AOP以及需要配合一些业务逻辑的场景(注入Bean等)。
这里顺便提一下监听器的作用,一般用于统计在线人数、清除过期session等。
小结
本篇主要基于SpringBoot学习了如何使用过滤器、监听器、拦截器和AOP,其实无论是过滤器还是拦截器,它们都是AOP的一种实现,同时也学习了另一种更加灵活的AOP实现,基于Aspect切面的方式,这种方式似乎更加优雅。参考文章:一口气说出 过滤器 和 拦截器 6个区别,别再傻傻分不清了SpringBoot 过滤器、监听器、拦截器、AOP 比较Java三大器之拦截器(Interceptor)的实现原理及代码示例。