拦截器 HandlerInterceptor
的实现分为以下两个步骤:
HandlerInterceptor
接口的 preHandle
(执行具体方法之前的预处理)方法。WebMvcConfigurer
的 addInterceptors
方法中。① 创建自定义拦截器,实现用户登录的权限效验,自定义拦截器是一个普通类,具体实现代码如下:
@Component
public class LoginInterceptor implements HandlerInterceptor {
// 返回 true 表示验证通过,可以执行后面的方法;
// 返回 false 表示验证失败,后面的代码就不能执行了
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
// 已登录
return true;
}
// 执行到此表示验证未通过,自动跳转到登录页面
response.sendRedirect("/login.html");
return false;
}
}
② 将上一步中的自定义拦截器加入到系统配置信息中,具体实现代码如下:
public class AppConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
/**
* 拦截url
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**") // 拦截所有的 url
.excludePathPatterns("/user/reg") // 不拦截注册接口 /user/reg
.excludePathPatterns("/user/login") // 拦截登录接口 /user/login
.excludePathPatterns("/**/*.html"); // 不拦截所有的 html 页面
}
}
说明:
addPathPatterns
:表示需要拦截的 URL,**
表示拦截任意方法(也就是所有方法)。excludePathPatterns
:表示需要排除的 URL。创建 UserController
类, 实现注册登录方法:
@RestController
@RequestMapping("/user")
public class UerController {
/**
* 用户注册功能
* @param userInfo 用户
* @return 是否注册成功
*/
@RequestMapping("/reg")
public boolean reg(UserInfo userInfo) {
return true;
}
@RequestMapping("/login")
public boolean login(String username, String password, HttpServletRequest request) {
if (StringUtils.hasLength(username) && StringUtils.hasLength(password)) {
if (username.equals("admin") && password.equals("admin")) {
HttpSession session = request.getSession(true);
session.setAttribute("userinfo", new UserInfo(1, "张三", "123"));
return true;
}
}
return false;
}
@RequestMapping("/upload")
public UserInfo upload() {
UserInfo userInfo = new UserInfo();
userInfo.setId(1);
userInfo.setUsername("张三");
userInfo.setPassword("123");
return userInfo;
}
}
添加登录页面 login.html :
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录页面title>
head>
<body>
<h1>登录页面h1>
body>
html>
访问 user/upload :
url 被拦截了,跳转到了登录页面。
输入正确的用户名和密码进行登录:
再次访问 user/upload :
成功访问。 当登录成功写入 session 后, 拦截的页面就可以正常访问了。
正常情况下的调用顺序:
然而有了拦截器之后,会在调用 Controller
之前进行相应的业务处理,执行的流程如下图所示:
实现原理源码分析:
所有的 Controller
执行都会通过一个调度器 DispatcherServlet 来实现,这一点可以从 Spring Boot 控制台的打印信息看出,如下图所示:
而所有方法都会执行 DispatcherServlet
中的 doDispatch
调度方法,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 {
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
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;
}
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
this.applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new NestedServletException("Handler dispatch failed", var21);
}
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
} catch (Exception var22) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
} catch (Throwable var23) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
}
} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
} else if (multipartRequestParsed) {
this.cleanupMultipart(processedRequest);
}
}
}
从上述源码可以看出在开始执行 Controller
之前,会先调用预处理方法 applyPreHandle
,而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)) {
this.triggerAfterCompletion(request, response, (Exception)null);
return false;
}
}
return true;
}
从上述源码可以看出,在 applyPreHandle
中会获取所有的拦截器 HandlerInterceptor
并执行拦截器中的 preHandle
方法,这样就会咱们前面定义的拦截器对应上了,如下图所示:
统一异常处理使用的是 @ControllerAdvice
+ @ExceptionHandler
来实现的,@ControllerAdvice
表示控制器通知类,@ExceptionHandler
是异常处理器,两个结合表示当出现异常的时候执行某个通知,也就是执行某个方法事件,具体实现代码如下:
@ControllerAdvice
@ResponseBody
public class MyExceptionResult {
/**
* 算数异常处理类
* @param e 异常
* @return 处理
*/
@ExceptionHandler(ArithmeticException.class)
public HashMap<String, Object> myArithmeticException(ArithmeticException e) {
HashMap<String, Object> result = new HashMap<>();
result.put("state", -1);
result.put("msg", "算数异常" + e.getMessage());
result.put("data", null);
return result;
}
/**
* 空指针异常
* @param e 异常
* @return 处理
*/
@ExceptionHandler(NullPointerException.class)
public HashMap<String, Object> myNullPointerException(NullPointerException e) {
HashMap<String, Object> result = new HashMap<>();
result.put("state", -1);
result.put("msg", "空指针异常" + e.getMessage());
result.put("data", null);
return result;
}
/**
* 默认异常
* @param e 异常
* @return 处理
*/
@ExceptionHandler(Exception.class)
public HashMap<String, Object> myException(Exception e) {
HashMap<String, Object> result = new HashMap<>();
result.put("state", -1);
result.put("msg", "默认异常" + e.getMessage());
result.put("data", null);
return result;
}
}
注意
- 方法名和返回值可以⾃定义,其中最重要的是要加上
@ExceptionHandler(Exception.class)
注解。- 如果出现了异常就返回给前端一个
HashMap
的对象,其中包含的字段如代码中定义的。- 当有多个异常通知时,匹配顺序为当前类及其子类向上依次匹配。
统⼀的数据返回格式可以使用 @ControllerAdvice
+ ResponseBodyAdvice
的方式实现,具体实现代码如下:
@ControllerAdvice
public class MyResponseBodyAdvice implements ResponseBodyAdvice {
// 是否进行重写的方法改为 true,true 表示在返回数据之前,进行统一的格式封装
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
/**
* 方法返回之前调用此方法
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
HashMap<String, Object> result = new HashMap<>();
result.put("state", 1);
result.put("data", body);
result.put("msg", "");
return result;
}
}
此时再访问 user/upload :
最后返回给前端的数据就是经过统一处理过的。