红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 专栏
  3. Spring Boot 学习笔记
  4. 正文

Spring 源码学习 9:MVC

2025年6月28日 12点热度 0人点赞 0条评论

Spring 中,用于加载和启动 Web Server 的容器类型是AnnotationConfigServletWebServerApplicationContext:

AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext(Config.class);
new CountDownLatch(1).await();

其配置类需要至少三个 Bean:

static class Config {
    /**
     * Web Server,这里使用 Tomcat
     * @return
     */
    @Bean
    public ServletWebServerFactory servletContainer() {
        return new TomcatServletWebServerFactory();
    }
​
    /**
     * 作为入口的 Servlet
     * @return
     */
    @Bean
    public DispatcherServlet dispatcherServlet() {
        return new DispatcherServlet();
    }
​
    /**
     * 将 Servlet 注册到 Web Server
     * @return
     */
    @Bean
    public ServletRegistrationBean<DispatcherServlet> servletRegistrationBean() {
        return new ServletRegistrationBean<>(dispatcherServlet(), "/");
    }
}

在将作为入口的DispatcherServlet绑定到 Web Server 的时候需要指定路径,通常是/或/*,它们有一些微妙区别:

  • /,作为备选的路径路由选项,如果匹配不到其它的 Servlet,就使用这个 Servlet。

  • /*,任何路径请求都会匹配到这个 Servlet。

DispatcherServlet

容器启动后并不会立即对DispatcherServlet进行初始化。只有在有请求进入时(比如访问 http://localhost:8080/)才会初始化,此时日志中会出现信息:

web.servlet.DispatcherServlet -- Context initialization failed

要让容器创建启动 Tomcat 后立即对 DispatcherServlet 初始化,可以:

@Bean
public ServletRegistrationBean<DispatcherServlet> servletRegistrationBean() {
    ServletRegistrationBean<DispatcherServlet> registrationBean = new ServletRegistrationBean<>(dispatcherServlet(), "/");
    // 立即初始化
    registrationBean.setLoadOnStartup(1);
    return registrationBean;
}

只要通过setLoadOnStartup指定一个大于0的值即可。这个值决定了启动 Tomcat 时 Servlet 的加载(初始化)顺序(值越小越优先)。

当然,这些信息最好从配置文件中获取,比如:

server.port=8081
spring.mvc.servlet.load-on-startup=-1

通过配置类读取:

@PropertySource("classpath:application.properties")
@EnableConfigurationProperties({ServerProperties.class, WebMvcProperties.class})
static class Config {
    @Bean
    public ServletWebServerFactory servletContainer(ServerProperties serverProperties) {
        return new TomcatServletWebServerFactory(serverProperties.getPort());
    }
    // ...
    @Bean
    public ServletRegistrationBean<DispatcherServlet> servletRegistrationBean(WebMvcProperties mvcProperties) {
        ServletRegistrationBean<DispatcherServlet> registrationBean = new ServletRegistrationBean<>(dispatcherServlet(), "/");
        // 立即初始化
        registrationBean.setLoadOnStartup(mvcProperties.getServlet().getLoadOnStartup());
        return registrationBean;
    }
}

initStrategies

Tomcat 初始化 DispatchServlet 时会调用其onRefresh方法:

@Override
protected void onRefresh(ApplicationContext context) {
    initStrategies(context);
}

这个方法实际调用了initStrategies:

/**
     * Initialize the strategy objects that this servlet uses.
     * <p>May be overridden in subclasses in order to initialize further strategy objects.
     */
protected void initStrategies(ApplicationContext context) {
    initMultipartResolver(context);
    initLocaleResolver(context);
    initThemeResolver(context);
    initHandlerMappings(context);
    initHandlerAdapters(context);
    initHandlerExceptionResolvers(context);
    initRequestToViewNameTranslator(context);
    initViewResolvers(context);
    initFlashMapManager(context);
}

DispatcherServlet内部使用一组策略对象(Strategy Objects)决定具体如何处理请求。这些 init 方法对应初始化相应的策略对象,比如:

  • initMultipartResolver,初始化文件上传的策略

  • initLocaleResolver,初始化本地化的策略

  • initHandlerMappings,初始化路径映射的策略

  • initHandlerAdapters,初始化适配器策略

  • initHandlerExceptionResolvers,初始化异常处理策略

这些策略会优先从容器中获取:

image-20250627145709906

this.detectAllHandlerMappings如果为true,会先在本容器查找,如果查找不到,继续从父容器查找,直到根容器。

如果容器中没有,会提供默认的实现:

image-20250627145906676

这些默认实现通过配置文件加载:

image-20250627145954761

image-20250627150056842

image-20250627150121207

RequestMappingHandlerMapping

Spring 中RequestMappingHandlerMapping负责解析控制器中的@RequestMapping注解,并生成请求到处理器方法的映射:

RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
// 获取请求-方法映射
Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
handlerMethods.forEach((k,v)->{
    System.out.println("请求:%s,处理器:%s".formatted(k,v));
});

容器中的控制器:

@Controller
@RequestMapping("/hello")
public class HelloController {
    @GetMapping
    @ResponseBody
    public String hello() {
        return "hello";
    }
​
    @PostMapping("/say")
    @ResponseBody
    public String say(@RequestParam String name) {
        return "say " + name;
    }
​
    @RequestMapping("/bye")
    @ResponseBody
    public String bye(@RequestParam String name) {
        return "bye " + name;
    }
}

打印结果:

请求:{POST [/hello/say]},处理器:com.example.demo.controller.HelloController#say(String)
请求:{GET [/hello]},处理器:com.example.demo.controller.HelloController#hello()
请求:{ [/hello/bye]},处理器:com.example.demo.controller.HelloController#bye(String)

当一个 HTTP 请求产生,Spring 会通过RequestMappingHandlerMapping决定负责处理请求的处理器方法:

HttpServletRequest httpRequest = new MockHttpServletRequest("GET", "/hello");
HandlerExecutionChain handler = handlerMapping.getHandler(httpRequest);

这里通过MockHttpServletRequest构造一个 HTTP 请求的假数据。

返回的结果HandlerExecutionChain不是一个单一的处理器方法,而是包含了处理器方法的一个处理器执行链(Handler Execution Chain),里边包含要执行的处理器方法以及相应的拦截器。

在这个示例中,输出:

HandlerExecutionChain with [com.example.demo.controller.HelloController#hello()] and 2 interceptors

包含两个拦截器(Interceptor)。

RequestMappingHandlerAdapter

Spring 中负责调用处理器方法的是HandlerAdapter,其用于处理@RequestMapping注解定义处理器的实现为RequestMappingHandlerAdapter。

具体调用处理器的方法为invokeHandlerMethod,但其访问修饰符为protected,为了能够模拟调用,需要自定义一个子类以暴露该方法:

public class MyRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {
    @Override
    public ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
        return super.invokeHandlerMethod(request, response, handlerMethod);
    }
}

通过 Bean 方法添加该类:

@Bean
public MyRequestMappingHandlerAdapter handlerAdapter() {
    return new MyRequestMappingHandlerAdapter();
}

使用其调用处理器:

MyRequestMappingHandlerAdapter handlerAdapter = context.getBean(MyRequestMappingHandlerAdapter.class);
HttpServletResponse response = new MockHttpServletResponse();
handlerAdapter.invokeHandlerMethod(httpRequest, response, (HandlerMethod) executionChain.getHandler());

类似的,也可以调用带参数的 POST 方法,只需要为假数据添加相应参数:

MockHttpServletRequest httpRequest = new MockHttpServletRequest("POST", "/hello/say");
httpRequest.setParameter("name", "Tom");
// ...
handlerAdapter.invokeHandlerMethod(httpRequest, response, (HandlerMethod) executionChain.getHandler());

参数和返回值解析器

HandlerAdapter内使用参数解析器(Argument Resolver)来解析和处理控制器方法中的参数注解,比如@RequestParam、@RequestBody等。

List<HandlerMethodArgumentResolver> argumentResolvers = handlerAdapter.getArgumentResolvers();
for (HandlerMethodArgumentResolver argumentResolver : argumentResolvers) {
    System.out.println(argumentResolver);
}

打印:

org.springframework.web.method.annotation.RequestParamMethodArgumentResolver@5b733ef7
org.springframework.web.method.annotation.RequestParamMapMethodArgumentResolver@43a4a9e5
org.springframework.web.servlet.mvc.method.annotation.PathVariableMethodArgumentResolver@764fffa0
...

同样的,由不同注解标记的返回值也有做不同处理,这些由返回值解析器(Return Value Resolver)完成:

List<HandlerMethodReturnValueHandler> returnValueHandlers = handlerAdapter.getReturnValueHandlers();
for (HandlerMethodReturnValueHandler returnValueHandler : returnValueHandlers) {
    System.out.println(returnValueHandler);
}

打印:

org.springframework.web.servlet.mvc.method.annotation.ModelAndViewMethodReturnValueHandler@73bb1337
org.springframework.web.method.annotation.ModelMethodProcessor@685f5d0d
org.springframework.web.servlet.mvc.method.annotation.ViewMethodReturnValueHandler@3830b06c
...

自定义参数解析器

添加一个自定义注解:

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Token {
}

用于标记处理器参数,用其标记的参数表示由前端传入的 token:

@GetMapping
@ResponseBody
public String hello(@Token String token) {
    log.info("hello() is called.");
    log.info("token:%s".formatted(token));
    return "hello";
}

要让这个注解能正常解析,需要添加一个处理这个注解的参数解析器:

public class TokenArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        Token parameterAnnotation = parameter.getParameterAnnotation(Token.class);
        return parameterAnnotation != null;
    }
​
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        String token = webRequest.getHeader("token");
        if (token != null) {
            return token;
        }
        // 头信息中没有,尝试通过查询参数获取
        token = webRequest.getParameter("token");
        return token;
    }
}

supportsParameter方法用于表示能否处理指定参数,resolveArgument将处理参数并返回。

在配置类中添加这个参数处理器:

@Bean
public MyRequestMappingHandlerAdapter handlerAdapter() {
    MyRequestMappingHandlerAdapter handlerAdapter = new MyRequestMappingHandlerAdapter();
    // 添加自定义参数解析器
    handlerAdapter.setCustomArgumentResolvers(List.of(new TokenArgumentResolver()));
    return handlerAdapter;
}

测试:

// ...
RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hello2");
request.addHeader("token", "abc123");
HandlerExecutionChain chain = handlerMapping.getHandler(request);
HandlerMethod handlerMethod = (HandlerMethod) chain.getHandler();
HttpServletResponse response = new MockHttpServletResponse();
handlerAdapter.invokeHandlerMethod(request, response, handlerMethod);

自定义返回值解析器

通常我们需要将控制器方法的输出包装为统一的标准输出格式提供给前端,有两种做法:

  • 在控制器方法中手动封装然后返回。

  • 使用控制器拦截器统一处理封装后返回。

前者太过麻烦,而后者不够灵活。可以通过使用自定义返回值解析器来实现第三种方式——基于注解的控制器方法返回值封装。

添加一个自定义注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResultOk {
}

对需要封装返回值的控制器方法添加这个注解:

@Controller
@RequestMapping("/hello2")
@Slf4j
public class HelloController2 {
    //...
​
    @GetMapping("/info")
    @ResultOk
    public UserInfo info() {
        return new UserInfo("张三", 20);
    }
}

添加处理这个注解的返回值解析器:

public class ResultOkReturnValueResolver implements HandlerMethodReturnValueHandler {
    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        // 有 @ResultOk 注解才处理
        boolean updated = returnType.hasMethodAnnotation(ResultOk.class);
        return updated;
    }
​
    @Override
    public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        // 将返回值包装成标准返回
        StandardResult<?> standardResult = null;
        if (returnValue == null) {
            standardResult = StandardResult.success();
        } else {
            standardResult = StandardResult.success(returnValue);
        }
        // JSON 后写入返回报文的输出流
        ObjectMapper mapper = new ObjectMapper();
        String resultStr = mapper.writeValueAsString(standardResult);
        HttpServletResponse httpServletResponse = webRequest.getNativeResponse(HttpServletResponse.class);
        httpServletResponse.setContentType("application/json");
        httpServletResponse.getWriter().write(resultStr);
        // 告诉 mvc 请求已经处理完毕,无需再处理
        mavContainer.setRequestHandled(true);
    }
}

注意,在将处理后的返回值写入返回报文体后,需要通过mavContainer.setRequestHandled(true)方法告诉 ModelAndView 容器这个请求已经处理完,不需要再处理。

ModelAndView 在 MVC 中负责将控制器返回的数据结合页面模版由模版引擎生成最终的页面视图,在这个示例以及目前流行的前后端分离开发中,不再需要 MAV 处理页面。

在配置类中将返回值解析器添加到HandlerAdapter使其生效:

@Bean
public MyRequestMappingHandlerAdapter handlerAdapter() {
    MyRequestMappingHandlerAdapter handlerAdapter = new MyRequestMappingHandlerAdapter();
    // 添加自定义返回值解析器
    handlerAdapter.setCustomReturnValueHandlers(List.of(new ResultOkReturnValueResolver()));
    return handlerAdapter;
}

测试用例:

RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hello2/info");
HandlerExecutionChain chain = handlerMapping.getHandler(request);
HandlerMethod handlerMethod = (HandlerMethod) chain.getHandler();
MockHttpServletResponse response = new MockHttpServletResponse();
handlerAdapter.invokeHandlerMethod(request, response, handlerMethod);
String content = response.getContentAsString();
System.out.println(content);

本文的完整示例可以从这里获取。

The End.

参考资料

  • 黑马程序员Spring视频教程,深度讲解spring5底层原理

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: mvc spring boot
最后更新:2025年6月28日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇
下一篇 >

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号