1. 问题
在工作中我们常遇到一个场景,以下Json结构,需要根据Json字符串的 type
字段来选择转换对应的实体类,一般情况就是可能先将字符串转换为 Map
结构然后获取到 type
字段然后判断具体类型,再转换成实体,这样很麻烦,这里我们可以利用 Jackson 框架提供的能力来进行多态的实体转换。
{
"payload": [
{
"type": "Payload100",
"payload": {
"name": "zs"
}
},
{
"type": "Payload200",
"payload": {
"name": "zs"
}
}
]
}
上面是每个实体转换时都按照各自的类型进行实例化,下面这种就是按照实体中的某个字段进行转换
{
"type": "Payload100",
"payload": {
"name": "ls"
}
}
2. 解决方案一
按照上面的结构我们可以先定义一个接口,其中上面标记的注解后续进行详解
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type",
visible = true //定义了标识符是否进行序列化,默认false,反序列化时不会包含type字段
)
@JsonSubTypes(
value = {
@JsonSubTypes.Type(value = Payload100.class, name = "Payload100"),
@JsonSubTypes.Type(value = Payload200.class, name = "Payload200")
}
)
public interface JacksonInterface {
}
定义两个实体类来实现上面的接口,一个 Payload100 一个 Payload200
@Data
public class Payload100 implements JacksonInterface {
private String name;
}
@Data
public class Payload200 implements JacksonInterface {
private String name;
}
定义一个包含的实体类,如果外层不包括实体,会导致 type
生成标识符的丢失
@Data
public class Payload {
private List<JacksonInterface> payload;
}
测试结果,从下面测试代码可以看到,我们先将 Payload 序列化成字符串,然后在每个 payload 下会生成一个 type
字段,这样的json字符串就可以进行相互转换
@Test
public void test_Jackson_serializer() throws IOException {
Payload payload = new Payload();
Payload100 payload100 = new Payload100();
payload100.setName("zs");
Payload200 payload200 = new Payload200();
payload200.setName("ls");
List<JacksonInterface> list = new ArrayList<>();
list.add(payload100);
list.add(payload200);
payload.setPayload(list);
String s = objectMapper.writeValueAsString(payload);
System.out.println("序列化成字符串:" + s);
System.out.println("转换后的实体类结果:");
System.out.println(objectMapper.readValue(s, Payload.class));
}
2.1 注解
@JsonTypeInfo
用于标记json转换时需要生成的字段名称以及从那里取到对应的字段
- use:采用什么样的数据生成标识符
- NAME:通过名称进行生成,需要配合 @JsonSubTypes 注解一起食用
- NONE:不配置按照默认方式
- CLASS:获取到类名,通过类名进行生成(这个方式会暴露类名出去,不安全)
- MINIMAL_CLASS:按照主类的类名进行生成
- CUSTOM:自定义生成方式,需要配合 @JsonTypeIdResolver 注解一起食用自定义标识符的生成器
- include:通过什么样的方式生成标识符
- PROPERTY:生成一个属性字段在json字符串中
- WRAPPER_OBJECT:按照一个对象生成到字符串中
- WRAPPER_ARRAY:封装成数组生成
- EXTERNAL_PROPERTY:通过外部生成的字段
- EXISTING_PROPERTY:通过已经存在的字段生成
- property:生成标识的名称
@JsonSubTypes
自定名称对应的子类型有哪些,里面包含了一个 @JsonSubTypes.Type 用于指定名称和类的对应
@JsonTypeIdResolver
自定义标识符生成器,自定义标识符的生成器的好处是可以不需要在 @JsonSubTypes 中每新增一个类就要添加一个子类名称对应,需要实现 TypeIdResolver 接口其中方法如下:
- init:初始化
- idFromValue:从对象中获取(序列化时)
- idFromValueAndType:从对象和类中来获取(序列化时)
- idFromBaseType:从基础类型中进行获取
- typeFromId:反序列化通过标识符进行转换
下面有个例子,具体使用可以自行探索:
public class IdResolver implements TypeIdResolver {
private JavaType javaType;
@Override
public void init(JavaType javaType) {
}
@Override
public String idFromValue(Object o) {
return idFromValueAndType(o, o.getClass());
}
@Override
public String idFromValueAndType(Object o, Class<?> aClass) {
String value = null;
try {
Field field = aClass.getDeclaredField(Payload.PAYLOAD_FILED_NAME);
field.setAccessible(true);
Object payload = field.get(o);
if (payload != null && JacksonInterface.class.isAssignableFrom(payload.getClass())) {
Type type = payload.getClass().getAnnotation(Type.class);
if (type != null) {
value = type.value();
}
}
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
return value;
}
@Override
public String idFromBaseType() {
return idFromValueAndType(null, this.javaType.getClass());
}
@Override
public JavaType typeFromId(DatabindContext databindContext, String s) throws IOException {
ServiceLoader<JacksonInterface> jacksonInterfaces = ServiceLoader.load(JacksonInterface.class);
return this.javaType;
}
@Override
public String getDescForKnownTypeIds() {
return null;
}
@Override
public JsonTypeInfo.Id getMechanism() {
return JsonTypeInfo.Id.CUSTOM;
}
}
3. 解决方案二
如果要按照方案二进行json的转换,那么就需要自定义序列化和反序列化器了,下面是实战案例
3.1 PayloadSerialize
定义序列化器,将实体类属性转换成 Json字符串
public class PayloadSerialize extends StdSerializer<Payload> {
private static final long serialVersionUID = 7679701332948432903L;
protected PayloadSerialize() {
this(null);
}
protected PayloadSerialize(Class<Payload> t) {
super(t);
}
@Override
public void serialize(Payload value, JsonGenerator gen, SerializerProvider provider) throws IOException {
JacksonInterface payload = value.getPayload();
gen.writeStartObject();
if (payload != null) {
Type type = payload.getClass().getAnnotation(Type.class);
if (type != null) {
gen.writeStringField("type", type.value());
}
}
gen.writeObjectField("payload", payload);
gen.writeEndObject();
}
}
3.2 PayloadDeserialize
定义反序列化器,主要目的是将 Json字符串
中对应的标识符,转换成对应的实体类型,下面对 Payload 类型判断是写死了,这里可以改用 SPI 来读取对应的接口实现类,SPI 的时候后续我在单独开一个专栏写
public class PayloadDeserialize extends StdDeserializer<Payload> {
private static final long serialVersionUID = 8851541065777997216L;
protected PayloadDeserialize() {
this(null);
}
protected PayloadDeserialize(Class<Payload> t) {
super(t);
}
@Override
public Payload deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
TreeNode treeNode = p.getCodec().readTree(p);
String type = null;
TreeNode typeNode = treeNode.get("type");
if (TextNode.class.isAssignableFrom(typeNode.getClass())) {
type = ((TextNode) Objects.requireNonNull(typeNode, "type标识符不能为空")).textValue();
}
Payload payload = new Payload();
Field[] fields = Payload.class.getDeclaredFields();
for (Field field : fields) {
try {
field.setAccessible(true);
Class<?> fieldType = field.getType();
TreeNode node = treeNode.get("payload");
if (JacksonInterface.class.isAssignableFrom(fieldType)) {
if ("Payload100".equalsIgnoreCase(type)) {
Payload100 payload100 = node.traverse(p.getCodec()).readValueAs(Payload100.class);
field.set(payload, payload100);
}
}
} catch (IllegalAccessException ignored) {
}
}
return payload;
}
}
3.3 Payload
这里标记 Payload 需要的序列化器和反序列化器,其中 type
字段不需要定义,通过 payload
字段中的 @Type
注解来标记是什么类型
@Data
@JsonSerialize(using = PayloadSerialize.class)
@JsonDeserialize(using = PayloadDeserialize.class)
public class Payload {
private JacksonInterface payload;
}
3.4 @Type
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Type {
String value();
}
转换后的结果显示,可以实现第二种 Json
结构的转换
@Test
public void test_Jackson_serializer() throws IOException {
Payload payload = new Payload();
Payload100 payload100 = new Payload100();
payload100.setName("zs");
payload.setPayload(payload100);
String s = objectMapper.writeValueAsString(payload);
System.out.println("序列化成字符串:" + s);
System.out.println("转换后的实体类结果:");
System.out.println(objectMapper.readValue(s, Payload.class));
}