Jackson自定义枚举值反序列化

分类: 工作

本文代码示例:Github

场景

在工作中,经常需要定义一些枚举提供常量。例如,周一,周二等一些常用的属性。在前端通过Json格式传递此类参数到后端,需要将参数转换到枚举。

在Spring Web中默认使用的是 Jackson 来进行的 json 序列化化与反序列化。

Spring Mvc是通过 HttpMessageConverter接口,进行参数到实体类的转换,提供了MappingJackson2HttpMessageConverter实现类使用Jackson进行序列化反序列化。

Jackson支持枚举到序列化,不过默认使用的是使用枚举的ordinal枚举定义的顺序。但是很多时候需要自定义枚举的值,使用下标无法满足需求。

Jackson 默认通用枚举序列化器EnumDeserializerObject deserialize(JsonParser p, DeserializationContext ctxt) 方法以枚举下标实现Json到枚举的反序列化。

需求

需求:实现一个自定义的枚举值反序列化到实体类。

枚举

public enum WeekDay implements BaseEnum {

    Mon(1),

    Tue(2),

    Wed(3),

    Thu(4),

    Fri(5),

    Sat(6),

    Sun(7);

    private final int code;

    WeekDay(int code){
        this.code = code;
    }

    @Override
    public int getCode() {
        return this.code;
    }
}

请求实体类

@Data
public class WeekDayReq {

    private WeekDay weekDay;
}

spring web接口,方法签名

@PostMapping("/put")
public Object req(@RequestBody WeekDayReq req)

请求Json

POST http://localhost:8081/demo1/put
Content-Type: application/json;charset=UTF-8

{
 "weekDay":1
}

期望 json 中 weekDay的值,对应枚举的 Code值。eg: 1 对应枚举中的,WeekDay.Mon。

在使用默认的枚举处理器的情况下,返回的是 WeekDay.Tue。与期望值不符合。

现有方式

根据百度搜索到现有的实现方式。

  1. 使用@JsonDeserialize注解

    @JsonDeserialize注解标注到指定的字段,其中参数using指定使用自定义的序列化器.

    public class WeekDayReq {
       
        @JsonDeserialize(using = WeekDayDeserialize.class)
        private WeekDay weekDay;
    }
    

    其中 using参数需要指定一个JsonDeserializer实现类,

    public class WeekDayDeserialize extends JsonDeserializer<WeekDay> {
       
        @Override
        public WeekDay deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
       
            int value = p.getValueAsInt();
       
            for (WeekDay d:WeekDay.values()){
              if (value == d.getCode()){
                  return d;
              }
            }
            return null;
        }
    }
    

    每次都需要使用注解,每个枚举都要写对应的实现类,可以实现,但不具备通用性。

源码分析

通过debug,查阅源码。

关于对应类型的JsonDeserializer创建查找主要在com.fasterxml.jackson.databind.deser.DeserializerCache#_createDeserializer方法中,会先从对应字段的注解配置中找是否有指定的JsonDeserializer,没有的话则会从ObjectMapper配置中查找有无对应类型的JsonDeserializer。找到后会new出实例对象,并存在缓存中。

查阅代码com.fasterxml.jackson.databind.deser.DeserializerCache#_createDeserializer

20210718-1-01

第一个箭头处,尝试从注解中查找是否有指定。没有的话就会进入第二个红箭头处,进入另个一个方法。

image-20200626224646623.png

可以很清楚得看到,红箭头处。在判断是枚举类型后从factroy中创建枚举的Deserializer。

进入方法createEnumDeserializer可以看到。

image-20200626225004097.png

会先 _findCustomEnumDeserializer中查找,找不到的话会调用AbstractDeserializer#constructForNonPOJO()方法创建一个默认的序列化器。

进入``_findCustomEnumDeserializer`方法

image-20200626225545014.png

可以看到会尝试从所有Deserializers 中查找对应类型的JsonDeserializer.

Deserializers是一个接口,现在情况很清楚只要我们实现Deserializers接口注入到Jackson中,就可以实现通用的枚举的反序列化器。

实现

实现Deserializers接口

public class BaseEnumDeserializers implements Deserializers {

    @Override
    public JsonDeserializer<?> findEnumDeserializer(Class<?> type, DeserializationConfig config, BeanDescription beanDesc) throws JsonMappingException {
        if (!type.isEnum() && !type.isAssignableFrom(BaseEnum.class)){
            return null;
        }
        return new BaseEnumJsonDeserializer(type);
    }
    //省略部分接口
}

我新加入了一个接口BaseEnum,自己的枚举都会继承这个接口实现获取自定义枚举code的方法,方便序列化以及反序列化。

public interface BaseEnum {

    /**
     *  获取枚举对应的值
     * @return 枚举对应的值
     */
    int getCode();
}

这里判断一些传入的type值是否为枚举并且继承了BaseEnum,通过的话则会返回一个BaseEnumJsonDeserializer对象,实现如下。

public class BaseEnumJsonDeserializer<T extends Enum<T> & BaseEnum> extends JsonDeserializer<T> {

    private final Class<T> clz;

    private final T[] clzEnums;

    public BaseEnumJsonDeserializer(Class<T> t){
        this.clz = t;
        this.clzEnums = t.getEnumConstants();
    }

    @Override
    public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        int value = p.getValueAsInt();
        for (T clzEnum : clzEnums) {
            if (value == clzEnum.getCode()) {
                return clzEnum;
            }
        }
        return null;
    }
}

BaseEnumJsonDeserializer根据类型,获取列举。调用getCode方法获取对应的枚举值进行判断。

将自己实现的BaseEnumDeserializers注入到 ObjectMapper 中。

  1. 继承Module配置

    继承com.fasterxml.jackson.databind.Module

    public class EnumModule extends Module {
       
     @Override
     public String getModuleName() {
         return "BaseEnumModule";
     }
       
     @Override
     public Version version() {
         return Version.unknownVersion();
     }
       
     @Override
     public void setupModule(SetupContext context) {
         context.addDeserializers(new BaseEnumDeserializers());
     }
    }
    
  2. 配置到ObjectMapper

    objectMapper.registerModule(new EnumModule());
    

配置到spring mvc

获取所有的HttpMessageConverter接口,判断是否为MappingJackson2HttpMessageConverter对象,这个对象使用Jackson进行序列化的。

提示:HttpMessageConverter使用的ObjectMapper 是在WebMvcConfigurationSupport#addDefaultHttpMessageConverters()方法中,根据Jackson2ObjectMapperBuilder实例化出来的,并没有进入容器进行管理。如有自己编写JsonUtils,注意需要自己配置EnumModule到ObjectMapper

@Configuration
public class WebConfig extends WebMvcConfigurationSupport {

    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        for (HttpMessageConverter<?> converter : converters) {
            if (converter instanceof MappingJackson2HttpMessageConverter){
                ObjectMapper o = ((MappingJackson2HttpMessageConverter) converter).getObjectMapper();
                o.registerModule(new EnumModule());
            }
        }
    }
}

上一篇: 这是第一篇
下一篇: Spring MVC Controller自定义参数和返回值解析