红茶的个人站点

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

Spring 源码学习 12:控制器方法

2025年7月2日 11点热度 0人点赞 0条评论

全局和局部 initBinder

@InitBinder注解定义的方法不仅可以在 Controller 中添加,还可以在@ControllerAdvice定义的类中:

@ControllerAdvice
private static class MyControllerAdvice {
    @InitBinder
    public void globalInitBinder(WebDataBinder binder) {
    }
}

Controller 中的 InitBinder 方法仅对所在的 Controller 生效(局部 initBinder),而在 ControllerAdvice 中添加的 initBinder 方法对所有的 Controller 都有效(全局 initBinder)。

在 前文 中提到过,requestMappingHandlerAdapter负责具体的 Controller 方法调用,因此用于 Controller 方法类型转换的 initBinder 方法也由该类持有:

image-20250630164949024

  • initBinderCache:保存 Controller 中定义的 initBinder,key 为 Controller 的类对象,值为 initBinder 方法集合。

  • initBinderAdviceCache:保存全局 initBinder,key 为 ControllerAdvice 对象,值为 initBinder 方法集合。

这里使用了缓存机制,Spring 会在requestMappingHandlerAdapter创建后立即初始化 initBinderAdviceCache,但只有在相应 Controller 的请求调用时才会初始化相应的 initBinderCache。

可以通过一个示例验证这一点:

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
RequestMappingHandlerAdapter requestMappingHandlerAdapter = new RequestMappingHandlerAdapter();
requestMappingHandlerAdapter.setApplicationContext(context);
requestMappingHandlerAdapter.afterPropertiesSet();
showBindMethods(requestMappingHandlerAdapter);
log.info("模拟控制器1方法调用");
Method getDataBinderFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getDataBinderFactory", HandlerMethod.class);
getDataBinderFactory.setAccessible(true);
getDataBinderFactory.invoke(requestMappingHandlerAdapter, new HandlerMethod(context.getBean(Controller1.class), Controller1.class.getMethod("hello", String.class)));
showBindMethods(requestMappingHandlerAdapter);
log.info("模拟控制器2方法调用");
getDataBinderFactory.invoke(requestMappingHandlerAdapter, new HandlerMethod(context.getBean(Controller2.class), Controller2.class.getMethod("bye", String.class)));
showBindMethods(requestMappingHandlerAdapter);

Spring 框架在 RequestMappingHandlerAdapter 创建后会调用afterPropertiesSet初始化,该方法会初始化全局 initBinder,每次执行 Controller 方法前,RequestMappingHandlerAdapter 的 getDataBinderFactory 会被调用,此时会创建对应 Controller 的 initBinder。

showBindMethods用于打印相关信息。

控制器方法调用过程

控制器方法相关的类图:

image-20250630172628151

HandlerMethod 表示一个控制器方法,包含控制器对象(bean)和方法对象(method)。

ServletInvocableHandlerMethod 表示一个可以调用的控制器方法,除了包含 HandlerMethod(控制器方法)外,还包含:

  • WebDataBinderFactory:用于控制器方法参数的数据绑定和类型转换。

  • ParameterNameDiscoverer:用于获取参数名

  • HandlerMethodArgumentResolverComposite:一组参数解析器,用于从请求或环境变量中解析方法参数

  • HandlerMethodReturnValueHandlerComposite:一组返回值解析器,用于处理返回值(比如包装成 ModelAndView 类型)

    具体的控制器方法调用过程:

image-20250630173331325

image-20250630173406417

详细的调用过程说明可以看视频说明。

用代码模拟调用过程:

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
ServletInvocableHandlerMethod handlerMethod = new ServletInvocableHandlerMethod(new HandlerMethod(
        context.getBean(HelloController.class),
        HelloController.class.getMethod("hello", User.class)
));
// 添加参数解析器组
handlerMethod.setHandlerMethodArgumentResolvers(getArgumentResolvers(context));
// 添加数据绑定工厂
handlerMethod.setDataBinderFactory(new ServletRequestDataBinderFactory(null,null ));
// 添加参数名称获取
handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
// 模拟请求
MockHttpServletRequest mockHttpServletRequest = new MockHttpServletRequest();
mockHttpServletRequest.addParameter("name", "Tom");
mockHttpServletRequest.addParameter("age", "20");
ServletWebRequest servletWebRequest = new ServletWebRequest(mockHttpServletRequest);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
// 添加返回值处理器,不加会报错
handlerMethod.setHandlerMethodReturnValueHandlers(getReturnValueHandler());
// 执行控制器方法调用
handlerMethod.invokeAndHandle(servletWebRequest, mavContainer);
context.close();

ModelAttribute

在控制器方法参数中,使用或不使用@ModelAttribute注解,都会由 ModelAttributeMethodProcessor 参数解析器处理,从请求参数中获取并填充属性,然后在 ModelAndView 容器中生成 Model:

// 执行控制器方法调用
handlerMethod.invokeAndHandle(servletWebRequest, mavContainer);
ModelMap model = mavContainer.getModel();
System.out.println(model);

输出:

{user=ControllerMethodCallTests.User(name=Tom, age=20), ...

默认情况生成的 Model 名称会使用形参类型的首字母小写形式。可以通过注解指定 Model 名称:

public String hello(@ModelAttribute("user1") User user) {

除了这种方式外,还可以用@ModelAttribute方法添加模型:

@Log4j2
@Controller("/hello")
private static class HelloController {
    // ...
​
    @ModelAttribute("user2")
    public User getUser() {
        User user = new User();
        user.setName("Bruce");
        user.setAge(20);
        return user;
    }
}

与 @InitBinder 类似,也可以在 ControllerAdvice 中添加 ModelAttribute 方法:

@ControllerAdvice
private static class MyControllerAdvice{
    @ModelAttribute("user3")
    public User user(){
        User user = new User();
        user.setName("Jack");
        user.setAge(18);
        return user;
    }
}

同样的,在 Controller 中添加的,仅对所在的 Controller 有效,在 ControllerAdvice 中添加的,对所有 Controller 都有效(全局)。

全局 Model 的添加由模型工厂(Model Factory)实现:

// 获取模型工厂方法
RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
adapter.setApplicationContext(context);
adapter.afterPropertiesSet();
Method getModelFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getModelFactory", HandlerMethod.class, WebDataBinderFactory.class);
getModelFactory.setAccessible(true);
ModelFactory modelFactory = (ModelFactory)getModelFactory.invoke(adapter, handlerMethod, dataBinderFactory);
// 初始化模型
modelFactory.initModel(servletWebRequest, mavContainer, handlerMethod);
// 执行控制器方法调用
handlerMethod.invokeAndHandle(servletWebRequest, mavContainer);

这里通过RequestMappingHandlerAdapter的getModelFactory获取模型工厂,不过该方法为受保护的,所以需要用反射进行调用。

返回值处理器

ModelAndView

控制器:

@Controller
@RequestMapping("/test")
private static class MyController {
    @GetMapping
    public ModelAndView test() {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("name", "Tom");
        modelAndView.setViewName("view1");
        return modelAndView;
    }
}

test方法返回的是ModelAndView类型,其中包含模型和视图信息,需要模版引擎加载并渲染。这些正是返回值处理器的工作。

用常见的返回值处理器组成返回值处理器组:

private static HandlerMethodReturnValueHandlerComposite getReturnValueHandler() {
    HandlerMethodReturnValueHandlerComposite composite = new HandlerMethodReturnValueHandlerComposite();
    composite.addHandler(new ModelAndViewMethodReturnValueHandler());
    composite.addHandler(new ViewNameMethodReturnValueHandler());
    composite.addHandler(new ServletModelAttributeMethodProcessor(false));
    composite.addHandler(new HttpEntityMethodProcessor(List.of(new MappingJackson2HttpMessageConverter())));
    composite.addHandler(new HttpHeadersReturnValueHandler());
    composite.addHandler(new RequestResponseBodyMethodProcessor(List.of(new MappingJackson2HttpMessageConverter())));
    composite.addHandler(new ServletModelAttributeMethodProcessor(true));
    return composite;
}

显然,ModelAndViewMethodReturnValueHandler用于处理类型为ModelAndView的返回值。

处理返回值:

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
MyController myController = context.getBean(MyController.class);
Method test = MyController.class.getMethod("test");
Object result = test.invoke(myController);
// 获取返回值处理器组
HandlerMethodReturnValueHandlerComposite valueHandlerComposite = getReturnValueHandler();
HandlerMethod handlerMethod = new HandlerMethod(myController, test);
MethodParameter returnType = handlerMethod.getReturnType();
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
ServletWebRequest request = new ServletWebRequest(new MockHttpServletRequest(), new MockHttpServletResponse());
// 判断是否支持的返回值类型
if (valueHandlerComposite.supportsReturnType(returnType)) {
    // 处理返回值
    valueHandlerComposite.handleReturnValue(result, returnType, mavContainer, request);
    ModelMap model = mavContainer.getModel();
    System.out.println(model);
    String viewName = mavContainer.getViewName();
    System.out.println(viewName);
    // 使用模板引擎进行页面渲染
    renderView(context, mavContainer, request);
}

配置类 MyConfig 中包含模版配置信息,renderView 方法用于渲染视图。

视图文件view1.ftl内容:

<!doctype html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>view1</title>
</head>
<body>
<h1>Hello! ${name}</h1>
</body>
</html>

执行测试用例后可以看到模版引擎用 Model 中的数据替换了${name}。

String

@Controller
@RequestMapping("/test")
private static class MyController {
    // ...

    public String test2(){
        return "view2";
    }
}

返回值为 String 类型的控制器方法,其返回值会被当做 VIew 名称处理。在这个示例中,会匹配到view2.ftl模版进行渲染:

image-20250702114048353

对应的返回值处理器是ViewNameMethodReturnValueHandler。

ModelAttribute

控制器中的 ModelAttribute 方法返回的对象将被加入 Model,用于视图渲染:

@ModelAttribute
@RequestMapping("/user")
public User test3(){
    User user = new User();
    user.setName("Tom");
    user.setAge(18);
    return user;
}

此时匹配视图时是根据@RequestMapping路径匹配的,在这个示例中,没有使用 RequestMapping 处理请求路径映射,因此需要用编程的方式在 Request 域中加入路径信息,以用于视图匹配:

// 在域中写入路径信息,用于视图匹配
mockHttpServletRequest.setRequestURI("/test3");
UrlPathHelper.defaultInstance.resolveAndCacheLookupPath(mockHttpServletRequest);

此时视图test3.ftl就可以正常被渲染:

<!doctype html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>test3</title>
</head>
<body>
    <h1>Hello! ${user.name} ${user.age}</h1>
</body>
</html>

在这过程中,使用的返回值处理器是ServletModelAttributeMethodProcessor(false)。

@ModelAttribute并不是必须的:

@RequestMapping("/user")
public User test4(){
    User user = new User();
    user.setName("Tom");
    user.setAge(18);
    return user;
}

返回值依然会被当做 Model 用于视图渲染。区别是此时使用的返回值处理器是ServletModelAttributeMethodProcessor(true)。

HttpEntity

返回值类型为HttpEntity的控制器方法:

public HttpEntity<User> test5() {
    User user = new User("Jerry", 20);
    return new HttpEntity<>(user);
}

其返回值会被当做响应体进行处理:

// 处理返回值
valueHandlerComposite.handleReturnValue(result, returnType, mavContainer, request);
if (!mavContainer.isRequestHandled()) {
    renderView(context, mavContainer, request);
}
// 打印响应头
Collection<String> headerNames = response.getHeaderNames();
for (String headerName : headerNames) {
    String header = response.getHeader(headerName);
    System.out.printf("%s: %s%n", headerName, header);
}
// 打印响应体
byte[] contentAsByteArray = response.getContentAsByteArray();
String content = new String(contentAsByteArray, StandardCharsets.UTF_8);
System.out.println(content);

输出:

Content-Type: application/json
{"name":"Jerry","age":20}

这里使用的返回值处理器是HttpEntityMethodProcessor(List.of(new MappingJackson2HttpMessageConverter()))。

MappingJackson2HttpMessageConverter用于将对象转换为 JSON 字符串。

类似这样的处理返回值后不需要通过模版引擎渲染的处理器,其handleReturnValue方法调用后都会将 ModelAndViewContainer 的 requestHandled 属性设置为 true,这样模版引擎就知道请求已经被处理,无需再次处理:

image-20250702122607933

HttpHeaders

与HttpEntity类似,不过HttpHeaders类型返回的是响应头信息:

public HttpHeaders test6(){
    HttpHeaders headers = new HttpHeaders();
    headers.add("Content-Type", "text/html;charset=utf-8");
    return headers;
}

输出:

Content-Type: text/html;charset=utf-8

使用的返回值处理器是HttpHeadersReturnValueHandler()。

ResponseBody

使用@ResponseBody标记的控制器方法是最常见的:

@ResponseBody
public User test7(){
    return new User("Jerry", 20);
}

其作用与HttpEntity类似,都是返回响应体:

Content-Type: application/json
{"name":"Jerry","age":20}

不同的是,会自动将响应头中的Content-Type设置为 JSON 字符串的格式。

MessageConverter

服务端与客户端之间的请求和响应如果被称为消息,这些消息通常由 JSON 或 XML 格式的对象表示。Spring 使用消息转换器(Message Converter)将对象转换为特定格式的消息,或者将特定格式的消息转换成对象。

换言之,涉及消息转换的参数解析器和返回值处理器,需要指定消息转换器,比如:

composite.addHandler(new ServletModelAttributeMethodProcessor(false));
composite.addHandler(new HttpEntityMethodProcessor(List.of(new MappingJackson2HttpMessageConverter())));
composite.addHandler(new HttpHeadersReturnValueHandler());
composite.addHandler(new RequestResponseBodyMethodProcessor(List.of(new MappingJackson2HttpMessageConverter())));

将对象转换为消息

使用 MessageConverter 将对象转换为 JSON 格式的消息:

// 将对象转换为 JSON 格式的消息
MockHttpOutputMessage mockHttpOutputMessage = new MockHttpOutputMessage();
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
if (converter.canWrite(User.class, MediaType.APPLICATION_JSON)){
    converter.write(new User("Tom", 20), MediaType.APPLICATION_JSON, mockHttpOutputMessage);
    String body = mockHttpOutputMessage.getBodyAsString();
    System.out.println(body);
}

方法canWrite返回值表示消息转换器是否支持目标格式消息的转换。write方法将对象转换为目标格式字符串后写入消息对象。

输出:

{"name":"Tom","age":20}

将对象转换为 XML 格式消息:

// 将对象转换为 JSON 格式的消息
MockHttpOutputMessage mockHttpOutputMessage = new MockHttpOutputMessage();
MappingJackson2XmlHttpMessageConverter converter = new MappingJackson2XmlHttpMessageConverter();
if (converter.canWrite(User.class, MediaType.APPLICATION_XML)){
    converter.write(new User("Tom", 20), MediaType.APPLICATION_XML, mockHttpOutputMessage);
    String body = mockHttpOutputMessage.getBodyAsString();
    System.out.println(body);
}

Spring Boot 默认不包含 jackson xml 模块,需要单独引用:

<dependency>
 <groupId>com.fasterxml.jackson.dataformat</groupId>
 <artifactId>jackson-dataformat-xml</artifactId>
 <version>2.19.1</version> <!-- 需与其他Jackson模块版本一致 -->
</dependency>

版本要与其他 jackson 组件版本保持一致。

XML 格式的 jackson 消息转换器是 MappingJackson2XmlHttpMessageConverter,Media 格式是MediaType.APPLICATION_XML。

将消息转换为对象

将 JSON 消息转换为对象:

// 模拟输入消息
MockHttpInputMessage mockHttpInputMessage = new MockHttpInputMessage("""
        {"name":"LiLei","age":11}
        """
        .getBytes(StandardCharsets.UTF_8));
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
// 判断能否转换消息
if (converter.canRead(User.class, MediaType.APPLICATION_JSON)){
    // 转换消息
    Object user = converter.read(User.class, mockHttpInputMessage);
    System.out.println(user);
}

将 XML 消息转换为对象:

// 模拟输入消息
MockHttpInputMessage mockHttpInputMessage = new MockHttpInputMessage("""
        <User><name>Tom</name><age>20</age></User>
        """
        .getBytes(StandardCharsets.UTF_8));
MappingJackson2XmlHttpMessageConverter converter = new MappingJackson2XmlHttpMessageConverter();
// 判断能否转换消息
if (converter.canRead(User.class, MediaType.APPLICATION_XML)){
    // 转换消息
    Object user = converter.read(User.class, mockHttpInputMessage);
    System.out.println(user);
}

消息转换器的优先级

如果返回值处理器有多个消息转换器:

// 存在多个消息转换器
RequestResponseBodyMethodProcessor requestResponseBodyMethodProcessor = new RequestResponseBodyMethodProcessor(
        List.of(new MappingJackson2XmlHttpMessageConverter(), new MappingJackson2HttpMessageConverter())
);
HandlerMethod handlerMethod = new HandlerMethod(new TestController(), TestController.class.getMethod("test"));
MockHttpServletResponse response = new MockHttpServletResponse();
NativeWebRequest webRequest = new ServletWebRequest(new MockHttpServletRequest(), response);
// 处理控制器方法返回值
requestResponseBodyMethodProcessor.handleReturnValue(new User("Tom", 21),
        handlerMethod.getReturnType(), new ModelAndViewContainer(), webRequest);
String content = response.getContentAsString();
System.out.println(content);

默认情况下按照声明的顺序执行,比如上面的示例,XML 消息转换器会生效,输出:

<User><name>Tom</name><age>21</age></User>

如果将 JSON 消息转换器放在前面,输出结果将是 JSON 字符串。

可以由客户端通过请求报文头指定(希望)服务端返回的响应报文格式:

MockHttpServletRequest request = new MockHttpServletRequest();
// 由客户端指定返回报文格式
request.addHeader("Accept", "text/xml");
NativeWebRequest webRequest = new ServletWebRequest(request, response);

此时返回值处理器会按照Accept指定的 Media 类型进行匹配,以选取一个合适的消息转换器进行转换,因此这个示例将返回 XML 格式的消息(即使 JSON 消息转换器先定义)。

服务端并不是非要按照客户端希望的格式返回响应报文,完全可以强行指定:

// 由客户端指定返回报文格式
request.addHeader("Accept", "text/xml");
// 由服务端强制指定响应报文格式
response.setContentType("application/json;charset=utf-8");
NativeWebRequest webRequest = new ServletWebRequest(request, response);

此时返回值处理器会选取服务端通过响应头Content-Type指定的 Media 类型匹配的消息转换器转换消息,因此最终输出的是 JSON 格式的消息。

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

The End.

参考资料

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

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: message converter spring boot 控制器方法 消息转换器 返回值处理器
最后更新:2025年7月2日

魔芋红茶

加一点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号