【翻译】使用Jackson反序列化接口(有修改)
作者:Andrew Tarry
原文链接:Deserializing an interface with Jackson
原文发表时间:2020-05-27 15:10 +0100
原文更新时间:2023-01-31 11:22 +0200
在将Json和Java对象互相转换的库中,我最喜欢的是Jackson。它可以自动把对象映射到POJO。但反序列化接口需要多写些代码。Jackson能从POJO中读取类型,但使用接口时,Jackson无法自动找到接口的实现,因而无法反序列化。
我将在下面的例子中展示如何将一个简单的json反序列化为POJO。json如下:
{ "myInterface": { "a": "Z", "b": "Y" } }
要转换成的POJO如下:
public class MyPojo { private MyInterface myInterface; // getters and setters }
MyInterface 类型只有一些getter方法
public interface MyInterface { String getA(); String getB(); }
这个例子相当刻意,但它展示了一种解决问题的通用思路,即直接告诉Jackson使用接口的哪个实现类。
状况1:能掌控接口且只有一个实现类
这是最简单的情况,因为接口只有一种实现类。使用接口可以避免接口与实现的紧耦合。这种方法要求您确定确实不需要多个实现类。
可以在接口上使用注释告诉Jackson如何反序列化它。
package com.andrewtarry.jackson.single; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; @JsonDeserialize(as = MyInterfaceImpl.class) public interface MyInterface { String getA(); String getB(); }
这段代码告诉Jackson将其反序列化为 MyInterfaceImpl 类的实例(要求 MyInterfaceImpl 是 MyInterface 的实现类)。
状况2:能掌控接口且有多个实现类
在代码中引入多态后,情况变得有点复杂。如果有多个可选的实现类,需要让Jackson知道使用哪个实现类。
package com.andrewtarry.jackson.multiple; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type", defaultImpl = MyInterfaceImpl.class ) @JsonSubTypes({ @JsonSubTypes.Type(value = MyInterfaceImpl.class, name = "standard"), @JsonSubTypes.Type(value = MyOtherInterfaceImpl.class, name = "other") }) public interface MyInterface { String getA(); String getB(); }
使用 @JsonSubType 注解可以支持多个实现类。在 @JsonTypeInfo 注解中指示Jackson从json中读取 type 属性,并据此选择实现类。
这个方法的好处是可以轻松地控制反序列化的实现类,但我们不会将反序列化逻辑的细节暴露到API中(译者注:存疑,使用这种方法反序列化明明就需要把实现类的类型写在JSON中,怎么能说没有暴露反序列化逻辑的细节呢?)。JSON需要像下面这样。
{ "myInterface": { "a": "Z", "b": "Y", "type": "other" } }
type 值只被Jackson使用,在API中使用它可能是个糟糕的方法。不同的实现类会产生不同的API调用结果,甚至可能要求用户来设置正确的实现类。
如果你的API只在应用内部使用,或者 type 属性是一个现有的、易于理解的值,那么这种方法是合适的。对于暴露到外部的API,这种方法不合适。
状况3:无法掌控接口
有一种情况没法向接口添加注解。你可能需要将JSON反序列化为第三方POJO,并且无法注释接口。
在这种情况下,有一种比注解麻烦一些的方法,就是通过向ObjectMapper注册模块来告诉Jackson如何反序列化。ObjectMapper是序列化和反序列化的核心类,模块则告诉ObjectMapper怎么做。可以用如下代码告诉Jackson,当要将JSON反序列化为 MyInterface ,实际上可以反序列化为 MyInterfaceImpl 。
public void registerModule(ObjectMapper objectMapper) { SimpleModule module = new SimpleModule(); module.addAbstractTypeMapping(MyInterface.class, MyInterfaceImpl.class); objectMapper.registerModule(module); }
如果使用模块指定要反序列化的接口的实现类仍然不能满足你的要求,可以尝试下面的方法。
在这种情况下,我们必须自定义反序列化的代码。
Jackson允许自定义反序列化器,可以不需要注释就将JSON反序列化为任何类型的数据。
package com.andrewtarry.jackson.custom; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import java.io.IOException; public class MyInterfaceDeserialize extends StdDeserializer<MyInterface> { public MyInterfaceDeserialize() { this(null); } protected MyInterfaceDeserialize(Class<?> vc) { super(vc); } @Override public MyInterface deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { JsonNode node = p.readValueAsTree(); String a = node.get("a").asText(); String b = node.get("b").asText(); MyInterfaceImpl impl = new MyInterfaceImpl(); impl.setA(a); impl.setB(b); return impl; } }
然后向Jackson注册这个反序列化器。
SimpleModule deserialization = new SimpleModule(); deserialization.addDeserializer(MyInterface.class, new MyInterfaceDeserialize()); objectMapper.registerModule(deserialization);
现在Jackson处理 MyInterface 都会使用自定义的反序列化器。这种方法可以将代码与json完全解耦,并解析其他方法无法解析的数据类型。只要我们想,甚至可以在这里返回一个匿名类,而不用创建实现类。
这种方法的缺点是需要写更多代码,在大型项目中会增加复杂性。我认为自定义反序列化是最后的手段,为了让项目保持简单,我们应该先尝试使用注释,如果注释无法解决问题,再使用自定义代码。
有一种和反序列化接口差不多的问题。当Jackson反序列化时,默认使用类的无参构造方法实例化类。当类没有无参构造方法,Jackson反序列化为这种类时,如不进行额外配置,将会产生错误。解决方法是在类的构造方法上使用 @JsonCreator 注解以指定它作为Jackson反序列化时的实例化方法,并且指定的构造方法的每个参数都必须使用 @JsonProperty 注解。在上面的例子中,如果 MyInterfaceImpl 只有一个全参构造方法,那么它的构造方法应该像下面这么写。
public class MyInterfaceImpl implements MyInterface { private String a; private String b; @JsonCreator public MyInterfaceImpl(@JsonProperty("a") String a, @JsonProperty("b") String b) { this.a = a; this.b = b; } // 省略setter和getter方法 }
如果类既没有无参构造方法,又是第三方类,那就没法在类的有参构造方法上使用注解来指定实例化方法了。这种情况下,可以自己创建一个类指定这个第三方类反序列化时的实例化方法。自己的类应该有唯一一个有参构造方法,这个有参构造方法的参数类型、名字、顺序和第三方类应该一模一样。不同的是,自己的类的有参构造方法上有 @JsonCreator 注解,且方法参数上应该有 @JsonProperty 注解。这个自定义类是为了替换第三方类上的注解。要使用这个自定义类,可以向ObjectMapper注册一个模块,在模块中使用自定义类指定第三方类反序列化时的实例化方法,代码如下。
public class Main { public void registerModule(ObjectMapper objectMapper) { SimpleModule module = new SimpleModule(); module.setMixInAnnotation(MyInterfaceImpl.class, MyInterfaceImplMixin.class); objectMapper.registerModule(module); } } class MyInterfaceImplMixin { @JsonCreator public MyInterfaceMixin(@JsonProperty("a") String a, @JsonProperty("b") String b) {} }
总结
将Java和json结合起来使用可能会遇到一些困难,因为一个使用强类型和运行时无法变更结构的数据结构,而另一个则十分灵活。处理此问题的方法因项目而异,我在这里提供几种将json转换为Java接口的方法。
API的设计不应该依赖于实现技术和接口的最佳实践,也不应该为了使用API而放弃Java中的多态(译者注:看不懂,自己去看原文吧)。上面就是一些解决API和Java代码不完全匹配的方案。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~