Spring中的拦截器

637 阅读7分钟

SpringBoot 中的拦截器主要分为两种:一个是HandlerInterceptor, 一个是MethodInterceptor.

概述

    两者的区别:HandlerInterceptor是springMVC项目中的拦截器,它拦截的目标是请求地址,比MethodInterceptor先执行。
    HandlerInterceptors是一个全局的拦截器, 可以对Controller层所有方法都进行拦截. MethodInterceptor则可以对所有层特定方法进行特定的拦截。
   HandlerInterceptoer拦截的是请求地址,所以针对请求地址做一些验证、预处理等操作比较合适。另外,HandlerInterceptoer可以对整个请求的生命周期进行拦截处理,可以跨越很多方法,而MethodInterceptor则是利用了AOP可以对自定义 拦截的方法, 对其切入点, 切入方式 都有较高的 自由度支持

HandlerInterceptor

  HandlerInterceptors的实现既可以通过implements HandlerInterceptor接口, 又可以通过extend HandlerInterceptorAdapter类,主要定义了三个方法
preHandle(方法执行前), postHandler(方法执行后). afterCompletion(请求返回结果后)。
该拦截器只能过滤action请求,SPring允许多个拦截器同时存在,通过拦截器链管理。
当preHandle return true时,执行下一个拦截器,直到所有拦截器执行完,再运行被拦截的请求。
当preHandle return false时, 不再执行后续的拦截器链及被拦截的请求。

下面展示 基于实现HandlerInterceptor 接口实现一个全局拦截器

/** * 自定义拦截器-基于springmvc
* @ClassName: CustomInterceptor
* @Description: springMVC项目中的拦截器,它拦截的目标是请求的地址,比MethodInterceptor先执行。
*                 该拦截器只能过滤action请求,SPring允许多个拦截器同时存在,通过拦截器链管理。
*                 当preHandle return true时,执行下一个拦截器,直到所有拦截器执行完,再运行被拦截的请求。
*                 当preHandle return false时, 不再执行后续的拦截器链及被拦截的请求。
* @author OnlyMate
* @Date 2018年8月28日 下午2:30:22  
* */
public class CustomInterceptor implements HandlerInterceptor  {

    @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // TODO Auto-generated method stub
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        if (Objects.nonNull(method.getAnnotation(NoAuthenticate.class))
            || Objects.nonNull(method.getDeclaringClass().getAnnotation(NoAuthenticate.class))) {
            //不需要验证权限
            return true;
        }
        String accessToken = request.getHeader("accesstoken");
        log.info("------------accessToken:"+accessToken);
        log.info("------------redisHost:"+redisHost+"---redisPort:"+redisPort);
        if (accessToken == null || accessToken.isEmpty()) {
            throw new BaseException(100011, "请登陆!");
        }
        SessionHelper.setAccessToken(accessToken);
        String str= (String) redisTemplate.opsForValue().get(accessToken);
        if (str == null) {
            throw new BaseException(100011, "请登陆!");
        }
        return true;
    }

    @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            ModelAndView modelAndView) throws Exception { // TODO Auto-generated method stub
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // TODO Auto-generated method stub
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }

}

  这三个方法都是干什么的,有什么作用,什么时候调用,不同的拦截器之间是怎样的调用顺序呢?这还得参考一下DispatcherServlet的doDispatch方法

下面这段代码,封装了SpringMVC
  处理请求的整个过程.首先根据请求找到对应的HandlerExecutionChain,它包含了处理请求的handler和所有的HandlerInterceptor拦截器;然后在调用hander之前分别调用每个HandlerInterceptor拦截器的preHandle方法,若有一个拦截器返回false,则会调用triggerAfterCompletion方法,并且立即返回不再往下执行;若所有的拦截器全部返回true并且没有出现异常,则调用handler返回ModelAndView对象;再然后分别调用每个拦截器的postHandle方法;最后,即使是之前的步骤抛出了异常,也会执行triggerAfterCompletion方法。

  根据以上的代码,分析一下不同拦截器及其方法的执行顺序。假设有5个拦截器编号分别为12345,若一切正常则方法的执行顺序是12345的preHandle,54321的postHandle,54321的afterCompletion。若编号3的拦截器的preHandle方法返回false或者抛出了异常,接下来会执行的是21的afterCompletion方法。这里要注意的地方是,我们在写一个拦截器的时候要谨慎的处理preHandle中的异常,因为这里一旦有异常抛出就不会再受到这个拦截器的控制。12345的preHandle的方法执行过之后,若handler出现了异常或者某个拦截器的postHandle方法出现了异常,则接下来都会执行54321的afterCompletion方法,因为只要12345的preHandle方法执行完,当前拦截器的拦截器就会记录成编号5的拦截器,而afterCompletion总是从当前的拦截器逆向的向前执行。

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);

                // Determine handler for the current request.
                mappedHandler = getHandler(processedRequest);
                if (mappedHandler == null) {
                    noHandlerFound(processedRequest, response);
                    return;
                }

                // Determine handler adapter for the current request.
                HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

                // Process last-modified header, if supported by the handler.
                String method = request.getMethod();
                boolean isGet = "GET".equals(method);
                if (isGet || "HEAD".equals(method)) {
                    long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                    if (logger.isDebugEnabled()) {
                        logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
                    }
                    if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                        return;
                    }
                }

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

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

                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }

                applyDefaultViewName(processedRequest, mv);
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            }
            catch (Exception ex) {
                dispatchException = ex;
            }
            catch (Throwable err) {
                // As of 4.3, we're processing Errors thrown from handler methods as well,
                // making them available for @ExceptionHandler methods and other scenarios.
                dispatchException = new NestedServletException("Handler dispatch failed", err);
            }
            processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
        }
        catch (Exception ex) {
            triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
        }
        catch (Throwable err) {
            triggerAfterCompletion(processedRequest, response, mappedHandler,
                    new NestedServletException("Handler processing failed", err));
        }
        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);
                }
            }
        }
    }

  最后, implements HandlerIntercetor接口的拦截器 还需要 添加到SpringBoot 实现了WebMvcCongigurer的配置类中配置类中. 具体代码如下 另外, add的拦截器会越靠外,即越靠近浏览器

@Configuration
public class AppConfig implements WebMvcConfigurer {
    //实现了implements HandlerIntercetor接口的拦截器
    @Autowired
    SessionInterceptor sessionInterceptor;

    //重写addInterceptors方法,将sessionInterceptor添加入其中
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(sessionInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/swagger-ui.html", "/v2/api-docs/**", "/swagger-resources/**",
                        "/webjars/**", "/api/v1/passwordforget");
    }
    
   //跨域的配置    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**").allowedOrigins("*")
                .allowedMethods("*");
    }
}

MethodInterceptor

  MethodInterceptor是AOP项目中的拦截器,它拦截的目标是方法,即使不是controller中的方法。实现MethodInterceptor拦截器大致也分为两种,一种是实现MethodInterceptor接口,另一种利用AspectJ的注解或配置。(这里只讲基于@AspectJ注解实现的方法)

下面介绍 一下 AOP 的切入点 和 切入方式
先来介绍一下切入点:
切入点既可以通过自定义注解切入(这里不谈论), 也可以通过切入点表达式(excution)切入,下面讲讲excution

例如定义切入点表达式 execution (*

com.sample.service.impl..*. *(..))
execution()是最常用的切点函数,其语法如下所示:
整个表达式可以分为五个部分:
1execution(): 表达式主体。
2、第一个*号:表示返回类型, *号表示所有的类型。
3、包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,com.sample.service.impl包、子孙包下所有类的方法。
4、第二个*号:表示类名,*号表示所有的类。 这里的..* 表示的是 impl包下面任意的递归目录下面的所有类,  如果是.*的话只能表示impl包下面的所有类
5、*(..):最后这个星号表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数

这里介绍一下AOP切入方式几个注解

@Before(方法执行前)
@After(方法执行后)
@Around(在切入点前切入内容, 并可以控制是否执行切入点自身的内容, 切入点后也可以切入内容)
@AfterReturning(在切入点return后切入内容 ,一般用来处理返回值)
@AfterThrowing(用来处理切入内容抛出异常后的处理逻辑)

下面是示例代码: 拦截同一个Session 同一个api在指定时间只能执行一次

@Slf4j  //log注解
@Aspect //Aop注解
@Component  //让该拦截器注入到SpringBoot中
public class NoRepeatSubmitAop {
   @Autowired
   private RedisTemplate redisTemplate;

   //拦截所有带有@NoRepeatSubmit注解的方法
   @Pointcut("@annotation(cn.jwis.digitaldevicemate.base.annotations.NoRepeatSubmit)")
   public void pointCut() {}  

   //这里需要注意的是 @annotation(xxx) 中的名字  与 around(ProceedingJoinPoint point, MyAnnotation xxx)中的名字  xxx必须一致, 才能够取到noRepeatSubmit中的值


   @Around("pointCut() && @annotation(noRepeatSubmit)")
   public Object arround(ProceedingJoinPoint point, NoRepeatSubmit noRepeatSubmit) throws Throwable{
      try {
         int i = noRepeatSubmit.repeatTimeOut();
         ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder
               .getRequestAttributes();
         String sessionId = RequestContextHolder.getRequestAttributes().getSessionId();
         String servletPath = attributes.getRequest().getServletPath();
         String key = sessionId + "-" +servletPath;
         //如果缓存中有这个key视为重复提交
         if (redisTemplate.hasKey(key)) {
            return ResultInfo.Fail("重复提交");
         } else {
            //被切方法继续执行
            redisTemplate.opsForValue().set(key, 0, noRepeatSubmit.repeatTimeOut(), TimeUnit.MILLISECONDS);
            Object o = point.proceed();
            return o;
         }
      } catch (Throwable e) {
         return ResultInfo.Fail(e.getMessage());
      }
   }
}

执行时机

这两个的执行时机如下图:

Spring拦截器执行顺序.png