Возникла ошибка: «Не найден сериализатор для класса java.lang.Long ...» из контроллера при сериализации объекта JPA, содержащего ленивое свойство «многие-к-одному» - PullRequest
0 голосов
/ 06 ноября 2018

Я нахожусь на 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;

Но при отправке запроса, подобного /pets через клиента (например, PostMan), метод 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;
}

Мои наблюдения

  1. Попытка явно вызвать геттеры в пределах от owner до pet, чтобы принудительно запустить отложенную загрузку из прокси-объекта javaassist owner в пределах pet. Но не сработало.

    @GetMapping("/pets")
    public @ResponseBody List<Pet> get() {
       List<Pet> pets = petRepository.findAll();
       pets.forEach( pet -> pet.getOwner().getId());
       return pets;
    }   
    
  2. Попытка, как предложено в этом ответе stackoverflow в https://stackoverflow.com/a/51129212/5107365, чтобы иметь вызов контроллера для делегирования компоненту службы в рамках транзакции для принудительной отложенной загрузки. Но это тоже не сработало.

    @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;
        }
    

    }

  3. Работает, когда Service / Controller возвращает DTO, созданный из объекта. Очевидно, причина в том, что сериализатор JSON начинает работать с POJO вместо сущности ORM без каких-либо фиктивных объектов.

  4. Изменение режима выборки сущностей на FetchType.EAGER решит проблему, но я не хотел его менять.

Мне любопытно узнать, почему выбрасывается исключение в случае (1) и (2). Те должны были принудительно загружать ленивые объекты.

Вероятно, ответ может быть связан с жизнью и областью действия созданных javassist объектов для поддержки ленивых объектов. Тем не менее, интересно, как сериализатор Джексон не может найти сериализатор для типа оболочки Java, как java.lang.Long. Пожалуйста, помните здесь, что сгенерированное исключение указывает, что сериализатор Джексона получил доступ к owner.getId, поскольку он распознал тип свойства ownerId как java.lang.Long.

Любые подсказки будут высоко оценены.

Редактировать

Отредактированная часть из принятого ответа объясняет причины. Предложение использовать собственный сериализатор очень полезно, если мне не нужно идти по пути DTO.

Я немного просмотрел источники в Джексоне, чтобы выяснить причины. Мысль поделиться этим тоже.

Джексон кэширует большинство метаданных сериализации при первом использовании. Логика, связанная с обсуждаемым сценарием использования, начинается с этого метода com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serializeContents(Collection<?> value, JsonGenerator g, SerializerProvider provider). И соответствующий фрагмент кода: -

enter image description here

Оператор serializer = _findAndAddDynamic(serializers, cc, provider) в строке # 140 запускает поток для назначения сериализаторов для свойств pet -уровня, пропуская ownerId для последующей обработки через serializer.serializeWithType в строке # 147.

Назначение сериализаторов производится методом com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.resolve(SerializerProvider provider). Соответствующий фрагмент показан ниже: -

enter image description here

Сериализаторы назначаются в строке # 340 только для тех свойств, которые подтверждены как final посредством проверки в строке # 333.

Когда сюда приходит owner, его прокси-свойства имеют тип com.fasterxml.jackson.databind.type.SimpleType. Если бы эта связанная сущность была загружена eagerly, прокси-свойства явно не были бы там. Вместо этого исходные свойства будут найдены со значениями, которые набираются конечными классами, такими как Long, String и т. Д. (Как и свойства pet).

Интересно, почему Джексон не может решить эту проблему с их конца, используя тип получателя вместо использования свойства прокси. Во всяком случае, это может быть другая тема для обсуждения :-)

1 Ответ

0 голосов
/ 06 ноября 2018

Это связано со способом, которым Hibernate (внутренне используемый весенней загрузкой для JPA по умолчанию) увлажняет объекты. Ленивый объект не загружается, пока не будет запрошен какой-либо параметр объекта. Hibernate возвращает прокси, который делегирует dto после запуска запросов для гидратации объектов.

В вашем сценарии загрузка OwnerId не помогает, потому что это ключ, через который вы ссылаетесь на объект владельца, т.е. OwnerId уже присутствует в объекте Pet, поэтому гидратация не будет иметь место.

Как в 1, так и в 2 вы фактически не загрузили объект-владелец, поэтому, когда Джексон пытается сериализовать его на уровне контроллера, происходит сбой. В 3 и 4 объект владельца был загружен явно, поэтому у Джексона не возникает никаких проблем.

Если вы хотите, чтобы 2 работал, загрузите какой-либо параметр владельца, кроме id, и hibernate гидратирует объект, а затем Джексон сможет его сериализовать.

Отредактированный ответ

Проблема здесь со стандартным сериализатором Джексона. Это проверяет возвращаемый класс и извлекает значение каждого атрибута через отражение. В случае объектов гибернации возвращаемый объект является прокси-классом-делегатором, в котором все параметры имеют значение null, но все методы получения перенаправляются на содержащийся экземпляр. Когда объект проверяется, значения каждого атрибута по-прежнему равны нулю, по умолчанию это ошибка, как объяснено здесь

Так что, в основном, вы должны сказать Джексону, как сериализовать этот объект. Вы можете сделать это, создав сериализатор класса

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. Контроллер / домен должен быть отделен от уровня хранения.

...