目录
2. 统一异常的处理 (@ControllerAdvice 和 @ExceptionHandler)
前言
1. Spring 拦截器
对于上述问题, Spring 提供的拦截器就可以很好地解决.
一个项目里面实现统一用户验证登录的处理, 一般有三种解决方案:
- 使用传统的 AOP,
- 使用拦截器,
- 使用过滤器.
既然有三种解决方案, 为什么要选择使用拦截器呢 ?
- 对于传统的 AOP, 功能比较简单, 写法过于复杂, 所以不使用.
- 对于过滤器 (web容器提供的), 因为它的执行时机太靠前了, Spring 框架还没初始化, 也就是说触发过滤器的时候, request, response 对象还没有实例化. 所以过滤器用的也比较少.
🍁实现拦截器的两大步骤
-
创建自定义拦截器, 实现 HandlerInterceptor 接口并重写preHandle (执行方法前的预处理) 方法.
-
将自定义拦截器加入 WebMvcConfigurer 的 addInterceptors 方法中. 【配置拦截规则】
1.1 自定义拦截器
/**
* 拦截器
*/
@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;
}
//登录失败,页面返回一个错误状态码
response.setStatus(401);
return false;
}
}
自定义的拦截器是一个普通的类, 如果返回 true, 才会继续执行后续代码.
1.2 将自定义拦截器加入到系统配置中
前面写的自定义拦截器, 只是一个普通的类, 需要把它加入到系统配置中, 并配置拦截规则, 才是一个真正有用的拦截器.
@Configuration
public class myConfig implements WebMvcConfigurer {
@Autowired //属性注入(注入loginInterceptor对象)
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**") //拦截所有url
.excludePathPatterns("/user/login") //排除url /user/login 登录不拦截
.excludePathPatterns("/user/reg") //注册不拦截
.excludePathPatterns("/image/**") //排除image 文件夹下的所有文件
;
}
}
咱们定义个UserController,用于验证我们自定义拦截器的功能:
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/login")
public String login() {
return "登录成功";
}
@RequestMapping("/reg")
public String reg() {
return "注册成功";
}
@RequestMapping("/index")
public String index() {
return "其他方法";
}
}
前两个步骤我们已经做好了准备工作, 并配置好了拦截规则, 规定除了登录和注册功能不拦截外, 拦截其他所有 URL (getInfo). 下面来进行验证一下拦截器是否生效.
1.3 拦截器实现原理
有了拦截器之后,会在调用Controller之前进行相应的业务处理,流程如下:
- 首先我们要知道 Controller 的执行都会通过一个调度器 (DispatcherServlet) 来实现.
- 随便访问 controller 中的一个方法就能在控制台的打印信息就能看到, 这个可以类比到线程的调度上.
然后所有 Controller 中方法都会执行 DispatcherServlet 中的调度方法 doDispatch().
进入 applyPreHandle() 方法继续分析:
通过前面的分析, 我们就能发现 Spring 中的拦截器其实就是封装了传统的 AOP , 它也是通过 动态代理的和环绕通知的思想来实现的
统一访问前缀添加 (扩展)
如果我们想要在所有的请求地址前面加一个地址,我们可以进行以下操作,我们以加前缀api为例
@Configuration
public class MyConfig implements WebMvcConfigurer {
// 所有的接口添加 api 前缀
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("api", c -> true);
}
}
我们addPathPrefix的第二个参数是一个表达式,设置为true表示启动前缀
在浏览器输入url,测试结果如下:
2. 统一异常的处理 (@ControllerAdvice 和 @ExceptionHandler)
为什么要统一异常的处理呢 ??
实现统一异常的处理是需要两个注解来实现的:
- @ControllerAdvice : 定义一个全局异常处理类,用于处理在Controller层中抛出的各种异常,并对这些异常进行统一的处理。使用@ControllerAdvice注解可以将异常处理逻辑从Controller中解耦,提高代码复用性。
- @ExceptionHandler : 定义异常处理方法,使用@ExceptionHandler,可以根据不同类型异常进行处理
二者结合表示, 当出现异常的时候执行某个通知 (执行某个方法事件)
1.建立统一异常处理类,并加入@ControllerAdvice注解
@ControllerAdvice //表示这个类将被用于全局的异常处理
@ResponseBody //表示该类返回的是json数据
public class MyExceptionAdvice {
}
2.定义异常处理方法,使用@ExceptionHandler,可以根据不同类型异常进行处理
我们来进行一个空指针异常处理:
@ControllerAdvice //表示这个类将被用于全局的异常处理
@ResponseBody //表示该类返回的是json数据
public class MyExceptionAdvice {
//这个注解表示当应用程序中发生NullPointerException时,会调用此方法进行处理
@ExceptionHandler(NullPointerException.class)
//这个方法返回一个HashMap,其中包含了异常处理的结果。结果以键值对的形式存储,键是字符串,值是Object对象。
public HashMap<String,Object> doNullPointerException(NullPointerException e) {
HashMap<String ,Object> result = new HashMap<>();
result.put("code",-1); //"code":异常的状态码,这里是-1,表示发生了错误
//"msg":异常的信息,这里添加了"空指针"的前缀,并附加上异常的具体信息(e.getMessage()获取异常的信息)。
result.put("msg","空指针" + e.getMessage());
//data":这里设置为null,表示没有返回的数据。
result.put("data", null);
return result;
}
}
定义一个UserController 类:
@RestController //它结合了@Controller和@ResponseBody两个注解的功能。
//这个类级别的注解指定了这个类处理的所有请求的URL路径的公共部分
@RequestMapping("/user")
public class UserController {
@RequestMapping("/login")
public int login() {
Object obj = null;
//因为obj是null,所以当调用hashCode方法时,会抛出NullPointerException。
System.out.println(obj.hashCode());
return 1;
}
}
我们再来访问login,看是什么情况:
为了看出区别,我们来做两个测试:
1.首先我们去掉@ControllerAdvice注解
- 我们发现直接报了500的错误信息了
2.如果不是空指针异常,而是算术异常呢
- 我们发现同样还是报了500的错误信息,因为我们统一异常处理只处理了空指针异常
- 这种情况,我们需要再写一个算术处理异常的处理类,我们可以直接使用所有异常类的父类Exception进行异常处理,这样所有的异常都能处理到了
@ExceptionHandler(Exception.class)
public HashMap<String,Object> doException(Exception e) {
HashMap<String ,Object> result = new HashMap<>();
result.put("code",-1); //"code":异常的状态码,这里是-1,表示发生了错误
//"msg":异常的信息,这里添加了"空指针"的前缀,并附加上异常的具体信息(e.getMessage()获取异常的信息)。
result.put("msg","Exception" + e.getMessage());
//data":这里设置为null,表示没有返回的数据。
result.put("data", null);
return result;
}
- 我们再来进行测试:
3.统一数据返回格式
为什么需要进行统一数据返回格式:
- 方便前端程序员更好的接收和解析后端数据接口返回的数据
- 降低前端程序员和后端程序员的沟通成本,按照某个格式进行
- 有利于项目统一数据的维护和修改
- 后端的统一规范的标准制定
实现统一数据返回格式的功能
实现步骤可以分为以下两步:
- 自定义统一数据返回处理类,标注上@ControllerAdvice注解同时实现ResponseBodyAdvice接口。
- 重写接口中的 supports方法和beforeBodyWrite方法 并在该方法中进行统一数据格式的处理。
实现代码如下:
- supports决定是否执行beforeBodyWrite(数据重写),返回true表示重写,false表示不重写
- 我们这里假设标准的数据格式是HashMap
//定义的全局响应体建议(Response Body Advice)。
//其作用是在返回数据到客户端之前,对返回的数据进行处理或修改
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
//是否执行 beforeBodyWrite 方法,true = 执行, 重写返回结果
@Override
//supports方法,用于决定是否对返回的数据进行处理
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) {
//Hash<String, Object> -> code,msg,data
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("/reg")
public HashMap<String, Object> reg() {
HashMap<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg", "");
result.put("data", 1);
return result;
}
2.返回其他格式数据:
@RequestMapping("/login1")
public int login1() {
return 1;
}
我们可以发现即使我们在Controller层返回的不是标准格式的数据,也会进行重写。
特殊情况,返回String类型:
@RequestMapping("/sayHi")
public String sayHi() {
return "say hi";
}
这里报了类型转换异常,HashMap不能转换为String,为什么会出现这个问题呢,我们返回String会进行三个步骤:
- 方法返回String
- 统一数据格式返回的是 -> String 转为 HashMap
- 将HashMap转换为application/json字符串给前端
那么问题到底出现在哪一步呢?答案是我们在进行类型转换时出错了
- 对于String 类型的数据 -> 会使用 StringHttpMessageConverter 这个转换器 进行类型转换(由于这个转换器 无法将HashMap转换为String,所以会报错)
- 对于非String 类型的数据 ->会使用 HttpMessageConverter 这个转换器 进行类型转换
解决方案:
1. 将 StringHttpMessageConverter 转换器去掉,那么在进行类型转换的时候就会使用 HttpMessageConverter 这个转换器了
@Configuration
public class MyConfig implements WebMvcConfigurer {
@Override
//configureMessageConverters 方法使用 removeIf 方法删除了所有 StringHttpMessageConverter 的实例。
// 这样,Spring MVC 就不会再使用 StringHttpMessageConverter 来处理 String 类型的数据了。
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.removeIf(converter -> converter instanceof StringHttpMessageConverter);
}
}
2.在统一数据重写时,单独处理String类型,直接让其返回一个json格式的字符串,而不是HashMap
//返回数据之前进行数据重写
@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) {
//将String类型的字符串转换成json格式的字符串返回
return objectMapper.writeValueAsString(result);
}
return result;
}
}