《学会 SpringMVC 系列 · 消息转换器 MessageConverters》

📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗
🌻 CSDN入驻不久,希望大家多多支持,后续会继续提升文章质量,绝不滥竽充数,欢迎多多交流。👍

CSDN.gif

写在前面的话

前几篇博文,大致了解了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了,代表消息转换器只会执行一个。
image.png


注意事项

如果接口入参去掉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)
image.png

【针对 @RequestBody 和 @ResponseBody 场景】
image.png


总结陈词

本篇博文继请求链路源码分析后,继续介绍了MessageConverters的用法,它既可以用在入参处理,也可以用于返回值处理,挺方便的,欲知后事如何,请听下回分解。
💗 后续会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。

CSDN_END.gif

  • 39
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

战神刘玉栋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值