这里聊聊使用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);