美文网首页
老生常谈:你真的理解过滤器、拦截器、ControllerAdvi

老生常谈:你真的理解过滤器、拦截器、ControllerAdvi

作者: 喝杯Java润润BUG | 来源:发表于2023-03-19 10:33 被阅读0次

    面试官:说说过滤器和拦截器的区别? 这个问题面试题库算是比较经典的,这两个我相信很多同学在工作中都有接触过,但如果没有经过系统的整理,还真的不好说出个123来,那老湿机在此这面就把它俩和常用的AOP、ControllerAdvice放一起,带你做一个比较全面的认识。

    1. 了解4种拦截方法的执行顺序

    先上一个栗子,看四种拦截方法并驾齐驱使用时,谁先谁后:

    /**
     * ============Filter过滤器============
     */
    @Slf4j
    @WebFilter(urlPatterns = "/*")
    public class DemoFilter implements Filter {
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
            log.info("Filter 进入");
            filterChain.doFilter(request, response);
            log.info("Filter 退出");
        }
    }
    l
    /**
     * ============Interceptor过滤器============
     */
    public class DemoInterceptor 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 进入");
        }
    }
    
    /**
     * ============ControllerAdvice============
     */
    @ControllerAdvice
    public class DemoControllerAdvice {
    
        @InitBinder
        public void init(WebDataBinder binder) {
            log.info("ControllerAdvice init 进入");
            binder.setFieldDefaultPrefix("user.");
        }
        @ExceptionHandler(Exception.class)
        public String handleException(Exception e) {
            log.info("ControllerAdvice handleException 进入");
            return "error";
        }
    }
    
    /**
     * ============AOP============
     */
    @Aspect
    @Component
    public class DemoAspect {
        @Pointcut("(@target(org.springframework.web.bind.annotation.RestController)) " +
                "&& within(cn.demo.api..*) && execution(public * *(..))")
        public void pointcut() {
        }
    
        @Around(value = "pointcut()")
        public Object around(ProceedingJoinPoint pjp) throws Throwable {
            log.info("Aop 进入");
            Object proceed = pjp.proceed();
            log.info("Aop 退出");
            return proceed;
        }
    }
    
    /**
     * ============控制器入口============
     */
    @RestController
    @RequestMapping("/demo")
    public class DemoController {
        @RequestMapping("/test")
        public Map<String, String> test(@ModelAttribute("user")User user) {
            log.info("业务:user.name: {}", user.getName());
            Map<String, String> result = new HashMap<>(2);
            result.put("code", "200");
            int i  = 1;
            i = i / 0;
            return result;
        }
    }
    

    定义好Demo示例,接着来一发:
    http://localhost:8080/demo/test?user.name=宫三公子 :

    结果:
    Filter 进入
    Interceptor preHandle 进入
    ControllerAdvice init 进入
    Aop 进入
    业务:user.name: 宫三公子
    ControllerAdvice handleException 进入
    Interceptor afterCompletion 进入
    Filter 退出
    

    好了,我们似乎已经看到,4种拦截方法的执行顺序是这样子:


    image.png

    2. 知晓4种拦截方法应用场景

    那么针对这4种拦截方法,它们涉及的应用场景、实现技术、作用力度都各不相同,为了有一个比较清晰的对比,老司机我简单粗暴,直接整理好啦,大伙干:


    image.png

    3. 掌握4种拦截方法的原理

    如果只晓得执行顺序和应用场景可能过不了面试大大狠辣的毒眼,知其然并知其所以然,我们就自废10根头发,来研究一下它们的原理吧。

    3.1 过滤器

    虽然一个过滤器在一次请求中只能调用一次,但是根据具体业务需求,可以生成多个不同类型的Filter并依次执行,这就是过滤器的链式调用,可以通过指定Order排序,值越小越靠前(默认根据Filter的名称进行自然排序),新建了4个Filter,Order依次为0-3,演示一波,符合预期:

    image.png

    接下来我们关注整个链式调用的核心: FilterChain接口,内部定义了doFilter接口方法,tomcat容器提供了ApplicationFilterChain作为FilterChain的具体实现:

    public final class ApplicationFilterChain implements FilterChain {
    // 
    private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
    private Servlet servlet;
    public void doFilter(ServletRequest request, ServletResponse response) {
        internalDoFilter(request, response);
    }
    
    private void internalDoFilter(ServletRequest request, ServletResponse response) {
        if (this.pos < this.n) {
            ApplicationFilterConfig filterConfig = this.filters[this.pos++];
            Filter filter = filterConfig.getFilter();
            filter.doFilter(request, response, this);             
        } else {
            this.servlet.service(request, response);
        }
     }
    }
    

    内部定义了ApplicationFilterConfig[] filters 过滤器配置列表,每一个ApplicationFilterConfig内部持有一个Filter实例,另一个比较重要的是Servlet,实例化后对应原生HttpServlet或SpringMVC的DispatcherServlet,当拦截器链路执行完成后,会调用Servlet中service方法做后续的Url路由映射、业务处理以及视图响应等流程了(这个后面研究SpringMVC的请求流程来详细再分析), 好了,我们通过Debug可以看到,filters中除了服务默认的一些请求filter,我们自己定义的4个filter也以定义好的顺序排入其中了:

    image.png

    过滤器整体执行流程Like this:

    image.png
    3.2 拦截器

    拦截器调用流程比较复杂,我这面根据源码梳理了核心请求流程和简要说明,感兴趣的同学可以继续一探究竟:源码位于:
    org.springframework.web.servlet.DispatcherServlet#doDispatch方法

    image.png

    Tip一下:网上有些朋友说拦截器是基于反射、动态代理来实现的,通过司机对源码分析,反射倒是有用到,主要从Spring的IOC容器中获取了拦截器对象,并放在AbstractHandlerMapping的adaptedInterceptors全局对象中,在上图的第二步就匹配满足的拦截器作为当前请求的拦截器列表,没有动态代理就没有影子!。

    3.3 ControllerAdvice

    其实,ControllerAdvice和拦截器实现有异曲同工之处,要是说用什么技术手段,那应该也只能说是反射吧,在也主要在上一步的doDispatch方法中,它主要是在分布第4步t通过反射对InitBinder的参数的设置和第6步进行统一的异常捕获,重点看看第6步:在processDispatchResult处理结果方法内部,调用processHandlerException方法进行异常相关的处理逻辑,我们可以看到它的主要工作就是遍历handlerExceptionResolvers来进行异常对应的处理,我们自定义的全局异常
    ExceptionHandlerExceptionResolver实例控制着所有的Controller类,debug源码:

    image.png

    那什么时候进行设置的以及如何设置的呢,值得我们思考一下,怀着好奇心,我们点开
    ExceptionHandlerExceptionResolver来看看,原来重点是实现了InitializingBean的afterPropertiesSet方法,在容器启动时候检测带ControllerAdvice注解的类和类中带ExceptionHandler注解的方法,并加入到我们刚才看到的exceptionHandlerCache中:

    public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver implements ApplicationContextAware, InitializingBean {
        private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerCache = new ConcurrentHashMap(64);
    
       // 重点实现了afterPropertiesSet
        public void afterPropertiesSet() {
            this.initExceptionHandlerAdviceCache();
        }
    
        private void initExceptionHandlerAdviceCache() {
            if (this.getApplicationContext() != null) {
           // 寻找所有带有ControllerAdvice注解的类
                List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(this.getApplicationContext());
                Iterator var2 = adviceBeans.iterator();
    
                while(var2.hasNext()) {
                    ControllerAdviceBean adviceBean = (ControllerAdviceBean)var2.next();
                    Class<?> beanType = adviceBean.getBeanType();
               
            // 寻找该类下面的方法是否有ExceptionHandler注解,如果有,则收集到mappedMethods列表中
                    ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
            // 如果不为空,则加入到全局异常拦截缓存中
                    if (resolver.hasExceptionMappings()) {
                        this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
                    }            
                }     
            }
        }
    }
    
    
    3.4 AOP

    AOP这块想着网上其实很多同学梳理得很好了,到底要不要整理,但考虑到完整性,也避免同学们再额外找文章跳来跳去,就一并简化整理了,接着继续: 前面说到,AOP是有两种实现方式,相信大家都耳熟能详:JDK动态代理和CGLib,带大家复习一下,先来一个JDK动态代理的栗子:

    //1.  动态代理是基于接口访问的,先定义用户服务接口
    public interface UserService {
        User getUser(String name);
    }
    
    // 2. 具体用户服务实现类,也即被代理对象
    public class UserServiceImpl implements UserService{
        @Override
        public User getUser(String name) {
            User user = new User();
            user.setName("宫三公子");
            System.out.println("用户名:" + user.getName());
            return user;
        }
    }
    
    // 3. 代理工具,用于服务增强
    public class JDKProxyr implements InvocationHandler {
        private Object target;
        public JDKProxy(Object target) {
            this.target = target;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("before-------JDK Proxy");
            Object invoke = method.invoke(target, args);//通过反射执行,目标类的方法
            System.out.println("after-------JDK Proxy");
            return invoke;
        }
    }
    
    // 4. 测试方法
    public class JDKProxyTest {
        public static void main(String[] args) {
            UserServiceImpl userService = new UserServiceImpl();
            JDKProxy handler = new JDKProxy(userService);
            UserService proxyInstance = (UserService) Proxy.newProxyInstance(userService.getClass().getClassLoader(),
                    userService.getClass()
                            .getInterfaces(), handler);
            User user = proxyInstance.getUser("宫三公子");
        }
    }
    

    测试结果:

    before-------JDK Proxy
    用户名:宫三公子
    after-------JDK Proxy
    

    CGLib的代理方式是为我们需要被代理的具体类生成一个子类,即将需被代理的方法进行Override重写:

    // CGLib代理工具,用于生成业务增强
    public class CGLibProxy implements MethodInterceptor {
       public Object intercept(Object arg0, Method method, Object[] objects, MethodProxy proxy) throws Throwable {
           System.out.println("before-------CGLib Proxy");
           Object result = proxy.invokeSuper(arg0, objects);
           System.out.println("after-------CGLib Proxy");
           return result;
       }
    }
    
    // 测试方法
    public class CGLibProxyTest {
       public static void main(String[] args) {
           CGLibProxy proxy = new CGLibProxy();
           Enhancer enhancer = new Enhancer();
           enhancer.setSuperclass(UserServiceImpl.class);
           //回调方法的参数为代理类对象CglibProxy,最后增强目标类调用的是代理类对象CglibProxy中的intercept方法
           enhancer.setCallback(proxy);
           UserServiceImpl userService = (UserServiceImpl) enhancer.create();
           userService.getUser("宫三公子");
           // 打印增强效果
           System.out.println("打印userService的增强结果:");
           ReflectUtils.printMethods(userService.getClass());
       }
    }
    

    打印结果,我们可以看到被代理的方法已经执行了增强逻辑:

    before-------CGLib Proxy
    用户名:宫三公子
    after-------CGLib Proxy
    
    打印userService的增强结果:
     public final getUser(java.lang.String);
      public setCallback(int,net.sf.cglib.proxy.Callback);
      public static CGLIB$findMethodProxy(net.sf.cglib.core.Signature);
      public static CGLIB$SET_THREAD_CALLBACKS([Lnet.sf.cglib.proxy.Callback;);
      public static CGLIB$SET_STATIC_CALLBACKS([Lnet.sf.cglib.proxy.Callback;);
      public getCallback(int);
      public getCallbacks();
      public setCallbacks([Lnet.sf.cglib.proxy.Callback;);
      private static final CGLIB$BIND_CALLBACKS(java.lang.Object);
      static CGLIB$STATICHOOK1();
      final CGLIB$hashCode$3();
      final CGLIB$clone$4();
      final CGLIB$toString$2();
      final CGLIB$equals$1(java.lang.Object);
      final CGLIB$getUser$0(java.lang.String);
      public final equals(java.lang.Object);
      public final toString();
      public final hashCode();
      protected final clone();
      public newInstance([Lnet.sf.cglib.proxy.Callback;);
      public newInstance([Ljava.lang.Class;,[Ljava.lang.Object;,[Lnet.sf.cglib.proxy.Callback;);
      public newInstance(net.sf.cglib.proxy.Callback);
    

    好了,我们看完了基于Jdk动态代理和CGLib代理的两种Demo,那我们继续看看Spring是怎么玩的吧。 在Spring的启动过程中,有很多的扩展点,比较让我们熟知的BeanPostPorcessor,主要在Bean进行initializeBean初始化后,调用
    org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsAfterInitialization,内部对所有的BeanPostPorcessor对象进行遍历,调用
    postProcessAfterInitialization进行处理,而Aop就是基于该扩展点实现的:

    image.png

    沿着这条链路Debug调试,最终可以定位到具体决定调用JDK动态代理还是使用CGLib,我这代理的是DemoController,那生成的就是CGLig代理啦:

    image.png

    OK,关于过滤器、拦截器、ControllerAdvice以及AOP,本文从执行顺序、应用场景、实现技术以及原理等进行了总结,看完了,朋友们如果有收获就是对我最大得肯定。

    相关文章

      网友评论

          本文标题:老生常谈:你真的理解过滤器、拦截器、ControllerAdvi

          本文链接:https://www.haomeiwen.com/subject/eytyrdtx.html