目录

Jackson问题汇总

前言

常见的json框架有:Jackson,FasJson(阿里的,万年没更新,积累了大量issue),Gson(谷歌的)。其中Jackson效率最高,性能最好,最为常用。本文基于2.11.3版本的Jackson。

Jackson在1.x版本使用的包名是codehaus,后来升级到2.x版本时,为了和旧版本区分开来,采用了新的包名fasterxml。有这种类似操作的还有Log4j和Log4j2,在使用时需要注意引入的包名,避免混用版本或误用版本。

Jackson可以轻松的将Java对象转换成json对象和xml文档(即序列化),同样也可以将json、xml转换成Java对象(即反序列化)。

序列化日期字段的时区问题

在将日期转为字符串时会使用@JsonFormat注解,默认使用零时区,因此在使用时要注意时区问题。比如项目部署在中国境内的服务器(东八区时区),不注意时区问题可能会导致json中的时间和预期的差距8个小时。

实际开发中,假如存在多个不同时区的客户,通常是把客户所在的时区时间转换为零时区,然后存入到数据库中。这样项目从数据库读取到的时间就是零时区,再根据客户所在时区修改显示在前端页面的时间。这样比较灵活,不怕时间字段的值发生混乱,不易管理。

在格式化成json时注解如下:

1
2
3
4
// 将日期格式化成ISO格式
// 这里转成零时区,北京时间东八区是GMT+8
@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone="GMT")
private LocalDateTime updatedOn;

另外,@DatetimeFormat是spring提供的一个注解,用以将日期转为String,提供给前端使用。

序列化时重命名字段

可以使用@JsonProperty来重命名字段,@JsonProperty能够在序列化和反序列化时给字段映射到指定的名字:

1
2
@JsonProperty("updatedBy")
private String updateUserName;

如上,updateUserName在序列化为json时会被重命名为updatedBy,在反序列化时json中updatedBy的值会被映射到updateUserName

将这个注解和MapStruct映射框架结合起来,可以通过定义@JsonProperty来避免额外定义MapStruct框架的不同名字段映射规则;比如:@Mapping(source = "updateUserName", target = "updatedBy")

还有个类似的注解@JsonAlias,其作用是给字段起别名,只在反序列化阶段有效,可以让字段接受更多的映射名称:

1
2
@JsonAlias("updatedBy")
private String updateUserName;

如上,在反序列化时,json中的updatedBy或者updateUserName都可以映射到pojo的updateUserName字段上。

序列化忽略指定的字段

按照字段进行忽略

可以使用@JsonIgnore或者@JsonIgnoreProperties来忽略字段,@JsonIgnoreProperties比前者多了一个使用方式,那就是加在类上:

1
2
3
4
5
6
7
8
// 在使用JPA-hibernate的懒加载功能时,懒加载对象会多出来`hibernateLazyInitializer`字段,这里将其忽略掉
@JsonIgnoreProperties(value = { "hibernateLazyInitializer" })
public class Item {

    @JsonIgnore
    private String tag;

}

按照字段的值进行忽略

上述两个注解会直接忽略指定的字段,如果想忽略满足某个条件的字段,比如忽略值为null的字段,或者值为0的数值型字段等,可以用@JsonInclude注解。

@JsonInclude用来指定序列化时需要包括哪些字段,本身提供了多种策略:ALWAYS、NON_NULL、NON_EMPTY等。如果需要自定义包括字段的策略,方式如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 指定自定义的序列化策略,同时提供相关的值过滤类ValueFilter
@JsonInclude(value = Include.CUSTOM , valueFilter = ValueFilter.class)
public class Item {

    //...
}

// 自定义的过滤类
public class ValueFilter {

    @Override
    public  boolean equals(final Object obj){
        // return true表示将其过滤,即在序列化时将其忽略。
        // 这里将所有值为null、false、0的字段全部忽略
        if (obj == null) {
            return true;
        }
        if (obj.equals(Boolean.FALSE)) {
            return true;
        }
        if (obj.equals(Integer.valueOf(0))) {
            return true;
        }
        if (obj instanceof BigDecimal) {
            final BigDecimal value = (BigDecimal) obj;
            if (value.compareTo(BigDecimal.ZERO) == 0) {
                return true;
            }
        }

        return false;
    }

    @Override
    public int hashCode() {
        return super.hashCode();
    }
}

pojo的循环依赖导致序列化时无限递归

当多个pojo之间存在循环依赖时,在序列化时会无限递归,最终堆栈溢出StackOverflowError。pojo如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 供应商Vendor,关联着中间表VendorFact
@Data
public class Vendor {

    private List<VendorFact> facts;

}

// 工厂Fact,关联着中间表VendorFact
@Data
public class Fact {

    private List<VendorFact> vendors;

}

// 中间表VendorFact,存放Vendor和Fact的关联关系(多对多)
@Data
public class VendorFact {

    private Fact fact;

    private Vendor vendor;

}

上述的pojo在序列化时会互相引用、无限递归,因此需要打断这个循环链。

经过本人测试,其他博客中提及的@JsonManagedReference@JsonBackReference并无效果,至少在多对多的关联关系中不起效(一对多没有测试)。

使用@JsonIgnore

直接在其中一方的字段上加上@JsonIgnore,这样在序列化时该字段就会被忽略,自然也就不会互相引用无限递归了。比如对于Vendor的序列化,可以将VendorFact里的vendor变量忽略:

1
2
3
4
5
6
7
8
9
@Data
public class VendorFact {

    private Fact fact;

    @JsonIgnore
    private Vendor vendor;

}

不过这个方案有个缺点,就是序列化后的json中只有一方持有另一方,在另一方的json中无法持有对方,在一对多或者多对多的情况中并不便利。比如上面的方式,虽然Vendor可以正常序列化了,但是Fact依然不能正常序列化。而在序列化VendorFact时,也缺少了vendor的信息。

当然有个方案是,为每一方都单独创建一套对应的dto来进行序列化,这样的话虽然类的数量变多了,但是可以解决多对多关系中只有一方能序列化的问题。比如上面的例子,需要两个VendorFact的dto类,各自对应Vendor和Fact。不过在序列化VendorFact时,依然缺少另一方的信息。不过正常情况下不会单独去序列化作为中间表的VendorFact,因此不予考虑这个缺点。

此外,和@JsonIgnore效果类似的@JsonIgnoreProperties,也能起到一样的效果,但是这个注解如果添加在集合变量上是无效的,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Data
public class Vendor {

    // 不会起效,因为这里的json结构实际上是数组,对应的是List对象,而不是VendorFact
    // 需要将该注解添加在VendorFact类上
    @JsonIgnoreProperties("vendor")
    private List<VendorFact> facts;

}

@Data
// 上面的注解应该加在这里
@JsonIgnoreProperties("vendor")
public class VendorFact {

    private Fact fact;

    private Vendor vendor;

}

自定义对象的序列化和反序列化

有时候只靠默认的序列化方式,或者常规的json注解,难以实现某个对象的序列化要求。比如,现在需要对一个HashMap对象进行序列化,这个对象中的字段和类型是未知的,此时可以用@JsonSerialize@JsonDeserialize来控制序列化和反序列化。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
@Data
// 指定自定义的序列化器CustomTableDtoSerializer
@JsonSerialize(using = CustomTableDtoSerializer.class)
// 指定自定义的反序列化器CustomTableDtoDeserializer
@JsonDeserialize(using = CustomTableDtoDeserializer.class)
public class CustomTableDto {

    public static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");

    private Map<String, Object> dynamicModelMap;
}

// 序列化器
public class CustomTableDtoSerializer extends JsonSerializer<CustomTableDto> {

    @Override
    public void serialize(final CustomTableDto customTable, final JsonGenerator gen,
                          final SerializerProvider serializers) throws IOException {
        final Map<String, Object> dynamicModelMap = customTable.getDynamicModelMap();
        if (dynamicModelMap != null) {
            gen.writeStartObject();
            for (final String key : dynamicModelMap.keySet()) {
                final Object value = dynamicModelMap.get(key);
                final Optional<CustomFieldType> typeOpt = CustomFieldType.findTypeByColumnName(key);

                if (typeOpt.isPresent() && value != null) {
                    writeType(gen, typeOpt.get(), key, value);
                }
            }
            gen.writeEndObject();
        } else {
            gen.writeNull();
        }
    }

    private static void writeType(final JsonGenerator gen, final CustomFieldType type,
                                  final String key, final Object value) throws IOException {
        final String jsonKey = StringUtils.replaceOnce(key, type.getColumnName(), type.getJsonName());

        // 根据不同的字段类型进行对应的业务处理
        // CustomFieldType是自定义的枚举类,这里就不写出来了
        switch (type) {
            case TEXT:
            case TEXT_AREA:
                gen.writeStringField(jsonKey, (String) value);
                break;
            case DATE:
                gen.writeStringField(jsonKey, CustomTableDto.DATE_FORMAT.format((LocalDate) value));
                break;
            case NUMBER:
                gen.writeNumberField(jsonKey, (Long) value);
                break;
            case DECIMAL:
                gen.writeNumberField(jsonKey, (BigDecimal) value);
                break;
            case CHECKBOX:
                gen.writeBooleanField(jsonKey, (Boolean) value);
                break;
            case CODELIST:
            case HCL_GROUP:
                gen.writeObjectField(jsonKey, value);
                break;
            default:
                //do nothing
        }
    }
}

// 反序列化器
public class CustomTableDtoDeserializer extends JsonDeserializer<CustomTableDto> {

    @Override
    public CustomTableDto deserialize(final JsonParser p, final DeserializationContext ctxt) throws IOException {
        final ObjectCodec oc = p.getCodec();
        final JsonNode root = oc.readTree(p);

        final Map<String, Object> dynamicModelMap = new HashMap<>();
        final Iterator<String> fieldIterator = root.fieldNames();

        while (fieldIterator.hasNext()) {
            final String jsonKey = fieldIterator.next();
            final JsonNode value = root.get(jsonKey);
            final Optional<CustomFieldType> typeOpt = CustomFieldType.findTypeByJsonName(jsonKey);

            if (typeOpt.isPresent() && value != null) {
                put(oc, dynamicModelMap, typeOpt.get(), jsonKey, value);
            }
        }

        final CustomTableDto customTableDto = new CustomTableDto();
        customTableDto.setDynamicModelMap(dynamicModelMap);
        return customTableDto;
    }

    private static void put(final ObjectCodec oc, final Map<String, Object> dynamicModelMap, final CustomFieldType type,
                            final String jsonKey, final JsonNode value) throws IOException {
        final String columnKey = StringUtils.replaceOnce(jsonKey, type.getJsonName(), type.getColumnName());

        // 根据不同的字段类型进行对应的业务处理
        // CustomFieldType是自定义的枚举类,这里就不写出来了
        switch (type) {
            case TEXT:
            case TEXT_AREA:
                dynamicModelMap.put(columnKey, value.textValue());
                break;
            case DATE:
                final String dateStr = value.textValue();
                dynamicModelMap.put(columnKey, LocalDate.from(CustomTableDto.DATE_FORMAT.parse(dateStr)));
                break;
            case NUMBER:
                dynamicModelMap.put(columnKey, value.longValue());
                break;
            case DECIMAL:
                dynamicModelMap.put(columnKey, value.decimalValue());
                break;
            case CHECKBOX:
                dynamicModelMap.put(columnKey, value.booleanValue());
                break;
            case CODELIST:
                dynamicModelMap.put(columnKey, oc.treeToValue(value, EmbedCodelist.class));
                break;
            case HCL_GROUP:
                dynamicModelMap.put(columnKey, oc.treeToValue(value, EmbedHcl.class));
                break;
            default:
                //do nothing
        }
    }
}

参考链接

注意
本文最后更新于 January 4, 2022,文中内容可能已过时,请谨慎使用。