@InitBinder
注解定义的方法不仅可以在 Controller 中添加,还可以在@ControllerAdvice
定义的类中:
private static class MyControllerAdvice {
public void globalInitBinder(WebDataBinder binder) {
}
}
Controller 中的 InitBinder 方法仅对所在的 Controller 生效(局部 initBinder),而在 ControllerAdvice 中添加的 initBinder 方法对所有的 Controller 都有效(全局 initBinder)。
在 中提到过,requestMappingHandlerAdapter
负责具体的 Controller 方法调用,因此用于 Controller 方法类型转换的 initBinder 方法也由该类持有:
-
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。
用于打印相关信息。
控制器方法调用过程
控制器方法相关的类图:
HandlerMethod 表示一个控制器方法,包含控制器对象(bean)和方法对象(method)。
ServletInvocableHandlerMethod 表示一个可以调用的控制器方法,除了包含 HandlerMethod(控制器方法)外,还包含:
-
WebDataBinderFactory:用于控制器方法参数的数据绑定和类型转换。
-
ParameterNameDiscoverer:用于获取参数名
-
HandlerMethodArgumentResolverComposite:一组参数解析器,用于从请求或环境变量中解析方法参数
-
HandlerMethodReturnValueHandlerComposite:一组返回值解析器,用于处理返回值(比如包装成 ModelAndView 类型)
具体的控制器方法调用过程:
详细的调用过程说明可以看。
用代码模拟调用过程:
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( ("user1") User user) {
除了这种方式外,还可以用@ModelAttribute
方法添加模型:
"/hello")
(private static class HelloController {
// ...
"user2")
( public User getUser() {
User user = new User();
user.setName("Bruce");
user.setAge(20);
return user;
}
}
与 @InitBinder
类似,也可以在 ControllerAdvice 中添加 ModelAttribute 方法:
private static class MyControllerAdvice{
"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);
}
配置类 中包含模版配置信息, 方法用于渲染视图。
视图文件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
模版进行渲染:
对应的返回值处理器是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
,这样模版引擎就知道请求已经被处理,无需再次处理:
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.
文章评论