在 SpringBoot 的开发中,为了进步程序运行的鲁棒性,咱们常常须要对各种程序异样进行解决,然而如果在每个出异样的中央进行独自解决的话,这会引入大量业务不相干的异样解决代码,减少了程序的耦合,同时将来想扭转异样的解决逻辑,也变得比拟艰难。这篇文章带大家理解一下如何优雅的进行全局异样解决。
为了实现全局拦挡,这里应用到了 Spring 中提供的两个注解,@RestControllerAdvice
和@ExceptionHandler
,联合应用能够拦挡程序中产生的异样,并且依据不同的异样类型别离解决。上面我会先介绍如何利用这两个注解,优雅的实现全局异样的解决,接着解释这背地的原理。
1. 如何实现全局拦挡?
1.1 自定义异样解决类
在上面的例子中,咱们继承了 ResponseEntityExceptionHandler
并应用 @RestControllerAdvice
注解了这个类,接着联合 @ExceptionHandler
针对不同的异样类型,来定义不同的异样解决办法。这里能够看到我解决的异样是自定义异样,后续我会开展介绍。
ResponseEntityExceptionHandler 中包装了各种 SpringMVC 在解决申请时可能抛出的异样的解决,处理结果都是封装成一个 ResponseEntity 对象。ResponseEntityExceptionHandler 是一个抽象类,通常咱们须要定义一个用来解决异样的应用
@RestControllerAdvice
注解标注的异样解决类来继承自 ResponseEntityExceptionHandler。ResponseEntityExceptionHandler 中为每个异样的解决都独自定义了一个办法,如果默认的解决不能满足你的需要,则能够重写对某个异样的解决。
@Log4j2
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
/**
* 定义要捕捉的异样 能够多个 @ExceptionHandler({}) *
* @param request request
* @param e exception
* @param response response
* @return 响应后果
*/
@ExceptionHandler(AuroraRuntimeException.class)
public GenericResponse customExceptionHandler(HttpServletRequest request, final Exception e, HttpServletResponse response) {AuroraRuntimeException exception = (AuroraRuntimeException) e;
if (exception.getCode() == ResponseCode.USER_INPUT_ERROR) {response.setStatus(HttpStatus.BAD_REQUEST.value());
} else if (exception.getCode() == ResponseCode.FORBIDDEN) {response.setStatus(HttpStatus.FORBIDDEN.value());
} else {response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
}
return new GenericResponse(exception.getCode(), null, exception.getMessage());
}
@ExceptionHandler(NotLoginException.class)
public GenericResponse tokenExceptionHandler(HttpServletRequest request, final Exception e, HttpServletResponse response) {log.error("token exception", e);
response.setStatus(HttpStatus.FORBIDDEN.value());
return new GenericResponse(ResponseCode.AUTHENTICATION_NEEDED);
}
}
1.2 定义异样码
这里定义了常见的几种异样码,次要用在抛出自定义异样时,对不同的情景进行辨别。
@Getter
public enum ResponseCode {SUCCESS(0, "Success"),
INTERNAL_ERROR(1, "服务器外部谬误"),
USER_INPUT_ERROR(2, "用户输出谬误"),
AUTHENTICATION_NEEDED(3, "Token 过期或有效"),
FORBIDDEN(4, "禁止拜访"),
TOO_FREQUENT_VISIT(5, "拜访太频繁,请劳动一会儿");
private final int code;
private final String message;
private final Response.Status status;
ResponseCode(int code, String message, Response.Status status) {
this.code = code;
this.message = message;
this.status = status;
}
ResponseCode(int code, String message) {this(code, message, Response.Status.INTERNAL_SERVER_ERROR);
}
}
1.3 自定义异样类
这里我定义了一个 AuroraRuntimeException
的异样,就是在下面的异样处理函数中,用到的异样。每个异样实例会有一个对应的异样码,也就是后面刚定义好的。
@Getter
public class AuroraRuntimeException extends RuntimeException {
private final ResponseCode code;
public AuroraRuntimeException() {super(String.format("%s", ResponseCode.INTERNAL_ERROR.getMessage()));
this.code = ResponseCode.INTERNAL_ERROR;
}
public AuroraRuntimeException(Throwable e) {super(e);
this.code = ResponseCode.INTERNAL_ERROR;
}
public AuroraRuntimeException(String msg) {this(ResponseCode.INTERNAL_ERROR, msg);
}
public AuroraRuntimeException(ResponseCode code) {super(String.format("%s", code.getMessage()));
this.code = code;
}
public AuroraRuntimeException(ResponseCode code, String msg) {super(msg);
this.code = code;
}
}
1.4 自定义返回类型
为了保障各个接口的返回对立,这里专门定义了一个返回类型。
@Getter
@Setter
public class GenericResponse<T> {
private int code;
private T data;
private String message;
public GenericResponse() {};
public GenericResponse(int code, T data) {
this.code = code;
this.data = data;
}
public GenericResponse(int code, T data, String message) {this(code, data);
this.message = message;
}
public GenericResponse(ResponseCode responseCode) {this.code = responseCode.getCode();
this.data = null;
this.message = responseCode.getMessage();}
public GenericResponse(ResponseCode responseCode, T data) {this(responseCode);
this.data = data;
}
public GenericResponse(ResponseCode responseCode, T data, String message) {this(responseCode, data);
this.message = message;
}
}
理论测试异样
上面的例子中,咱们想获取到用户的信息,如果用户的信息不存在,能够间接抛出一个异样,这个异样会被咱们下面定义的全局异样解决办法所捕捉,而后依据不同的异样编码,实现不同的解决和返回。
public User getUserInfo(Long userId) {
// some logic
User user = daoFactory.getExtendedUserMapper().selectByPrimaryKey(userId);
if (user == null) {throw new AuroraRuntimeException(ResponseCode.USER_INPUT_ERROR, "用户 id 不存在");
}
// some logic
....
}
以上就实现了整个全局异样的处理过程,接下来重点说说为什么 @RestControllerAdvice
和@ExceptionHandler
联合应用能够拦挡程序中产生的异样?
全局拦挡的背地原理?
上面会提到
@ControllerAdvice
注解,简略地说,@RestControllerAdvice 与 @ControllerAdvice 的区别就和 @RestController 与 @Controller 的区别相似,@RestControllerAdvice 注解蕴含了 @ControllerAdvice 注解和 @ResponseBody 注解。
接下来咱们深刻 Spring 源码,看看是怎么实现的,首先 DispatcherServlet 对象在创立时会初始化一系列的对象,这里重点关注函数initHandlerExceptionResolvers(context);
.
public class DispatcherServlet extends FrameworkServlet {
// ......
protected void initStrategies(ApplicationContext context) {initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
// 重点关注
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
// ......
}
在 initHandlerExceptionResolvers(context)办法中,会获得所有实现了 HandlerExceptionResolver 接口的 bean 并保存起来,其中就有一个类型为 ExceptionHandlerExceptionResolver 的 bean,这个 bean 在利用启动过程中会获取所有被 @ControllerAdvice 注解标注的 bean 对象做进一步解决,要害代码在这里:
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
implements ApplicationContextAware, InitializingBean {
// ......
private void initExceptionHandlerAdviceCache() {
// ......
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
AnnotationAwareOrderComparator.sort(adviceBeans);
for (ControllerAdviceBean adviceBean : adviceBeans) {ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType());
if (resolver.hasExceptionMappings()) {
// 找到所有 ExceptionHandler 标注的办法并保留成一个 ExceptionHandlerMethodResolver 类型的对象缓存起来
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
if (logger.isInfoEnabled()) {logger.info("Detected @ExceptionHandler methods in" + adviceBean);
}
}
// ......
}
}
// ......
}
当 Controller 抛出异样时,DispatcherServlet 通过 ExceptionHandlerExceptionResolver 来解析异样,而 ExceptionHandlerExceptionResolver 又通过 ExceptionHandlerMethodResolver 来解析异样,ExceptionHandlerMethodResolver 最终解析异样找到实用的 @ExceptionHandler 标注的办法是这里:
public class ExceptionHandlerMethodResolver {
// ......
private Method getMappedMethod(Class<? extends Throwable> exceptionType) {List<Class<? extends Throwable>> matches = new ArrayList<Class<? extends Throwable>>();
// 找到所有实用于 Controller 抛出异样的解决办法, 例如 Controller 抛出的异样
// 是 AuroraRuntimeException(继承自 RuntimeException), 那么 @ExceptionHandler(AuroraRuntimeException.class)和
// @ExceptionHandler(Exception.class)标注的办法都实用此异样
for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {if (mappedException.isAssignableFrom(exceptionType)) {matches.add(mappedException);
}
}
if (!matches.isEmpty()) {
/* 这里通过排序找到最实用的办法, 排序的规定根据抛出异样绝对于申明异样的深度, 例如
Controller 抛出的异样是是 AuroraRuntimeException(继承自 RuntimeException), 那么 AuroraRuntimeException
绝对于 @ExceptionHandler(AuroraRuntimeException.class)申明的 AuroraRuntimeException.class 其深度是 0,
绝对于 @ExceptionHandler(Exception.class)申明的 Exception.class 其深度是 2, 所以
@ExceptionHandler(BizException.class)标注的办法会排在后面 */
Collections.sort(matches, new ExceptionDepthComparator(exceptionType));
return this.mappedMethods.get(matches.get(0));
}
else {return null;}
}
// ......
}
整个 @RestControllerAdvice
解决的流程就是这样,联合 @ExceptionHandler
就实现了对不同异样的灵活处理。
关注公众号【码老思】,第一工夫获取最通俗易懂的原创技术干货。