升级springboot 4.0.1的坑——序列化

··babydragon
Table of Contents

前文提到了升级springboot到 4.0.1遇到的编译和启动的问题,后续测试中还发现一个大坑——序列化问题。

springboot 4.0.1除了升级spring相关的版本依赖之外,还对其他基础依赖版本进行了大幅度的更新,其中就包含了jackson

springboot 4.0.1直接将jackson依赖升级到了jackson3,由于jackson3的group id修改了,因此我们的业务代码在编译、应用启动的时候都没有问题, 但是涉及到web层对对象进行反序列化的时候,就遇到了问题。

背景

之前的代码中,一些业务pojo设计了一系列复杂的继承和组装关系,导致将json反序列化成Java对象的时候,需要依赖JSON中定义的类型字段。 因此针对Jackson,之前的代码有一整套基于自定义的type字段到实际反序列化Java类的定义,这些类定义会在Spring初始化的时候进行初始化, 并注册到Jackson的SimpleModule模块中,同时初始化一个自定义的ObjectMapper对象。也就是说,老的代码是通过扩展 com.fasterxml.jackson.databind.JsonDeserializer来实现的基于某个特定字段指定反序列化类,而不是通过jackson定义的 com.fasterxml.jackson.annotation.JsonTypeInfo注解来实现的。

因此,这部分实现逻辑,需要按照jackson3的API重新来进行装配。

改动

由于之前的实现已经保存了字段值和对应Java bean的映射关系,相关的类型很多,重新维护这个关系也很复杂, 因此需要将老版本的代码适配到新的tools.jackson.databind.ValueDeserializer接口的反序列化器, 同时,注册到spring web使用的jackson环境中。

首先实现这个代理:

public class Jackson3ValueDeserializeAdaptor<T extends Typed> extends ValueDeserializer<T> {

  private final AbstractTypedJsonObjectDeserializer<T> delegate;

  public Jackson3ValueDeserializeAdaptor(AbstractTypedJsonObjectDeserializer<T> delegate) {
    this.delegate = delegate;
  }

  @Override
  public T deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException {
    JsonNode jsonNode = p.readValueAsTree();
    JsonNode typeNode = jsonNode.get("type");

    if (typeNode != null && typeNode.isValueNode()) {
      String type = typeNode.asString();

      Map<String, Class<? extends T>> typedMapper = delegate.getTypedMapper();
      Class<? extends T> targetClass = typedMapper.get(type);
      if (targetClass != null) {
        return ctxt.readTreeAsValue(jsonNode, targetClass);
      }
    }
  }
}

这里的AbstractTypedJsonObjectDeserializer是之前基于jackson2的反序列化实现,这里只是为了获取字段type的值和对应Pojo的映射关系。

然后在之前的管理器中,获取jackson2的反序列化实例,装配刚实现的适配器:

public Deserializers getJackson3Deserializers() {
    return new Deserializers.Base() {
      @Override
      public boolean hasDeserializerFor(DeserializationConfig config, Class<?> valueType) {
        return typedDeserializers.containsKey(valueType);
      }

      @Override
      public ValueDeserializer<?> findBeanDeserializer(JavaType type, DeserializationConfig config,
          Supplier beanDescRef) {
        AbstractTypedJsonObjectDeserializer<?> deserializer =
            typedDeserializers.get(type.getRawClass());
        if (deserializer != null) {
          return new Jackson3ValueDeserializeAdaptor<>(deserializer);
        }
        return null;
      }
    };
  }

这里的typedDeserializers是之前初始化的时候,注册了所有已知类型反序列化器的Map,这里直接获取之后,包装成jackson3的 tools.jackson.databind.deser.Deserializers对象。

通过spring web使用的json转换器org.springframework.http.converter.json.JacksonJsonHttpMessageConverter, 的初始化代码可以发现,spring web初始化jackson3的JsonMapper的时候,是通过Java的SPI机制加载的模块(参考tools.jackson.databind.cfg.MapperBuilder#findModules(java.lang.ClassLoader方法的实现), 而不是通过springboot常用的自动装配来初始化的。 因此,最后还需要包装一个jackson3的SPI模块,用于JacksonJsonHttpMessageConverter的初始化:

public class HyperpaasJacksonModule extends JacksonModule {

  @Override
  public String getModuleName() {
    return "HyperpaasJacksonModule";
  }

  @Override
  public void setupModule(SetupContext context) {
    // 从 JsonMapperManager 获取 Jackson 3 的 Deserializers
    JsonMapperManager manager = JsonMapperManager.getInstance();
    Deserializers deserializers =
        manager.getJackson3Deserializers();
    
    // 注册 Deserializers
    context.addDeserializers(deserializers);
  }

  @Override
  public Version version() {
    return new Version(1, 0, 0, null,
        "com.hyperpaas.platform", "commons-json-serializer");
  }
}

除了代码之外,还需要创建SPI定义文件tools.jackson.databind.JacksonModule

com.hyperpaas.platform.commons.json.serializer.HyperpaasJacksonModule

里面的内容就是模块类的全路径。

这样,jackson3就能够使用之前定义的映射关系来进行反序列化,同时,由于使用了SPI机制,不需要修改业务层的代码,就可以完成初始化。