利用ByteBuddy实现抽象类的JSON序列化和反序列化

本文最后更新于:2023年4月15日 晚上

Builder模式

在实验室的一个项目中, 有一个将标准化YANG文件转化为JAVA类的工具, 这个工具采用的是Builder模式, 以下以Bar这个类做为示例

Bar: 作为抽象接口

1
2
3
4
5
6
package top.roccoshi.demo.builder;

public interface Bar {
String getBarName();
}

BuilderCheckedBuilder接口:

1
2
3
4
5
6
7
8
9
10
package top.roccoshi.demo.builder;

public interface Builder<T> extends CheckedBuilder<T, IllegalArgumentException> {
}


public interface CheckedBuilder<T, E extends Exception> {
T build() throws E;
}

BarBuilder: BarBuilder

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
package top.roccoshi.demo.builder;

import java.util.Objects;

public class BarBuilder implements Builder<Bar> {
private String _barName;

@Override
public Bar build() throws IllegalArgumentException {
return new BarImpl(this);
}

public BarBuilder() {
}

public BarBuilder(Bar base) {
this._barName = base.getBarName();
}

public String getBarName() {
return _barName;
}

public BarBuilder setBarName(String _barName) {
this._barName = _barName;
return this;
}

private static final class BarImpl implements Bar {

private final String _barName;

private BarImpl(BarBuilder barBuilder) {
_barName = barBuilder._barName;
}

@Override
public String getBarName() {
return _barName;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof BarImpl)) return false;
BarImpl bar = (BarImpl) o;
return Objects.equals(_barName, bar._barName);
}

@Override
public int hashCode() {
return Objects.hash(_barName);
}

@Override
public String toString() {
return "BarImpl{" +
"_barName='" + _barName + '\'' +
'}';
}
}
}

可以看到BarBuilder中先是实现了build方法, 然后通过builder通过实例化并返回一个内部类BarImpl的方式实现Bar的实例化

JSON的反序列化的问题

为了进一步阐述问题, 我们采用builder模式定义了一个包含Bar类成员的Foo类:

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
package top.roccoshi.demo.builder;

// Foo
public interface Foo {
String getName();
String getMessage();
Bar getBar();
}

// Builder
public class FooBuilder implements Builder<Foo> {

private String _name;
private String _message;
private Bar _bar;

// 实现两种构造函数
public FooBuilder() {
}

public FooBuilder(Foo base) {
this._name = base.getName();
this._message = base.getMessage();
this._bar = base.getBar();
}

@Override
public Foo build() throws IllegalArgumentException {
return new FooImpl(this);
}

public String getName() {
return _name;
}

public FooBuilder setName(String _name) {
this._name = _name;
return this;
}

public String getMessage() {
return _message;
}

public FooBuilder setMessage(String _message) {
this._message = _message;
return this;
}

public Bar getBar() {
return _bar;
}

public FooBuilder setBar(Bar _bar) {
this._bar = _bar;
return this;
}

private static final class FooImpl implements Foo {

private final String _name;
private final String _message;
private final Bar _bar;

FooImpl(FooBuilder base) {
this._name = base.getName();
this._bar = base.getBar();
this._message = base.getMessage();
}

@Override
public String getName() {
return _name;
}

@Override
public String getMessage() {
return _message;
}

@Override
public Bar getBar() {
return _bar;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof FooImpl)) return false;
FooImpl foo = (FooImpl) o;
return Objects.equals(_name, foo._name) && Objects.equals(_message, foo._message) && Objects.equals(_bar, foo._bar);
}

@Override
public int hashCode() {
return Objects.hash(_name, _message, _bar);
}

@Override
public String toString() {
return "FooImpl{" +
"_name='" + _name + '\'' +
", _message='" + _message + '\'' +
", _bar=" + _bar +
'}';
}
}
}

然后我们使用jackson测试序列化的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void testBuilderSerializeJacksonNoAbstractClass() throws IOException {
Bar bar = new BarBuilder().setBarName("bar").build();
Foo foo = new FooBuilder().setMessage("hello").setBar(bar).build();
ObjectMapper mapper = new ObjectMapper();
try {
// 序列化为JSON对象
String s = mapper.writeValueAsString(foo);
System.out.println("convert foo in String: " + s);
// 输出: {"name":null,"message":"hello","bar":{"barName":"bar"}}

// 反序列化并build
Foo convertedFoo = mapper.readValue(s, FooBuilder.class).build();
System.out.println("convertedFoo = " + convertedFoo);
assertNotEquals(foo, convertedFoo);
} catch (Exception e) {
assertEquals(e.getClass(), JsonMappingException.class);
System.out.println(e);
/**
com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of top.roccoshi.demo.builder.Bar, problem: abstract types either need to be mapped to concrete types, have custom deserializer, or be instantiated with additional type information
at [Source: {"name":null,"message":"hello","bar":{"barName":"bar"}}; line: 1, column: 38] (through reference chain: top.roccoshi.demo.builder.FooBuilder["bar"])
**/
}
}

可以看到输出的原因在于Bar是一个抽象类型(interface), 没有被映射到具体的类型或者指定反序列化的方法

原因很简单, 光看FooBar这俩接口:

1
2
3
4
5
6
7
8
9
10
11
// Foo
public interface Foo {
String getName();
String getMessage();
Bar getBar();
}

// Bar
public interface Bar {
String getBarName();
}

谁在解析字符串(反序列化)的时候知道你这个Bar怎么生成啊?

解决方式

  • 第一种解决方式是用jackson对应的注解

但是问题在于: 这个项目的代码都是由工具生成在target中的, 除非修改工具库的源代码, 否则手动添加注解是个费事且麻烦的事情

有没有非侵入性修改源代码的方式, 能解决这种模式下的JSON序列化和反序列化问题呢?

在jackson中手动添加AbstractTypeMapping

这个方法相当于告诉jackson, 对于每一个抽象类型, 我应该用哪个实现类去实现序列化

方式如下:

  • 首先定义一个继承了BarBarBuilder的类型
1
2
3
4
package top.roccoshi.demo.builder;

public class BarExtend extends BarBuilder implements Bar {
}
  • 添加typeMapping
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testBuilderSerializeJacksonAbstractClassWithoutSrcChanged() throws IOException {
Bar bar = new BarBuilder().setBarName("bar").build();
Foo foo = new FooBuilder().setMessage("hello").setBar(bar).build();
ObjectMapper mapper = new ObjectMapper();
// ========= this is the difference ==========
SimpleModule mod = new SimpleModule();
mod.addAbstractTypeMapping(Bar.class, BarExtend.class);
mapper.registerModule(mod);
// ========= this is the difference ==========
String s = mapper.writeValueAsString(foo);
System.out.println("convert foo in String: " + s);
// convert foo in String: {"name":null,"message":"hello","bar":{"barName":"bar"}}
Foo convertedFoo = mapper.readValue(s, FooBuilder.class).build();
System.out.println("convertedFoo = " + convertedFoo);
// convertedFoo = FooImpl{_name='null', _message='hello', _bar=top.roccoshi.demo.builder.BarExtend@5c5a1b69}
assertEquals(mapper.writeValueAsString(foo), mapper.writeValueAsString(convertedFoo));
}

这样的方式不用修改BarBuilder和Bar的源代码, 但是问题是, 自己还是需要手动为每个类定义Extend类, 并且还要手动一个个将这些类的mapping关系添加进去

除了解决了入侵性, 工作量并没有减少

有没有什么方式, 可以实现不用手动做额外的事情呢?

这时候就想到了之前提到的ByteBuddy了: ByteBuddy — 简单介绍和入门 _

利用ByteBuddy动态生成类

基于ByteBuddy动态生成字节码的特性, 我们可否将上述思路转化为动态生成class的模式?

答案是当然可以

首先我们需要知道

  • 如何动态构建一个public class BarExtend extends BarBuilder implements Bar {}这样的类?

答案是

1
2
3
4
Class<?> dynamic = new ByteBuddy()
.subclass(BarBuilder.class)
.implement(Bar.class)
.make()

就可以了

  • 然后是如何动态添加一个abstractTypeMapping?

答案是实现一个SimpleAbstractTypeResolver就可以了,

该类具有一个findTypeMapping的方法, 目的是让jackson能在运行时找到abstractType对应的type, 注意到_mapping中保存了所有的typeMapping关系, 只有当_mapping找不到映射就会返回null, 那么我们只要覆写_mapping找不到映射关系时的逻辑即可

1
2
3
4
5
public JavaType findTypeMapping(DeserializationConfig config, JavaType type) {
Class<?> src = type.getRawClass();
Class<?> dst = (Class)this._mappings.get(new ClassKey(src));
return dst == null ? null : config.getTypeFactory().constructSpecializedType(type, dst);
}

整个实现如下:

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
@Test    
public void testBuilderSerializeJacksonAbstractClassWithoutSrcChangedAndManuallyAdd() throws IOException {
Bar bar = new BarBuilder().setBarName("bar").build();
Foo foo = new FooBuilder().setMessage("hello").setBar(bar).build();
ObjectMapper mapper = new ObjectMapper();
// ========= this is the difference ==========
SimpleModule mod = new SimpleModule();
SimpleAbstractTypeResolver resolver = new SimpleAbstractTypeResolver() {
private static final String Suffix = "$dynamicType$";

@Override
public JavaType findTypeMapping(DeserializationConfig config, JavaType type) {
Class<?> srcClass = type.getRawClass();
String srcClassName = srcClass.getName();
JavaType t = super.findTypeMapping(config, type);
if (t != null || srcClassName.endsWith(Suffix)) {
return t;
}
try {
Class<?> builder = Class.forName(srcClassName + "Builder");
System.out.println("find builder for " + srcClassName + ", builder = " + builder.getName());
Class<?> dynamic;
try {
dynamic = Class.forName(srcClassName + Suffix);
} catch (ClassNotFoundException e) {
dynamic = new ByteBuddy()
.subclass(builder)
.implement(srcClass)
.name(srcClassName + Suffix)
.make()
.load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
.getLoaded();
}
System.out.println("dynamic is loaded, className = " + dynamic.getName());
return config.getTypeFactory().constructSpecializedType(type, dynamic);
} catch (Exception e) {
System.out.println("error in abstract types: " + e);
return null;
}
}
};
mod.setAbstractTypes(resolver);
mapper.registerModule(mod);
// ========= this is the difference ==========
String s = mapper.writeValueAsString(foo);
System.out.println("convert foo in String: " + s);
Foo convertedFoo = mapper.readValue(s, FooBuilder.class).build();
System.out.println("convertedFoo = " + convertedFoo);
assertEquals(mapper.writeValueAsString(foo), mapper.writeValueAsString(convertedFoo));
String json = " {\"message\":\"hello\",\"bar\":{\"barName\":\"bar\"}}";
Foo fromJsonFoo = mapper.readValue(json, FooBuilder.class).build();
assertEquals(fromJsonFoo.getBar().getClass().getName(), "top.roccoshi.demo.builder.Bar$dynamicType$");
}
/**
convert foo in String: {"name":null,"message":"hello","bar":{"barName":"bar"}}

find builder for top.roccoshi.demo.builder.Bar, builder = top.roccoshi.demo.builder.BarBuilder

dynamic is loaded, className = top.roccoshi.demo.builder.Bar$dynamicType$

convertedFoo = FooImpl{_name='null', _message='hello', _bar=top.roccoshi.demo.builder.Bar$dynamicType$@149e0f5d}
**/


利用ByteBuddy实现抽象类的JSON序列化和反序列化
https://blog.roccoshi.top/posts/40145/
作者
RoccoShi
发布于
2022年8月26日
许可协议