如何让SpringMVC框架使用我们封装的JsonUtils实现消息的序列化和反序列化

时间:2022-07-24
本文章向大家介绍如何让SpringMVC框架使用我们封装的JsonUtils实现消息的序列化和反序列化,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

关注 “Java艺术” 我们一起成长!

你好!我是吴就业,一名普普通通的Java开发者。

这是我的第165篇原创文章,欢迎阅读!

spring mvc默认使用的json序列化和反序列工具是jackson,虽然我们项目中也是默认使用jackson,但由于一些历史项目存在日期格式不统一问题,我们需要自定义针对日期类型字段自适应解析,支持将时间戳、各种格式日期字符串都能解析为Date类型实例、LocalDateLocalDateTime类型实例。

除此之外,笔者选择自己封装json解析框架,项目中引入了哪个json框架的依赖,JsonUtils工具类就自动切换使用哪个框架,因此笔者约定禁用json框架的注解,统一使用Javatransient关键字声明不需要序列化和反序列的字段,以适配各种框架,也适配Java的序列化。

综上,我们不得不想办法让Spring MVC框架使用我们封装的JsonUtils实现交互消息的序列化和反序列化。

那么,如何让Spring MVC框架使用我们封装的JsonUtils实现交互消息的序列化和反序列化?

替换SpringMVC消息转换器

Spring MVC在接收到请求时,先根据url找到HandlerMethod,在调用HandlerMethod之前,会先根据请求头的Content-Type将“请求body”转换为Java对象,例如json反序列化。

在调用HandlerMethod之后,根据响应头的Content-Type将返回值转换为“响应body”返回给客户端,例如完成json序列化。这些操作称为消息转换,由http消息转换器完成。

HttpMessageConverter:处理http请求的消息转换器,根据http协议数据包的请求或响应头的Content-Typebody解析为Java对象。

HandlerMethodArgumentResolver:方法参数解析器,负责调用消息转换器将“请求body”转换为Java对象,每个解析器有每个解析器的职责,使用策略模式实现。

例如RequestResponseBodyMethodProcessor负责解析被@RequestBody注解注释的参数,调用消息转换器将“请求body”转换为对应方法参数类型的对象;

HandlerMethodReturnValueHandler:方法返回值处理器,负责调用消息转换器将方法返回值转换为“响应body”,每个解析器有每个解析器的职责,使用策略模式实现。

例如,当方法或者类上存在@ResponseBody注解时,由RequestResponseBodyMethodProcessor调用消息转换器将返回值解析为“响应body”返回给客户端。

我们看下消息转换器接口:HttpMessageConverter

public interface HttpMessageConverter<T> {
    boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
    boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
    List<MediaType> getSupportedMediaTypes();
    T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException;
    void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException;
}
  • getSupportedMediaTypes:获取该消息转换器支持的Content-TypeMediaType);
  • canRead:根据请求的Content-Type判断自己是否支持读取该类型的消息;
  • canWrite:根据响应的Content-Type判断自己是否支持写该类型的消息;
  • read:读取消息,将请求body转换为Java对象;
  • write:写消息,将响应的Java对象转换为“响应body”;

其中getSupportedMediaTypescanReadcanWrite方法用于实现策略模式,HttpMessageConverter可以有多个,而Spring MVC每次处理消息的转换,只会使用第一个canReadcanWrite方法返回true的消息转换器,在转换请求消息时使用canRead,在转换响应消息时使用canWrite

Spring MVC默认使用MappingJackson2HttpMessageConverter完成Content-Typeapplication/json的消息转换,即完成序列化和反序列化。

要让Spring MVC框架使用我们自己封装的JsonUtils解析Content-Typeapplication/json的请求body或响应body,就要实现自定义HttpMessageConverter,并替换MappingJackson2HttpMessageConverter

如何替换?

使用WebMvcConfigurationSupport

原理:当WebMvcConfigurationSupport#configureMessageConverters方法未注册http消息转换器时,调用WebMvcConfigurationSupport#addDefaultHttpMessageConverters方法添加默认的http消息转换器,这时就会注册MappingJackson2HttpMessageConverter

首先自定义消息转换器:JsonHttpMessageConverter

@Slf4j
public class JsonHttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
    // 支持的MediaType
    private final static List<MediaType> SUPPOR_MEDIA_TYPE;
    static {
        SUPPOR_MEDIA_TYPE = Arrays.asList(MediaType.APPLICATION_JSON);
    }
    
    @Override
    public List<MediaType> getSupportedMediaTypes() {
        return SUPPOR_MEDIA_TYPE;
    }

    @Override
    public boolean canRead(Class<?> clazz, MediaType mediaType) {
        return super.canRead(mediaType);
    }

    @Override
    public boolean canWrite(Class<?> clazz, MediaType mediaType) {
        return super.canRead(mediaType);
    }

    @Override
    protected void writeInternal(Object object, Type type, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {
        // 实现序列化
        MediaType contentType = httpOutputMessage.getHeaders().getContentType();
        JsonEncoding encoding = this.getJsonEncoding(contentType);
        String json = JsonUtils.toJsonString(object);
        OutputStream outputStream = httpOutputMessage.getBody();
        outputStream.write(json.getBytes(encoding.getJavaName()));
        outputStream.flush();
    }

    @Override
    public Object read(Type type, Class<?> aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException {
        // 实现反序列化
        InputStream inputStream = httpInputMessage.getBody();
        return JsonUtils.fromJson(inputStream, type);
    }
}

如上代码所示,自定义的消息转换器支持转换MediaType.APPLICATION_JSON类型的消息,在read方法中调用自己封装的JsonUtils反序列化消息,在writeInternal方法(由父类write方法调用)中调用自己封装的JsonUtils序列化消息,并将消息写入到输出流。

最后只需编写继承WebMvcConfigurationSupport的配置类,重写configureMessageConverters方法,注册自定义的JsonHttpMessageConverter

@Configuration
@Slf4j
public class MessageConverterConfiguration extends WebMvcConfigurationSupport {
    @Override
    protected void configureMessageConverters(List<HttpMessageConverter<?>> converters){
        converters.add(new JsonHttpMessageConverter());
    }
}

需要注意,如果重写configureMessageConverters方法只注册自定义JsonHttpMessageConverter,将会导致Content-Typeapplication/json之外的消息都无法处理。

适配spring-boot-actuator框架

使用自定义的消息转换器必然也要准备好面对可能出现的一些问题,例如。

运维要求应用需要提供一个心跳接口,以配置kubernetespod重启时,使用心跳接口判断新的pod是否启动成功,以此为依据停止旧的pod。刚好,spring-boot-actuator框架提供了心跳接口,然而,接口却一直抛出如下异常。

异常给的信息也很明确,找不到可以转换当前Content-Type的消息转化器。

根据异常栈消息,找到抛出异常的地方,下断点调试后得出如下图所示的信息。

从图中可以看出,/actuator/health心跳接口响应的http数据包,请求头的Content-Type并非application/json,而是application/vnd.spring-boot.actuator.v3+json

所以解决办法就是修改我们自定义的消息转换器,让消息转换器支持application/vnd.spring-boot.actuator.v3+json类型。

public class JsonHttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {

    private final static List<MediaType> SUPPOR_MEDIA_TYPE;
    static {
        SUPPOR_MEDIA_TYPE = Arrays.asList(
                MediaType.APPLICATION_JSON,
                // actuator框架使用
                MediaType.valueOf("application/vnd.spring-boot.actuator.v3+json")
        );
    }
}

Spring MVC多处使用组合模式+策略模式,为框架提供了非常好扩展性,例如本文提到的:

  • HttpMessageConverter(策略接口:消息转换器):Strategy interface that specifies a converter that can convert from and to HTTP requests and responses.
  • HandlerMethodReturnValueHandler(策略接口:方法返回值处理器):Strategy interface to handle the value returned from the invocation of a handler method .
  • HandlerMethodArgumentResolver(策略接口:方法参数解析器):Strategy interface for resolving method parameters into argument values in the context of a given request.

如果觉得这篇内容对你有帮助,欢迎点赞和转发!

[Java艺术] 微信号:javaskill

一个只推送原创文章的技术公众号,分享Java后端相关技术。