引发错误:在序列化包含惰性“多对一”属性的 JPA 实体时,来自控制器的“No serializer found for class java.lang.Long ...”
Posted
技术标签:
【中文标题】引发错误:在序列化包含惰性“多对一”属性的 JPA 实体时,来自控制器的“No serializer found for class java.lang.Long ...”【英文标题】:Error thrown: "No serializer found for class java.lang.Long..." from controller while serializing JPA entity containing lazy "many-to-one" property 【发布时间】: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://***.com/a/51129212/5107365 的这个 *** 答案的建议尝试让控制器调用委托给事务范围内的服务 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 的类型而不是使用代理属性的类型来解决这个问题。无论如何,这可能是一个不同的话题来讨论:-)
【问题讨论】:
【参考方案1】:这与 Hibernate(Spring Boot 默认在 JPA 内部使用的)水合对象的方式有关。在请求对象的某些参数之前,不会加载惰性对象。 Hibernate 返回一个代理,该代理在触发查询以水合对象后委托给 dto。
在您的场景中,加载 OwnerId 没有帮助,因为它是您引用所有者对象的关键,即 OwnerId 已经存在于 Pet 对象中,因此不会发生水合。
在 1 和 2 中,您实际上都没有加载所有者对象,因此当 Jackson 尝试在控制器级别对其进行序列化时,它会失败。在 3 和 4 中,所有者对象已被显式加载,这就是 Jackson 没有遇到任何问题的原因。
如果你想让 2 工作,那么加载一些所有者的参数,而不是 id,hibernate 将水合对象,然后杰克逊将能够序列化它。
已编辑答案
这里的问题在于默认的 Jackson 序列化程序。这将检查返回的类并通过反射获取每个属性的值。在休眠实体的情况下,返回的对象是一个委托代理类,其中所有参数都为空,但所有 getter 都被重定向到包含的实例。检查对象时,每个属性的值仍然为null,默认为错误解释here
所以基本上,你需要告诉杰克逊如何序列化这个对象。你可以通过创建一个序列化器类来做到这一点
public class OwnerSerializer extends StdSerializer<Owner>
public OwnerSerializer()
this(null);
public OwnerSerializer(Class<Owner> t)
super(t);
@Override
public void serialize(Owner value, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonProcessingException
jgen.writeStartObject();
jgen.writeNumberField("id", value.getId());
jgen.writeStringField("firstName", value.getFirstName());
jgen.writeStringField("lastName", value.getLastName());
jgen.writeEndObject();
并将其设置为对象的默认序列化程序
@JsonSerialize(using = OwnerSerializer.class)
public class Owner extends AbstractPersistable<Long>
或者,您可以从代理类创建一个 Owner 类型的新对象,手动填充它并在响应中设置它。
这有点迂回,但作为一般做法,无论如何您都不应该将 DTO 暴露在外部。控制器/域应该与存储层解耦。
【讨论】:
这听起来很合理。但是,即使我尝试使用firstName
而不是id
作为pets.forEach(pet -> pet.getOwner().getFirstName());
来补充水分,它也会引发异常。我从控制器和服务都试过了,但没有成功。
对惰性对象上的非 id getter 的显式调用应该会导致水合。但没有发现它是这样发生的。通过调试器模式还注意到,pet
中的 javassist 代理对象 owner
即使在杰克逊序列化时也是活动的。我想知道它是否与观察到的行为有任何关系。
代理是要返回的。这是意料之中的。通过调试器,尝试访问服务外部和服务内部对象中的所有属性。这会让你知道哪里出了问题。
能够通过调试器通过代理方法访问属性,没有任何问题。但是,序列化继续在 java.lang.Long 上抛出异常,这是 ownerId 的类型。我已经在bitbucket.org/rajpk/petserver 上发布了我的代码,以便任何人都能获得更好的图片。我的全部好奇心是想知道为什么显式调用惰性对象的非 id setter 本身并没有帮助序列化(不使用 dto 左右)。
使示例正常工作并使用代码 sn-ps 添加了编辑后的答案以上是关于引发错误:在序列化包含惰性“多对一”属性的 JPA 实体时,来自控制器的“No serializer found for class java.lang.Long ...”的主要内容,如果未能解决你的问题,请参考以下文章