Skip to content

Spring MVC 流程概述

Spring MVC 最新官方文档:https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#spring-web

Spring MVC 时序图

官方流程图

web 项目初始化过程分析

Servlet 3.0 规范加入的内容

  • Servlet3.0规范提供的标准接口,在web容器启动的时候,首先触发ServletContainerInitializer此接口的实现类的onStartup()方法
java
/* Servlet3.0规范提供的标准接口 */
public interface ServletContainerInitializer {
    /*
     * 启动容器是做一些初始化操作,例如注册Servlet、Filter、Listener等等
     */
    public void onStartup(Set<Class<?>> c, ServletContext ctx)
        throws ServletException;
}
  • @HandlesTypes注解用于标识在ServletContainerInitializer的实现类中,指定要加载到ServletContainerInitializer接口实现类中的字节码,在onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext);方法的入参webAppInitializerClasses中可以获取到此注解的值
java
/* 用于指定要加载到ServletContainerInitializer接口实现类中的字节码 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface HandlesTypes {
    /*
     * 指定要加载到ServletContainerInitializer实现类的onStartUp方法中类的字节码。
     * 字节码可以是接口,抽象类或者普通类。
     */
    Class<?>[] value();
}

SpringMVC 框架使用 Servlet 3.0 规范

任何要使用Servlet3.0规范且脱离web.xml的配置,在使用时都必须在对应的jar包的META-INF/services目录创建一个名为javax.servlet.ServletContainerInitializer的文件,文件内容指定具体的ServletContainerInitializer实现类,那么,当web容器启动时就会运行这个初始化器做一些组件内的初始化工作。

AbstractDispatcherServletInitializer 中的 onStartUp 方法

AbstractDispatcherServletInitializer类是Spring MVC提供的WebApplicationInitializer接口的实现抽象类。

注册 DisptatcherServlet

TODO: 整理中

前端控制器 DispatcherServlet

简介

在 web.xml 中配置,实质是一个 Servlet

作用:接收请求,响应结果。相当于转发器,中央处理器

用户请求到达前端控制器,它就相当于 MVC 模式中的 C,DispatcherServlet 是整个流程控制的中心,由它调用其它组件处理用户的请求,DispatcherServlet的存在降低了组件之间的耦合性

执行过程分析

doService方法

此方法在接收到请求首先执行的方法,通过跟踪源码得知,它重写父类FrameworkServlet的,FrameworkServlet是继承了HttpServlet,所以它就相当于执行了 Servlet 中的 service 方法。

java
/**
 * Override the parent class implementation in order to intercept PATCH requests.
 */
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
    if (httpMethod == HttpMethod.PATCH || httpMethod == null) {
        processRequest(request, response);
    } else {
        super.service(request, response);
    }
}

processRequest 方法实现源码如下:

java
protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
   // 省略一堆初始化配置
   try {
       // 真正执行逻辑的方法
       doService(request, response);
   }
   catch (ServletException | IOException ex) {
       ...
   }
}

其中 doService 方法是抽象方法,由子类 DispatcherServlet 重写实现了,源码如下:

java
protected abstract void doService(HttpServletRequest request, HttpServletResponse response) throws Exception;

doDispatche方法

doService 方法执行的逻辑中,会调用 doDispatche 方法,此方法是处理请求分发的核心方法。它负责通过反射调用控制器方法、执行拦截器和处理结果视图

java
@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
    // 省略初始化过程...
    try {
        doDispatch(request, response);
    }
    finally {
		// 省略其他...
    }
}

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    // 获取原生请求
    HttpServletRequest processedRequest = request;
    // 获取Handler执行链
    HandlerExecutionChain mappedHandler = null;
    // 是否为文件上传请求, 默认为false
    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.
            // 获取能处理此请求的Handler
            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;
            }
            // 真正的执行对应方法
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
            if (asyncManager.isConcurrentHandlingStarted()) {
                return;
            }
            applyDefaultViewName(processedRequest, mv);
            mappedHandler.applyPostHandle(processedRequest, response, mv);
        }
        // 忽略其他...
}

通过上述 doDispatch 请求的核心源码,包含的主要执行流程如下:

  1. 调用 HandlerExecutionChain 获取处理器:DispatcherServlet 首先调用 getHandler 方法,通过 HandlerMapping 获取请求对应的 HandlerExecutionChain 对象,包含了处理器方法和拦截器列表。
  2. 调用 HandlerAdapter 执行处理器方法:DispatcherServlet 使用 HandlerAdapter 来执行处理器方法。根据 HandlerExecutionChain 中的处理器方法类型不同,选择对应的 HandlerAdapter 进行处理。常用的适配器有 RequestMappingHandlerAdapter 和 HttpRequestHandlerAdapter。
  3. 解析请求参数:DispatcherServlet 调用 HandlerAdapter 的 handle 方法,解析请求参数,并将解析后的参数传递给处理器方法执行。
  4. 调用处理器方法:DispatcherServlet 通过反射机制调用处理器方法,执行业务逻辑。
  5. 处理拦截器:在调用处理器方法前后,DispatcherServlet 会调用拦截器的 preHandle 和 postHandle方法进行相应的处理。
  6. 渲染视图:处理器方法执行完成后,DispatcherServlet 会通过 ViewResolver 解析视图名称,找到对应的 View 对象,并将模型数据传递给 View 进行渲染。
  7. 生成响应:View 会将渲染后的视图内容生成响应数据。

处理器映射器 HandlerMapping

作用

HandlerMapping 负责根据用户请求找到相应的 Handler(即处理器),Spring MVC 提供了不同的映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。

RequestMappingHandlerMapping 的执行时机

RequestMappingHandlerMappingHandlerMapping 接口的实现,是在项目启动的时候就进行

因为 DispatcherServlet 本质是一个 Servlet,所以在项目启动时必定会触发 init 方法。通过源码可以看到,init()方法的具体实现在定义在HttpServlet抽象类中,该方法又调用子类FrameworkServletinitServletBean()方法,该中有一个无实现的onRefresh(),这个是方法由子类DispatcherServlet来实现,方法实现如下:

java
@Override
protected void onRefresh(ApplicationContext context) {
	initStrategies(context);
}
/**
 * Initialize the strategy objects that this servlet uses.
 * <p>May be overridden in subclasses in order to initialize further strategy objects.
 */
/* 此方法就是DispatcherServlet中的初始化方法 */
protected void initStrategies(ApplicationContext context) {
	// 初始化文件上传的处理器
	initMultipartResolver(context);
	// 初始化国际化资源的处理器
	initLocaleResolver(context);
	// 初始化主题资源处理器
	initThemeResolver(context);
	// 初始化控制器映射的处理器,重点程度【5】
	initHandlerMappings(context);
	// 初始化适配器
	initHandlerAdapters(context);
	// 初始化异常解析器
	initHandlerExceptionResolvers(context);
	initRequestToViewNameTranslator(context);
	// 初始化视图解析器
	initViewResolvers(context);
	initFlashMapManager(context);
}

处理控制器映射的方法源码

java
/* 初始化处理器映射器方法 */
private void initHandlerMappings(ApplicationContext context) {
	this.handlerMappings = null;
	if (this.detectAllHandlerMappings) {
		// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
		/*
		 * 此方法会初始化三个处理器
		 * 	RequestMappingHandlerMapping:请求映射处理器
		 * 		此对象中MappingRegistry属性(LinkedHashMap结构),就是用来存储所有@RequestMapping中定义的url与相应的方法的映射
		 * 	BeanNameUrlHandlerMapping:beanName与url映射处理器
		 * 	SimpleUrlHandlerMapping:资源处理器
		 */
		Map<String, HandlerMapping> matchingBeans =
				BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
		if (!matchingBeans.isEmpty()) {
			this.handlerMappings = new ArrayList<>(matchingBeans.values());
			// We keep HandlerMappings in sorted order.
			AnnotationAwareOrderComparator.sort(this.handlerMappings);
		}
	}
	else {
		try {
			HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
			this.handlerMappings = Collections.singletonList(hm);
		}
		catch (NoSuchBeanDefinitionException ex) {
			// Ignore, we'll add a default HandlerMapping later.
		}
	}
	// Ensure we have at least one HandlerMapping, by registering
	// a default HandlerMapping if no other mappings are found.
	if (this.handlerMappings == null) {
		this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
		if (logger.isTraceEnabled()) {
			logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
					"': using default strategies from DispatcherServlet.properties");
		}
	}
}

处理器适配器 HandlerAdapter

作用

适配器模式

适配器模式就是把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口原因不匹配而无法一起工作的两个类能够一起工作。适配类可以根据参数返还一个合适的实例给客户端。

通过HandlerAdapter对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进行执行。

SpringMVC控制器的三种编写方式

使用Controller注解

java
@Controller
public class BasicController {
    @RequestMapping("/hello")
    public String sayHello() {
        System.out.println("BasicController控制器sayHello()方法执行了...");
        return "success";
    }
}

实现Controller接口(少用)

此实现方式的,返回值也是让spring mvc框架来处理后生成的ModelAndView

java
/* 编写SpringMVC控制器,可以实现此接口 */
@FunctionalInterface
public interface Controller {
	/* 用于处理请求并返回ModelAndView */
	@Nullable
	ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
}

实现HttpRequestHandler接口(少用)

此实现方式的与实现Controller接口方式的区别在于,返回值是让使用者通过response来处理

java
/* 编写SpringMVC控制器,可以实现此接口 */
@FunctionalInterface
public interface HttpRequestHandler {
	/* 用于处理器请求,并由使用者提供相应返回处理 */
	void handleRequest(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException;
}

视图解析器 ViewResovler 和 View

注:现在互联网项目,都不会使用直接响应视图的方式返回。都是前后端分离,将数据以流的方式返回到前端,在html中显示。所以此部分的内容很少用,只作了解即可

View

视图的作用是渲染模型数据,将模型里的数据以某种形式呈现给用户。

为了实现视图模型和具体实现技术的解耦,Spring在org.springframework.web.servlet包中定义了一个高度抽象的View接口。视图是无状态的,无状态是指对于每一个请求,都会创建一个View对象,所以他们不会有线程安全的问题。

在SpringMVC中常用的视图类型:

分类视图类型说明
URL视图InternalResourceView将JSP或者其他资源封装成一个视图,是InternaleResourceViewResolver默认使用的视图类型
JstlView它是当我们在页面中使用了JSTL标签库的国际化标签后,需要采用的类型
文档类视图AbstractPdfViewPDF文档视图的抽象类
AbstarctXlsViewExcel文档视图的抽象类,该类是4.2版本之后才有的。之前使用的是AbstractExcelView
JSON视图MappingJackson2JsonView将模型数据封装成Json格式数据输出。它需要借助Jackson开源框架
XML视图MappingJackson2XmlView将模型数据封装成XML格式数据。它是从4.1版本之后才加入的

ViewResolver

ViewResolver负责将处理结果生成View视图,ViewResolver首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成View视图对象,最后对View进行渲染将处理结果通过页面展示给用户。视图对象是由视图解析器负责实例化。

视图解析器的作用是将逻辑视图转为物理视图,所有的视图解析器都必须实现ViewResolver接口

SpringMVC为逻辑视图名的解析提供了不同的策略,可以在Spring WEB上下文中配置一种或多种解析策略,并指定他们之间的先后顺序。每一种映射策略对应一个具体的视图解析器实现类。程序员可以选择一种视图解析器或混用多种视图解析器。可以通过order属性指定解析器的优先顺序,order越小优先级越高,SpringMVC会按视图解析器顺序的优先顺序对逻辑视图名进行解析,直到解析成功并返回视图对象,否则抛出ServletException异常。

分类解析器类型说明
解析为Bean的名称BeanNameViewResolverBean的id即为逻辑视图名称
解析为URL文件InternalResourceViewResolver将视图名解析成一个URL文件,一般就是一个jsp或者html文件。文件一般都存放在WEB-INF目录中
解析指定XML文件XmlViewResolver解析指定位置的XML文件,默认在/WEB-INF/views.xml
解析指定属性文件ResourceBundleViewResolver解析properties文件

请求参数封装的源码分析

传统表单数据封装原理(!待整理)

第1步:DispatcherServletdoService 方法执行

第2步:DispatcherServletdoDispatch 方法执行

第3步:RequestMappingHandlerAdapterhandleInternal 方法执行

第4步:RequestMappingHandlerAdapterinvokeHandlerMethod 方法执行

第5步:ServletInvocableHandlerMethodinvokeAndHandle 方法执行

第6步:InvocableHandlerMethodinvokeForRequest 方法执行。其中有会调用到最后一步(第10步):InvocableHandlerMethoddoInvoke 方法执行

第7步:InvocableHandlerMethodgetMethodArgumentValues 方法执行

第8步:HandlerMethodArgumentResolverCompositeresolveArgument 方法执行

第9步:RequestParamMethodArgumentResolverresolveName 方法执行

@RequestBody 注解执行原理(!待整理)

@PathVariable 注解实现原理(!待整理)

对象绑定与类型转换

底层第一套转换接口与实现

  • Printer 把其它类型转为 String
  • Parser 把 String 转为其它类型
  • Formatter 综合 Printer 与 Parser 功能
  • Converter 把任意类型 S 转为任意类型 T
  • PrinterParserConverter 经过适配转换成 GenericConverter 类放入 org.springframework.core.convert.support.GenericConversionService 类中的 Converters 属性中(ConvertersGenericConversionService 定义的内部类)
  • FormattingConversionService 继承了 GenericConversionService 类,利用其它们实现转换。类关系图如下:

底层第二套转换接口

此套接口是 JDK 原生

  • PropertyEditor 把 String 与其它类型相互转换
  • PropertyEditorRegistry 可以注册多个 PropertyEditor 对象
  • 与第一套接口直接可以通过 FormatterPropertyEditorAdapter 来进行适配

高层接口与实现

org.springframework.beans.TypeConverter 是转换功能的最顶层接口,在转换时,会用到 TypeConverterDelegate 委派 ConversionServicePropertyEditorRegistry 真正执行转换(Facade 门面模式)。具体处理逻辑如下:

  • 先判断是否有自定义转换器,@InitBinder 添加的即属于这种 (用了适配器模式把 Formatter 转为需要的 PropertyEditor)
  • 再判断有没有 ConversionService 转换
  • 再利用默认的 PropertyEditor 转换
  • 最后进行特殊处理

Spring 中 TypeConverter 默认实现有如下:

  • SimpleTypeConverter 仅做类型转换
  • BeanWrapperImpl 为 bean 的属性赋值,当需要时做类型转换,通过 getter/setter 方法来设置 Property
  • DirectFieldAccessor 为 bean 的属性赋值,当需要时做类型转换,通过反射来设置 Field(无需提供 setter 方法)
  • ServletRequestDataBinder 从配置文件中读取值,为 bean 的属性执行绑定,当需要时做类型转换,根据 directFieldAccess 属性来选择 Property 方式还是 Field 方式,具备校验与获取校验结果功能

拦截器的执行时机和调用过程

拦截器的执行流程图

拦截器的源码执行过程分析

拦截器的责任链模式

责任链模式是一种常见的行为模式。它是使多个对象都有处理请求的机会,从而避免了请求的发送者和接收者之间的耦合关系。将这些对象串成一条链,并沿着这条链一直传递该请求,直到有对象处理它为止。

  • 优点:
    • 解耦了请求与处理
    • 请求处理者(节点对象)只需关注自己感兴趣的请求进行处理即可,对于不感兴趣的请求,直接转发给下一级节点对象
    • 具备链式传递处理请求功能,请求发送者无需知晓链路结构,只需等待请求处理结果
    • 链路结构灵活,可以通过改变链路结构动态地新增或删减责任
    • 易于扩展新的请求处理类(节点),符合开闭原则
  • 缺点:
    • 责任链路过长时,可能对请求传递处理效率有影响
    • 如果节点对象存在循环引用时,会造成死循环,导致系统崩溃

SpringMVC 中的文件上传

MultipartFile

源码

java
/* SpringMVC中对上传文件的封装 */
public interface MultipartFile extends InputStreamSource {
	/* 获取临时文件名称 */
	String getName();

	/* 获取真实(原始)文件名称 */
	@Nullable
	String getOriginalFilename();

	/* 获取上传文件的MIME类型 */
	@Nullable
	String getContentType();

	/* 是否是空文件 */
	boolean isEmpty();

	/* 获取上传文件的字节大小 */
	long getSize();

	/* 获取上传文件的字节数组 */
	byte[] getBytes() throws IOException;

	/* 获取上传文件的字节输入流 */
	@Override
	InputStream getInputStream() throws IOException;

	/* 把上传文件转换成一个Resource对象 */
	default Resource getResource() {
		return new MultipartFileResource(this);
	}

	/* 把临时文件移动到指定位置并重命名,参数是一个文件对象 */
	void transferTo(File dest) throws IOException, IllegalStateException;

	/* 把临时文件移动到指定位置并重命名,参数是一个文件路径 */
	default void transferTo(Path dest) throws IOException, IllegalStateException {
		FileCopyUtils.copy(getInputStream(), Files.newOutputStream(dest));
	}
}

commons-fileupload的实现

MultipartFile的实现类其中一个实现是CommonsMultipartFile,通过导包就看出了,此类是借助apache的commons-fileupload实现的文件上传

MultipartResolver

源码

java
/*
 * 它是SpringMVC中文件解析器的标准
 * 通过一个接口规定了文件解析器中必须包含的方法
 */
public interface MultipartResolver {

	/* 判断是否支持文件上传 */
	boolean isMultipart(HttpServletRequest request);

	/* 解析HttpServletRequest */
	MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;

	/* 删除临时文件和一些清理操作 */
	void cleanupMultipart(MultipartHttpServletRequest request);
}

CommonsFileUploadResolver

MultipartResolver的实现类是CommonsMultipartResolver,此类继承抽象类CommonsFileUploadSupport,解析CommonsMultipartFile逻辑在此抽象类

java
public class CommonsMultipartResolver extends CommonsFileUploadSupport
		implements MultipartResolver, ServletContextAware {
}

从导包能得知,也是借助apache的commons-fileupload实现的文件上传。具体方法作用,查看源码工程注释

@ControllerAdvice 注解调用流程

@ControllerAdvice 与 @InitBinder 配合实现流程

流程图

@InitBinder 在整个 HandlerAdapter 调用过程中所处的位置

上图图示解释:

  • RequestMappingHandlerAdapter 在图中缩写为 HandlerAdapter
  • HandlerMethodArgumentResolverComposite 在图中缩写为 ArgumentResolvers
  • HandlerMethodReturnValueHandlerComposite 在图中缩写为 ReturnValueHandlers

调用流程大致如下:

  1. RequestMappingHandlerAdapter 初始化时会解析 @ControllerAdvice 中的 @InitBinder 方法
  2. RequestMappingHandlerAdapter 会以类为单位,在该类首次使用时,解析此类的 @InitBinder 方法
  3. 以上两种情况 @InitBinder 的解析结果都会缓存来避免重复解析
  4. 控制器方法调用时,会综合利用本类的 @InitBinder 方法和 @ControllerAdvice 中的 @InitBinder 方法创建绑定工厂

模拟 @InitBinder 在控制器方法调用示例

源码详见 springmvc-sample 项目中的 10-handleradapter 模块中 ControllerAdviceInitBinderTest 测试类

  • 准备测试的带 @InitBinder 注解的 controller
java
// 带 @InitBinder 注解的增强控制器
@ControllerAdvice
public class InitBinderControllerAdvice {
    @InitBinder
    public void binder(WebDataBinder webDataBinder) {
        System.out.println("InitBinderControllerAdvice.binder 转换器方法执行了...");
    }
}

// 带 @InitBinder 注解的控制器1
@Controller
public class InitBinderController1 {
    @InitBinder
    public void binder1(WebDataBinder webDataBinder) {
        System.out.println("InitBinderController1.binder1 转换器方法执行了...");
    }

    public void foo() {
    }
}

// 带 @InitBinder 注解的控制器2
@Controller
public class InitBinderController2 {
    @InitBinder
    public void binder1(WebDataBinder webDataBinder) {
        System.out.println("InitBinderController2.binder1 转换器方法执行了...");
    }

    @InitBinder
    public void binder2(WebDataBinder webDataBinder) {
        System.out.println("InitBinderController2.binder2 转换器方法执行了...");
    }

    public void bar() {
    }
}
  • 测试代码,观察 @InitBinder 标识的方法解析与缓存的时机
java
public class ControllerAdviceInitBinderTest {

    private static final Logger log = LoggerFactory.getLogger(ControllerAdviceInitBinderTest.class);

    @Test
    public void test() throws Exception {
        /*
            @InitBinder 的来源有两个
            1. @ControllerAdvice 中 @InitBinder 标注的方法,由 RequestMappingHandlerAdapter 在初始化时解析并记录
            2. @Controller 中 @InitBinder 标注的方法,由 RequestMappingHandlerAdapter 会在控制器方法首次执行时解析并记录
         */
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfiguration.class);
        // 手动创建 RequestMappingHandlerAdapter
        RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter();
        handlerAdapter.setApplicationContext(context);
        handlerAdapter.afterPropertiesSet();

        log.debug("1. 初始化...");
        showBindMethods(handlerAdapter); // 打印目前已经绑定的 @InitBinder 标注的方法

        Method getDataBinderFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getDataBinderFactory", HandlerMethod.class);
        getDataBinderFactory.setAccessible(true);

        log.debug("2. 模拟调用 InitBinderController1 的 foo 方法时 ...");
        getDataBinderFactory.invoke(handlerAdapter, new HandlerMethod(new InitBinderController1(), InitBinderController1.class.getMethod("foo")));
        showBindMethods(handlerAdapter); // 打印目前已经绑定的 @InitBinder 标注的方法

        log.debug("3. 模拟调用 InitBinderController2 的 bar 方法时 ...");
        getDataBinderFactory.invoke(handlerAdapter, new HandlerMethod(new InitBinderController2(), InitBinderController2.class.getMethod("bar")));
        showBindMethods(handlerAdapter); // 打印目前已经绑定的 @InitBinder 标注的方法

        context.close();
    }

    private void showBindMethods(RequestMappingHandlerAdapter handlerAdapter) throws NoSuchFieldException, IllegalAccessException {
        // 通过反射查看 RequestMappingHandlerAdapter 对象中的 initBinderAdviceCache 属性,该属性是用于缓存 @ControllerAdvice + @InitBinder 注解的解析结果,避免重复解析
        Field initBinderAdviceCache = RequestMappingHandlerAdapter.class.getDeclaredField("initBinderAdviceCache");
        initBinderAdviceCache.setAccessible(true);
        Map<ControllerAdviceBean, Set<Method>> globalMap = (Map<ControllerAdviceBean, Set<Method>>) initBinderAdviceCache.get(handlerAdapter);
        log.debug("全局的 @InitBinder 方法 {}",
                globalMap.values().stream()
                        .flatMap(ms -> ms.stream().map(Method::getName))
                        .collect(Collectors.toList())
        );
        // 通过反射查看 RequestMappingHandlerAdapter 对象中的 initBinderCache 属性,该属性是用于缓存 @InitBinder 注解的解析结果,避免重复解
        Field initBinderCache = RequestMappingHandlerAdapter.class.getDeclaredField("initBinderCache");
        initBinderCache.setAccessible(true);
        Map<Class<?>, Set<Method>> controllerMap = (Map<Class<?>, Set<Method>>) initBinderCache.get(handlerAdapter);
        log.debug("控制器的 @InitBinder 方法 {}",
                controllerMap.entrySet().stream()
                        .flatMap(e -> e.getValue().stream().map(v -> e.getKey().getSimpleName() + "." + v.getName()))
                        .collect(Collectors.toList())
        );
    }
}

运行结果

16:24:20.355 [main] DEBUG com.moon.springmvc.test.ControllerAdviceInitBinderTest - 1. 初始化...
16:24:20.358 [main] DEBUG com.moon.springmvc.test.ControllerAdviceInitBinderTest - 全局的 @InitBinder 方法 [binder]
16:24:20.359 [main] DEBUG com.moon.springmvc.test.ControllerAdviceInitBinderTest - 控制器的 @InitBinder 方法 []
16:24:20.361 [main] DEBUG com.moon.springmvc.test.ControllerAdviceInitBinderTest - 2. 模拟调用 InitBinderController1 的 foo 方法时 ...
16:24:20.367 [main] DEBUG com.moon.springmvc.test.ControllerAdviceInitBinderTest - 全局的 @InitBinder 方法 [binder]
16:24:20.368 [main] DEBUG com.moon.springmvc.test.ControllerAdviceInitBinderTest - 控制器的 @InitBinder 方法 [InitBinderController1.binder1]
16:24:20.368 [main] DEBUG com.moon.springmvc.test.ControllerAdviceInitBinderTest - 3. 模拟调用 InitBinderController2 的 bar 方法时 ...
16:24:20.368 [main] DEBUG com.moon.springmvc.test.ControllerAdviceInitBinderTest - 全局的 @InitBinder 方法 [binder]
16:24:20.368 [main] DEBUG com.moon.springmvc.test.ControllerAdviceInitBinderTest - 控制器的 @InitBinder 方法 [InitBinderController2.binder1, InitBinderController2.binder2, InitBinderController1.binder1]

@ControllerAdvice 与 @ModelAttribute 配合实现流程

流程图

@ModelAttribute 在整个 HandlerAdapter 调用过程中所处的位置

调用流程大致如下:

  1. RequestMappingHandlerAdapter 初始化时会解析 @ControllerAdvice 中的 @ModelAttribute 方法
  2. RequestMappingHandlerAdapter 会以类为单位,在该类首次使用时,解析此类的 @ModelAttribute 方法
  3. 以上两种 @ModelAttribute 的解析结果都会缓存来避免重复解析
  4. 控制器方法调用时,会综合利用本类的 @ModelAttribute 方法和 @ControllerAdvice 中的 @ModelAttribute 方法创建模型工厂

模拟 @ModelAttribute 在控制器方法调用示例

源码详见 springmvc-sample 项目中的 10-handleradapter 模块中 ControllerAdviceModelAttributeTest 测试类

  • 准备测试的带 @ModelAttribute 注解的 controller
java
// 带 @ModelAttribute 注解的增强控制器
@ControllerAdvice
public class ModelAttributeControllerAdvice {
    @ModelAttribute("AA")
    public String foo() {
        return "ModelAttributeControllerAdvice.foo()方法返回值";
    }
}

// 带 @ModelAttribute 注解的控制器
public class ModelAttributeController {
    @ModelAttribute("BB")
    public String foo() {
        return "ModelAttributeController.foo()方法返回值";
    }

    @ResponseStatus(HttpStatus.OK)
    public ModelAndView bar(@ModelAttribute("u") User user) {
        System.out.println("ModelAttributeController.bar 方法执行了...");
        return null;
    }
}
  • 测试代码,观察 @ModelAttribute 标识的方法与参数相应的值是否加入到 ModelAndViewContainer
java
public class ControllerAdviceModelAttributeTest {

    @Test
    public void test() throws Exception {
        // 创建基于注解的容器
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfiguration.class);
        // 手动创建 RequestMappingHandlerAdapter
        RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
        adapter.setApplicationContext(context);
        // 初始化参数处理器(HandlerMethodArgumentResolverComposite)与返回值处理器(HandlerMethodReturnValueHandlerComposite)
        // 这里会解析标识 @ControllerAdvice 的类中的 @ModelAttribute
        adapter.afterPropertiesSet();

        MockHttpServletRequest request = new MockHttpServletRequest();
        request.setParameter("name", "张三");
        /* 现在可以通过 ServletInvocableHandlerMethod 把这些整合在一起, 并完成控制器方法的调用, 如下 */
        ServletInvocableHandlerMethod handlerMethod =
                new ServletInvocableHandlerMethod(new ModelAttributeController(), ModelAttributeController.class.getMethod("bar", User.class));

        ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);

        handlerMethod.setDataBinderFactory(factory);
        handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
        HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
        composite.addResolvers(
                new RequestParamMethodArgumentResolver(context.getDefaultListableBeanFactory(), false),
                new PathVariableMethodArgumentResolver(),
                new RequestHeaderMethodArgumentResolver(context.getDefaultListableBeanFactory()),
                new ServletCookieValueMethodArgumentResolver(context.getDefaultListableBeanFactory()),
                new ExpressionValueMethodArgumentResolver(context.getDefaultListableBeanFactory()),
                new ServletRequestMethodArgumentResolver(),
                new ServletModelAttributeMethodProcessor(false),
                new RequestResponseBodyMethodProcessor(Arrays.asList(new MappingJackson2HttpMessageConverter())),
                new ServletModelAttributeMethodProcessor(true),
                new RequestParamMethodArgumentResolver(context.getDefaultListableBeanFactory(), true)
        );
        handlerMethod.setHandlerMethodArgumentResolvers(composite);
        // 初始化 ModelAndViewContainer
        ModelAndViewContainer container = new ModelAndViewContainer();

        // 获取模型工厂方法,因为 getModelFactory 方法是私有,此处使用反射调用
        Method getModelFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getModelFactory", HandlerMethod.class, WebDataBinderFactory.class);
        getModelFactory.setAccessible(true);
        ModelFactory modelFactory = (ModelFactory) getModelFactory.invoke(adapter, handlerMethod, factory);

        // 初始化模型数据,将之前记录的 @ModelAttribute 的方法,反射调用后将返回值保存到 ModelAndView 对象
        modelFactory.initModel(new ServletWebRequest(request), container, handlerMethod);

        handlerMethod.invokeAndHandle(new ServletWebRequest(request), container);

        System.out.println(container.getModel());
        context.close();
    }
}

运行结果

ModelAttributeController.bar 方法执行了...
{AA=ModelAttributeControllerAdvice.foo()方法返回值, BB=ModelAttributeController.foo()方法返回值, u=User[name='张三', age=0], org.springframework.validation.BindingResult.u=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

@ControllerAdvice 之 ResponseBodyAdvice

概述

ResponseBodyAdvice 接口是用于返回响应体前进行包装处理,一般配置 @ControllerAdvice 注解使用。

流程图

ResponseBodyAdvice 增强在整个 HandlerAdapter 调用过程中所处的位置

ResponseBodyAdvice 返回值增强示例

示例需求:将标识了 @ResponseBody 注解的控制器方法的返回值包装成统一响应格式。示例源码详见 springmvc-sample 项目中的 10-handleradapter 模块中 ControllerAdviceResponseBodyTest 测试类

  • 创建用于测试的控制器
java
@RestController
// @Controller
// @ResponseBody
public class ResponseBodyDemoController {

    // @ResponseBody
    public User user() {
        return new User("王五", 18);
    }
}
  • 创建控制器增强类,并实现 org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice 接口
java
@ControllerAdvice
public class ResponseBodyControllerAdvice implements ResponseBodyAdvice<Object> {

    /**
     * 用于指定满足哪些条件才进行转换
     *
     * @param returnType    返回的类型
     * @param converterType 选定转换器的类型
     * @return 如返回 true 则代表可以进行转换,否则返回 false
     */
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        /*
         * 这里需要判断三种情况
         *      1. 在控制器方法上标识 @ResponseBody 注解
         *      2. 在控制器类上标识 @ResponseBody 注解  可以使用 returnType.getContainingClass().isAnnotationPresent(ResponseBody.class) 来判断
         *      3. 在控制器类上标识 @RestController 注解
         * 其中 Spring 提供的 AnnotationUtils 工具类的 findAnnotation 方法,可以判断类上或者类上组合注解是否包含某个注解
         */
        if (returnType.getMethodAnnotation(ResponseBody.class) != null ||
                AnnotationUtils.findAnnotation(returnType.getContainingClass(), ResponseBody.class) != null) {
            return true;
        }
        return false;
    }

    /**
     * 响应体转换处理逻辑
     *
     * @param body                  响应数据
     * @return
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof Result) {
            // 如果是 Result 统一响应类型,则直接返回
            return body;
        }
        return Result.ok(body);
    }
}
  • 测试程序
java
@Test
public void test1() throws Exception {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfiguration.class);

    ServletInvocableHandlerMethod handlerMethod = new ServletInvocableHandlerMethod(
            context.getBean(ResponseBodyDemoController.class),
            ResponseBodyDemoController.class.getMethod("user")
    );
    handlerMethod.setDataBinderFactory(new ServletRequestDataBinderFactory(Collections.emptyList(), null));
    handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
    // 此示例的控制器方法没有入参数,所以可以不用设置参数解析器
    // handlerMethod.setHandlerMethodArgumentResolvers(getArgumentResolvers(context));
    // 添加 advice
    List<ControllerAdviceBean> annotatedBeans = ControllerAdviceBean.findAnnotatedBeans(context);
    List<Object> collect = annotatedBeans.stream()
            .filter(b -> ResponseBodyAdvice.class.isAssignableFrom(b.getBeanType()))
            .collect(Collectors.toList());

    HandlerMethodReturnValueHandlerComposite composite = new HandlerMethodReturnValueHandlerComposite();
    // 此示例只测试 @ResponseBody,所以只加 RequestResponseBodyMethodProcessor 返回值处理器
    composite.addHandler(new RequestResponseBodyMethodProcessor(Arrays.asList(new MappingJackson2HttpMessageConverter()), collect));
    handlerMethod.setHandlerMethodReturnValueHandlers(composite);

    MockHttpServletRequest request = new MockHttpServletRequest();
    MockHttpServletResponse response = new MockHttpServletResponse();
    ModelAndViewContainer container = new ModelAndViewContainer();
    handlerMethod.invokeAndHandle(new ServletWebRequest(request, response), container);

    System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
}

控制器方法执行流程

方法调用处理类 ServletInvocableHandlerMethod 关系图

HandlerMethod 方法处理接口有多属性,其中包含:

  • bean 即是调用哪个 Controller
  • method 即是调用 Controller 中的哪个方法

ServletInvocableHandlerMethod 对方法调用进行处理,

  • WebDataBinderFactory 负责对象绑定、类型转换
  • ParameterNameDiscoverer 负责参数名解析
  • HandlerMethodArgumentResolverComposite 负责解析参数
  • HandlerMethodReturnValueHandlerComposite 负责处理返回值

控制器方法执行流程图

图1:方法调用前准备阶段流程图

图2:方法调用阶段流程图

I know where dry desert ends, green grass grows · MooN