
java的异常体系
ThrowableErrorExceptionimplimpl
Throwable
作为最顶层的类,下面分为Exception
(异常)和Error
(错误)
- Error程序中无法处理的错误,表示运行应用程序中出现了严重的错误,一半由
jvm
引起,常见的有NoClassDefFoundError
、OutOfMemoryError
- Exception程序运行过程中产生的异常,又分为可查异常(checked exception) 和 不可查异常(unchecked exception)
- 可查异常(checked exception)
try-catch
捕获或者throws
语句抛出否则编译不通过,常见的有ClassNotFoundException
、NoSuchMethodException
等。- 不可查异常(unchecked exception)
RuntimeException
以及其子类,常见的有NullPointerException
、IllegalArgumentException
等。
全局异常处理
Spring Mvc
使用@ExceptionHandler
注解来处理由控制层抛出的异常
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
Class<? extends Throwable>[] value() default {};
}
复制代码
@ExceptionHandler
只有一个方法value()可以填写的值为继承自Throwable
的Class
数组,也就是说一个ExceptionHandler
可以处理多个异常或错误
首先我们新建一个自定义异常方便后面的演示,接收一个message
参数
public class CustomException extends RuntimeException{
public CustomException(String message) {
super(message);
}
}
复制代码
@RestController
和@Controller
在控制层使用如下@RestController @Slf4j public class DomainController { @ExceptionHandler(CustomException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ResponseEntity<Object> domainExceptionHandler(CustomException e){ log.error("exception from DomainController", e); return ResponseEntity.status(500).body(e.getMessage()); } @GetMapping("/domain") public void test(int code){ if(code == 1){ throw new CustomException("自定义异常"); } } } 复制代码
请求接口传入参数code = 1,可以看到控制台打印了exception from DomainController 复制代码
说明异常被ExceptionHandler
处理了,该方式只能处理单个控制器的异常@ControllerAdvice
和@RestControllerAdvice
@ControllerAdvice
和@RestControllerAdvice
可以同时处理多个的控制器,通过下列的方式可以调整处理范围,因为在某些情况下我们并不想让异常控制器处理有些第三方框架的异常// 处理所有@RestController注解 @ControllerAdvice(annotations = RestController.class) public class ExampleAdvice1 {} // 处理包路径下的所有控制器 @ControllerAdvice(basePackages = "org.example.controllers") public class ExampleAdvice2 {} // 指定特定的类处理 @ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class}) public class ExampleAdvice3 {} 复制代码
注意:annotations和basePackages生效的前提是被处理的控制器已经被扫描成Spring Bean
在大部分情况下我们只需要配置全局的异常处理,也就是通过@RestControllerAdvice
处理所有的控制器,在单个控制器中配置的@ExceptionHandler
优先级会高于全局的,我们可以利用这点来处理有些特殊的异常或者某些定制化需求(当然最好少一些定制化需求,会导致项目后期维护困难)。
关于Error
我们现在定义一个@ExceptionHandler
处理所有的异常,如下
@ExceptionHandler({Exception.class})
public ResponseEntity<Object> handler(Exception e){
log.error("error happened ", e);
return ResponseEntity.status(500).body(e.getMessage());
}
复制代码
修改测试代码code = 2
时抛出Error
@GetMapping("/domain")
public void test(int code){
if(code == 1){
throw new CustomException("自定义异常");
}
if(code == 2){
throw new AssertionError("code is 2");
}
}
复制代码
前面我们已经介绍过了Error
和Exception
的区别,在这抛出Error
,我们的异常处理应该不会处理,因为我们只处理了所有的Exception
,并没有处理Error
,启动项目调用接口,控制台得到如下信息
error happened Caused by: java.lang.AssertionError: code is 2
复制代码
可以看到AssertionError被异常处理器处理了,这是因为在Spring 4.3
以后,DispatcherServlet
的doDispatch
方法会处理从处理程序抛出的错误,使它们可用于@ExceptionHandler方法和其他场景。
...
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
...
复制代码
统一结果返回
在实际项目中我们通常会定义统一的返回结构,常见如下
@Getter
@Builder
public class ResponseData<T> {
private long timestamp;
private int status;
private String message;
private T data;
}
复制代码
我们不想在每个Controller上写重复的包装代码,可以定义一个统一的返回处理,实现ResponseBodyAdvice
接口
@RestControllerAdvice
@Slf4j
public class ResponseHandler 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) {
return null;
}
}
复制代码
其中提供的两个方法
supports
此方法有两个参数returnType
:控制器返回值类型converterType
:http消息转换器类型,
true
时,beforeBodyWrite
才会执行,我们可以利用这个方法做一些配置,比如通过自定义注解@IgnoreBodyAdvice
让某些接口不使用统一返回结构。
@Override
public boolean supports(final @NotNull MethodParameter methodParameter,
final Class<? extends HttpMessageConverter<?>> aClass) {
Class<?> clz = methodParameter.getDeclaringClass();
if (method == null) {
return false;
}
// 检查注解是否存在
if (clz.isAnnotationPresent(IgnoreBodyAdvice.class)) {
return false;
} else
return !method.isAnnotationPresent(IgnoreBodyAdvice.class);
}
复制代码
beforeBodyWrite
此方法有6个参数,会在HttpMessageConverter
的write
方法之前调用body
:返回的消息returnType
: controller的返回值类型selectedContentType
:选定的消息类型,比如application/json
selectedConverterType
:http消息转换器类型,比如StringHttpMessageConverter
request
:当前请求response
:当前响应
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if(body == null){
return ResponseData.builder().message("ok").build();
} else if(body instanceof ResponseData){
return body;
} else {
return ResponseData.builder().data(body).build();
}
}
复制代码
这只是一个最简单的例子,在实际项目可能还会有很多判断条件,可以根据项目情况自行添加。
原创文章,作者:睿达君,如若转载,请注明出处:https://zrrd.net.cn/1973.html