Spring Boot 统一事务处理(拦截器)
1. 什么是拦截器?
1.1 含义
在Spring Boot
中,拦截器是一种用于拦截和处理HTTP请求的机制。它是Spring框架提供的一种中间件,用于在请求到达控制器(Controller)之前或之后执行一些共享的逻辑。
Spring Boot
的拦截器基于Spring MVC
框架中的HandlerInterceptor
接口实现。通过创建一个自定义的拦截器类并实现HandlerInterceptor
接口,可以定义拦截器要执行的逻辑和行为。
1.2 作用
身份验证和权限控制: 拦截器可以用于检查用户的身份验证状态和权限,并根据需要进行相关处理。例如,可以使用拦截器验证用户的登录状态,如果未登录则重定向到登录页面或返回相应的错误信息。
异常处理和统一错误处理: 拦截器可以捕获并处理请求处理过程中发生的异常。可以根据异常类型进行适当的处理,如返回自定义错误页面或错误信息,或执行特定的错误处理逻辑。
当然它还有其它的应用场景,这里就不一一列举了。
2. ⽤户登录权限效验
2.1 自定义拦截器
@Component
public class LoginInterceptor implements HandlerInterceptor {
//调用目标方法之前执行的方法
//如果返回ture表示拦截器验证成功,执行目标方法
//如果返回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;
}
response.setStatus(401);
return false;
}
}
代码中的preHandle
方法是拦截器的主要方法,在目标方法调用之前执行。它接收三个参数:HttpServletRequest
对象表示当前的HTTP
请求,HttpServletResponse
对象表示当前的HTTP
响应,Object handler
表示被拦截的处理器(一般是Controller中的方法)。
在preHandle
方法中,首先通过request.getSession(false)
获取当前请求的HttpSession
对象(如果存在的话),然后判断该HttpSession
对象是否为null
并且是否存在名为"session_userinfo"
的属性。如果这个条件成立,说明用户已经登录,可以继续执行后续的业务,于是返回true,否则验证失败,将HTTP响应的状态码设置为401,表示未授权,然后返回false,不再继续执行后续业务。
2.2 将自定义拦截器加入到系统配置
@Configuration
public class MyConfig implements WebMvcConfigurer {
//注入
@Autowired
private LoginInterceptor loginInterceptor;
//将拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**") //拦截所有的 url
.excludePathPatterns("/user/login")//排除url: /user/login (登录)
.excludePathPatterns("/user/reg") //排除url: /user/reg (注册)
.excludePathPatterns("/image/**")//排除 image(图像) 文件夹下的所有文件
.excludePathPatterns("/**/*.js")//排除任意深度目录下的所有".js"文件
.excludePathPatterns("/**/*.css");
}
}
在配置类中,重写了addInterceptors
方法,该方法用于注册拦截器。在这里,通过调用InterceptorRegistry
的addInterceptor
方法来添加拦截器,并设置拦截的路径和排除的路径。
具体地,通过调用addInterceptor(loginInterceptor)
来添加LoginInterceptor
拦截器。然后使用addPathPatterns
方法指定需要拦截的URL路径模式,这里使用"/**"
表示拦截所有的URL
。使用excludePathPatterns
方法来排除一些特定的URL
路径,这些路径不会被拦截。
对于"/**/*.js"
,"**"
:表示零个或多个路径段(目录或文件夹),可以匹配任意深度的目录结构。"/*.js"
:表示以".js"
结尾的文件名。
UserController:
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/login")
public String login(){
return "login";
}
@RequestMapping("/index")
public String index(){
return "index";
}
@RequestMapping("/reg")
public String reg(){
return "reg";
}
}
访问“login”
:
访问“index”
:
返回了401
访问“reg”
:
2.3 统⼀访问前缀添加
所有请求地址添加test
前缀:
在WebMvcConfigurer
接口中,configurePathMatch
方法用于配置路径匹配规则。
@Configuration
public class MyConfig implements WebMvcConfigurer {
//统一访问前缀的添加
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("test", new Predicate<Class<?>>() {
@Override
public boolean test(Class<?> aClass) {
return true;
}
});
}
}
在这个例子中,传递给addPathPrefix
方法的前缀是"test",而Predicate
对象是一个匿名内部类,实现了Predicate<Class<?>>
接口。Predicate
接口是Java 8中引入的函数式接口,它的test
方法用于判断传入的类是否符合条件。
在这个匿名内部类中,test
方法被重写为总是返回true
,这意味着所有的类都符合条件,都会被添加统一访问前缀。
因此,通过这段代码的配置,所有的请求路径都会在前面添加"test"前缀。例如,原始路径为"/example",添加了前缀后的路径就变为"/test/example"。这样可以实现对请求路径的统一处理。
注意:如果加了前缀,拦截器的排除路径也要跟着改动:
//将拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**") //拦截所有的 url
.excludePathPatterns("/**/user/login")//排除url: /user/login (登录)
.excludePathPatterns("/**/user/reg") //排除url: /user/reg (注册)
.excludePathPatterns("/**/image/**")//排除 image(图像) 文件夹下的所有文件
.excludePathPatterns("/**/*.js")//排除任意深度目录下的所有".js"文件
.excludePathPatterns("/**/*.css");
}
3. 统一异常处理
下面的代码在访问后会返回什么?
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/login")
public Integer login(){
Object object = null;
object.hashCode();
return 1;
}
}
答案是:
有没有一种手段可以在发生异常的时候返回有用的信息(此时的响应状态为200),而不是冰冷的报错信息呢?
那就是统⼀异常处理:
@ControllerAdvice
@ResponseBody
public class MyExceptionAdvice {
@ExceptionHandler(NullPointerException.class)
public HashMap<String,Object> doNullPointerException(NullPointerException e){
HashMap<String,Object> result = new HashMap<>();
result.put("code",-1);
result.put("msg","空指针:" + e.getMessage());
result.put("data",null);
return result;
}
}
@ControllerAdvice
注解标识该类是一个全局异常处理器,它将捕获应用程序中抛出的异常,并执行相应的处理逻辑。@ExceptionHandler(NullPointerException.class)
注解指定了处理NullPointerException
类型异常的方法doNullPointerException()
。doNullPointerException()
方法的参数是NullPointerException
类型的异常对象,表示捕获到的具体异常实例。doNullPointerException()
方法返回一个HashMap<String, Object>
对象,用于封装异常处理结果。
这段代码的作用是当捕获到NullPointerException
异常时,执行doNullPointerException()
方法,并返回一个包含异常处理结果的HashMap对象。该结果以JSON格式返回给客户端。
当有多个异常处理器的时候,它们的处理次序:
@ControllerAdvice
@ResponseBody
public class MyExceptionAdvice {
//处理 NullPointerException 异常
@ExceptionHandler(NullPointerException.class)
public HashMap<String,Object> doNullPointerException(NullPointerException e){
//处理
HashMap<String,Object> result = new HashMap<>();
result.put("code",-1);
result.put("msg","NullPointerException:" + e.getMessage());
result.put("data",null);
return result;
}
//处理 Exception 异常
@ExceptionHandler(Exception.class)
public HashMap<String,Object> doException(Exception e){
//处理
HashMap<String,Object> result = new HashMap<>();
result.put("code",-1);
result.put("msg","Exception:" + e.getMessage());
result.put("data",null);
return result;
}
}
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/login")
public Integer login(){
Object object = null;
object.hashCode();
return 1;
}
}
结论:如果能匹配就子类优先,如果没有匹配到就找父类。
4. 统一数据返回格式
4.1 为什么需要统一数据返回格式?
- 统一数据返回格式可以帮助前端程序员更好地接收和解析后端数据接口返回的数据,
- 规范性和可读性:统一数据返回格式可以定义一种统一的数据结构和字段,使得不同接口的返回数据具有一致性。这样可以提高代码的可读性和维护性,降低前后端开发人员之间的沟通成本。
- 统一数据维护和修改:通过统一数据返回格式,项目中的所有接口都遵循相同的数据格式,使得数据维护和修改变得更加方便和统一。如果需要修改数据返回的结构或字段,只需要在统一数据返回格式的定义处进行修改,而不需要逐个接口进行修改。
- 有利于制定后端技术规范标准:统一数据返回格式可以作为后端技术部门制定的一项规范标准。它可以避免出现各种奇怪的返回内容,提供统一的数据规范和结构,使得开发人员在后端开发过程中能够更好地遵循规范,提高代码质量和可维护性。
4.2 统一数据返回格式案例
假设这里我们以{"msg": *,"code": *,"data": *}
这种形式来作为标准返回格式。使用 @ControllerAdvice
注解结合 ResponseBodyAdvice
接口可以实现统一的数据返回格式。
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
/**
* @return 如果为 true,就执行 beforeBodyWrite
*/
@Override
public boolean supports(MethodParameter returnType, Class converterType){
return true;
}
/**
* 返回数据之前进行数据重写
* @param body 原始返回值
* @return
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType, Class selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
// 在返回数据之前进行处理,可以修改、包装响应体,添加额外信息等
// 这里可以对返回的数据进行统一的格式处理
if(body instanceof HashMap){
// 如果已经是统一响应格式,则直接返回
return body;
}
// 如果不是统一响应格式,则进行包装
HashMap<String,Object> result = new HashMap<>();
result.put("code",200);
result.put("data",body);
result.put("msg","");
return result;
}
}
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/login")
public HashMap<String,Object> login(){
HashMap<String,Object> hashMap = new HashMap<>();
hashMap.put("code",200);
hashMap.put("data",1);
hashMap.put("msg","");
return hashMap;
}
@RequestMapping("/reg")
public Integer reg(){
return 10;
}
}
4.3 返回值为 String 的情况
假如我要对下面的代码进行格式统一处理:
@RequestMapping("/hi")
public String hi(){
return "Hello World";
}
可以发现报错了,为什么没有成功呢?
返回执行流程:
- 方法返回的是 String
- 统一数据返回之前处理:将 String 装入 HashMap 中。
- 将 HashMap 转换为 application/json 字符串给前端。
问题就出在第三步,在进行转换 application/json
的时候,会先判断原 Body 的类型,如果是 String 类型,将会启用 StringHttpMessageConverter
进行类型转换,如果不是 String
类型,就用HttpMessageConverter
来进行转换。
在 Spring MVC 中
,HttpMessageConverter
负责处理请求和响应的消息体,将请求的数据转换为方法参数的类型,以及将方法返回值转换为响应的数据格式。StringHttpMessageConverter
特别用于处理字符串类型的数据。
StringHttpMessageConverter
只能把String
转换为其他类型,例如将字符串作为响应的内容返回给客户端。而代码中是把 HashMap
转换为 application/json
字符串。所以报错了。
解决方法:
- 直接对 String 做特殊处理:直接把 HashMap 转为
json
字符串再发送给前端。
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
.......
略
.......
//重写返回结果
HashMap<String,Object> result = new HashMap<>();
result.put("code",200);
result.put("data",body);
result.put("msg","");
if(body instanceof String){
//将 HashMap 转换为 json 字符串
return objectMapper.writeValueAsString(request);
}
.......
略
.......
}
- 删除
StringHttpMessageConverter
@Configuration
public class MyConfig implements WebMvcConfigurer {
/**
* 移除 StringHttpMessageConverter
*
* @param converters
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.removeIf(converter -> converter instanceof StringHttpMessageConverter);
}
}
转载自:https://juejin.cn/post/7235547967113248805