Spring 的统一功能

目录

前言

一、拦截器

[1. 定义拦截器](#1. 定义拦截器)

[2. 注册配置拦截器](#2. 注册配置拦截器)

[3. 拦截路径](#3. 拦截路径)

[4. DispatcherServlet 源码分析](#4. DispatcherServlet 源码分析)

[1. 初始化](#1. 初始化)

[2. 处理请求](#2. 处理请求)

[5. 适配器模式](#5. 适配器模式)

二、统一数据返回格式

[1. 配置](#1. 配置)

[2. 源码分析](#2. 源码分析)

三、统一异常处理

[1. 配置](#1. 配置)

[2. 源码分析](#2. 源码分析)


前言

本文详细介绍了Spring框架中的拦截器、统一数据返回格式和统一异常处理的实现原理。拦截器通过实现HandlerInterceptor接口,在请求处理前后执行特定逻辑;统一数据返回格式通过ResponseBodyAdvice接口实现响应数据的标准化处理;统一异常处理则利用@ControllerAdvice和@ExceptionHandler注解来捕获和处理异常。文章还分析了DispatcherServlet的源码实现,包括初始化流程和请求处理机制,并探讨了适配器模式在Spring中的应用场景。这些核心功能共同构成了Spring MVC框架的基础架构,为开发者提供了灵活、高效的Web开发支持。


一、拦截器

拦截器是 Spring 框架提供的核心功能之一,主要作用是拦截用户请求,在指定方法前后,根据业务需要执行预先设定的代码;

比如在用户访问时,判断用户是否登录,如果登录了就可以正常访问,没登录就拦截请求,返回登录页面,让用户进行登录;

1. 定义拦截器

定义拦截器,实现 HandlerInterceptor 接口,重写里面的三个方法;

preHandle() 用户访问时,目标方法执行前,执行的逻辑,可以是校验用户是否登录;返回 true 继续执行后续的操作,返回 false 拦截请,中断后续的操作;

postHandle() 用户访问时,目标方法执行后,执行的逻辑;

afterCompletion() 视图渲染完毕后执行的逻辑;

java 复制代码
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 {
    @Override
    public 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;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

2. 注册配置拦截器

需要通类实例化一个拦截器,也可以将拦截器类交给 Spring 进行管理,再通过 IoC 的方式进行注入;

实现 WebMvcConfigure 接口,重写 addInterceptor() 方法;

注册拦截器后,根据业务需求,配置拦截器拦截的目录或者放行的目录;

java 复制代码
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 {
    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public 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 的生命周期:

  1. init():初始化;
  2. service:执行服务;
  3. destroy():销毁;

1. 初始化

DispatcherServlet 的初始化方法 init() 在其父类 HttpServletBean 中实现的;

java 复制代码
    @Override
	public 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 中:

java 复制代码
    @Override
	protected 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 的容器:

java 复制代码
    protected WebApplicationContext initWebApplicationContext() {
		WebApplicationContext rootContext =
				WebApplicationContextUtils.getWebApplicationContext(getServletContext());
		WebApplicationContext wac = null;

		if (this.webApplicationContext != null) {
			// A context instance was injected at construction time -> use it
			wac = 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, etc
				if (cwac.getParent() == null) {
					// The context instance was injected without an explicit parent -> set
					// the root application context (if any; may be null) as the parent
					cwac.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 id
			wac = findWebApplicationContext();
		}
		if (wac == null) {
			// No context instance is defined for this servlet -> create a local one
			wac = 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;
	}
java 复制代码
    @Override
	protected 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;

java 复制代码
    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 afterCompletion
				if (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. 适配器模式

适配器模式就是将某个接口转化为客户期望的接口,让原本不兼容的接口可以兼容;

以如下代码为例:

java 复制代码
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 进行处理;

java 复制代码
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();

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        // 判断是否要执行下面的 beforeBodyWrite() 方法
        // 需要统一结果返回,就要返回 true
        // 不需要统一结果返回,就返回 false
        return true;
    }

    @Override
    public 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() 方法;

java 复制代码
@Override
	protected 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() 方法;

java 复制代码
public abstract class AbstractHandlerMethodAdapter extends WebContentGenerator implements HandlerAdapter, Ordered {

    ...

	/**
	 * This implementation expects the handler to be an {@link HandlerMethod}.
	 */
	@Override
	@Nullable
	public 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() 方法;

java 复制代码
    @Override
	@Nullable
	protected 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 necessary
				mav = 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() 方法;

java 复制代码
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 类中的实现;

java 复制代码
    @Override
	public 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() 方法,使用转换器实现;

java 复制代码
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 注解;

java 复制代码
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 {
    @ExceptionHandler
    public ReturnResult<Object> handler1(Exception e){
        log.error("发生错误:e", e);
        return ReturnResult.error("内部错误");
    }

    @ExceptionHandler
    public ReturnResult<Object> handler2(ArithmeticException e){
        log.error("发生错误:e", e);
        return ReturnResult.error("算术错误");
    }

    @ExceptionHandler
    public ReturnResult<Object> handler3(NullPointerException e){
        log.error("发生错误:e", e);
        return ReturnResult.error("空指针异常");
    }
}

注意加上 @Slf4j 打印日志,否则出现异常的时候,没有错误信息;

2. 源码分析

@ControllerAdvice:

java 复制代码
@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 对象;

java 复制代码
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
		implements BeanFactoryAware, InitializingBean {
    ...

    @Override
	public void afterPropertiesSet() {
		// Do this first, it may add ResponseBody advice beans
		initControllerAdviceCache();
		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 对象;

java 复制代码
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
		implements 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 标注的方法;

java 复制代码
public class ExceptionHandlerMethodResolver {

    ...

    @Nullable
	private 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;
		}
	}
    
    ...

}

相关推荐
小许学java4 小时前
Spring AI-流式编程
java·后端·spring·sse·spring ai
canonical_entropy5 小时前
对《DDD本质论》一文的解读
后端·架构·领域驱动设计
码事漫谈5 小时前
我用亲身经历告诉你,为什么程序员千万别不把英语当回事
后端
码事漫谈5 小时前
C++ const 用法全面总结与深度解析
后端
haogexiaole5 小时前
Java高并发常见架构、处理方式、api调优
java·开发语言·架构
间彧5 小时前
分布式单例模式在微服务架构中的实际应用案例
后端
间彧5 小时前
分布式系统中保证单例唯一性的Java解决方案
后端
间彧5 小时前
为什么避免在单例中保存上下文状态
后端
间彧5 小时前
单例模式防御反射与序列化攻击的意义与实践
后端