本文最后更新于: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 () ; }
Builder
和CheckedBuilder
接口:
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
: Bar
的Builder
类
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;public interface Foo { String getName () ; String getMessage () ; Bar getBar () ; }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 { 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); assertNotEquals(foo, convertedFoo); } catch (Exception e) { assertEquals(e.getClass(), JsonMappingException.class); System.out.println(e); } }
可以看到输出的原因在于Bar
是一个抽象类型(interface), 没有被映射到具体的类型或者指定反序列化的方法
原因很简单, 光看Foo
和Bar
这俩接口:
1 2 3 4 5 6 7 8 9 10 11 public interface Foo { String getName () ; String getMessage () ; Bar getBar () ; }public interface Bar { String getBarName () ; }
谁在解析字符串(反序列化)的时候知道你这个Bar怎么生成啊?
解决方式
但是问题在于: 这个项目的代码都是由工具生成在target中的, 除非修改工具库的源代码, 否则手动添加注解是个费事且麻烦的事情
有没有非侵入性修改源代码的方式, 能解决这种模式下的JSON序列化和反序列化问题呢?
有
在jackson中手动添加AbstractTypeMapping 这个方法相当于告诉jackson, 对于每一个抽象类型, 我应该用哪个实现类去实现序列化
方式如下:
首先定义一个继承了Bar
和BarBuilder
的类型
1 2 3 4 package top.roccoshi.demo.builder;public class BarExtend extends BarBuilder implements Bar { }
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 (); SimpleModule mod = new SimpleModule (); mod.addAbstractTypeMapping(Bar.class, BarExtend.class); mapper.registerModule(mod); 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)); }
这样的方式不用修改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 (); 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); 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$" ); }