Jackson处理关联映射与Spring

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...

你可能感兴趣的:(序列化,关联对象,spring,jackson)