之前的博客中讲解了关于 Spring AOP的思想和原理,而实际开发中Spring Boot对于AOP的思想的具体实现就是Spring Boot Interceptor。在 Spring Boot 应用程序开发中,拦截器(Interceptor)是一个非常有用的工具。它允许我们在 HTTP 请求到达 Controller 之前或响应离开 Controller 之后执行一些自定义逻辑。本文将介绍 Spring Boot 中如何使用拦截器,并提供一些实际的使用示例。
我们还是以对用户是否登录进行校验这个场景来展开叙述。
1 不使用拦截器的用户登陆验证
比如我们的UserController中有多个方法,那么执行这些方法之前肯定要对用户是否登录进行校验。每个⽅法中都要单独写⽤户登录验证的⽅法,即使封装成公共⽅法,也⼀样要传参调⽤和在⽅法中进⾏判断。 随着添加的控制器越来越多,调用用户登陆验证的方法也就越来越多,这样就会增加后期的修高成本和维护成本,并且这些代码和实际要实现的业务是没有关系的,实际开发中是不希望一些无关代码侵入业务代码的。
上面的问题可不就是AOP所解决的问题吗!所以提供一个公共的AOP方法来进行统一的用户登录验证是必要的,Spring Boot也为我们提供了使用的方法。
原生的Spring Boot AOP也可以通过前置通知或者环绕通知来进行拦截,但是在配饰拦截规则的时候使用的aspectj时十分不友好的,并且再拦截的时候想要排除一些特定的方法,比如登录和注册,那个拦截规则是很难定义的,甚至根本没办法实现。
对于以上问题 Spring 中提供了具体的实现拦截器:HandlerInterceptor。
2 Spring 拦截器
在 Spring Boot 应用程序开发中,拦截器(Interceptor)是一个非常有用的工具。它允许我们在 HTTP 请求到达 Controller 之前或响应离开 Controller 之后执行一些自定义逻辑。拦截器的实现可以分为以下两个步骤:
- 创建自定义拦截器:实现HandlerInterceptor接口并重写接口的preHander方法(执行具体方法之前的预处理方法)。
- 注册定义拦截器:将⾃定义拦截器加⼊ WebMvcConfigurer 的 addInterceptors ⽅法中。(自定义配置类实现WebMvcConfigurer 并且重写addInterceptors方法,重写addInterceptors方法的目的就是将自定义的拦截器注册到项目中,在这个过程中可以配置拦截规则)
2.1 创建自定义拦截器
首先,我们需要创建一个实现 HandlerInterceptor
接口的类。HandlerInterceptor
接口提供了三个方法:
preHandle
:在请求处理之前调用postHandle
:在请求处理之后调用,但在视图渲染之前afterCompletion
:在整个请求完成之后调用,通常用于资源清理
现在的业务需求下我们只需要在重写preHandle方法即可:
java
/*自定义拦截器 实现HandlerInterceptor接口 */
@Component
public class LoginInterceptor implements HandlerInterceptor {
// 调用目标方法之前执行的方法
// 此方法返回 boolean 类型的值,
// 如果返回的是true 表示(拦截器)验证成功,继续走后续的流程,执行目标方法
// 如果返回的是 false 表示拦截器验证失败,后续的流程和目标方法就不执行。
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//用户登录判断业务
HttpSession session = request.getSession(false);
if(session != null && session.getAttribute("admin")!=null){
System.out.println("登录了");
// 用户已登录了
return true;
}
//用户没有登录
System.out.println("还没有登录");
response.sendRedirect("/user/login");//可以重定向到系统的登录路由
//response.setStatus(401);//向前端返回相应的状态码 401:没有权限
return false;
}
}
2.2 注册自定义拦截器
在自定义拦截器之后我们还需要将自定义的拦截器注册到系统的配置中。
addPathPatterns:表示需要拦截的 URL,"**"表示拦截任意⽅法(也就是所有⽅法)。
excludePathPatterns:表示需要排除的 URL。
UserController代码:
java
@RestController
@RequestMapping("user")
public class UserController {
@GetMapping("login")
public String login(HttpServletRequest req){
System.out.println("执行了login!");
// 登录成功,创建会话
HttpSession session = req.getSession(true);
session.setAttribute("admin","lisi");
return "lisi login";
}
@GetMapping("reg")
public String reg(){
return "reg";
}
@GetMapping("index")
public String index(HttpServletRequest req){
HttpSession session = req.getSession(false);
Object admin = session.getAttribute("admin");
System.out.println(admin);
return "index";
}
}
运行程序观察执行情况:
- 没有登陆的状态下访问index:此时请求成功被拦截器拦截下来
2.访问login:此时lisi成功的登录,并且设置了session
3.登陆后再次访问index:触发拦截器但是通过了后端的验证,所以后端会打印"登录了"和session的value也即是lisi,同时返回给前端"index"。
结果:
这样一个通过拦截器实现用户登陆验证的简单案例就完成了。
3 拦截器实现原理
在没有加入拦截器的时候,程序的调用顺序如图所示:
然⽽有了拦截器之后,会在调⽤ Controller 之前进⾏相应的业务处理,执⾏的流程如下图所示:
Spring 中的拦截器也是通过动态代理和环绕通知的思想实现的。
通过源码分析进一步理解拦截器的工作原理
所有的 Controller 执⾏都会通过⼀个调度器 DispatcherServlet 来实现,这⼀点可以从 Spring Boot 控制台的打印信息看出,如下图所示:
⽽所有⽅法都会执⾏ DispatcherServlet 中的 doDispatch 调度⽅法,doDispatch 源码如下 :
java
@SuppressWarnings("deprecation")
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 = 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;
}
}
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 ServletException("Handler dispatch failed: " + err, err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new ServletException("Handler processing failed: " + err, 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);
}
}
}
}
从上述源码可以看出在开始执⾏ Controller 之前,会先调⽤ 预处理⽅法 applyPreHandle,而applyPreHandle ⽅法的实现源码如下:
从上述源码可以看出,在 applyPreHandle 中会获取所有的拦截器 HandlerInterceptor 并执⾏拦截器中的 preHandle ⽅法,这样就会咱们前⾯定义的拦截器对应上了:
此时⽤户登录权限的验证⽅法就会执⾏,这就是拦截器的实现原理。