前言
我们SpringBoot统一功能处理模块,相当于是AOP的具体实现,我们一共实现三个方面:
1.统一用户登录权限验证
2.统一数据格式返回
3.统一异常处理
一、统一用户登录权限验证
我们之前是如何进行用户登录验证的?每个方法都有相同的用户登录权限验证,它有以下弊端:
1.每个方法都要单独写用户登录的方法
2.增大了后期的修改成本与维护成本
3.这些用户登录权限验证和主要业务几乎没有任何关系
使用公共的AOP方法来进行统一的用户登录权限验证是大势所趋
拦截器
我们Spring提供了具体的拦截器HandlerInterceptor,大致分为两步:
1.创建自定义1拦截器,实现HandlerInceptor接口,并重写preHandle方法
java">package com.example.demo.common;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@Component
public class LoginInterceptor implements HandlerInterceptor {
//调用目标方法之前需要执行的方法
//如果返回true表示验证成功,继续进行接下来的操作.如果返回false,表示验证失败,表示后续流程不在进行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//用户登录判断
HttpSession session = request.getSession(false);
if(session != null && session.getAttribute("username") != null) {
//登录成功
return true;
}
response.setStatus(401);
return false;
}
}
2.将自定义拦截器加入到WebMvcConfigurer的addInterInceptor方法中,并且设置拦截规则
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 MyConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor) //注册拦截器
.addPathPatterns("/**") //拦截所有url
.excludePathPatterns("/user/login") //登录与注册不拦截
.excludePathPatterns("/user/reg");
}
}
咱们定义个UserController,验证下我们自定义拦截器的功能:
package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Created with IntelliJ IDEA.
* Description:
* User: 86187
* Date: 2023-05-18
* Time: 8:55
*/
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/login")
public String login() {
//登录方法
return "login";
}
@RequestMapping("/reg")
public String reg() {
//注册方法
return "reg";
}
@RequestMapping("/hello")
public String hello() {
//其他方法
return "hello";
}
}
和我们访问hello时
多个拦截器执行preHandle的顺序与Spring MVC配置文件中的配置顺序有关
实现原理
我们正常的程序调用顺序:
当我们加入了拦截器后,我们会在调用Controller之前进行相应的业务处理:
我们所有的Controller执行都会通过一个调度器DispatcherServlet来实现,我们可以来看控制台
所有的方法都会执行DispatcherServlet中的doDispatch调度方法:
我们看看applyPreHandle方法:
我们可以发现,会遍历我们所有的拦截器,因为我们有进行addInterceptor操作,如果有一个拦截器返回false,就不会进行后续操作
只有所有的拦截器都返回了true,我们的程序才会进行后续操作
统一访问前缀添加
如果我们想要在所有的请求地址前面加一个地址,我们可以进行以下操作,我们以com为例
@Configuration
public class MyConfig implements WebMvcConfigurer {
// 所有的接口添加 com 前缀
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("com", c -> true);
}
}
我们addPathPrefix的第二个参数是一个表达式,设置为true表示启动前缀
二、统一异常处理
在Web应用程序开发中,异常处理是一个非常重要的部分。应用程序可能会出现各种各样的异常,如用户输入错误、服务器内部错误、网络连接异常等等。如果没有良好的异常处理机制,这些异常可能会导致应用程序崩溃或行为异常,给用户带来不良体验,甚至破坏应用程序的可靠性和安全性。
因此,需要统一异常处理机制来处理应用程序中的各种异常情况。通过统一异常处理机制,可以实现以下目标:
1.提高系统可靠性。无论是哪种异常情况,都必须进行处理,否则可能会导致应用程序的崩溃或行为异常。
2.提高用户体验。异常信息应该以友好的方式展示给用户,而不是以技术术语或错误码的形式呈现出来。
3.简化代码实现。通过集中处理所有异常情况,可以避免重复的代码片段和代码维护的问题。
4.方便排查问题。通过记录和跟踪异常信息,可以更方便地识别和解决可能存在的问题,并修复潜在的漏洞。
通过引入统一异常处理机制,可以将应用程序中的全部异常情况都经过拦截器进行拦截并统一处理。在Web应用程序开发中,通常使用AOP和异常过滤器来实现统一异常处理。在Spring框架中,我们可以通过@ControllerAdvice注解定义一个异常处理类,来实现全局异常处理。
我们在login功能中加上这样一行
我们可以发现当出现错误时,直接报错了,并没有给前端返回任何信息,这并不是我们想看到的,所以我们需要进行统一的异常处理。
我们进行统一异常处理需要两步:
1.建立统一异常处理类,并加入@ControllerAdvice注解
@ControllerAdvice注解的作用是定义一个全局异常处理类,用于处理在Controller层中抛出的各种异常,并对这些异常进行统一的处理。使用@ControllerAdvice注解可以将异常处理逻辑从Controller中解耦,提高代码复用性。
2.定义异常处理方法,使用@ExceptionHandler,可以根据不同类型异常进行处理
我们来进行一个空指针异常处理:
@ControllerAdvice
@RestController
public class MyExceptionAdvice {
@ExceptionHandler(NullPointerException.class)
public HashMap<String, Object> doNullPointerException(NullPointerException e) {
HashMap<String, Object> res = new HashMap<>();
res.put("code",-1);
res.put("msg","空指针: " + e.getMessage());
res.put("data",null);
return res;
}
}
定义一个Controller类:
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/login")
public int login() {
String str = null;
str.length();
return 1;
}
}
我们再来访问login,看是什么情况:
异常处理程序异常解析程序
我们发现并没有直接报错误了,而是将错误信息发给前端,这样符合了我们开发的预期。
我们来做两个测试:
1.我们去掉@ControllerAdvice注解
直接报500了。
2.如果不是空指针异常呢?
同样也会报500,因为我们统一异常处理只处理了空指针异常
这种情况,我们需要再写一个算术处理异常的处理类,我们可以直接使用所有异常类的父类Exception进行异常处理,这样所有的异常都能处理到了
@ExceptionHandler(Exception.class)
public HashMap<String, Object> doException(Exception e) {
HashMap<String, Object> res = new HashMap<>();
res.put("code",-1);
res.put("msg","Exception: " + e.getMessage());
res.put("data",null);
return res;
}
我们再来进行测试:
这样就可以处理了。我们这个方法就相当于是最后一道防线,如果异常匹配上了就执行对应的异常处理就可以,如果没有匹配上,就会执行该方法。
三、统一数据返回格式
为什么需要进行统一数据返回格式:
1.方便前端程序员更好的接收和解析后端数据接口返回的数据
2.降低前端程序员和后端程序员的沟通成本,按照某个格式进行
3.有利于项目统一数据的维护和修改
4.后端的统一规范的标准制定
统一数据返回(强制性统一数据返回,是在返回数据之前进行数据重写):
需要加@ControllerAdvice注解,并且实现ResponseBodyAdvice接口,需要重写两个方法:
supports决定是否执行beforeBodyWrite(数据重写),返回true表示重写,false表示不重写
我们这里假设标准的数据格式是HashMap:
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return false;
}
@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;
}
}
我们写一下Contoller,试着返回两组数组:
@RestController
@RequestMapping("/user")
public class UserController {
}
1.HashMap格式数据:
@RequestMapping("/login2")
public HashMap<String,Object> login2() {
HashMap<String,Object> result = new HashMap<>();
result.put("code",200);
result.put("data",1);
result.put("msg","");
return result;
}
2.返回其他格式数据:
@RequestMapping("/login1")
public int login1() {
return 1;
}
我们可以发现即使我们在Controller层返回的不是标准格式的数据,也会进行重写。
特殊情况,返回String类型:
@RequestMapping("/login3")
public String login3() {
return "hello";
}
我们可以发现进行了统一异常处理,我们这里的代码也没问题呀,为什么不是我们预想的进行数据封装的返回,data是hello呢?
这里报了类型转换异常,HashMap不能转换为String,为什么会出现这个问题呢,我们返回String会进行三个步骤:
1.方法返回String
2.统一数据格式返回的是 -> String 转为 HashMap
3.将HashMap转换为application/json字符串给前端
我们在进行转换时,就判断HashMap中原body的类型:
1.是String -> StringHttpMessageConverter 进行类型转换(这个转换 无法将HashMap转换为String,所以会报错)
2.非String -> HttpMessageConverter进行类型转换
解决方案:
1.将StringHttpMessageConverter去掉
@Configuration
public class MyConfig implements WebMvcConfigurer {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.removeIf(converter -> converter instanceof StringHttpMessageConverter);
}
}
我们发现也是没问题的
2.在统一数据重写时,单独处理String类型,让其返回一个String字符串,而不是HashMap
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@SneakyThrows
@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","");
if(body instanceof String) {
//将HashMap转换为String 返回一个String字符串
return objectMapper.writeValueAsString(result);
}
return result;
}
}
可以正常返回,没有任何问题