Spring 的统一功能
目录
前言
一、拦截器
1. 定义拦截器
2. 注册配置拦截器
3. 拦截路径
4. DispatcherServlet 源码分析
1. 初始化
2. 处理请求
5. 适配器模式
二、统一数据返回格式
1. 配置
2. 源码分析
三、统一异常处理
1. 配置
2. 源码分析
前言
本文详细介绍了Spring框架中的拦截器、统一数据返回格式和统一异常处理的实现原理。拦截器通过实现HandlerInterceptor接口,在请求处理前后执行特定逻辑;统一数据返回格式通过ResponseBodyAdvice接口实现响应数据的标准化处理;统一异常处理则利用@ControllerAdvice和@ExceptionHandler注解来捕获和处理异常。文章还分析了DispatcherServlet的源码实现,包括初始化流程和请求处理机制,并探讨了适配器模式在Spring中的应用场景。这些核心功能共同构成了Spring MVC框架的基础架构,为开发者提供了灵活、高效的Web开发支持。
一、拦截器
拦截器是 Spring 框架提供的核心功能之一,主要作用是拦截用户请求,在指定方法前后,根据业务需要执行预先设定的代码;
比如在用户访问时,判断用户是否登录,如果登录了就可以正常访问,没登录就拦截请求,返回登录页面,让用户进行登录;
1. 定义拦截器
定义拦截器,实现 HandlerInterceptor 接口,重写里面的三个方法;
preHandle() 用户访问时,目标方法执行前,执行的逻辑,可以是校验用户是否登录;返回 true 继续执行后续的操作,返回 false 拦截请,中断后续的操作;
postHandle() 用户访问时,目标方法执行后,执行的逻辑;
afterCompletion() 视图渲染完毕后执行的逻辑;
import com.example.demo.constant.Constant;
import com.example.demo.model.UserInfo;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1. 校验用户是否登录HttpSession session = request.getSession();UserInfo userInfo = (UserInfo) session.getAttribute(Constant.SESSION_KEY_USERINFO);// 2. 校验失败,拦截if(userInfo == null){response.setStatus(401);return false;}// 3. 校验成功,放行return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {HandlerInterceptor.super.afterCompletion(request, response, handler, ex);}
}
2. 注册配置拦截器
需要通类实例化一个拦截器,也可以将拦截器类交给 Spring 进行管理,再通过 IoC 的方式进行注入;
实现 WebMvcConfigure 接口,重写 addInterceptor() 方法;
注册拦截器后,根据业务需求,配置拦截器拦截的目录或者放行的目录;
import com.example.demo.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns("/user/login").excludePathPatterns("/css/**").excludePathPatterns("/js/**").excludePathPatterns("/pic/**").excludePathPatterns("/**/*.html").excludePathPatterns("/test/**");}
}
3. 拦截路径
addPathPatterns() 指定要拦截哪些请求;
excludePathPatterns() 指定不拦截哪些请求;
拦截路径:
/* :一级路径;
/** :任意级路径;
/book/* :/book 下的一级路径;
/book/** book 下的任意路径;
三层架构的执行逻辑是 controller 调用 service,service 调用 mapper,有了拦截器之后,拦截器先预处理,再执行 controller 后面的逻辑;
4. DispatcherServlet 源码分析
Tomcat 项目启动后会看到 DispatcherServlet 相关的日志;
DispatcherServlet 是一个核心类,用于控制程序的执行顺序;
所有的请求都会先进入到 DispatcherServlet 执行 doDispach 调度方法,如果有拦截器,先执行拦截器的 preHandle() 方法,返回 true 后继续执行 controller 中的方法,执行完毕后,再回来执行 postHandle() 和 afterCompletion(),返回给 DispatcherServlet;
Servlet 的生命周期:
- init():初始化;
- service:执行服务;
- destroy():销毁;
1. 初始化
DispatcherServlet 的初始化方法 init() 在其父类 HttpServletBean 中实现的;
@Overridepublic final void init() throws ServletException {// Set bean properties from init parameters.PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);if (!pvs.isEmpty()) {try {BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));initBeanWrapper(bw);bw.setPropertyValues(pvs, true);}catch (BeansException ex) {if (logger.isErrorEnabled()) {logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);}throw ex;}}// Let subclasses do whatever initialization they like.initServletBean();}
initServletBean() 是在 FrameworkServlet 类中实现的,主要作用是建立 WebApplicationContext 容器,并加载 SpringMVC 配置文件定义的 Bean 到该容器中,最后将该容器添加到 ServletContext 中:
@Overrideprotected final void initServletBean() throws ServletException {getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");if (logger.isInfoEnabled()) {logger.info("Initializing Servlet '" + getServletName() + "'");}long startTime = System.currentTimeMillis();try {this.webApplicationContext = initWebApplicationContext();initFrameworkServlet();}catch (ServletException | RuntimeException ex) {logger.error("Context initialization failed", ex);throw ex;}if (logger.isDebugEnabled()) {String value = this.enableLoggingRequestDetails ?"shown which may lead to unsafe logging of potentially sensitive data" :"masked to prevent unsafe logging of potentially sensitive data";logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +"': request parameters and headers will be " + value);}if (logger.isInfoEnabled()) {logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");}}
初始化 web 容器的过程中,会通过 onRefresh() 来初始化 SpringMVC 的容器:
protected WebApplicationContext initWebApplicationContext() {WebApplicationContext rootContext =WebApplicationContextUtils.getWebApplicationContext(getServletContext());WebApplicationContext wac = null;if (this.webApplicationContext != null) {// A context instance was injected at construction time -> use itwac = this.webApplicationContext;if (wac instanceof ConfigurableWebApplicationContext cwac && !cwac.isActive()) {// The context has not yet been refreshed -> provide services such as// setting the parent context, setting the application context id, etcif (cwac.getParent() == null) {// The context instance was injected without an explicit parent -> set// the root application context (if any; may be null) as the parentcwac.setParent(rootContext);}configureAndRefreshWebApplicationContext(cwac);}}if (wac == null) {// No context instance was injected at construction time -> see if one// has been registered in the servlet context. If one exists, it is assumed// that the parent context (if any) has already been set and that the// user has performed any initialization such as setting the context idwac = findWebApplicationContext();}if (wac == null) {// No context instance is defined for this servlet -> create a local onewac = createWebApplicationContext(rootContext);}if (!this.refreshEventReceived) {// Either the context is not a ConfigurableApplicationContext with refresh// support or the context injected at construction time had already been// refreshed -> trigger initial onRefresh manually here.synchronized (this.onRefreshMonitor) {onRefresh(wac);}}if (this.publishContext) {// Publish the context as a servlet context attribute.String attrName = getServletContextAttributeName();getServletContext().setAttribute(attrName, wac);}return wac;}
@Overrideprotected void onRefresh(ApplicationContext context) {initStrategies(context);}protected void initStrategies(ApplicationContext context) {initMultipartResolver(context);initLocaleResolver(context);initThemeResolver(context);initHandlerMappings(context);initHandlerAdapters(context);initHandlerExceptionResolvers(context);initRequestToViewNameTranslator(context);initViewResolvers(context);initFlashMapManager(context);}
上述过程,方法都是在父类中定义的,但是具体实现都是在子类中完成的;这种在父类中定义,子类中实现的方式叫做模板方法模式;
2. 处理请求
DispatcherServlet 接收到请求后,执行 doDispatcher 调度方法,将请求传给 Controller;
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 = HttpMethod.GET.matches(method);if (isGet || HttpMethod.HEAD.matches(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 ServletException("Handler dispatch failed: " + err, err);}processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);}catch (Exception ex) {triggerAfterCompletion(processedRequest, response, mappedHandler, ex);}catch (Throwable err) {triggerAfterCompletion(processedRequest, response, mappedHandler,new ServletException("Handler processing failed: " + err, err));}finally {if (asyncManager.isConcurrentHandlingStarted()) {// Instead of postHandle and afterCompletionif (mappedHandler != null) {mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);}asyncManager.setMultipartRequestParsed(multipartRequestParsed);}else {// Clean up any resources used by a multipart request.if (multipartRequestParsed || asyncManager.isMultipartRequestParsed()) {cleanupMultipart(processedRequest);}}}}
HandlerAdapter 在 SpringMVC 中使用了适配器模式,可以看到执行 controller 中的方法前会执行拦截器的 preHandle(),执行后会执行 postHandle() 和 afterCompletion() 方法;
5. 适配器模式
适配器模式就是将某个接口转化为客户期望的接口,让原本不兼容的接口可以兼容;
以如下代码为例:
public class Executor {public void process(){System.out.println("我是 executor,正在执行服务...");}
}public class Adapter {private Executor executor;public void process(Executor executor){this.executor = executor;System.out.print("我是 adapter,正在执行:");executor.process();}
}public class Custom {public static void main(String[] args) {Adapter adapter = new Adapter();System.out.print("我是客户端,正在执行:");adapter.process(new Executor());}
}
一般在出现接口不兼容的情况下才会使用适配器模式,适配器模式一般出现在老项目中,对代码进行扩展改造,避免影响原有的逻辑;
二、统一数据返回格式
1. 配置
统一数据返回格式,需要添加一个返回结果的配置类,实现 ResponseBodyAdvice 接口,并加上 @ControllerAdvice 注解;
supports() 方法:判断是否要执行 beforeBodyWrite() 方法,true 为执行,false 为不执行;可以对指定的类或者方法对 response 进行处理;
beforeBodyWrite():指定对 response 进行处理;
import com.example.demo.model.ReturnResult;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;@ControllerAdvice
public class ResultConfig implements ResponseBodyAdvice {private ObjectMapper objectMapper = new ObjectMapper();@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {// 判断是否要执行下面的 beforeBodyWrite() 方法// 需要统一结果返回,就要返回 true// 不需要统一结果返回,就返回 falsereturn true;}@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {// 返回前设置返回数据的格式,配合上面的 supports 方法进行统一结果返回if(body instanceof ReturnResult<?>){return body;}if(body instanceof String){try {return objectMapper.writeValueAsString(ReturnResult.success(body));} catch (JsonProcessingException e) {throw new RuntimeException(e);}}return ReturnResult.success(body);}
}
注意,如果要返回的数据格式是 String 类型的,不能直接返回,需要使用 ObjectMapper 对象将字符串转化为格式化数据,才能返回,否则会出现报错;
2. 源码分析
将原本的数据格式转换为统一数据返回格式这个步骤是在 service 的阶段完成的;
需要用到 HttpMessageConverter 实现数据格式的转换;
过程:
DispatcherServlet 的 doService() 方法调用 doDispatch() 方法;
@Overrideprotected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {logRequest(request);...try {doDispatch(request, response);}finally {if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {// Restore the original attribute snapshot, in case of an include.if (attributesSnapshot != null) {restoreAttributesAfterInclude(request, attributesSnapshot);}}if (this.parseRequestPath) {ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request);}}}
doDispatch() 调用 handle() 方法调度服务,handle() 方法有多个子类实现,用的是 AbstractHandlerMethodAdapter 类中的 handle() 方法;
public abstract class AbstractHandlerMethodAdapter extends WebContentGenerator implements HandlerAdapter, Ordered {.../*** This implementation expects the handler to be an {@link HandlerMethod}.*/@Override@Nullablepublic final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {return handleInternal(request, response, (HandlerMethod) handler);}...
}
Spring 中既能支持 controller 方式实现的路由映射,也能支持 servlet 方式实现的映射,是因为使用了适配器模式, 能适配不同的接口;因此提供服务也是通过适配器实现了,所以会在源码中经常看到 Adapter;
RequestMappingHandlerAdapter 类实现了 AbstractHandlerMethodAdapter 的 handleInternal() 方法,再调用 invokeHandlerMethod() 方法;
@Override@Nullableprotected ModelAndView handleInternal(HttpServletRequest request,HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {ModelAndView mav;checkRequest(request);// Execute invokeHandlerMethod in synchronized block if required.if (this.synchronizeOnSession) {HttpSession session = request.getSession(false);if (session != null) {Object mutex = WebUtils.getSessionMutex(session);synchronized (mutex) {mav = invokeHandlerMethod(request, response, handlerMethod);}}else {// No HttpSession available -> no mutex necessarymav = invokeHandlerMethod(request, response, handlerMethod);}}else {// No synchronization on session demanded at all...mav = invokeHandlerMethod(request, response, handlerMethod);}if (!response.containsHeader(HEADER_CACHE_CONTROL)) {if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);}else {prepareResponse(response);}}return mav;}
调用 ServletInvocableHandlerMethod 类中的 invokeAndHandle() 方法,调用 handleReturnValue() 方法;
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);setResponseStatus(webRequest);if (returnValue == null) {if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {disableContentCachingIfNecessary(webRequest);mavContainer.setRequestHandled(true);return;}}else if (StringUtils.hasText(getResponseStatusReason())) {mavContainer.setRequestHandled(true);return;}mavContainer.setRequestHandled(false);Assert.state(this.returnValueHandlers != null, "No return value handlers");try {this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest);}catch (Exception ex) {if (logger.isTraceEnabled()) {logger.trace(formatErrorForReturnValue(returnValue), ex);}throw ex;}}
handleReturnValue() 实现了返回数据的格式转换,有多个类实现,使用的是 RequestResponseBodyMethodProcessor 类中的实现;
@Overridepublic void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,ModelAndViewContainer mavContainer, NativeWebRequest webRequest)throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {mavContainer.setRequestHandled(true);ServletServerHttpRequest inputMessage = createInputMessage(webRequest);ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);if (returnValue instanceof ProblemDetail detail) {outputMessage.setStatusCode(HttpStatusCode.valueOf(detail.getStatus()));if (detail.getInstance() == null) {URI path = URI.create(inputMessage.getServletRequest().getRequestURI());detail.setInstance(path);}invokeErrorResponseInterceptors(detail, null);}// Try even with null return value. ResponseBodyAdvice could get involved.writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);}
调用 AbstarctMessageConverter 类中的 writeWithMessageConverters() 方法,使用转换器实现;
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {...if (selectedMediaType != null) {selectedMediaType = selectedMediaType.removeQualityValue();ResolvableType targetResolvableType = null;for (HttpMessageConverter converter : this.messageConverters) {ConverterType converterTypeToUse = null;if (converter instanceof GenericHttpMessageConverter genericConverter) {if (genericConverter.canWrite(targetType, valueType, selectedMediaType)) {converterTypeToUse = ConverterType.GENERIC;}}else if (converter instanceof SmartHttpMessageConverter smartConverter) {targetResolvableType = getNestedTypeIfNeeded(ResolvableType.forMethodParameter(returnType));if (smartConverter.canWrite(targetResolvableType, valueType, selectedMediaType)) {converterTypeToUse = ConverterType.SMART;}}else if (converter.canWrite(valueType, selectedMediaType)){converterTypeToUse = ConverterType.BASE;}if (converterTypeToUse != null) {body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,(Class<? extends HttpMessageConverter<?>>) converter.getClass(), inputMessage, outputMessage);if (body != null) {Object theBody = body;LogFormatUtils.traceDebug(logger, traceOn ->"Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");addContentDispositionHeader(inputMessage, outputMessage);switch (converterTypeToUse) {case BASE -> converter.write(body, selectedMediaType, outputMessage);case GENERIC -> ((GenericHttpMessageConverter) converter).write(body, targetType, selectedMediaType, outputMessage);case SMART -> ((SmartHttpMessageConverter) converter).write(body, targetResolvableType, selectedMediaType, outputMessage, null);}}else {if (logger.isDebugEnabled()) {logger.debug("Nothing to write: null body");}}return;}}}...}
上述代码中的 getAdvice() .beforeBodyWrite() 就是调用统一数据返回格式的配置;
三、统一异常处理
1. 配置
统一异常处理使用的 @ControllerAdvice 和 @ExceptionHandler 注解实现的;
如果返回的不是页面,而是数据,要加上 @ResponseBody 注解;
import com.example.demo.model.ReturnResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;@Slf4j
@ResponseBody
@ControllerAdvice
public class ExceptionAdvice {@ExceptionHandlerpublic ReturnResult<Object> handler1(Exception e){log.error("发生错误:e", e);return ReturnResult.error("内部错误");}@ExceptionHandlerpublic ReturnResult<Object> handler2(ArithmeticException e){log.error("发生错误:e", e);return ReturnResult.error("算术错误");}@ExceptionHandlerpublic ReturnResult<Object> handler3(NullPointerException e){log.error("发生错误:e", e);return ReturnResult.error("空指针异常");}
}
注意加上 @Slf4j 打印日志,否则出现异常的时候,没有错误信息;
2. 源码分析
@ControllerAdvice:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {/*** Alias for {@link Component#value}.* @since 6.1*/@AliasFor(annotation = Component.class, attribute = "value")String name() default "";...}
@ControllerAdvice 是基于 @Component 实现的,也会被 Spring 管理;
对于 @ControllerAdvice 注解,Servlet 在初始化阶段会初始化适配器和异常解析器;
initHandlerAdapters():
初始化阶段就会取得所有的 HandlerAdapter 接口的 bean 并保存起来,其中有一个类型为 RequestMappingHandlerAdapter 的 bean;这个 bean 是 @RequestMapping 注解能起作用的关键,这个 bean 在启动过程中会获取所有被 @ControllerAdvice 注解标注的 bean 对象;
RequestMappingHandlerAdapter 类实现了 InitializingBean 接口,重写了 afterPropertiesSet() 方法,用于获取 被 @ControllerAdvice 注解标注的 bean 对象;
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapterimplements BeanFactoryAware, InitializingBean {...@Overridepublic void afterPropertiesSet() {// Do this first, it may add ResponseBody advice beansinitControllerAdviceCache();initMessageConverters();if (this.argumentResolvers == null) {List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);}if (this.initBinderArgumentResolvers == null) {List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);}if (this.returnValueHandlers == null) {List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);}if (BEAN_VALIDATION_PRESENT) {List<HandlerMethodArgumentResolver> resolvers = this.argumentResolvers.getResolvers();this.methodValidator = HandlerMethodValidator.from(this.webBindingInitializer, this.parameterNameDiscoverer,methodParamPredicate(resolvers, ModelAttributeMethodProcessor.class),methodParamPredicate(resolvers, RequestParamMethodArgumentResolver.class));}}}private void initControllerAdviceCache() {if (getApplicationContext() == null) {return;}List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());List<Object> requestResponseBodyAdviceBeans = new ArrayList<>();for (ControllerAdviceBean adviceBean : adviceBeans) {Class<?> beanType = adviceBean.getBeanType();if (beanType == null) {throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);}Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);if (!attrMethods.isEmpty()) {this.modelAttributeAdviceCache.put(adviceBean, attrMethods);}Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);if (!binderMethods.isEmpty()) {this.initBinderAdviceCache.put(adviceBean, binderMethods);}if (RequestBodyAdvice.class.isAssignableFrom(beanType) || ResponseBodyAdvice.class.isAssignableFrom(beanType)) {requestResponseBodyAdviceBeans.add(adviceBean);}}if (!requestResponseBodyAdviceBeans.isEmpty()) {this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans);}if (logger.isDebugEnabled()) {int modelSize = this.modelAttributeAdviceCache.size();int binderSize = this.initBinderAdviceCache.size();int reqCount = getBodyAdviceCount(RequestBodyAdvice.class);int resCount = getBodyAdviceCount(ResponseBodyAdvice.class);if (modelSize == 0 && binderSize == 0 && reqCount == 0 && resCount == 0) {logger.debug("ControllerAdvice beans: none");}else {logger.debug("ControllerAdvice beans: " + modelSize + " @ModelAttribute, " + binderSize +" @InitBinder, " + reqCount + " RequestBodyAdvice, " + resCount + " ResponseBodyAdvice");}}}
initHandlerExceptionResolvers():
初始化阶段就会取得所有的 HandlerExceptionResolver 接口的 bean 并保存起来,其中有一个类型为 ExceptionHandlerExceptionResolver 的 bean;这个 bean 在启动过程中会获取所有被 @ControllerAdvice 注解标注的 bean 对象;
ExceptionHandlerExceptionResolver 类实现了 InitializingBean 接口,重写了 afterPropertiesSet() 方法,用于获取被 @ControllerAdvice 注解标注的 bean 对象;
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolverimplements ApplicationContextAware, InitializingBean {...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);}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");}}}...}
当 Controller 抛出异常,DispatcherServlet 通过 ExceptionHandlerExceptionResolver 解析异常, ExceptionHandlerExceptionResolver 通过 ExceptionHandlerMethodResolver 解析异常,最终找到适用的 @ExceptionHandler 标注的方法;
public class ExceptionHandlerMethodResolver {...@Nullableprivate ExceptionHandlerMappingInfo getMappedMethod(Class<? extends Throwable> exceptionType, MediaType mediaType) {List<ExceptionMapping> matches = new ArrayList<>();for (ExceptionMapping mappingInfo : this.mappedMethods.keySet()) {if (mappingInfo.exceptionType().isAssignableFrom(exceptionType) && mappingInfo.mediaType().isCompatibleWith(mediaType)) {matches.add(mappingInfo);}}if (!matches.isEmpty()) {if (matches.size() > 1) {matches.sort(new ExceptionMapingComparator(exceptionType, mediaType));}return this.mappedMethods.get(matches.get(0));}else {return NO_MATCHING_EXCEPTION_HANDLER;}}...}