目标
Spring Boot 统⼀功能处理模块,也是 AOP 的实战环节,要实现的目标有以下 3 个:
- 使用拦截器实现用户登录权限的统一验证;
- 统⼀数据格式返回;
- 统⼀异常处理。
用户登录权限效验
用户登录权限的发展从之前每个方法中自己验证用户登录权限,到现在统⼀的用户登录验证处理,它是⼀个逐渐完善和逐渐优化的过程。
每个方法中都有相同的用户登录验证权限,它的缺点是:
- 每个方法中都要单独写用户登录验证的方法,即使封装成公共方法,也⼀样要传参调用和在方法中进行判断。
- 添加控制器越多,调用用户登录验证的方法也越多,这样就增加了后期的修改成本和维护成本。
- 这些用户登录验证的方法和接下来要实现的业务几何没有任何关联,但每个方法中都要写⼀遍。 所以提供⼀个公共的 AOP 方法来进行统⼀的用户登录权限验证迫在眉睫。
一个项目里面实现统一用户验证登录的处理, 一般有三种解决方案:
使用传统的 AOP,
使用拦截器,
使用过滤器.
说到统⼀的用户登录验证,我们想到的第⼀个实现方案是 Spring AOP 前置通知或环绕通知来实现
如果要在以上 Spring AOP 的切面中实现用户登录权限效验的功能,有以下两个问题:
- 定义拦截的规则(表达式)非常难。(我们要对⼀部分方法进行拦截,而另⼀部分方法不拦截,如注册方法和登录方法是不拦截的,这样 的话排除方法的规则很难定义,甚至没办法定义)。
- 在切面类中拿到 HttpSession 比较难
使用过滤器.
对于过滤器 (web容器提供的), 因为它的执行时机太靠前了, Spring 框架还没初始化, 也就是说触发过滤器的时候, request, response 对象还没有实例化. 所以过滤器用的也比较少.
那如何解决呢?
Spring 拦截器
对于以上问题 Spring 中提供了具体的实现拦截器:HandlerInterceptor,拦截器的实现分为以下两个步骤:
- 创建自定义拦截器,实现 HandlerInterceptor 接口的 preHandle(执行具体方法之前的预处理方法。
- 将自定义拦截器加入WebMvcConfigurer 的 addInterceptors 方法中。
实现拦截器
关键步骤:
a.实现 HandlerInterceptor 接口
b.重写 preHeadler 方法,在方法中编写自己的业务代码package com.example.demo.config; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; public class LoginInterceptor implements HandlerInterceptor { /** * 此方法返回一个 boolean,如果为 true 表示验证成功,可以继续执行后续流程如果是 false 表示验证失败,后面的流程不能执行了 * @param request * @param response * @param handler * @return * @throws Exception */ @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; } // 可以调整到登录页面 or 返回一个 401/403 没有权限码 response.sendRedirect("/login.html"); // response.setStatus(403); return false; } }
将拦截器添加到配置文件中,并且设置拦截的规则
package com.example.demo.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class AppConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") // 拦截所有请求 .excludePathPatterns("/user/login") // 排除的url地址(不拦截的url地址) .excludePathPatterns("/user/reg"); } }
其中:
- addPathPatterns:表示需要拦截的 URL,**表示拦截任意方法(也就是所有方法)。
- excludePathPatterns:表示需要排除的 URL。
说明:以上拦截规则可以拦截此项目中的使用 URL,包括静态文件(图片文件、JS 和 CSS 等文件)。
排除所有的静态资源:
// 拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") // 拦截所有接⼝ .excludePathPatterns("/**/*.js") .excludePathPatterns("/**/*.css") .excludePathPatterns("/**/*.jpg") .excludePathPatterns("/login.html") .excludePathPatterns("/**/login"); // 排除接⼝ }
UserController:
package com.example.demo.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/user") public class UserController { /* @GetMapping public String getMethod(){ return "执行GET请求!"; } @PostMapping public String postMethod(){ return "执行POST请求!"; }*/ @RequestMapping("/getuser") public String getUser() { System.out.println("执行了 get User~"); return "get user"; } @RequestMapping("/login") public String login() { System.out.println("执行了 login~"); return "login~"; } @RequestMapping("/reg") public String reg() { System.out.println("执行了 reg~"); return "reg~"; } }
拦截器实现原理
首先我们要知道 Controller 的执行都会通过一个调度器 (DispatcherServlet) 来实现.
所有的 Controller 执行都会通过⼀个调度器 DispatcherServlet 来实现,而所有方法都会执行 DispatcherServlet 中的 doDispatch 调度方法
有了拦截器之后,会在调用 Controller 之前进行相应的业务处理
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 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 方法,这样就和咱们前面定义的拦截器对应上了
此时用户登录权限的验证方法就会执行,这就是拦截器的实现原理。
通过前面的分析, 我们就能发现 Spring 中的拦截器其实就是封装了传统的 AOP , 它也是通过 动态代理的和环绕通知的思想来实现的
统⼀异常处理
统一异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的,@ControllerAdvice 表示控制器通知类,@ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执行某个通知, 也就是执行某个方法事件,具体实现代码如下:
package com.example.demo.config;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
@ControllerAdvice
@ResponseBody
public class MyExHandler {
/**
* 拦截所有的空指针异常,进行统一的数据返回
*/
@ExceptionHandler(NullPointerException.class)
public HashMap<String, Object> nullException(NullPointerException e) {
HashMap<String, Object> result = new HashMap<>();
result.put("code", "-1");
result.put("msg", "空指针异常:" + e.getMessage()); // 错误码的描述信息
result.put("data", null);
return result;
}
@ExceptionHandler(Exception.class)//保底的默认异常
public HashMap<String, Object> exception(Exception e) {
HashMap<String, Object> result = new HashMap<>();
result.put("code", "-1");
result.put("msg", "异常:" + e.getMessage()); // 错误码的描述信息
result.put("data", null);
return result;
}
}
此时若发生空指针异常:走上面那个
发生除零异常走下面那个
统一数据返回格式
1.创建一个类,并添加 @ControllerAdvice
2.实现ResponseBodyAdvice接口,并重写supports和beforeBodywrite (统一对象就是此方法中实现的)
统⼀的数据返回格式可以使用 @ControllerAdvice + ResponseBodyAdvice 的方式实现。
package com.example.demo.config;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.util.HashMap;
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
/**
* 此方法返回 true 则执行下面 beforeBodyWrite 方法
* 反之则不执行
*/
@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("code", 200);
result.put("msg", "");
result.put("data", body);
// 需要特殊处理,因为 String 在转换的时候会报错
if (body instanceof String) {
try {
return objectMapper.writeValueAsString(result);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
return result;
}
}
如果没有对Sting进行特殊处理,就会有异常出现:
访问 login() 方法:正常
@RequestMapping("/login")
public boolean login(HttpServletRequest request,
String username, String password) {
访问 reg() 方法时 :
@RequestMapping("/reg")
public String reg() {
HashMap 在转换为 Json格式的 String时, 框架使用的是转换器.
返回 String 类型, 封装成 HashMap , 转换成 Json 格式的字符串时, 使用的是 org.springframework.http.converter.StringHttpMessageConverter 转换器, 而返回其他类型时, 使用的是 org.springframework.http.converter.json.MappingJackson2HttpMessageConverter 转换器.
StringHttpMessage 转换器的执行时机比较晚, 在进行类型转换的时候, 该转换器还没加载好, 所以就会报错, 而 MappingJackson2 转换器的执行时机比较早, 所以不会报错.
上面只是以一种简单的方式来向大家介绍如何去统一数据返回格式,实际中并不会这样去使用,
而是将统⼀的返回格式(包含:status、data、msg 字段)进行封装成一个类:
根据不同操作成功和操作失败的情况,重写了不同的方法:
package com.example.demo.common;
import lombok.Data;
import java.io.Serializable;
/**
* 统一数据格式返回
*/
@Data
public class AjaxResult implements Serializable {//支持序列化
// 状态码
private Integer code;
// 状态码描述信息
private String msg;
// 返回的数据
private Object data;
/**
* 操作成功返回的结果,需要 data
*/
public static AjaxResult success(Object data) {
AjaxResult result = new AjaxResult();
result.setCode(200);
result.setMsg("");
result.setData(data);
return result;
}
//重载success
public static AjaxResult success(int code, Object data) {
AjaxResult result = new AjaxResult();
result.setCode(code);
result.setMsg("");
result.setData(data);
return result;
}
public static AjaxResult success(int code, String msg, Object data) {
AjaxResult result = new AjaxResult();
result.setCode(code);
result.setMsg(msg);
result.setData(data);
return result;
}
/**
* 返回失败结果
*/
public static AjaxResult fail(int code, String msg) {
AjaxResult result = new AjaxResult();
result.setCode(code);
result.setMsg(msg);
result.setData(null);
return result;
}
public static AjaxResult fail(int code, String msg, Object data) {
AjaxResult result = new AjaxResult();
result.setCode(code);
result.setMsg(msg);
result.setData(data);
return result;
}
}
@RequestMapping("/reg")
public Object reg(String username, String password) {
// return AjaxResult.success("注册成功!", 1);
return AjaxResult.fail(-1, "数据库添加出错!");
}