引言
最近在前后端分离的项目中,后端人员需要将异常信息返回给前端,并且提供不同的响应码,根据不同的响应,前端再进行友好的提示。于是乎,就需要在项目中做异常的处理,由于处理的地方太多太多,所以采用@ControllerAdvice注解进行全局异常统一处理。工作之余,便进一步的对此注解进行了学习。
@ControllerAdvice介绍
@ControllerAdvice
是Spring框架提供的一个注解,它可以用来集中处理应用程序中的异常情况,并且在一个地方进行全局的数据绑定和预处理。通常结合@ExceptionHandler
、@InitBinder
和@ModelAttribute
注解来实现全局的异常处理、全局数据绑定和全局数据预处理,我们姑且可以把这三个注解称之为"全局三剑客"。
这里简单说下"全局三剑客"
-
@ExceptionHandler
:在@ControllerAdvice类中定义一些方法,使用@ExceptionHandler注解来捕获和处理应用程序中出现的异常。 -
@InitBinder
:在@ControllerAdvice类中定义方法,使用@InitBinder注解,可以进行全局的数据绑定操作。例如,将请求参数转换成指定类型的对象,比如格式化日期。 -
@ModelAttribute
:使用@ModelAttribute注解定义的方法,在每个请求处理之前都会被调用,可以为所有请求添加一些全局的模型数据。避免了在每个Controller中都进行重复设置。
通过查看@ControllerAdvice
注解的源码,不难发现,其实它就是就是@Component
的一个封装
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
@AliasFor("basePackages")
String[] value() default {};
@AliasFor("value")
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] assignableTypes() default {};
Class<? extends Annotation>[] annotations() default {};
}
被@ControllerAdvice
注解的类会被Spring扫描到,并实例化一个bean, 注册到Spring容器中。
全局异常处理@ExceptionHandler
异常测试:
@RestController
@RequestMapping("/api/v1/")
public class ExceptionTestController {
/**
* 测试异常
*
* @return
*/
@GetMapping("/error")
public ResponseEntity<String> testException() {
int temp = 1 / 0;
return new ResponseEntity<>("请求成功",HttpStatus.OK);
}
}
此时请求这个方法肯定会出错,且页面呈现下面的样子
控制台抛出错误
加入全局异常处理
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 处理算术异常,例如除数为0的情况。
*
* @param exception 算术异常对象
* @return ResponseEntity<String> 响应实体对象,包含异常信息和HTTP状态码
*/
@ExceptionHandler(ArithmeticException.class)
public ResponseEntity<String> bizException(ArithmeticException exception) {
log.error("异常信息:{}", exception.getMessage());
return new ResponseEntity<>("除数不能为0", HttpStatus.NOT_FOUND);
}
}
上述方法注解@ExceptionHandler
中指定了处理ArithmeticException类型的异常。
我们再看下该注解的源码
/**
* 用于标注处理异常的方法的注解。
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
/**
* 指定被注解方法处理的异常类型。如果为空,则默认为方法参数列表中列出的任何异常。
*/
Class<? extends Throwable>[] value() default {};
}
因为Throwable是所有异常的父类,所以,如果不写,则会处理任何异常,因此,我们可以写不同的方法来处理不同的异常信息。
自定义异常
我们再自定义一个异常看看
/**
* 自定义异常
*/
public class MyException extends RuntimeException{
}
/**
* 处理自定义异常
* @param exception
* @return
*/
@ExceptionHandler(MyException.class)
public ResponseEntity<String> myException(MyException exception) {
log.error("自定义异常信息:{}", exception.getMessage());
return new ResponseEntity<>("自定义异常信息", HttpStatus.BAD_REQUEST);
}
/**
* 测试自定义的异常
*
* @return
*/
@GetMapping("/myerror")
public ResponseEntity<String> testMyException() {
throw new MyException();
}
请求此方法就会进入自定义异常。
我们知道,通过在一个类上添加@ControllerAdvice
注解,就可以使这个类成为全局的异常处理器。那么如果我有多个类都加了这个注解,并且两个异常处理类中都有对同一异常的处理方法,到底进入哪一个呢?
我们先增加一个异常处理类,并且处理我们的ArithmeticException异常
@ControllerAdvice
@Slf4j
public class DivGlobalExceptionHandler {
/**
* 处理算术异常,例如除数为0的情况。
*
* @param exception 算术异常对象
* @return ResponseEntity<String> 响应实体对象,包含异常信息和HTTP状态码
*/
@ExceptionHandler(ArithmeticException.class)
public ResponseEntity<String> bizException(ArithmeticException exception) {
log.error("进入DivGlobalExceptionHandler异常处理器,异常信息:{}", exception.getMessage());
return new ResponseEntity<>("除数不能为0", HttpStatus.NOT_FOUND);
}
}
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 处理算术异常,例如除数为0的情况。
*
* @param exception 算术异常对象
* @return ResponseEntity<String> 响应实体对象,包含异常信息和HTTP状态码
*/
@ExceptionHandler(ArithmeticException.class)
public ResponseEntity<String> bizException(ArithmeticException exception) {
log.error("进入GlobalExceptionHandler异常处理器,异常信息:{}", exception.getMessage());
return new ResponseEntity<>("除数不能为0", HttpStatus.NOT_FOUND);
}
}
我们访问下测试方法,看看进入哪一个异常处理器,通过测试,我们发现它只会进入一个异常处理器
那么疑问就来了,另外一个没生效吗,我们访问下之前自定义异常的方法
不难发现其实是生效的,对于上面的ArithmeticException异常不进入GlobalExceptionHandler异常处理类,难道和加载顺序还有关系?我们通过@Order
改变下加载顺序,让GlobalExceptionHandler类先加载
@Order
注解可以用来控制这些组件的加载顺序,数值越小,优先级越高。
@ControllerAdvice
@Slf4j
@Order(1) // 先加载
public class GlobalExceptionHandler {
}
@ControllerAdvice
@Slf4j
@Order(2) // 后加载
public class DivGlobalExceptionHandler {
}
直接看结果
总结下:当有多个异常处理类时,如果某个异常在所有的类中都有,则会进入先加载的异常处理类中,若先加载的类中没有某个特定异常,而后加载的类中有,则会进入后加载的类。一般情况下,我们可以通过@Order注解来改变异常处理类加载的顺序,比如将特殊的异常处理类先加载。
全局数据绑定@InitBinder
通过 @InitBinder
注解标记的方法可以自定义数据绑定规则,将请求参数转换为控制器方法的参数类型。比如,将请求参数字符串转换为日期对象、对字符串进行特殊处理等等。
/**
* 初始化参数绑定器,用于自定义数据绑定规则。
*
* @param webDataBinder WebDataBinder对象,负责将HTTP请求参数转换为控制器方法的参数类型。
*/
@InitBinder
public void initParam(WebDataBinder webDataBinder) {
// 注册自定义的日期编辑器,将字符串形式的日期参数转换为Date对象
webDataBinder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false));
// 注册自定义的字符串编辑器,将字符串在绑定前添加后缀"qwe"
webDataBinder.registerCustomEditor(String.class, new PropertyEditorSupport() {
@Override
public void setAsText(String text) throws IllegalArgumentException {
setValue(text + "qwe");
}
});
}
// 请求地址:http://localhost:8000/api/v1/param?date=2024-5-9&str=123
@GetMapping("/param")
public Map<String, Object> testParam(Date date, String str) {
Map<String, Object> map = new HashMap<>();
map.put("string", str);
map.put("date", date);
return map;
}
访问地址http://localhost:8000/api/v1/param?date=2024-5-9&str=123
已经达到了我们想要的效果。
全局数据预处理@ModelAttribute
可以将一些常用的数据在每个请求处理方法之前添加到模型Model中,可以避免在每个方法中重复添加相同的数据。
/**
* 使用 @ModelAttribute 注解定义一个方法,在每个请求处理方法之前生成一个随机的 UUID,并将其添加到模型中。
* 方法的返回值将会被添加到模型中,并且可以在控制器中的其他方法中使用。
*
* @return
*/
@ModelAttribute("uuid")
public String generateRandomUUID() {
return UUID.randomUUID().toString();
}
/**
* 使用 @ModelAttribute 注解将之前生成的随机UUID注入到方法参数中。
*
* @param str 从模型中注入的随机UUID字符串
* @return
*/
@GetMapping("/modelParam")
public Map<String, Object> testModelAttrParam(@ModelAttribute("uuid") String str) {
Map<String, Object> map = new HashMap<>();
map.put("uuid", str);
return map;
}
请求http://localhost:8000/api/v1/modelParam 结果如下:
这里就会自动生成一个uuid字符串
再举个例子:
@ModelAttribute // 注意这里没有name
public void addGlobalAttributes(Model model) {
// 添加一个版本号
model.addAttribute("version", "1.0.0");
}
@GetMapping("/modelParam")
public Map<String, Object> testModelAttrParam(@ModelAttribute("uuid") String str, Model model) {
Map<String, Object> map = new HashMap<>();
map.put("uuid", str);
Map<String, Object> modelMap = model.asMap();
map.put("version", (String) modelMap.get("version"));// 从model里面获取注入的version
return map;
}
访问下就可以看到已经取到了注入的version字段
其实无论哪种方式都是从Model
中存储属性的Map
里取数据,通过key值就可以获取相应的属性值,非常的方便。