引发错误:在序列化包含惰性“多对一”属性的 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;

我的观察

    试图显式调用ownerpet 中的getter,以强制从petowner 的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&lt;?&gt; 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 -&gt; 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 ...”的主要内容,如果未能解决你的问题,请参考以下文章

Hibernate 中一对一、多对一和一对多的默认获取类型

关于JPA一对一,一对多(多对一),多对多的详解

JPA多对一单向关联

Hibernate/JPA 多对一与单对多

spring-boot-jap-layui-mysql 完整的jpa多对一

spring-boot-jap-layui-mysql 完整的jpa多对一