spring boot 拦截器的实现需要有两步:
拦截器
自定义一个拦截器
package com.example.demo.common;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.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("session_userinfo") != null) {
// 说明用户已经登陆
return true;
}
// 用户没登陆就设置一个 401 状态码,不然就是一个空白页面
response.setStatus(401);
return false;
}
}
将拦截器设置到配置项中,并设定拦截规则
package com.example.demo.config;
import com.example.demo.common.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
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 LoginConfig implements WebMvcConfigurer { // 使用这个接口中的方法把自定义拦截器添加到项目配置中
@Autowired
private LoginInterceptor loginInterceptor;
// 添加拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor) // registry 是一个注册器,往里面添加自定义的拦截器
.addPathPatterns("/**") // 所有的 url 都会进行拦截判断
.excludePathPatterns("/user/login") // 设置不进行拦截的 url
.excludePathPatterns("/user/reg");
}
}
运行结果:
拦截器实现原理
实现源码
所有的 Controller 执⾏都会通过⼀个调度器 DispatcherServlet 来实现,而所有⽅法都会执行 DispatcherServlet 中的 doDispatch 调度方法。从源码可以看出在开始执行 Controller 之前,会先调用 预处理方法 applyPreHandle,
在 applyPreHandle 中会获取所有的拦截器 HandlerInterceptor 并执⾏拦截器中的 preHandle ⽅法,这样就会前⾯定义的拦截器对应上了,此时⽤户登录权限的验证⽅法就会执⾏,这就是拦截器的实现原理。
统一异常处理
在写代码的时候经常会遇到各种错误,但每个地方都写一个异常处理就不太合适,或者直接不管就让程序报错也不合适,相对较好的办法就是:先保证程序不会直接报错,然后再返回一些信息给前端。因此就需要专门创建个处理异常的类。
统⼀异常处理使⽤的是 @ControllerAdvice + @ExceptionHandler 来实现的,@ControllerAdvice 表示控制器通知类,@ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执⾏某个通知,也就是执⾏某个⽅法事件,具体实现代码如下:
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 ResponseAdvice { // 继承这个接口表示对数据进行加工(而不是返回一个数据)
@ExceptionHandler(NullPointerException.class) // 定义异常的类型,空指针异常
public Object NullPointerException(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 Object Exception(Exception e) {
HashMap<String, Object> result = new HashMap<>();
result.put("code", -1);
result.put("msg ", "Exception:" + e.getMessage());
result.put("data", null);
return result;
}
}
运行结果:
统一数据返回格式
主要作用:降低前端和后端的沟通成本、有利于项⽬统⼀数据的维护和修改。
统⼀的数据返回格式可以使用 @ControllerAdvice + ResponseBodyAdvice 的⽅式实现,具体实现代码如下:
package com.example.demo.common;
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 MyResponseBodyAdvice implements ResponseBodyAdvice { // 继承这个接口表示对数据进行加工(而不是返回一个数据)
// 是否执行 beforeBodyWrite 方法,true = 执行,重写返回结果
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
// 执行数据的重写的方法,这个是保底的做法,保证不会抛异常,但是重写的返回结果都是千篇一律的
// 假设如果数据是 HashMap 格式的,那么就认为数据统一了是正确的,否则就重写成 HashMap 的格式
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 如果原始数据 body 是 HashMap 格式的话
if (body instanceof HashMap) {
return body;
}
// 不是的话就把原始数据重写成 HashMap 格式
HashMap<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg ", "修改了数据");
result.put("data", body); // body 是原始的返回值
return result;
}
}
运行结果:
但是当返回值为 String 并且需要重写数据的时候:
这是因为:1. 当返回的是 String 类型的时候,2. 在统一数据返回之前会先将 String 转换成 HashMap,3. 然后再将 HashMap 转换成 application / json 字符串传给前端。
出错的原因发生在第 3 步:转换的时候会对原 body 的类型进行判断:1. String 类型,会使用 StringHttpMessageConverter 进行类型转换、2. 非 String 类型,会使用 HttpMessageConverter 进行类型转换。
因此解决方案就有两种解决方案:
1. 将 StringHttpMessageConverter 从配置文件中去掉
@Configuration
public class MyConfig implements WebMvcConfigurer {
// 移除 StringHttpMessageConverter
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.removeIf(converter -> converter instanceof StringHttpMessageConverter);
}
}
2. 在统一数据重写时,单独处理,返回一个 String 字符串而不是 HashMap
// 不是的话就把原始数据重写成 HashMap 格式
HashMap<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg ", "修改了数据");
result.put("data", body); // body 是原始的返回值
if (body instanceof String) {
// 把对象转成 json 再进行返回
return objectMapper.writeValueAsString(result);
}
return result;
运行结果: