一、用户登录拦截器
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
方法来执行相应的业务逻辑。