这里聊聊使用Jackson提供的ObjectMapper来实现报文转换。

Json转Bean

直接使用ObjectMapper类来操作:

ObjectMapper objectMapper=new ObjectMapper();

SpringMVC中的MappingJackson2HttpMessageConverter会使用ObjectMapper来序列化/反序列化Http的RequestBody和ResponseBody。如果要保持系统内使用的ObjectMapper配置一致,可以手动注入一个ObjectMapper或者Jackson2ObjectMapperBuilder:

/**声明一个Jackson2ObjectMapperBuilder,并配置一些属性*/
public static final Jackson2ObjectMapperBuilder JACKSON_2_OBJECT_MAPPER_BUILDER = 
    new Jackson2ObjectMapperBuilder()
    .serializationIncLusion(JsonIncLude.IncLude.NON_NULL)
    .featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
/**手动操作Json时,使用上述Builder生成这个ObjectMapper*/
private static ObjectMapper OBJECT_MAPPER = JACKSON_2_OBJECT_MAPPER_BUILDER.build();

/**SpringMVC自动装配MappingJackson2HttpMessageConverter时,会使用这个Builder来生成ObjectMapper*/
@Bean
public Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder(){
    return JACKSON_2_OBJECT_MAPPER_BUILDER;
}

Json与Bean结构相同

例如,Json串如下:

{
    "name":"linjun",
    "age":18
}

对应的bean结构如下:

public class User{
    private String name;
    private int age;
    // getters/setters,略
}

需要注意:Jackson是通过getter/setter方法来实现序列化/反序列化的。因此Bean中的字段,必须要有对应的方法。

则可以直接进行转换:

ObjectMapper objectMapper = new ObjectMapper();
// 报文内容在上面,这里不重复了
String json = "";
User user = ojbectMapper.readValue(json, User.class);

Json与Bean结构不同

字段不同

如果Json与Bean的结构差异仅仅是字段多少的不同,那么可以给ObjectMapper或者Jackson2ObjectMapperBuilder配置一个属性即可:

JACKSON_2_OBJECT_MAPPER_BUILDER = 
    new Jackson2ObjectMapperBuilder()
    // 这个属性保证了Bean中为null的字段不序列化为Json字符串
    .serializationIncLusion(JsonIncLude.IncLude.NON_NULL)
    // 这个属性保证了Json中有、而Bean中没有的属性不会导致反序列化报错。
    .featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

或者使用@JsonProperty、@JsonIgnore、@JsonGetter、@JsonSetter等注解来更加细致地指定序列化、反序列化时的字段。如果只是字段名不同,可以在注解中指定json串中的名称。

public class User{
    /** 标记@JsonProperty的字段,既参与序列化,也参与反序列化。在json串中的字段名是“name” */
    @JsonProperty("name")
    private String userName;
    /** 标记@JsonIgnore的字段,不参与序列化,也不参与反序列化 */
    @JsonIgnore
    private int age;
    /** 标记@JsonGetter的字段,只参与序列化,不参与反序列化 */
    @JsonGetter
   private String idNo;
   /** 标记@JsonSetter的字段,不参与序列化,只参与反序列化 */
   @JsonSetter
   private String addr;
   // accessors,略
}

由于Jackson是使用getter/setter方法来做序列化/反序列化的,因此,我们也可以通过增减字段的getter/setter方法,来指定字段是否参加序列化/反序列化:指定了getter方法,则参与序列化,等同于使用了@JsonGetter注解;指定了setter方法,则参与反序列化,等同于使用了@JsonSetter注解;@JsonProperty和@JsonIgnore注解可以类推。

不过这样一来,在代码中使用getter和setter方法也会受限。所以,如果结构相同、只是字段不同时,一般会使用注解。

层级不同

例如,我们有如下Json串:

{
    "name":"linjun",
    "age":18,
    "idNo":"123456",
    "addr":"北京市"
}

对应的Bean却是这样的:

public class User{
    private String name;
    private int age;
    private IdCard idCard;
    // accessors,略
}
public Class IdCard{
    private String idNo;
    private String addr;
    private Date avariableStart;
    private Date avariableEnd;
    // accessors,略
}

显然,Json串的层次结构与Bean的层次结构并不能直接匹配。Jackson也没有提供合适的注解来处理这一问题。不过,由于Jackson是通过getter/setter方法来处理序列化/反序列化的,因此,我们可以在getter/setter方法上做点“手脚”:

public class User{
    private String name;
    private int age;
    /**保证反序列化时不处理这个字段*/
    @JsonIgnore
    private IdCard idCard = new IdCard();
    /**反序列化时使用,将idCard.inNo直接反序列化为idNo*/
    @JsonGetter
    public String getIdNo(){
        return idCard.getIdNo();
    }
    /**序列化时使用,将idNo直接序列化为idCard.idNo*/
    @JsonSetter
    public void setIdNo(Stirng idNo){
        idCard.setIdNo(idNo);
    }
    // addr做同样处理
    // 其它accessors,略
}

Json转泛型子类

泛型实例的序列化并没有什么特别之处。需要特别处理的是它的反序列化。

容器内的泛型

这种泛型转换最简单,借助TypeReference类即可:

String jsonInput = "{\"key\": \"value\"}";
TypeReference<HashMap<String, String>> typeRef = new TypeReference<HashMap<String, String>>() {};
Map<String, String> map = mapper.readValue(jsonInput, typeRef);
// List同理,略

对自定义的泛型容器,也可以这样处理:

public class Dto<T>{
    private String code;
    private String msg;
    private T data;
    // accessors,略
}
String jsonInput = "";
TypeReference<Dto<User>> typeRef = new TypeReference<Dto<User>>() {};
Dto<User> dto = mapper.readValue(jsonInput, typeRef);

不确定泛型类型

上面这个例子中,虽然使用了泛型,但是在执行反序列化时,泛型类型就已经确定下来了:是HashMap<String, Object>或User类型。

如果在反序列化时,我们还不能确定使用哪个子类呢?

在核算交易中心就有这样一个场景:还款接口可以接受两种Json报文——按期还款,或按金额还款。两种报文中的字段有差异。在业务代码中,我们使用了一个泛型和两个子类来处理。但是在接口层——即在Controller层,我们只知道泛型,不知道具体的子类。

我们可以借助@JsonDeserialize注解来实现反序列化:

public class RepayController{
    /**还款接口;接口统一为一个;报文需解析为repayApplyVo类;不同报文需解析为RepayApplyBasicInfo的不同子类。*/
    @RequestMapping("/apply")
    public AccTraRetVo apply(@RequestBody RepayApplyVo<RepayApplyBasicInfo> repayApplyVo, String channelNo){
        // 略
    }    
}
public class RepayApplyReqBody<T extends RepayApplyBasicInfo>{
    /**这就是那个对应两个子类的泛型字段*/
    @JsonDeserialize(using = RepayApplyBasicBodyDeserializer.class)
    private T basicInfo;
}
/**RepayApplyBasicInfo的反序列化器*/
public class RepayApplyBasicBodyDeserializer extends StdDeserializer<RepayApplyBasicInfo>{
    @Override
    public RepayApplyBasicInfo deseriaLize(JsonParser p,DeseriatizationContext ctxt)throws IOException{
        // 先解析为JsonNode
       JsonNode node = JsonNodeDeseriatizer.deserialize(p, ctxt);
       RepayApplyBasicInfo basicInfo;
       if (node == null){
           basicInfo = null;
       }else if (node中包含有按期还款的特定字段){
           // 解析为按期还款的子类
           basicInfo = Jsonutits.toObject(node,RepayApplyTermBasicInfo.class);
       }else{
           // 解析为按金额还款的子类
           basicInfo = Jsonutils.toObject(node, RepayApplyAmtBasicInfo.class);
       }
    return basicInfo;
}

Json转枚举

可以参考How To Serialize and Deserialize Enums with Jackson

按name字段处理

如果Json串中使用枚举的name字段,那么可以直接对枚举进行序列化/反序列化。

按自定义字段处理

但很多使用我们的报文中使用的是枚举中的自定义字段,而非name字段。如我们有如下json串:

{
    "cardType":"01"
}

其中它所对应的类和枚举如下:

public class Card{
    private CardType cardType;
    // getter和setter
}

public enum CardType{
    DEBT("01"),
    CREDIT("02");
    private final String code;
    CardType(Stirng c){
        this.code = c;
    }
    // getter,略
}

如果我们把Card中的cardType字段声明为String,就失去了使用枚举的各种优点;但如果要把cardType字段直接声明为枚举,序列化与反序列化又会变成一个问题。

这个问题,可以用@JsonValue字段来解决:

public enum CardType{
    DEBT("01"),
    CREDIT("02");
    /**指定这个枚举使用这个字段来做json的序列化/反序列化 */
    @JsonValue
    private final String code;
    CardType(Stirng c){
        this.code = c;
    }
    // getter,略
}

按普通bean处理

有些特殊情况下,对枚举进行序列化处理时,我们不想把它序列化为一个String,而希望能像普通的Bean一样,把它序列化为一个对象。例如,我们有如下枚举:

public enum RepaySence{
    NORMAL("1","principal","interest","serviceFee"),
    OVERDUE("2","principal","interest","serviceFee","penaltyInterest"),
    ADVANCE("3","principal","liquidatedDamages")
    private final String code;
    private final String[] amtTypes;
    RepaySence(String c, String... t){
        this.code = c;
        this.amtTypes = t;
    }
    // getters,略
}

我们希望将它反序列化为这样一个Json串(以Normal为例):

{
    "repaySence":{
        "code":"1",
        "amtTypes":[
            "principal",
            "interest",
            "serviceFee"
        ]
    }
}

为了达到这个目标,我们只需要在枚举上声明 @JsonFormat(shape = JsonFormat.Shape.OBJECT)即可:

@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum RepaySence{
    // 其它,略
}

注意事项

必须有public的无参数构造方法

默认情况下,Jackson执行反序列化时,必须先通过public的无参数构造方法来初始化一个实例,然后才能借助实例的setter方法来赋值。因此,反序列化的目标对象上必须有public的无参数构造方法。

如果我们没有声明任何构造方法,Java会自动生成一个无参数构造方法。但是,如果我们手写了一个带参数构造方法(包括使用Lombok的@AllArgConstructor或者@Builder注解),Java就不会再自动生成这个无参数构造方法了。此时,我们一定要再手写一个public的无参数构造方法。

必须有getter/setter方法

Jackson是借助getter/setter方法来实现序列化、反序列化的。千万不要忘记声明这两个方法。

尽量不要使用Map、JsonObject、JsonNode等类作为反序列化目标

使用这些类时,我们无法确知其中有哪些节点、节点名称和节点类型是什么。因此,除非我们真的不关心这些信息(例如直接透传报文),否则,还是自定义一个Bean比较好。

ps,借助IDEA的“JSON2POJO”或“JSON2POJO with lombok”插件,我们可以轻松地把Json串转为POJO类。

Xml转Bean

Jackson提供了一个XmlMapper类,用于处理XML报文的序列化、反序列化操作:

ObejctMapper xmlMapper = new XmlMapper();

显然,XmlMapper继承自ObjectMapper。这给我们编程提供了很大的方便:所有使用ObjectMapper进行JSON序列化/反序列化的操作,都适用于XmlMapper。这里就不啰嗦了。注解方面,XmlMapper有一套针对Xml的注解可用。

与ObjectMapper类似,我们也可以用Jackson2ObjectMaperBuilder来定制XmlMapper,也有MappingJackson2XmlHttpMessageConverter类可以直接用:

ObjectMapper xmlMapper = Jackson2ObjectMapperBuilder.xml().build();

注意事项

maven依赖

XmlMapper类并不在Jackson的主依赖(core、data-bind等)包内。因此使用XmlMapper,需要我们单独引入其maven依赖:

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
    <version>2.11.3</version>
</dependency>

反序列化为Map时丢失根节点

xml标准要求每个xml文件都有一个根节点。例如,下面这个报文中,user节点就是其根节点。:

<?xml version="1.0" encoding="UTF-8"?>
<user>
    <name>linjun</name>
    <age>18</age>
</user>

但是,如果我们把它反序列化到一个Map里,这个user节点会莫名消失:

Map map = new XmlMapper().readValue(xml, Map.class);
// 这里得到的结果是{name=linjun, age=18},而不是{user={name=linjun, age=18}}

这也是我们不建议使用Map来作为反序列化目标的原因之一。

反序列化时丢失重复节点

默认的XmlMpaaer对重复节点的处理存在问题。详情参见:XmlMapper/UntypedObjectDeserializer swallows duplicated elements in XML documents #205。在这个issue中,作者已经给出了解决方案。

拿到作者给出的类(下文中命名为XmLDeserializer4DupTagImp)之后,对xmlMapper做如下配置即可:

XmLMapper xmLMapper = new XmLMapper();
// 把作者提供的反序列化器注册到我们的XmlMapper中。
Module module = new SimpLeModule().addDeserializer(object.cLass,new.XmLDeserializer4DupTagImp(());
xmLMapper.registerModule(module);