0. 序言
在Spring项目中使用关联实体可以享受到面向对象设计(OOD)带来的优势, 但也会面临一些问题,例如在大量的关联会导致一定的性能下降,配置错误则会带来更严重的性能问题。非瞬时数据在离开内存后主要流向两个方向,数据库和网络。数据库与对象的转换由JPA规范处理,如何处理网络传输时的序列化是另一个重要问题,其中JSON是事实标准且十分简洁,Jackson是Spring Boot标定的行业“习惯”,用来处理JSON序列化。
完整的Jackson知识可能在后续系列中介绍,本文主要介绍如何定义一对关联对象,但在序列化与反序列化时,关联对象都以Id来表示(与采用非关联方案的系统在序列化时一致)
1. @JsonIdentityReference 注解
@Entity
public class Human extends Carrier {
@Id
private String id;
@JsonIdentityReference(alwaysAsId = true) //(1)
@OneToOne
private Head head;
private String hand;
}
@JsonIdentityInfo( //(2)
generator = ObjectIdGenerators.PropertyGenerator.class,//TODO test
property = "id")
@Entity
public class Head {
@Id
private String id;
}
通过在被管理实体上增加@JsonIdentityInfo(2)指明哪一列代表本类序列化结果的id列,并在主实体的关联属性上标注@JsonIdentityReference(alwaysAsId = true)(1)指明该对象属性将在序列化过程中被其Id替换。
如上述例子得到的json是{id:'Bob', head:'bob_head_id', hand: 'bob_hand'},
而不是{id:'Bob', head:{id:'bob_head_id'}, hand: 'bob_hand'}。
2. ObjectIdResolver
完成第一节后,关联对象可以正常的序列化,但当你试图通过在第一步中取得的JSON对象更新这个实体,如 PUT: /humans BODY:{id:'Bob', head:bob_head_id, hand: 'bob_new_hand'},你将获得一个报错,声称string 类型的属性 head 无法反序列化为 Head 对象。这是因为你没有显示的通知Jackson如何处理反序列化。
这时你要为被管理的实体补充必须的信息,即提供一个包含反序列化时如何将Id转化为其所代表的对象的方法的类(3),这个方法通常都是从数据库中读取这个实体。
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id",
resolver = EntityIdResolver.class, //(3)
scope=Head.class)
@Entity
public class Head {
@Id
private String id;
}
通过scope指明我们要操作的实体,并提供给Jackson 一个自定义的解析类(3),Jackson将在反序列化时通过我们定义的方法把id转换为关联实体。转化类如下,
@Component
@Scope("prototype") // must not be a singleton component as it has state
public class EntityIdResolver extends SimpleObjectIdResolver {
private EntityManager entityManager;
@Autowired
public EntityIdResolver (EntityManager entityManager) {
this.entityManager = entityManager;
}
@Override
public void bindItem(final ObjectIdGenerator.IdKey id, final Object pojo {
super.bindItem(id, pojo);
}
@Override
public Object resolveId(ObjectIdGenerator.IdKey id) {
Object resolved = super.resolveId(id);
if (resolved == null) {
resolved = _tryToLoadFromSource(id);
bindItem(id, resolved);
}
return resolved;
}
private Object _tryToLoadFromSource(ObjectIdGenerator.IdKey idKey) {
requireNonNull(idKey.scope, "global scope does not supported");
return entityManager.find(idKey.scope, idKey.key);
}
@Override
public ObjectIdResolver newForDeserialization(Object context) {
return new EntityIdResolver(entityManager);
}
@Override
public boolean canUseFor(ObjectIdResolver resolverType) {
return Objects.equals(resolverType.getClass(), EntityManager.class);
}
}
这里是一个ObjectIdResolver的实现,这个接口是Jackson提供的,在反序列化带有@JsonIdentityReference(alwaysAsId = true)注解的属性时,Jackson会实例化这个接口的实现完成转换工作,我们使用Jpa查询转换Id为实体.
3. HandlerInstantiator
当你在一个非Spring环境中时,你已经完成了全部的工作。不幸的是,在第二步的例子中EntityManager是由Spring管理的bean,而我们实现的ObjectIdResolver的子类 EntityIdResolver 必须由Jackson直接实例化,这将导致EntityIdResolver中的EntityManager对象被调用时抛出一个NPE。
为此我们希望Jackson能从Spring 上下文中获取EntityIdResolver,而不是自己进行实例化,通过在装配Jackson的工作对象(ObjectMapper)时提供一个自定义的 HandlerInstantiator可以实现这个的功能。
HandlerInstantiator中列出了Jackson的大量需要用户提供工作类来拓展功能的接口(常见的实现形式是如本例一样在注解中指出自定义的类,由Jackson在运行时动态加载), 覆写该类中的方法将自定义Jackson实例化这些拓展类的方式。查看源码发现 @JsonIdentityInfo中使用的ObjectIdResolver也包含在HandlerInstantiator定义的方法resolverIdGeneratorInstance()中。HandlerInstantiator一个简单的例子如下,
@Component
public class SpringBeanHandlerInstantiator extends HandlerInstantiator {
@Autowired
private ApplicationContext applicationContext;
public ObjectIdResolver resolverIdGeneratorInstance(MapperConfig> config, Annotated annotated, Class> implClass) {
try {
return (EntityIdResolver)applicationContext.getBean("EntityIdResolver");
} catch(e) {
return null;
}
}
...
}
不要忘了把这个自定义的实例化模版添加到Jackson的构造器中。
@Configuration
public class JacksonConfiguration {
@Autowired
SpringBeanHandlerInstantiator springBeanHandlerInstantiator;
@Bean
public Jackson2ObjectMapperBuilder jacksonBuilder(HandlerInstantiator handlerInstantiator) {
return new Jackson2ObjectMapperBuilder().handlerInstantiator(handlerInstantiator);
}
}
4. SpringHandlerInstantiator
当然实际使用中,你不必创建自定义HandlerInstantiator,因为Spring已经实现了一个SpringHandlerInstantiator,覆写了全部方法。你只需要Spring配置中将它连接到Jackson2ObjectMapperBuilder.
@Configuration
public class JacksonConfiguration {
@Bean
public HandlerInstantiator handlerInstantiator(ApplicationContext applicationContext) {
return new SpringHandlerInstantiator(applicationContext.getAutowireCapableBeanFactory());
}
@Bean
public Jackson2ObjectMapperBuilder jacksonBuilder(HandlerInstantiator handlerInstantiator) {
return new Jackson2ObjectMapperBuilder().handlerInstantiator(handlerInstantiator);
}
}
5. 总结与参考文献
未研究的部分包括
1> @JsonIdentityInfo 注解中generator的使用
2> SpringHandlerInstantiator的实现
关于Jackson在关联关系中的完整应用请参考这篇文章
https://www.baeldung.com/jack...
本文的思路来源于这篇文章
https://stackoverflow.com/que...