抛出错误:序列化包含惰性 "many-to-one" 属性 的 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
我正在使用 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;
}
我的观察
试图通过 pet
显式调用 owner
中的 getter 以强制从 javaassist 代理对象进行延迟加载owner
内 pet
。但是没有用。
@GetMapping("/pets")
public @ResponseBody List<Pet> get() {
List<Pet> pets = petRepository.findAll();
pets.forEach( pet -> pet.getOwner().getId());
return pets;
}
尝试按照 处的 Whosebug 答案的建议让控制器调用委托给事务范围内的服务 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;
}
}
当 Service/Controller 返回从实体创建的 DTO 时有效。显然,原因是 JSON 序列化器开始使用 POJO 而不是 ORM 实体,其中没有任何模拟对象。
把entity fetch mode改成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)
。并且,相应的代码片段是:-
第 140 行的语句 serializer = _findAndAddDynamic(serializers, cc, provider)
触发流程为 pet
级别的属性分配序列化程序,同时跳过 ownerId
以稍后通过第 serializer.serializeWithType
行进行处理#147。
序列化程序的分配在 com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.resolve(SerializerProvider provider)
方法中完成。相应的片段如下所示:-
序列化程序在第 340 行仅分配给那些通过第 333 行的检查确认为 final
的属性。
当owner
来到这里时,发现它的代理属性是com.fasterxml.jackson.databind.type.SimpleType
类型。如果已加载此关联实体 eagerly
,则代理属性显然不会存在。相反,将使用最终 类 键入的值找到原始属性,如 Long、String 等(就像 pet
属性)。
想知道为什么 Jackson 不能通过使用 getter 的类型而不是使用代理 属性 的类型来解决这个问题。无论如何,这可能是一个不同的话题来讨论 :-)
这与 Hibernate(spring boot 默认用于 JPA 的内部方式)混合对象的方式有关。在请求对象的某些参数之前,不会加载惰性对象。 Hibernate returns 一个代理,它在触发查询以水化对象后委托给 dto。
在您的场景中,加载 OwnerId 没有帮助,因为它是您引用所有者对象的关键,即 OwnerId 已经存在于 Pet 对象中,因此水合作用不会发生。
在 1 和 2 中,您都没有实际加载所有者对象,因此当 Jackson 尝试在控制器级别序列化它时失败了。在 3 和 4 中,所有者对象已被显式加载,这就是 Jackson 没有 运行 任何问题的原因。
如果你想让 2 工作然后加载所有者的一些参数,除了 id,hibernate 将水化对象,然后 jackson 将能够序列化它。
已编辑答案
此处的问题与默认的 Jackson 序列化程序有关。这将检查返回的 class 并通过反射获取每个属性的值。在休眠实体的情况下,返回的对象是委托代理 class,其中所有参数都为空,但所有 getter 都被重定向到包含的实例。检查对象时,每个属性的值仍然为空,默认为错误,如解释
所以基本上,您需要告诉杰克逊如何序列化该对象。您可以通过创建序列化程序 class
来实现
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> {
或者,您可以从代理 class 创建一个所有者类型的新对象,手动填充它并在响应中设置它。
这有点迂回,但作为一般做法,您无论如何都不应该在外部公开您的 DTO。 controller/domain 应该与存储层解耦。
我正在使用 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;
}
我的观察
试图通过
pet
显式调用owner
中的 getter 以强制从 javaassist 代理对象进行延迟加载owner
内pet
。但是没有用。@GetMapping("/pets") public @ResponseBody List<Pet> get() { List<Pet> pets = petRepository.findAll(); pets.forEach( pet -> pet.getOwner().getId()); return pets; }
尝试按照 处的 Whosebug 答案的建议让控制器调用委托给事务范围内的服务 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; }
}
当 Service/Controller 返回从实体创建的 DTO 时有效。显然,原因是 JSON 序列化器开始使用 POJO 而不是 ORM 实体,其中没有任何模拟对象。
把entity fetch mode改成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)
。并且,相应的代码片段是:-
第 140 行的语句 serializer = _findAndAddDynamic(serializers, cc, provider)
触发流程为 pet
级别的属性分配序列化程序,同时跳过 ownerId
以稍后通过第 serializer.serializeWithType
行进行处理#147。
序列化程序的分配在 com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.resolve(SerializerProvider provider)
方法中完成。相应的片段如下所示:-
序列化程序在第 340 行仅分配给那些通过第 333 行的检查确认为 final
的属性。
当owner
来到这里时,发现它的代理属性是com.fasterxml.jackson.databind.type.SimpleType
类型。如果已加载此关联实体 eagerly
,则代理属性显然不会存在。相反,将使用最终 类 键入的值找到原始属性,如 Long、String 等(就像 pet
属性)。
想知道为什么 Jackson 不能通过使用 getter 的类型而不是使用代理 属性 的类型来解决这个问题。无论如何,这可能是一个不同的话题来讨论 :-)
这与 Hibernate(spring boot 默认用于 JPA 的内部方式)混合对象的方式有关。在请求对象的某些参数之前,不会加载惰性对象。 Hibernate returns 一个代理,它在触发查询以水化对象后委托给 dto。
在您的场景中,加载 OwnerId 没有帮助,因为它是您引用所有者对象的关键,即 OwnerId 已经存在于 Pet 对象中,因此水合作用不会发生。
在 1 和 2 中,您都没有实际加载所有者对象,因此当 Jackson 尝试在控制器级别序列化它时失败了。在 3 和 4 中,所有者对象已被显式加载,这就是 Jackson 没有 运行 任何问题的原因。
如果你想让 2 工作然后加载所有者的一些参数,除了 id,hibernate 将水化对象,然后 jackson 将能够序列化它。
已编辑答案
此处的问题与默认的 Jackson 序列化程序有关。这将检查返回的 class 并通过反射获取每个属性的值。在休眠实体的情况下,返回的对象是委托代理 class,其中所有参数都为空,但所有 getter 都被重定向到包含的实例。检查对象时,每个属性的值仍然为空,默认为错误,如解释
所以基本上,您需要告诉杰克逊如何序列化该对象。您可以通过创建序列化程序 class
来实现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> {
或者,您可以从代理 class 创建一个所有者类型的新对象,手动填充它并在响应中设置它。
这有点迂回,但作为一般做法,您无论如何都不应该在外部公开您的 DTO。 controller/domain 应该与存储层解耦。