Spring源码 (三) - 服务器处理请求流程
errol发表于2025-10-01 | 分类为 编程 | 标签为springspring-mvcjava

本文是spring框架源码学习系列的第三篇。

在有了bean和ioc的基础认识之后,这次来学习一个稍微重量级的框架,spring-webmvc(spring-mvc)。

它是用于构建基于传统MVC(Model-View-Controller)设计模式的web应用程序,是对servlet技术栈的高级封装,支持构建动态网页、RESTful API等服务。

一般来说,只要涉及到web服务器开服,就离不开作为基石的spring-mvc,因此,其重要性是不言而喻的。

当然,现在还有用于构建非阻塞、响应式应用的框架,Spring WebFlux,是对传统spring-mvc的补充,但不在今天的讨论范围内

基本的工作流程如下:

image

图1 spring-mvc处理请求

本文会基于该流程图作进一步分析,尽可能地把工作原理捋清楚。

一、准备阶段

配置本项目的运行环境,包括依赖、代码等。

1、项目结构

整体上,与之前变化不大。

.
|___debug-2.iml
|___pom.xml
|___readme.txt
|___src
| |___test
| | |___java
| |___main
| | |___resources
| | |___java
| | | |___com
| | | | |___err0l
| | | | | |___spring
| | | | | | |___debug2
| | | | | | | |___Map2JsonHttpMessageConverter.java
| | | | | | | |___Debug2Application.java
| | | | | | | |___TestController.java
| | | | | | | |___AppConfig.java

2、添加依赖

在pom.xml文件中添加如下配置,新增了webmvc和tomcat的依赖。

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>6.2.9</version>
    </dependency>
    <dependency>
        <groupId>jakarta.servlet</groupId>
        <artifactId>jakarta.servlet-api</artifactId>
        <version>6.0.0</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-core</artifactId>
        <version>11.0.9</version>
    </dependency>
    <dependency>
        <groupId>org.apache.tomcat</groupId>
        <artifactId>tomcat-juli</artifactId>
        <version>11.0.9</version>
    </dependency>
</dependencies>

3、基本代码

1)配置类

@EnableWebMvc注解在加载了spring-webmvc后存在,用于导入spring的WebMVC配置。

这个注解主要导入org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport类,该类向ioc容器中注册了必要的组件,如RequestMappingHandlerMapping、ContentNegotiationManager、Validator等。

支持定制化,如下面的代码中,通过重写默认方法来向容器中添加了一个自定义的HttpMessageConverter:一个简易的将Map转为json的实现。

@EnableWebMvc
@Configuration
@ComponentScan
public class AppConfig implements WebMvcConfigurer {
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new Map2JsonHttpMessageConverter());
    }
}
2)启动类

main()方法里编程式地创建一个tomcat服务器实例,同时实例化spring应用上下文(ioc容器),以及本文主要的研究对象,中央处理器DispatcherServlet

将其映射到"/"路径下,以接管全部来自客户端的请求,并挂载到tomcat实例中。

使用嵌入式tomcat的原因是,看起来比较直观;事实上,可以认为spring-webmvc注册的所有组件,都是为了辅助DispatcherServlet运行

public class Debug2Application {
    public static void main(String[] args) throws LifecycleException {
        Tomcat tomcat = new Tomcat();
        tomcat.setPort(8080);
        tomcat.getConnector();

        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        context.register(AppConfig.class);

        DispatcherServlet servlet = new DispatcherServlet(context);
        Context ctx = tomcat.addContext("", null);
        final ErrorPage errorPage = new ErrorPage();
        errorPage.setLocation("/error");
        ctx.addErrorPage(errorPage);
        Tomcat.addServlet(ctx, "dispatcher", servlet);
        ctx.addServletMappingDecoded("/", "dispatcher");

        tomcat.start();
        tomcat.getServer().await();
    }
}
3)控制器

包含了两个简单的接口实现,一个返回json数据,另一个返回html页面。

@Controller
public class TestController {
    @GetMapping("/test1")
    @ResponseBody
    public Map<String, String> test1() {
        Map<String, String> map = new HashMap<>();
        map.put("key", "hello");
        map.put("value", "world");
        return map;
    }

    @GetMapping(value = "/test2")
    public ModelAndView test2() {
        final ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("hello");
        return modelAndView;
    }
}

4、设置断点

已经配置DispatcherServlet处理所有的请求,因此,只需要在其中设置断点即可。

image

图1-1 在DispatcherServlet#doDispatch()中设置断点

完成后,启动应用。

二、"/test1"接口

本文是这样安排的,首先分析返回json数据的"/test1"接口,然后在此基础上,分析返回html页面的接口"/test2"。

向服务器发起请求,使得程序执行到断点处。

curl http://localhost:8080/test1 -H 'Accept: application/json'

1、获取请求处理器

首先是调用getHandler()方法,获取到可以处理当前请求的方法,顺便一提,如果没有找到的话,就走404流程。

image

图2-1 获取请求处理器-1

默认情况下,spring-mvc注册了三个处理器映射(HandlerMapping),分别是RouterFunctionMapping、RequestMappingHandlerMapping、BeanNameUrlHandlerMapping,里面保存了请求路由和请求处理程序的映射关系。

其中,控制器处理方法存储于RequestMappingHandlerMapping中。

image

图2-2 获取请求处理器-2

找到请求处理方法后,并不直接返回,而是又将其包装为处理器执行链(HandlerExecutionChain)

所谓执行链,其实就是一系列的拦截器,它们作用于请求处理前后,负责像是鉴权、校验参数这样的工作。

image

图2-3 获取请求处理器-3

2、将处理器包装为处理器适配器

这一步是spring为了屏蔽不同处理器之间的差异而采取的手段,将返回值统一定义为ModelAndView类型,像是中介一样的角色。

此处适配器的具体类型为RequestMappingHandlerAdapter。

image

图2-4 获取处理器适配器

3、调用请求处理器

- ha.handle(processedRequest, response, mappedHandler.getHandler());
-- handleInternal(request, response, (HandlerMethod) handler);
--- invokeHandlerMethod(request, response, handlerMethod);
---- invocableMethod.invokeAndHandle(webRequest, mavContainer);

其中,在invokeHandlerMethod(request, response, handlerMethod)里,设置一些必要的数据,如参数解析器(HandlerMethodArgumentResolver)、返回值处理器(HandlerMethodReturnValueHandler),以及ModelAndViewContainer等。

4、解析参数

spring-mvc已经提前注册了参数解析器,因此可以在控制器方法中自定义参数类型,在符合条件的情况下,spring会自动注入。

如果处理方法有参数的话,就在getMethodArgumentValues()方法中,使用参数解析器完成注入。

如比较常见的Request、Response、Session等对象。

此外,也支持声明请求路径、请求url等中的参数,但需要配合@PathVariable和@RequestParam注解使用,假设存在url参数:/test1?name=dd,则可以这样声明:

test1(@RequestParam String name)

一般来说,都是从ServletRequest实例中获取的数据,自定义参数只是一种“快捷方式”。

以Session对象为例,可以在方法中直接声明:test1(HttpSession session),也可以间接通过ServletRequest对象获取:test1(HttpServletRequest request) { HttpSession session = request.getSession(); }

@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
    Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
...
}

5、调用方法

将解析出的参数传入方法,通过反射调用,并拿到返回值。

image

图2-5 调用控制器方法

6、处理返回值

与参数解析类似,spring-mvc同样注册了大量的返回值处理器,以应对处理器中不同类型返回值。

image

图2-6 处理控制器方法返回值

7、选择处理器

默认有15个,由于test1()方法使用了@ResponseBody标注,指示返回值应该写入响应体,所以这里选择的是RequestResponseBodyMethodProcessor。

image

图2-7 获取可处理当前返回值的处理器

8、调用返回值处理器方法

一般情况下,Spring MVC控制器方法的返回值有两种处理方式:

第一种是返回字符串;被当作视图名称(View Name),用于查找模板(如JSP、Thymeleaf)进行渲染;

第二种是返回对象(非ModelAndView等对象);返回对象视为模型(Model),方法名被当做视图名称,然后渲染。

二者都会尝试返回html页面,也就是传统的服务器渲染(SSR)。

但在使用了@ResponseBody注解标准的情况下,将不再是以上的形式,而是会把返回值写入响应体。

其中,”mavContainer.setRequestHandled(true)“将当前请求标记为”已处理“状态,为了规避后续将当前请求使用默认的方式处理。

image

图2-8 将当前请求标记为"已处理"

9、使用消息转换器将返回值写入响应体

writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);

显然,java类型的返回值不能直接写入响应体,而必须转换为能在http协议中传输的格式,如字符串、字节等。

消息转换器就是为负责这部分工作而存在,如开头添加的Map2JsonHttpMessageConverter。

1)内容协商

服务器按客户端可接受的媒体类型来响应请求,这样的机制称为“内容协商(Content Negotiation)”。

spring-mvc也提供了这种机制,这里主要针对的是基于Accept请求头字段的内容协商。

大致运行方式是:读取客户端传输的Accept头部字段,与服务器可产出的媒体类型相比较,选出一个最佳的类型。

如,Accept为"application/json",而服务器也可以产出这种类型,且权重最高则"application/json"为最佳类型。

有两个触发内容协商的地方:

第一个是在选择处理器的时候,如果存在多个处理同一路径的处理器时,会进行内容协商,并选择出最佳处理器;

第二个是在处理返回值的时候,如果返回值是ResponseEntity类型,或者使用@ResponseBody注解标注(包括@RestController)时,会进行内容协商。

本文主要说的是第二种情况,对于第一种,一般一个路径只对应一个处理器,所以不做考虑。

这其实是依赖了一系列的消息转换器,它们可以将对应返回值类型转为对应的媒体类型,比如本的文自定义消息转换器,支持将Map类型转为json数据。

public class Map2JsonHttpMessageConverter extends AbstractHttpMessageConverter<Map<Object, Object>> {

    Map2JsonHttpMessageConverter() {
        super(StandardCharsets.UTF_8, MediaType.APPLICATION_JSON, MediaType.ALL);
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        return Map.class.isAssignableFrom(clazz);
    }

    @Override
    protected Map<Object, Object> readInternal(Class<? extends Map<Object, Object>> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        return null;
    }

    @Override
    protected void writeInternal(Map<Object, Object> map, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        HttpHeaders headers = outputMessage.getHeaders();
        final MediaType contentType = headers.getContentType();
        Charset charset = null;
        if (contentType != null) {
            charset = contentType.getCharset();
        }
        if (charset == null) {
            charset = getDefaultCharset();
        }
        StringBuilder sb = new StringBuilder("{");
        for (Map.Entry<Object, Object> entry : map.entrySet()) {
            sb.append("\"").append(entry.getKey()).append("\":");
            Object value = entry.getValue();
            if (value instanceof String) {
                sb.append("\"").append(value).append("\"");
            } else {
                sb.append(value);
            }
            sb.append(",");
        }
        if (sb.length() > 1) sb.setLength(sb.length() - 1);
        sb.append("}");
        assert charset != null;
        StreamUtils.copy(sb.toString(), charset, outputMessage.getBody());
    }
}

获取客户端可接收的,以及服务器可产出的媒体类型,并以此为依据,选出最佳的一个。

image

图2-9 获取服务器可产出的媒体类型

List<MediaType> compatibleMediaTypes = new ArrayList<>();
// 决策出客户端可接受的媒体类型
determineCompatibleMediaTypes(acceptableTypes, producibleTypes, compatibleMediaTypes);

// ...

// 根据权重排序
MimeTypeUtils.sortBySpecificity(compatibleMediaTypes);

// 选择一个最合适的类型
for (MediaType mediaType : compatibleMediaTypes) {
    if (mediaType.isConcrete()) {
        selectedMediaType = mediaType;
        break;
    }
    // ...
}
2)调用消息转换器的write()方法

选出支持写入"最佳"媒体类型的消息转换器,调用其write()方法,将返回值(body)写入响应对象中。

image

图2-10 写入

- converter.write(body, selectedMediaType, outputMessage)
-- writeInternal(t, outputMessage);
--- StreamUtils.copy(sb.toString(), charset, outputMessage.getBody());

调用StreamUtils.copy()方法,将字符串写入响应对象。

image

图2-11 调用消息转换器的writeInternal()完成写入

由于已经事先将当前请求标记为“已处理”状态,所以后续的getModelAndView()返回null,表示不需要处理视图相关的逻辑。

或换句话说,当前请求的主要流程已经完成,这就是服务器返回json等数据类型的一般流程。

image

图2-12 客户端成功接收到数据

三、"/test2"接口

虽然两个接口的返回类型不同(一个是json数据,另一个是html页面),但处理流程基本相同。

分歧点在于"处理返回值"的部分,所以这里直接从"选择处理器"部分开始分析,有需要时参考之前的内容。

请求服务器:curl http://localhost:8080/test2 -H 'Accept: text/html'

1、选择处理器

与"/test1"接口不同,这里选择的返回值处理器类型为:ModelAndViewMethodReturnValueHandler。

image

图3-1 获取到"/test2"接口的返回值处理器

2、调用返回值处理器方法

为mavContainer设置视图名称,并将返回值(即ModelAndView实例)的属性设置到其中。

image

图3-2 调用返回值处理器的处理方法

3、获取ModelAndView

调用getModelAndView(),又重新创建了一个ModelAndView对象。

image

图3-3 得到ModelAndView对象

虽然看起来确实有点鸡肋,但实则是为了兼容。

比方说,如果返回值不是ModelAndView,而是字符串时,就变成:String -> ModelAndViewContainer -> ModelAndView。

4、处理ModelAndView

处理器适配器得到返回值后,调用processDispatchResult()方法进行处理。

image

图3-4 得到ModelAndView对象

如果ModelAndView mv不为空的话,调用其渲染方法。

image

图3-5 渲染视图

根据视图名称解析出视图,需要用到视图解析器(ViewResolver)。

但由于本项目没有配置任何模板引擎的视图解析器(如jsp、freemaker、thymeleaf等),所以实际上它使用的是spring-mvc内部的视图解析器,解析出的视图的作用就是在内部转发了一次,相当于发起了一次"/hello"请求。

image

图3-6 解析视图

- resolveViewName(viewName, mv.getModelInternal(), locale, request)
-- resolveViewNameInternal(viewName, locale)
-- view.render(mv.getModelInternal(), request, response)
--- renderMergedOutputModel(mergedModel, getRequestToExpose(request), response)
---- rd.forward(request, response)

因为没有处理"/hello"的接口,所以最终的结果就是返回404。

image

图3-7 找不到"/hello"路径的处理器

image

图3-8 客户端收到404页面

5、集成Thymeleaf

在pom.xml文件中新增如下依赖:

<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf</artifactId>
    <version>3.1.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf-spring6</artifactId>
    <version>3.1.2.RELEASE</version>
</dependency>

修改配置类:

@EnableWebMvc
@Configuration
@ComponentScan
public class AppConfig implements WebMvcConfigurer, ApplicationContextAware {

    ApplicationContext applicationContext;

    // ...

    @Bean
    public SpringResourceTemplateResolver templateResolver(){
        SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
        templateResolver.setApplicationContext(applicationContext);
        templateResolver.setPrefix("classpath:/templates/");
        templateResolver.setSuffix(".html");
        templateResolver.setTemplateMode(TemplateMode.HTML);
        templateResolver.setCacheable(true);
        return templateResolver;
    }

    @Bean
    public SpringTemplateEngine templateEngine(){
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver());
        templateEngine.setEnableSpringELCompiler(true);
        return templateEngine;
    }

    @Bean
    public ThymeleafViewResolver viewResolver(){
        ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
        viewResolver.setTemplateEngine(templateEngine());
        viewResolver.setOrder(1);
        return viewResolver;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

然后在resources/templates/中新增hello.html:

image

图3-9 hello.html

6、解析视图

集成Thymeleaf后,再次发起请求时,将使用ThymeleafViewResolver来解析视图。

image

图3-10 使用ThymeleafViewResolver解析视图

在不启用缓存或无缓存的情况下,创建视图。

image

图3-11 创建视图

7、渲染视图

运行ThymeleafView的渲染逻辑。简单概括就是,定位到模板文件,并完成解析和填充模型中的数据。

- view.render(mv.getModelInternal(), request, response)
-- renderFragment(this.markupSelectors, model, request, response)
--- viewTemplateEngine.process(templateName, processMarkupSelectors, context, templateWriter)
---- process(new TemplateSpec(template, templateSelectors, null,  null,null), context, writer)
----- templateManager.parseAndProcess(templateSpec, context, writer)

image

图3-12 获取到模板文件路径

最后以writer.flush()结束,以保证所有的数据已写入到输出流中。

image

图3-13 完成数据写入

最终客户端接收到hello页面,当然,如果是浏览器的话,会渲染这个文档。

image

图3-14 客户端收到响应

至此,返回html页面的部分也分析完毕。

四、结语

本文先后分析了服务器返回json数据和返回html页面的流程。

在tomcat接收到请求后,通过调用中央处理器DispatcherServlet的doDispatch()方法来完成响应。

基本流程为:tomcat -> 中央处理器 -> 查找处理方法 -> 调用 -> 得到返回值 -> 查找返回值处理器 -> 调用 -> 处理ModelAndView -> 不为空则渲染。

对于返回json数据的接口,如使用@ResponseBody注解标注,其返回值使用RequestResponseBodyMethodProcessor来处理,通过HttpMessageConverter完成数据写入;

对于返回html页面的接口,如本文的"/test2"接口,其返回值ModelAndView则使用ModelAndViewMethodReturnValueHandler进行处理,使用视图解析器解析出视图解后,渲染视图。

尽管有所区别,但整体上保持一致。

如有需要需,请点击这里前往github获取代码。

以上。

返回