SpringBoot 统一功能处理
一、用户登录拦截器
1、拦截器实现步骤
步骤1:自定义拦截器
// 自定义拦截器
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(
HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 业务逻辑
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute(AppVar.SESSION_KEY) != null) {
// 返回 true -> 拦截器验证成功,继续执行后续的方法
return true;
}
// 返回 false -> 拦截器验证失败,不会执行后续的目标方法
return false;
}
}
代码解析:
通过 @Component 注解将 LoginInterceptor 类标记为一个 Spring 组件,使其成为 Spring 容器中的一个可被管理的 Bean。
LoginInterceptor 类实现了 Spring 提供的拦截器接口 HandlerInterceptor,并覆盖了其中的 preHandle 方法。preHandle 方法在目标方法执行前被调用。
在 preHandle 方法中,首先通过 HttpServletRequest 获取当前请求的 HttpSession 对象。如果 HttpSession 不为 null,且其中存储的 AppVar.SESSION_KEY 属性不为 null,表示用户已登录。
如果验证成功,即用户已登录,返回 true,表示拦截器验证通过,可以继续执行后续的目标方法;如果验证失败,即用户未登录,返回 false,表示拦截器验证失败,不会执行后续的目标方法。
步骤2:将自定义拦截器配置到系统设置中,并设置拦截规则
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
// 在系统配置中添加拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/login.html")
.excludePathPatterns("/reg.html")
.excludePathPatterns("/css/**")
.excludePathPatterns("/editor.md/**")
.excludePathPatterns("/img/**")
.excludePathPatterns("/js/**");
}
}
代码解析:
通过 @Autowired 注解将 LoginInterceptor 注入到 AppConfig 类中,该拦截器在上面已经定义好了。
在 addInterceptors 方法中,通过 registry.addInterceptor(loginInterceptor) 将 LoginInterceptor 拦截器添加到拦截器链中。
使用 addPathPatterns 方法设置需要拦截的 URL,这里使用 “/**” 表示拦截所有的请求。
使用 excludePathPatterns 方法设置不需要拦截的 URL,这些路由在拦截器中会被忽略。这里排除了一些静态资源和特定路径,比如登录页、注册页、CSS 文件、图片文件和 JavaScript
文件。
2、拦截器实现原理
本质上 Spring 中的拦截器也是通过动态代理和环绕通知的 思想
来实现的。在拦截器中,可以通过实现 HandlerInterceptor
接口并重写 preHandle
、postHandle
和 afterCompletion
方法来实现环绕通知的功能。
通过阅读源码我们可以看到,在 Spring
中所有 Controller 的执行都会通过一个核心调度器 DispatcherServlet
来实现,所有的请求方法都会执行 DispatcherServlet 中的 doDispatch
调度方法,doDispatch 方法中有一系列的事件处理方法,而在开始执行 Controller
中的目标方法 之前,会先调用预处理方法 applyPreHandle
,在 applyPreHandle 方法中会获取所有拦截器 HandlerInterceptor
并执行拦截器中的 preHandle
方法。如果拦截器中有一个返回了 false 那么后续的流程就不会执行了。
二、统一异常处理
通过使用 @RestControllerAdvice(@ControllerAdvice+@ResponseBody) 注解和 @ExceptionHandler 注解结合使用,可以实现全局的或是针对特定异常的统一异常处理,并将处理结果以统一的数据格式返回给客户端。
@RestControllerAdvice
public class ExceptionAdvice {
// 仅限于空指针异常的异常处理
@ExceptionHandler(NullPointerException.class)
public ResultAjax doNullPointException(NullPointerException e) {
ResultAjax resultAjax = new ResultAjax();
resultAjax.setCode(-1);
resultAjax.setMsg("异常"+e.getMessage());
return resultAjax;
}
// 适用于所有异常的异常处理
@ExceptionHandler(Exception.class)
public ResultAjax doException(Exception e) {
ResultAjax resultAjax = new ResultAjax();
resultAjax.setCode(-1);
resultAjax.setMsg("异常"+e.getMessage());
return resultAjax;
}
}
代码解析:
@RestControllerAdvice 注解表示该类是一个全局控制器增强器,并且结合了 @ControllerAdvice 和 @ResponseBody 注解的功能。
@ExceptionHandler 注解标注异常处理的方法。搭配 @RestControllerAdvice 注解可以在发生异常时统一处理异常并返回数据。
doNullPointException 方法使用 @ExceptionHandler(NullPointerException.class) 注解来指定它处理的异常类型为
NullPointerException。当发生空指针异常时,该方法会被调用。在方法体内,创建一个 ResultAjax
对象并设置相应的错误信息,然后将其返回。doException 方法没有指定特定的异常类型,因此它将会处理所有类型的异常。当发生任何异常时,该方法会被调用。它的处理逻辑与 doNullPointException 方法类似,也是创建一个 ResultAjax 对象并设置错误信息,然后返回。
三、统一数据返回格式
统一数据的返回格式可以降低前端程序员和后端程序员的沟通成本,方便前端程序员更好的接收和解析后端数据接口返回的数据。
一般情况下,我们可以创建一个统一返回对象,提供一些成功和失败的返回接口,后续返回的数据直接调用接口即可返回约定的统一对象。具体实现如下:
// 定义统一返回对象
@Data
public class ResultAjax {
// 状态码
private int code;
// 状态码的描述信息
private String msg;
// 返回数据
private Object data;
// 返回成功对象
public static ResultAjax succ(Object data) {
ResultAjax resultAjax = new ResultAjax();
resultAjax.setCode(200);
resultAjax.setMsg("");
resultAjax.setData(data);
return resultAjax;
}
public static ResultAjax succ(String msg, Object data) {
ResultAjax resultAjax = new ResultAjax();
resultAjax.setCode(200);
resultAjax.setMsg(msg);
resultAjax.setData(data);
return resultAjax;
}
// 返回失败对象
public static ResultAjax fail(int code,String msg){
ResultAjax resultAjax = new ResultAjax();
resultAjax.setCode(code);
resultAjax.setMsg(msg);
resultAjax.setData(null);
return resultAjax;
}
public static ResultAjax fail(int code,String msg,Object data){
ResultAjax resultAjax = new ResultAjax();
resultAjax.setCode(code);
resultAjax.setMsg(msg);
resultAjax.setData(data);
return resultAjax;
}
}
之后业务中所有返回类型都设置为上述定义的统一返回对象:
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/hello")
public ResultAjax sayHello(){
return ResultAjax.succ("hello");
}
@RequestMapping("/hi")
public ResultAjax sayHi(){
return ResultAjax.succ("hi");
}
}
当然虽然做出了上面的约定,但也不能保证在之后的业务代码不会误用其他返回类型,这个时候就需要使用到统一返回值的保底策略了。可以在 @ControllerAdvice
注解的类中实现 ResponseBodyAdvice
接口,对所有控制器方法的返回值进行统一的处理。
// 执行统一返回数据的保底策略
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
// * true -> 才会调用 beforeBodyWrite 方法,
// * 反之则永远不会调用 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) {
// 对返回值进行判断
// 如果返回值和统一返回格式一致直接返回
if (body instanceof ResultAjax) {
return body;
}
// 对字符串返回格式进行单独判断处理
if (body instanceof String) {
ResultAjax resultAjax = ResultAjax.succ(body);
try {
return objectMapper.writeValueAsString(resultAjax);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
// 其他情况
return ResultAjax.succ(body);
}
}
代码解析:
- @ControllerAdvice 是一个注解,用于声明一个类为全局控制器增强器。在 @ControllerAdvice 注解的类中实现 ResponseBodyAdvice 接口,对所有控制器方法的返回值进行统一的处理
- 实现 supports 方法,判断当前返回类型是否需要进行响应体重写处理。由于这里返回 true,因此所有返回值都会被拦截并进行响应体重写处理。
- 实现 beforeBodyWrite 方法,对所有返回值进行统一的响应体处理。
四、@ControllerAdvice 实现原理(了解)
通过上面统一异常处理和统一数据返回格式的介绍,我们发现二者都使用到了 @ControllerAdvice 这个注解,下面我们简单介绍一下它的底层是怎么实现的:
@ControllerAdvice 它更像是一个全局的拦截器,可以对控制器的行为进行统一的处理和管理:
当我们点击 @ControllerAdvice
的源码,可以看到 @ControllerAdvice 同样派生于 @Component
组件,而所有组件初始化都会调用 InitializingBean
接口,其中 Spring MVC 中的实现的子类中有一个 afterPropertiesSet()
方法,表示所有的参数设置完成之后执行的方法,这个方法中又有一个 initControllerAdviceCache
方法,当程序执行到特定事件发生的时候,比如返回数据前或发生异常时,Spring会根据规则查找所有使用了@ControllerAdvice 注解的类,并调用其中对应的 Advice
方法来执行相应的业务逻辑。