11、SpringMVC进阶:拦截器使用详解及流程分析

拦截器简介

什么是拦截器

Spring中的拦截器(Interceptor) ,用于拦截控制器方法的执行,可以在方法执行前后,添加自定义逻辑,类似于AOP编程思想。

实际应用中,可以使用拦截器实现,认证授权、日志记录、字符编码转换,敏感词过滤等等。

和过滤器有什么区别

过滤器也能实现拦截功能,具体和拦截器有什么不同呢

1. 触发机制

过滤器Filter是在请求进入容器后,但在进入servlet之前进行预处理,请求结束是在servlet处理完以后。

拦截器Interceptor 是在请求进入servlet后,在进入Controller之前进行预处理的,Controller 中渲染了对应的视图之后请求结束。
*

2. 实现原理

过滤器和拦截器底层实现方式大不相同,过滤器是基于函数回调的,拦截器则是基于Java的反射机制(动态代理)实现的。

3. 使用范围

过滤器实现的是 javax.servlet.Filter 接口,而这个接口是在Servlet规范中定义的,也就是说过滤器Filter 的使用要依赖于Tomcat等容器,导致它只能在web程序中使用。

拦截器(Interceptor) 它是一个Spring组件,并由Spring容器管理,并不依赖Tomcat等容器,是可以单独使用的。不仅能应用在web程序中,也可以用于Application、Swing等程序中。

4. 拦截的请求范围

过滤器几乎可以对所有进入容器的请求起作用。

拦截器只会对Controller中请求或访问static目录下的资源请求起作用。

核心类

HandlerInterceptor接口

Spring MVC提供了HandlerInterceptor接口来实现拦截器功能,我们可以实现这个接口,然后注册拦截器,以添加常见的预处理行为,而无需修改每个控制器方法。

HandlerInterceptor定义了三个方法,可在控制器方法执行前后添加自定义逻辑。

public interface HandlerInterceptor {
   
     
	/**
	 * 在 HandlerMapping 确定合适的处理程序对象之后,但在 HandlerAdapter 调用处理程序之前调用
	 */
	default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
   
     

		return true;
	}
	/**
	 * 拦截处理程序的执行。在 HandlerAdapter 实际上调用处理程序之后调用,但在 DispatcherServlet 呈现视图之前调用
	 */
	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 {
   
     
	}
}

AsyncHandlerInterceptor

AsyncHandlerInterceptor异步处理程序拦截器,继承自HandlerInterceptor接口,主要是扩展了一个afterConcurrentHandlingStarted方法。

	/**
	*处理程序被并发执行时,代替 {@code postHandle} 和 {@code 		  afterCompletion} 被调用。
 	* 实现可以使用提供的请求和响应,但应该 
	* 避免以与并发处理程序执行冲突的方式修改它们。这种方法的典型用途是清理线程局部变量。
	*/
	default void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response,
			Object handler) throws Exception {
   
     
	}

WebRequestInterceptor

WebRequestInterceptor也是拦截器接口,不过它拦截方法传入的参数是WebRequest。

WebRequest是Spring定义的接口,它是对HttpServletRequest的封装。对WebRequest 进行的操作都将同步到HttpServletRequest 中。

public interface WebRequestInterceptor {
   
     
	// 拦截请求处理程序,在其调用之前的执行。允许准备上下文资源(例如 Hibernate Session)并将它们公开为请求属性或线程本地对象
	void preHandle(WebRequest request) throws Exception;
	// 拦截请求处理程序的执行成功调用之后,就在视图呈现之前(如果有的话)。允许在成功的处理程序执行后修改上下文资源(例如,刷新休眠Session)。
	void postHandle(WebRequest request, @Nullable ModelMap model) throws Exception;
	// 请求处理完成后的回调,即渲染视图后。将在处理程序执行的任何结果上调用,从而允许进行适当的资源清理。 
	void afterCompletion(WebRequest request, @Nullable Exception ex) throws Exception;

}

InterceptorRegistration

InterceptorRegistration类,用来协助拦截器进行注册登记,可以为每个拦截器配置匹配路径、包含路径等。

它的属性如下:

	// 注册的拦截器
	private final HandlerInterceptor interceptor;
	// 包含路径
	@Nullable
	private List<String> includePatterns;
	// 排除路径
	@Nullable
	private List<String> excludePatterns;
	// 匹配器
	@Nullable
	private PathMatcher pathMatcher;
	// 顺序
	private int order = 0;

InterceptorRegistry

InterceptorRegistry翻译过来是拦截器注册表,它的主要作用是维护了应用程序所有的拦截器列表。

public class InterceptorRegistry {
   
     
	// 拦截器注册集合
	private final List<InterceptorRegistration> registrations = new ArrayList<>();
	// 添加Handler拦截器,
	public InterceptorRegistration addInterceptor(HandlerInterceptor interceptor) {
   
     
		InterceptorRegistration registration = new InterceptorRegistration(interceptor);
		this.registrations.add(registration);
		return registration;
	}
	// 添加WebRequestInterceptor 拦截器
	public InterceptorRegistration addWebRequestInterceptor(WebRequestInterceptor interceptor) {
   
     
		WebRequestHandlerInterceptorAdapter adapted = new WebRequestHandlerInterceptorAdapter(interceptor);
		InterceptorRegistration registration = new InterceptorRegistration(adapted);
		this.registrations.add(registration);
		return registration;
	}
}

自定义拦截器案例

1. 准备工作

添加一个用户对象,封装用户信息、权限值等。

@Data
public class User {
   
     
    String username;
    String password;
    Integer age;
    String name;
    Long userId;
    String permissionCode;
}

编写一个登陆接口,模拟登陆,将用户信息保存到Session 中。

    @GetMapping("/user/login")
    public Object login(HttpSession httpSession) {
   
     
        User user = new User();
        user.setName("吃个桃桃");
        user.setAge(15);
        user.setUserId(1111L);
        user.setPermissionCode("add:user");
        httpSession.setAttribute("user", user);
        return user;
    }

2. 编写拦截器

自定义拦截器,实现HandlerInterceptor 接口,模拟访问时,对用户是否登陆,是否具有权限访问进行校验。

public class MvcPermissionInterceptor implements HandlerInterceptor {
   
     

    /**
     * 前置处理
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
   
     
        // 可以通过handler获取执行方法的相关信息, 方法名,注解等。
        // 1. 放行路径,直接放行,或者在注册时使用excludePathPatterns排除
        String requestURI = request.getRequestURI();
        if ("test".equals(requestURI)) {
   
     
            System.out.println(requestURI + "放行!!!");
            return true;
        }
        Map<String, Object> result = new HashMap<>();
        // 2. 判断用户是有已登录
        User user = (User) request.getSession().getAttribute("user");
        if (user == null) {
   
     
            printMsg(result, response, HttpStatus.UNAUTHORIZED.value(), 401, "未登录!!!");
            return false;

        }
        // 3. 检查用户权限
        String permissionCode = user.getPermissionCode();
        if (!"add:user".equals(permissionCode)) {
   
     
            printMsg(result, response, HttpStatus.FORBIDDEN.value(), 403, "没权限");
            return false;
        }
        System.out.println("已登录,并拥有权限");
        return true;
    }

    /**
     * 响应错误信息
     */
    private void printMsg(Map<String, Object> result, HttpServletResponse response, int statusCode, int code, String msg) throws IOException {
   
     
        response.setStatus(statusCode);
        response.setContentType("application/json; charset=UTF-8");
        result.put("code", code);
        result.put("msg", msg);
        ObjectMapper mapper = new ObjectMapper();
        mapper.writeValue(response.getOutputStream(), result);
        response.setHeader("Cache-Control", "No-Cache");
        response.flushBuffer();
    }

    /**
     * 后置处理
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
   
     
        System.out.println("postHandle");
    }

    /**
     * 完成处理
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
   
     
        System.out.println("afterCompletion");
    }
}

3. 注册拦截器

在WebMvcConfigurer配置类中,重写addInterceptors方法,添加拦截器,并配置匹配路径。

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
   
     
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
   
     
        registry.addInterceptor(new MvcPermissionInterceptor()).addPathPatterns("/**").excludePathPatterns("/v1/user/login");
    }
}

4. 测试

直接访问接口,返回未登录信息。
*
使用登录接口登录,然后再访问资源接口,发现能正常访问,并打印了拦截器相关执行日志。
*

执行流程源码分析

首先看下大致流程,可以看到拦截器是在DispatcherServlet中进行执行的,所以将断点打在DispatcherServlet的doDispatch方法中。
*

1. 获取拦截器

doDispatch方法中会调用getHandler去获取当前请求的拦截器,所有的拦截器都在HandlerExecutionChain类中。
*

2. 调用前置方法

HandlerExecutionChain处理器执行链,接着会调用拦截器的前置方法,当返回false时,请求直接return了。

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

applyPreHandle会循环拦截器。

    boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
   
     
  	  // 循环拦截器
        for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
   
     
            HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
            // 执行每个拦截器
            if (!interceptor.preHandle(request, response, this.handler)) {
   
     
            // 拦截器返回false时,调用已执行完毕拦截器的afterCompletion方法
                this.triggerAfterCompletion(request, response, (Exception)null);
                return false;
            }
        }
        return true;
    }

3. 后置方法

所有拦截器执行完毕,且都返回ture后,处理器方法开始执行(controller方法)。

处理器方法执行完毕后,开始调用applyPostHandle执行控制器后置方法。

   mappedHandler.applyPostHandle(processedRequest, response, mv);

applyPostHandle很简单,就是循环拦截器,执行其postHandle方法。

    void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) throws Exception {
   
     
        for(int i = this.interceptorList.size() - 1; i >= 0; --i) {
   
     
            HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
            interceptor.postHandle(request, response, this.handler, mv);
        }

    }

执行完成方法

后置方法执行完毕后,DispatcherServlet进入到processDispatchResult方法,最终会调用HandlerExecutionChain的triggerAfterCompletion循环执行拦截器的完成方法。
*

到此,整个拦截器就执行完毕了。

多个拦截器执行顺序

再添加一个拦截器,然后发现其执行顺序好像有点乱。
*

1. 当拦截器都通过时

若每个拦截器的preHandle()都返回true时:

  • 此时多个拦截器的执行顺序和拦截器在SpringMVC的配置文件的配置顺序有关
  • preHandle()会按照配置的顺序执行;
  • postHandle()和afterComplation()会按照配置的反序执行

2. 当某个拦截器返回false时

若某个拦截器的preHandle()返回了false时:

  • preHandle()返回false和它之前的拦截器的preHandle()都会执行,postHandle()都不执行,返回false的拦截器之前的拦截器的afterComplation()会执行

版权声明:本文不是「本站」原创文章,版权归原作者所有 | 原文地址: