【发布时间】:2019-04-09 13:56:03
【问题描述】:
我在 Spring Boot 2.0.6 上,其中一个实体 pet 确实与另一个实体 owner 具有惰性多对一关系
宠物实体
@Entity
@Table(name = "pets")
public class Pet extends AbstractPersistable<Long> {
@NonNull
private String name;
private String birthday;
@JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id")
@JsonIdentityReference(alwaysAsId=true)
@JsonProperty("ownerId")
@ManyToOne(fetch=FetchType.LAZY)
private Owner owner;
但是在通过客户端(例如:PostMan)提交类似/pets 的请求时,controller.get() 方法会遇到如下异常:-
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class java.lang.Long and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: java.util.ArrayList[0]->com.petowner.entity.Pet["ownerId"])
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:77) ~[jackson-databind-2.9.7.jar:2.9.7]
at com.fasterxml.jackson.databind.SerializerProvider.reportBadDefinition(SerializerProvider.java:1191) ~[jackson-databind-2.9.7.jar:2.9.7]
Controller.get 实现
@GetMapping("/pets")
public @ResponseBody List<Pet> get() {
List<Pet> pets = petRepository.findAll();
return pets;
}
我的观察
-
试图显式调用
owner到pet中的getter,以强制从pet中owner的javaassist 代理对象进行延迟加载。但是没有用。@GetMapping("/pets") public @ResponseBody List<Pet> get() { List<Pet> pets = petRepository.findAll(); pets.forEach( pet -> pet.getOwner().getId()); return pets; } -
按照https://stackoverflow.com/a/51129212/5107365 的这个 stackoverflow 答案的建议尝试让控制器调用委托给事务范围内的服务 bean 以强制延迟加载。但这也没有用。
@Service @Transactional(readOnly = true) public class PetServiceImpl implements PetService { @Autowired private PetRepository petRepository; @Override public List<Pet> loadPets() { List<Pet> pets = petRepository.findAll(); pets.forEach(pet -> pet.getOwner().getId()); return pets; }}
它在服务/控制器返回从实体创建的 DTO 时起作用。显然,原因是 JSON 序列化程序可以使用 POJO 而不是 ORM 实体,其中没有任何模拟对象。
将实体获取模式更改为 FetchType.EAGER 可以解决问题,但我不想更改它。
我很想知道为什么在 (1) 和 (2) 的情况下会抛出异常。那些应该强制显式加载惰性对象。
可能答案可能与为维护惰性对象而创建的 javassist 对象的生命周期和范围有关。然而,想知道 Jackson 序列化器如何找不到像 java.lang.Long 这样的 java 包装器类型的序列化器。请记住,抛出的异常确实表明 Jackson 序列化程序可以访问 owner.getId,因为它将属性 ownerId 的类型识别为 java.lang.Long。
任何线索将不胜感激。
编辑
已接受答案中的编辑部分解释了原因。如果我不需要进入 DTO 的路径,使用自定义序列化程序的建议非常有用。
我对 Jackson 的来源进行了一些扫描,以挖掘根本原因。也想分享一下。
Jackson 会在首次使用时缓存大部分序列化元数据。与讨论中的用例相关的逻辑始于此方法com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serializeContents(Collection<?> value, JsonGenerator g, SerializerProvider provider)。并且,各自的代码sn-p是:-
第 140 行的 serializer = _findAndAddDynamic(serializers, cc, provider) 语句触发流程为 pet 级属性分配序列化程序,同时跳过 ownerId 以便稍后在第 147 行通过 serializer.serializeWithType 处理。
序列化程序的分配在com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.resolve(SerializerProvider provider) 方法中完成。各自的sn-p如下图:-
序列化程序仅在第 340 行分配给那些通过第 333 行检查确认为final 的属性。
当owner 来到这里时,它的代理属性被发现是com.fasterxml.jackson.databind.type.SimpleType 类型。如果此关联实体已加载eagerly,则代理属性显然不会存在。相反,原始属性将使用最终类(如 Long、String 等)键入的值找到(就像 pet 属性一样)。
想知道为什么 Jackson 不能通过使用 getter 的类型而不是使用代理属性的类型来解决这个问题。无论如何,这可能是一个不同的话题来讨论:-)
【问题讨论】:
标签: json spring-boot serialization lazy-loading many-to-one