统一异常处理使用及其原理
1. 使用
定义异常返回类ApiError
@Data
public class ApiError {
private Integer status = 400;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date date;
private String message;
private ApiError() {
date = new Date();
}
public static ApiError error(String message) {
ApiError apiError = new ApiError();
apiError.setMessage(message);
return apiError;
}
public static ApiError error(Integer status, String message) {
ApiError apiError = new ApiError();
apiError.setStatus(status);
apiError.setMessage(message);
return apiError;
}
}
全局异常处理类GlobalExceptionAdvice
@Slf4j
@RestControllerAdvice
public class GlobalExceptionAdvice {
/**
* 请求方式不支持
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ApiError handleException(HttpRequestMethodNotSupportedException e) {
log.error(e.getMessage(), e);
return ApiError.error("不支持' " + e.getMethod() + "'请求");
}
@ExceptionHandler(IllegalArgumentException.class)
public ApiError handleException(IllegalArgumentException e) {
log.error(e.getMessage(), e);
return ApiError.error(e.getMessage());
}
··· 省略,这里以 IllegalArgumentException举例子
}
测试类TestController
@RestController
public class TestController {
@GetMapping("test")
public String test() {
throw new IllegalArgumentException("Illegal exception");
}
}
启动项目,访问 localhost:8080/test,返回结果为
{
status: 400,
date: "2022-05-09 21:46:03",
message: "Illegal exception"
}
2. 原理
返回结果没有跑出异常,而是显示一个json。这说明最后走的逻辑是GlobalExceptionAdvice.handleException(IllegalArgumentException e)。
既然能走到这里,说明是通过反射调用的,那么是如何找到这个方法的呢?
@ExceptionHandler(IllegalArgumentException.class)
public ApiError handleException(IllegalArgumentException e) {
log.error(e.getMessage(), e);
return ApiError.error(e.getMessage());
}
可以看到,这个方法上面有个注解@ExceptionHandler注解,应该是将这个注解里面的值与方法名绑定在一起,调用接口/test的时候,如果抛出异常,那么就会去找一个这个异常关联的方法,然后通过反射调用方法。逻辑通了,那就开始分析。
断点直接打在上面代码的 log.error(e.getMessage(), e)处,然后debug调试,访问 localhost:8080/test
先查看代码入口org.springframework.web.servlet.DispatcherServlet#doDispatch。service入口,这里就不介绍了。
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
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);
}
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
boolean errorView = false;
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
// 处理异常代码
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
···
}
再来到处理异常代码org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionResolver#doResolveException
protected final ModelAndView doResolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
return doResolveHandlerMethodException(request, response, (HandlerMethod) handler, ex);
}
实现类org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#doResolveHandlerMethodException
这里核心步骤就是根据异常类去查找绑定的方法,然后通过反射去调用全局处理类中的方法。
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {
// 重点这里 根据异常类拿到对应的方法
ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
if (exceptionHandlerMethod == null) {
return null;
}
if (this.argumentResolvers != null) {
exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
ServletWebRequest webRequest = new ServletWebRequest(request, response);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
try {
if (logger.isDebugEnabled()) {
logger.debug("Using @ExceptionHandler " + exceptionHandlerMethod);
}
Throwable cause = exception.getCause();
if (cause != null) {
// Expose cause as provided argument as well
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, cause, handlerMethod);
}
else {
// 根据方法反射调用
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, handlerMethod);
}
}
catch (Throwable invocationEx) {
if (invocationEx != exception && invocationEx != exception.getCause() && logger.isWarnEnabled()) {
logger.warn("Failure in @ExceptionHandler " + exceptionHandlerMethod, invocationEx);
}
return null;
}
if (mavContainer.isRequestHandled()) {
return new ModelAndView();
}
else {
ModelMap model = mavContainer.getModel();
HttpStatus status = mavContainer.getStatus();
ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, status);
mav.setViewName(mavContainer.getViewName());
if (!mavContainer.isViewReference()) {
mav.setView((View) mavContainer.getView());
}
if (model instanceof RedirectAttributes) {
Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
}
return mav;
}
}
// 根据异常类去查找绑定的方法
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(
@Nullable HandlerMethod handlerMethod, Exception exception) {
Class<?> handlerType = null;
if (handlerMethod != null) {
// Local exception handler methods on the controller class itself.
// To be invoked through the proxy, even in case of an interface-based proxy.
handlerType = handlerMethod.getBeanType();
// exceptionHandlerCache这个map肯定是提前初始化的
ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType);
if (resolver == null) {
resolver = new ExceptionHandlerMethodResolver(handlerType);
this.exceptionHandlerCache.put(handlerType, resolver);
}
// 具体细节自己debug
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method);
}
// For advice applicability check below (involving base packages, assignable types
// and annotation presence), use target class instead of interface-based proxy.
if (Proxy.isProxyClass(handlerType)) {
handlerType = AopUtils.getTargetClass(handlerMethod.getBean());
}
}
for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
ControllerAdviceBean advice = entry.getKey();
if (advice.isApplicableToBeanType(handlerType)) {
ExceptionHandlerMethodResolver resolver = entry.getValue();
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(advice.resolveBean(), method);
}
}
}
return null;
}
什么时候提前绑定了异常类class与方法呢?答案时启动的时候。
全局搜索注解@ExceptionHandler,发现有个相关类ExceptionHandlerMethodResolver,该类实现了InitializingBean接口,在afterPropertiesSet()方法上打上断点
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
implements ApplicationContextAware, InitializingBean {
@Override
public void afterPropertiesSet() {
// Do this first, it may add ResponseBodyAdvice beans
initExceptionHandlerAdviceCache();
if (this.argumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.returnValueHandlers == null) {
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
}
private void initExceptionHandlerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
for (ControllerAdviceBean adviceBean : adviceBeans) {
Class<?> beanType = adviceBean.getBeanType();
if (beanType == null) {
throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
}
// 在这里绑定class与方法
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
if (resolver.hasExceptionMappings()) {
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
}
if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
this.responseBodyAdvice.add(adviceBean);
}
}
if (logger.isDebugEnabled()) {
int handlerSize = this.exceptionHandlerAdviceCache.size();
int adviceSize = this.responseBodyAdvice.size();
if (handlerSize == 0 && adviceSize == 0) {
logger.debug("ControllerAdvice beans: none");
}
else {
logger.debug("ControllerAdvice beans: " +
handlerSize + " @ExceptionHandler, " + adviceSize + " ResponseBodyAdvice");
}
}
}
最后在ExceptionHandlerMethodResolver完成class与method的绑定。具体细节需要自己debug
public class ExceptionHandlerMethodResolver {
/**
* A filter for selecting {@code @ExceptionHandler} methods.
*/
public static final MethodFilter EXCEPTION_HANDLER_METHODS = method ->
AnnotatedElementUtils.hasAnnotation(method, ExceptionHandler.class);
private final Map<Class<? extends Throwable>, Method> mappedMethods = new HashMap<>(16);
private final Map<Class<? extends Throwable>, Method> exceptionLookupCache = new ConcurrentReferenceHashMap<>(16);
/**
* A constructor that finds {@link ExceptionHandler} methods in the given type.
* @param handlerType the type to introspect
*/
public ExceptionHandlerMethodResolver(Class<?> handlerType) {
for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
addExceptionMapping(exceptionType, method);
}
}
}
···
}