📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗
🌻 CSDN入驻不久,希望大家多多支持,后续会继续提升文章质量,绝不滥竽充数,欢迎多多交流。👍
文章目录
- 写在前面的话
- MessageConverters
- 技术说明
- 基础示例
- 执行过程
- 注意事项
- 源码知识回顾
- 总结陈词
写在前面的话
前几篇博文,大致了解了SpringMVC
请求流程中的参数与返回值的源码分析,后续的几篇博文,会将流程中涉及的若干关键环节单独拿出来讲解,并结合实战中的运用,帮助领略SpringMVC
带来的定制和扩展能力。
本篇文章先介绍一下 MessageConverters 相关内容。
相关博文
《学会 SpringMVC 系列 · 基础篇》
《学会 SpringMVC 系列 · 剖析篇(上)》
《学会 SpringMVC 系列 · 剖析入参处理》
《学会 SpringMVC 系列 · 剖析出参处理》
《学会 SpringMVC 系列 · 返回值处理器》
《程序猿入职必会(1) · 搭建拥有数据交互的 SpringBoot 》
MessageConverters
技术说明
作用:MessageConverters 主要负责将 Controller 方法的返回值转换为 HTTP 响应的内容。
工作原理:当 Controller 方法返回一个对象时,Spring MVC 使用消息转换器将该对象转换为 HTTP 响应体的内容。消息转换器负责将 Java 对象转换为特定的媒体类型,例如 JSON、XML、HTML 等。Spring 提供了各种内置的消息转换器来支持不同的数据格式。
示例:如果你的 Controller 方法返回一个对象,Spring MVC 将根据请求的 Accept 头部信息和返回值类型选择适当的消息转换器,将对象转换为对应的媒体类型。
基础示例
描述:写了一个测试的效果,针对Student类型入参做了一个特定转换操作,部分代码见下方。
补充:要实现自定义消息转换器(入参和出参都适用),就继承 AbstractHttpMessageConverter,实现相应方法,有点类似参数解析器。
Step1、自定义消息转换器
public class MyMessageConverter extends AbstractHttpMessageConverter<Student> {
/**
* 新建一个我们自定义的媒体类型application/xxx-lw
*/
public MyMessageConverter() {
super(new MediaType("application", "xxx-lw", StandardCharsets.UTF_8));
}
/**
* 支持的类型
*/
@Override
protected boolean supports(Class<?> clazz) {
return Student.class.isAssignableFrom(clazz);
}
/**
* 入参处理器
*/
@Override
protected Student readInternal(Class<? extends Student> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
String str = StreamUtils.copyToString(inputMessage.getBody(), StandardCharsets.UTF_8);
String[] split = str.split(",");
String name = split[0].split("#")[1];
String age = split[1].split("#")[1];
return Student.builder()
.name(name)
.age(Integer.parseInt(age))
.id(1)
.build();
}
/**
* 重写writeInternal ,处理如何输出数据到response。
*/
@Override
protected void writeInternal(Student user, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
String outStr = "获取到名称为" + user.getName() + ",年龄" + user.getAge() + "岁的人";
outputMessage.getBody()
.write(outStr.getBytes());
}
}
Step2、配置消息转换器
//下方两个配置方式,保留一个,是JSON序列化解析的逻辑,和MyMessageConverter无关
/**
* 扩展原有方式实现
*/
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
for (HttpMessageConverter<?> httpMessageConverter : converters) {
if (MappingJackson2HttpMessageConverter.class.isAssignableFrom(httpMessageConverter.getClass())) {
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = (MappingJackson2HttpMessageConverter) httpMessageConverter;
ObjectMapper objectMapper = mappingJackson2HttpMessageConverter.getObjectMapper();
objectMapper.getSerializerProvider()
.setNullValueSerializer(new JsonSerializer<Object>() {
@Override
public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString("");
}
});
mappingJackson2HttpMessageConverter.setObjectMapper(objectMapper);
}
}
converters.add(0, new MyMessageConverter());
}
/**
* 添加自定义消息转换器
* BigInteger转String
* NULL转空字符串
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = SpringContextHolder.getBean(ObjectMapper.class);
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance);
simpleModule.addSerializer(BigDecimal.class, ToStringSerializer.instance);
objectMapper.registerModule(simpleModule);
jackson2HttpMessageConverter.setObjectMapper(objectMapper);
converters.add(0, jackson2HttpMessageConverter);
}
Step3、编写测试接口
/**
* 测试自定义消息转换器
*/
@RequestMapping("/stuTest")
public Student stuTest(@RequestBody Student stu) {
return Student.builder()
.id(1)
.name(stu.getName())
.age(stu.getAge())
.email(null)
.build();
}
Step4、启动服务,PostMan测试效果,信息如下:
curl --location 'http://localhost:8083/stuTest' \
--header 'Content-Type: application/xxx-lw' \
--header 'Cookie: JSESSIONID=2BFDA24061FF974C50BECD540FB916D1' \
--data 'name#张三,age#20'
返回结果:获取到名称为张三,年龄20岁的人
执行过程
1、由于添加了@RequestBody,直接进入 RequestResponseBodyMethodProcessor#resolveArgument;
2、接着代码走到 AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters,选取合适的入参转换;
3、由于 MyMessageConverter 是首个转换器,supports 方法也满足,被触发其 read 方法,实际是 MyMessageConverter#readInternal;
4、接着执行核心业务方法;
5、接着代码走到 AbstractMessageConverterMethodArgumentResolver#writeWithMessageConverters,选取合适的出参转换;
6、这里 MyMessageConverter 继续被匹配,执行 write 方法,实际是 MyMessageConverter#writeInternal;
7、最后就是 outputMessage.getBody().flush(),流程结束。
Tips:这里看到read前后,有beforeBodyRead和afterBodyRead,也都是可以扩展的,write 只有 beforeBodyWrite 方法,因为写完就结束了。
【截图补充】
AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters,可以看到注册的自定义消息转换器已经在第一个,判断符合要求后会执行。
可以看出找到符合的消息转换器,直接break了,代表消息转换器只会执行一个。
注意事项
如果接口入参去掉RequestBody注解,再测试一下。
那现象是进入自定义的入参解析器,不会进入入参转换器。但由于结果还是ResponseBody,因此还是会进入出参转换器。
这个也是很好理解的!
源码知识回顾
本篇为 SpringMVC 源码分析系列文章,正片开始前,先总结回顾一下全流程。
【一次请求的主链路节点】
DispatcherServlet#doDispatch(入口方法)
DispatcherServlet#getHandler(根据path
找到对应的HandlerExecutionChain
)
DispatcherServlet#getHandlerAdapter(根据handle
找到对应的HandlerAdapter
)
HandlerExecutionChain#applyPreHandle(触发拦截器的前置逻辑)
AbstractHandlerMethodAdapter#handle(核心逻辑)
HandlerExecutionChain#applyPostHandle(触发拦截器的后置逻辑)
【核心handle方法的主链路节点】
RequestMappingHandlerAdapter#handleInternal(入口方法)
RequestMappingHandlerAdapter#invokeHandlerMethod(入口方法2)
ServletInvocableHandlerMethod#invokeAndHandle(入口方法3)
InvocableHandlerMethod#invokeForRequest(参数和实际执行的所在,3.1)
InvocableHandlerMethod#getMethodArgumentValues(参数处理,3.1.1)
InvocableHandlerMethod#doInvoke(实际执行,3.1.2)
HandlerMethodReturnValueHandlerComposite#handleReturnValue(返回处理,3.2)
【针对 @RequestBody 和 @ResponseBody 场景】
总结陈词
本篇博文继请求链路源码分析后,继续介绍了MessageConverters
的用法,它既可以用在入参处理,也可以用于返回值处理,挺方便的,欲知后事如何,请听下回分解。
💗 后续会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。