Running with Spring Boot v2.5.4, Java 11.0.12
当前,Spring统一异常处理机制是Java开发人员普遍使用的一种技术,在业务校验失败的时候,直接抛出业务异常即可,这明显简化了业务异常的治理流程与复杂度。值得一提的是,统一异常处理机制并不是Spring Boot提供的,而是Spring MVC,前者只是为Spring MVC自动配置了刚好够用的若干组件而已,具体配置了哪些组件,感兴趣的读者可以到spring-boot-autoconfigure
模块中找到答案。
1 异常从何而来
DispatcherServlet
是Spring MVC的门户,所有Http请求都会通过DispatcherServlet进行路由分发,即使Http请求的处理流程抛出了异常。doDispatch()
方法是其核心逻辑,主要内容如下:
public class DispatcherServlet {
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HandlerExecutionChain mappedHandler = null;
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
// Determine handler for the current request.
mappedHandler = getHandler(request);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Invoke all HandlerInterceptor preHandle() in HandlerExecutionChain.
if (!mappedHandler.applyPreHandle(request, response)) {
return;
}
// Actually invoke the handler.
mv = ha.handle(request, response, mappedHandler.getHandler());
// Invoke all HandlerInterceptor postHandle() in HandlerExecutionChain.
mappedHandler.applyPostHandle(request, response, mv);
} catch (Exception ex) {
dispatchException = ex;
}
// Handle the result of handler invocation, which is either a ModelAndView or an Exception to be resolved to a ModelAndView.
processDispatchResult(request, response, mappedHandler, mv, dispatchException);
} catch (Exception ex) {
// Invoke all HandlerInterceptor afterCompletion() in HandlerExecutionChain.
triggerAfterCompletion(request, response, mappedHandler, ex);
}
}
}
阅读上述源码可以看出如果出现了异常,会先将该异常实例赋予dispatchException
这一局部变量,然后由processDispatchResult()
方法负责异常处理。很明显,在doDispatch()方法内有两处容易抛出异常,第一处在为Http请求寻找相匹配的Handler过程中,Handler是什么东东?一般就是那些由@Controller
或@RestController
标注的自定义Controller,这些Controller会由HandlerMethod
包装起来;另一处就是在执行Handler的过程中。
1.1 获取Handler过程中抛出异常
获取Handler离不开HandlerMapping
,由于@RequestMapping
注解的广泛应用,使得RequestMappingHandlerMapping
成为了一等宠臣,其继承关系如下图所示:
继承关系图清晰交代了HandlerMapping的子类AbstractHandlerMethodMapping
实现了InitializingBean
接口这一事实。众所周知:Spring IoC容器在构建Bean的过程中,如果当前Bean实现了InitializingBean
接口,那么就会通过后者的afterPropertiesSet()
方法来进行初始化操作,具体初始化逻辑如下:
public class RequestMappingHandlerMapping implements RequestMappingInfoHandlerMapping {
@Override
public void afterPropertiesSet() {
super.afterPropertiesSet();
}
}
public class AbstractHandlerMethodMapping extends AbstractHandlerMapping implements InitializingBean{
@Override
public void afterPropertiesSet() {
initHandlerMethods();
}
protected void initHandlerMethods() {
// obtainApplicationContext().getBeanNamesForType(Object.class))
for (String beanName : getCandidateBeanNames()) {
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
processCandidateBean(beanName);
}
}
}
protected void processCandidateBean(String beanName) {
Class<?> beanType = beanType = obtainApplicationContext().getType(beanName);
// AnnotatedElementUtils.hasAnnotation(beanType, Controller.class)
// || AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class)
if (beanType != null && isHandler(beanType)) {
detectHandlerMethods(beanName);
}
}
protected void detectHandlerMethods(Object handler) {
Class<?> handlerType = (handler instanceof String ? obtainApplicationContext().getType((String) handler) : handler.getClass());
if (handlerType != null) {
Class<?> userType = ClassUtils.getUserClass(handlerType);
// private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
// RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
// RequestCondition<?> condition = (element instanceof Class ? getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
// return createRequestMappingInfo(requestMapping, condition);
// }
Map<Method, RequestMappingInfo> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup) method -> getMappingForMethod(method, userType));
methods.forEach((method, mapping) -> {
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}
protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
this.mappingRegistry.register(mapping, handler, method);
}
}
public class AbstractHandlerMethodMapping.MappingRegistry {
private final Map<RequestMappingInfo, MappingRegistration> registry = new HashMap<>();
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void register(RequestMappingInfo mapping, Object handler, Method method) {
this.readWriteLock.writeLock().lock();
try {
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
Set<String> directPaths = getDirectPaths(mapping);
for (String path : directPaths) {
this.pathLookup.add(path, mapping);
}
this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directPaths));
} finally {
this.readWriteLock.writeLock().unlock();
}
}
}
上述初始化逻辑主要为:
- 从ApplicationContext中获取所有Bean,遍历每一个Bean;
- 判断当前Bean是否含有
@Controller
注解(注意:@RestController依然由@Controller标注),若无则遍历下一个Bean,若有则意味着这是一个Handler; - 从当前Handler中探测出所有由
@RequestMapping
标注的方法,然后构建出一个以Method
实例为key、RequestMappingInfo
实例为value的Map(注意:@GetMapping、@PostMapping等也由@RequestMapping标注); - 遍历该Map,填充
MappingRegistry
中Map<RequestMappingInfo, MappingRegistration>类型的成员变量registry
。registry中所填充的内容示例如下:
{
"registry": [
{
"key": {
"RequestMappingInfo": {
"patternsCondition": "/crimson_typhoon/v1/fire",
"methodsCondition": "POST"
}
},
"value": {
"MappingRegistration": {
"HandlerMethod": {
"bean": "customExceptionHandler",
"beanType": "com.example.crimson_typhoon.controller.CrimsonTyphoonController",
"method": "com.example.crimson_typhoon.controller.CrimsonTyphoonController.v1Fire(com.example.crimson_typhoon.dto.UserDto,java.lang.Boolean)"
}
}
}
}
]
}
贴了这么一大段源码,只是想说明一个事实:DispatcherServlet可以快速根据Http请求解析出Handler,因为Http请求与Handler的映射关系被预先缓存在MappingRegistry中了。
下面步入正题:在获取Handler过程中究竟是否会抛出异常?又是哪些异常呢?
根据上图,我们直接去看AbstractHandlerMethodMapping中lookupHandlerMethod()
方法的逻辑,如下:
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<AbstractHandlerMethodMapping.Match> matches = new ArrayList<>();
List<RequestMappingInfo> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
if (directPathMatches != null) {
for (T mapping : mappings) {
T match = getMatchingMapping(mapping, request);
if (match != null) {
matches.add(new AbstractHandlerMethodMapping.Match(match, this.mappingRegistry.getRegistrations().get(mapping)));
}
}
}
if (!matches.isEmpty()) {
// 详细决策逻辑跳过
return 最匹配的HandlerMethod;
} else {
// 没找到匹配的HandlerMethod
return handleNoMatch(this.mappingRegistry.getRegistrations().keySet(), lookupPath, request);
}
}
顺藤摸瓜,继续:
protected HandlerMethod handleNoMatch(Set<RequestMappingInfo> infos, String lookupPath, HttpServletRequest request) throws ServletException {
RequestMappingInfoHandlerMapping.PartialMatchHelper helper = new RequestMappingInfoHandlerMapping.PartialMatchHelper(infos, request);
if (helper.hasMethodsMismatch()) {
throw new HttpRequestMethodNotSupportedException();
}
if (helper.hasConsumesMismatch()) {
throw new HttpMediaTypeNotSupportedException();
}
if (helper.hasProducesMismatch()) {
throw new HttpMediaTypeNotAcceptableException();
}
if (helper.hasParamsMismatch()) {
throw new UnsatisfiedServletRequestParameterException();
}
return null;
}
最终,抛出哪些异常还是让我们定位到了,比如大名鼎鼎的HttpRequestMethodNotSupportedException
就是在这里被抛出的。
事实上,如果最终没有为Http请求寻找到相匹配的Handler,也将抛出异常,它就是NoHandlerFoundException
,前提是要在application.properties
配置文件中添加spring.mvc.throw-exception-if-no-handler-found=true
这一项配置!
protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception {
if (this.throwExceptionIfNoHandlerFound) {
throw new NoHandlerFoundException(request.getMethod(), getRequestUri(request), new ServletServerHttpRequest(request).getHeaders());
} else {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
1.2 Handler执行过程中抛出异常
Handler执行过程中抛出的异常比较宽泛,一般可以归纳为两种:一种是执行Handler后抛出的异常,比如:业务逻辑层中未知的运行时异常和开发人员自定义的异常;另一种是还未开始执行Handler,而是在为其方法参数进行数据绑定时抛出的异常,比如:BindingException
及其子类MethodArgumentNotValidException
。大家可能对MethodArgumentNotValidException
尤为熟悉,常见的异常抛出场景如下所示:
@RestController
@RequestMapping(path = "/crimson_typhoon")
public class CrimsonTyphoonController {
@PostMapping(path = "/v1/fire")
public Map<String, Object> v1Fire(@RequestBody @Valid UserDto userDto, @RequestParam("dryRun") Boolean dryRun) {
return ImmutableMap.of("status", "success", "code", 200, "data", ImmutableList.of(userDto));
}
}
public class UserDto {
@NotBlank
private String name;
@NotNull
private int age;
}
如果调用方传递的请求体参数不符合Bean Validation
的约束规则,那么就会抛出MethodArgumentNotValidException异常。
2 异常如何处理
无论是在获取Handler过程中、在为Handler的方法参数进行数据绑定过程中亦或在Handler执行过程中出现了异常,总是会先将该异常实例赋予dispatchException
这一局部变量,然后由processDispatchResult()
方法负责异常处理。下面来看看DispatcherServlet中processDispatchResult()
方法是究竟如何处理异常的,源码逻辑很直白,最终是将异常委派给HandlerExceptionResolver
处理的,如下:
public class DispatcherServlet {
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
HandlerExecutionChain mappedHandler,
ModelAndView mv, Exception exception) {
boolean errorView = false;
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
} else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
if (mv != null && !mv.wasCleared()) {
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
}
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
ModelAndView exMv = null;
if (this.handlerExceptionResolvers != null) {
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
if (exMv != null) {
if (exMv.isEmpty()) {
request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
return null;
}
if (!exMv.hasView()) {
String defaultViewName = getDefaultViewName(request);
if (defaultViewName != null) {
exMv.setViewName(defaultViewName);
}
}
WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
return exMv;
}
throw ex;
}
}
主角登场!HandlerExceptionResolver是一个函数式接口,即有且只有一个resolveException()
方法:
public interface HandlerExceptionResolver {
ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
}
HandlerExceptionResolver与HandlerMapping
、HandlerAdapter
等类似,一般不需要开发人员自行定义,Spring MVC默认会提供一些不同风格的HandlerExceptionResolver,这些HandlerExceptionResolver会通过initHandlerExceptionResolvers()
方法被提前填充到DispatcherServlet中handlerExceptionResolvers
这一成员变量中,具体地:
private void initHandlerExceptionResolvers(ApplicationContext context) {
if (this.detectAllHandlerExceptionResolvers) {
Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values());
AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
}
}
}
HandlerExceptionResolverComposite
是DispatcherServlet中handlerExceptionResolvers这一成员变量所持有的最重要的异常解析器,Composite后缀表明这是一个复合类,自然会通过其成员变量持有若干HandlerExceptionResolver类型的苦力小弟,而在这众多苦力小弟中最为重要的非ExceptionHandlerExceptionResolver
异常解析器莫属!查阅其源码后发现它也实现了InitializingBean
接口:
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
implements ApplicationContextAware, InitializingBean {
private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerCache =
new ConcurrentHashMap<>(64);
private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> exceptionHandlerAdviceCache =
new LinkedHashMap<>();
@Override
public void afterPropertiesSet() {
initExceptionHandlerAdviceCache();
}
private void initExceptionHandlerAdviceCache() {
// ControllerAdvice controllerAdvice = beanFactory.findAnnotationOnBean(name, ControllerAdvice.class)
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
for (ControllerAdviceBean adviceBean : adviceBeans) {
Class<?> beanType = adviceBean.getBeanType();
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
if (resolver.hasExceptionMappings()) {
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
}
}
}
}
public class ExceptionHandlerMethodResolver {
public static final ReflectionUtils.MethodFilter EXCEPTION_HANDLER_METHODS = method ->
AnnotatedElementUtils.hasAnnotation(method, ExceptionHandler.class);
private final Map<Class<? extends Throwable>, Method> mappedMethods = new HashMap<>(16);
public ExceptionHandlerMethodResolver(Class<?> handlerType) {
for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
this.mappedMethods.put(exceptionType, method);
}
}
}
private List<Class<? extends Throwable>> detectExceptionMappings(Method method) {
List<Class<? extends Throwable>> result = new ArrayList<>();
ExceptionHandler ann = AnnotatedElementUtils.findMergedAnnotation(method, ExceptionHandler.class);
result.addAll(Arrays.asList(ann.value()));
if (result.isEmpty()) {
for (Class<?> paramType : method.getParameterTypes()) {
if (Throwable.class.isAssignableFrom(paramType)) {
result.add((Class<? extends Throwable>) paramType);
}
}
}
return result;
}
}
上述关于ExceptionHandlerExceptionResolver的初始化逻辑很清晰:首先,从IoC容器中获取所有由@ControllerAdvice
注解接口标注的Bean,这个Bean一般就是我们平时自定义的全局异常统一处理器;然后,逐一遍历这些全局异常处理器Bean,将其作为ExceptionHandlerMethodResolver
构造方法的参数,后者会解析出含有@ExceptionHandler
注解的异常处理方法,按照以Class<? extends Throwable>
实例为key、以Method
实例为value的映射规则填充其成员变量mappedMethods
;最后,ExceptionHandlerExceptionResolver再按照以ControllerAdviceBean
实例为key、以ExceptionHandlerMethodResolver
实例为value的映射规则填充其成员变量exceptionHandlerAdviceCache
。本文为了更直观地展示这种映射关系,笔者这里通过JSON来表达:
{
"exceptionHandlerAdviceCache": {
"key": {
"ControllerAdviceBean": {
"beanName": "customExceptionHandler",
"beanType": "com.example.crimson_typhoon.config.exception.CustomExceptionHandler"
}
},
"value": {
"ExceptionHandlerMethodResolver": {
"mappedMethods": [
{
"key": "class java.lang.Exception",
"value": "public org.springframework.http.ResponseEntity com.example.crimson_typhoon.config.exception.CustomExceptionHandler.handleUnknownException(Exception)"
},
{
"key": "class java.lang.NullPointerException",
"value": "public org.springframework.http.ResponseEntity com.example.crimson_typhoon.config.exception.CustomExceptionHandler.handleNullPointerException(NullPointerException)"
}
]
}
}
}
}
ExceptionHandlerExceptionResolver
的初始化用意与RequestMappingHandlerMapping
一致,也是为了提前缓存,这样后期可以快速地根据异常获取相匹配的@ExceptionHandler异常处理方法
。
通过分析ExceptionHandlerExceptionResolver的初始化逻辑,大家应该明白了为什么它是最为重要的一个异常解析器,因为它与由@ControllerAdvice
标注的统一异常处理器息息相关。此外,大家不要把ExceptionHandlerExceptionResolver和ExceptionHandlerMethodResolver搞混淆了,从后者名称来看,它只是一个面向@ExceptionHandler注解的方法解析器,压根不会解析异常哈。
下面回过头来看看HandlerExceptionResolverComposite中的逻辑,核心内容如下:
public class HandlerExceptionResolverComposite implements HandlerExceptionResolver, Ordered {
private List<HandlerExceptionResolver> resolvers;
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
if (this.resolvers != null) {
for (HandlerExceptionResolver handlerExceptionResolver : this.resolvers) {
ModelAndView mav = handlerExceptionResolver.resolveException(request, response, handler, ex);
if (mav != null) {
return mav;
}
}
}
return null;
}
}
上述源码说明:HandlerExceptionResolverComposite会让其持有的异常解析器逐一解析异常,如果谁能返回一个非空的ModelAndView
实例对象,那么谁就是赢家;绝大多数情况下,都是ExceptionHandlerExceptionResolver获得最后的胜利。ExceptionHandlerExceptionResolver中的异常解析逻辑在doResolveHandlerMethodException()
方法中:
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
implements InitializingBean {
@Override
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, 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();
ArrayList<Throwable> exceptions = new ArrayList<>();
Throwable exToExpose = exception;
while (exToExpose != null) {
exceptions.add(exToExpose);
Throwable cause = exToExpose.getCause();
exToExpose = (cause != exToExpose ? cause : null);
}
Object[] arguments = new Object[exceptions.size() + 1];
exceptions.toArray(arguments);
arguments[arguments.length - 1] = handlerMethod;
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, arguments);
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;
}
}
}
从上述ExceptionHandlerExceptionResolver的源码中,最终看到了执行@ExceptionHandler异常处理方法
的身影,与执行Handler中目标方法的原理一致,都是通过反射调用的,不再赘述。这里必须要重点看一下getExceptionHandlerMethod()
方法的逻辑,如下:
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
implements InitializingBean {
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) {
Class<?> handlerType = null;
if (handlerMethod != null) {
handlerType = handlerMethod.getBeanType();
ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType);
if (resolver == null) {
resolver = new ExceptionHandlerMethodResolver(handlerType);
this.exceptionHandlerCache.put(handlerType, resolver);
}
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method);
}
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;
}
}
在刚才介绍ExceptionHandlerExceptionResolver的初始化逻辑时已经提到了:其成员变量缓存了ControllerAdviceBean与ExceptionHandlerMethodResolver的映射关系。可是这个成员变量却在最后时刻才被遍历,这是为什么呢?原来,ExceptionHandlerExceptionResolver并不会首先从统一异常处理器中寻找@ExceptionHandler异常处理方法
,而是先从当前Handler中查找,找到之后缓存在其另一个成员变量exceptionHandlerCache
中。
最后,再介绍一个容易被忽略的知识点。回忆一下,当我们访问服务中不存在的API时,往往会响应一种奇怪的格式;之所以奇怪,是因为咱们平时都会定制化API的响应格式,而此时的响应格式与咱们定制化的格式不匹配,这是咋回事呢?如下所示:
{
"timestamp": "2021-12-06T13:51:34.063+00:00",
"status": 404,
"error": "Not Found",
"path": "/crimson_typhoon/v4/fire"
}
这是因为在根据Http请求获取Handler时,常规的Handler是不可能匹配到了,只能由ResourceHttpRequestHandler
这一个HttpRequestHandler
来兜底,它在通过handleRequest()
方法处理该Http请求时发现自己也搞不定,于是就只能将其转发给Servlet容器中默认的Error Page
处理了。只需通过response.sendError(HttpServletResponse.SC_NOT_FOUND)
将Response
中的errorState
这一成员变量的值置为1,那么Servlet容器就会乖乖地进行服务端转发操作。Error Page会由Spring Boot注册到Servlet容器中,它就是BasicErrorController
,具体内容如下:
package org.springframework.boot.autoconfigure.web.servlet.error;
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
private final ErrorProperties errorProperties;
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
}
如果你有强迫症,就是忍不了响应格式不统一的现象,那你可以像下面这样做:
@Configuration
public class CustomErrorHandlerConfig {
@Resource
private ServerProperties serverProperties;
@Bean
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes, ServerProperties serverProperties) {
return new BasicErrorController(errorAttributes, serverProperties.getError()) {
@Override
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
Map<String, Object> finalBody = ImmutableMap.of("status", body.get("error"), "code", status.value(), "data", List.of());
return ResponseEntity.ok(finalBody);
}
};
}
}
然后响应内容就变了,具体响应格式参照各自项目规范修改即可:
{
"status": "Not Found",
"code": 404,
"data": []
}
3 总结
聊到统一异常治理,自然要对治理对象的分类有一个清晰的认知。异常也就两类:未知异常和已知异常。未知异常多半是由隐藏的BUG造成的,笔者认为统一异常处理层一定要有针对未知异常的处理逻辑,直白点说就是在由@RestControllerAdvice
标注的统一异常处理类中要有一个由@ExceptionHandler(value = Exception.class)
标注的方法,但千万不要通过getMessage()
将异常信息反馈给调用方,因为异常是未知的,可能会将很长串的异常堆栈信息暴漏出来,这样既不友好也不安全,建议反馈简短的信息即可,比如:Internal Server Error,但要在日志中完整地记录异常堆栈信息,方便后期排查。已知异常的范围比较宽泛,针对已知异常,向调用方暴漏的错误信息一定要简洁清晰,这也是完全可以做到的,尤其是开发人员主动抛出的自定义异常,这类异常在统一异常处理层中可以放心大胆地通过getMessage()
方式将异常信息反馈给调用方或前台用户,因为开发人员在抛出异常的时候会填充简短精炼的提示信息。
关于最佳实践思路,建议大家自定义的统一异常处理器能够继承ResponseEntityExceptionHandler
,大家可以去看看它的源码就知道为什么这么建议了!
4 参考文章
- https://docs.spring.io/spring-framework/docs/5.3.9/reference/html/web.html#mvc-exceptionhandlers
- https://docs.spring.io/spring-boot/docs/2.5.4/reference/html/features.html#features.developing-web-applications.spring-mvc.error-handling