Избегайте сериализации Джексона для невыбранных ленивых объектов

84

У меня есть простой контроллер, который возвращает объект User, у этого пользователя есть координаты атрибута, у которых есть свойство hibernate FetchType.LAZY.

Когда я пытаюсь получить этого пользователя, мне всегда нужно загружать все координаты, чтобы получить объект пользователя, иначе, когда Джексон попытается сериализовать пользователя, выдается исключение:

com.fasterxml.jackson.databind.JsonMappingException: не удалось инициализировать прокси - нет сеанса

Это связано с тем, что Джексон пытается забрать этот незавершенный объект. Вот объекты:

public class User{

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "user")
    @JsonManagedReference("user-coordinate")
    private List<Coordinate> coordinates;
}

public class Coordinate {

    @ManyToOne
    @JoinColumn(name = "user_id", nullable = false)
    @JsonBackReference("user-coordinate")
    private User user;
}

И контроллер:

@RequestMapping(value = "/user/{username}", method=RequestMethod.GET)
public @ResponseBody User getUser(@PathVariable String username) {

    User user = userService.getUser(username);

    return user;

}

Есть ли способ сказать Джексону не сериализовать невыгруженные объекты? Я искал другие ответы, опубликованные 3 года назад, о реализации модуля jackson-hibernate. Но, вероятно, этого можно было бы достичь с помощью новой функции Джексона.

Мои версии:

  • Весна 3.2.5
  • Гибернация 4.1.7
  • Джексон 2.2

Заранее спасибо.

r1ckr
источник
4
подход, описанный в этих двух ссылках, работал у меня: Spring 3.1, Hibernate 4 и Jackson-Module-Hibernate и FasterXML / jackson-datatype-hibernate
indybee
Спасибо, indybee, я смотрел учебник, в Spring 3.2.5 уже есть MappingJackson2HttpMessageConverter, но я не могу использовать его, чтобы избежать невыбранного ленивого объекта, также я пытался реализовать его в учебнике, но ничего. ..
r1ckr
1
вы получаете ту же ошибку "не удалось инициализировать прокси" или что-то другое? (я использовал дополнительный HibernateAwareObjectMapper, описанный в статьях о Spring 3.2.6, Hibernate 4.1.7 и jackson 2.3)
indybee
1
Понял!! Начиная с версии 3.1.2 Spring имеет собственный MappingJackson2HttpMessageConverter и почти такое же поведение, как в учебнике, но проблема заключалась в том, что я использую конфигурацию Spring java и начинаю с javaconfigs. Я пытался создать @Bean MappingJackson2HttpMessageConverter , вместо того, чтобы добавлять его в HttpMessageConverters Spring, может быть, ответ яснее :)
r1ckr
рад, что он работает :)
indybee

Ответы:

94

Наконец-то я нашел решение! Спасибо Indybee за то, что дал мне подсказку.

Учебник Spring 3.1, Hibernate 4 и Jackson-Module-Hibernate предлагает хорошее решение для Spring 3.1 и более ранних версий. Но начиная с версии 3.1.2 Spring имеет собственный MappingJackson2HttpMessageConverter с почти той же функциональностью, что и в руководстве, поэтому нам не нужно создавать этот настраиваемый HTTPMessageConverter.

С javaconfig нам также не нужно создавать HibernateAwareObjectMapper , нам просто нужно добавить Hibernate4Module к MappingJackson2HttpMessageConverter по умолчанию, который уже есть в Spring, и добавить его в HttpMessageConverters приложения, поэтому нам нужно:

  1. Расширьте наш класс конфигурации Spring из WebMvcConfigurerAdapter и переопределите метод configureMessageConverters .

  2. В этом методе добавьте MappingJackson2HttpMessageConverter с Hibernate4Module, зарегистрированным в предыдущем методе.

Наш класс конфигурации должен выглядеть так:

@Configuration
@EnableWebMvc
public class MyConfigClass extends WebMvcConfigurerAdapter{

    //More configuration....

    /* Here we register the Hibernate4Module into an ObjectMapper, then set this custom-configured ObjectMapper
     * to the MessageConverter and return it to be added to the HttpMessageConverters of our application*/
    public MappingJackson2HttpMessageConverter jacksonMessageConverter(){
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();

        ObjectMapper mapper = new ObjectMapper();
        //Registering Hibernate4Module to support lazy objects
        mapper.registerModule(new Hibernate4Module());

        messageConverter.setObjectMapper(mapper);
        return messageConverter;

    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        //Here we add our custom-configured HttpMessageConverter
        converters.add(jacksonMessageConverter());
        super.configureMessageConverters(converters);
    }

    //More configuration....
}

Если у вас есть конфигурация xml, вам также не нужно создавать собственный MappingJackson2HttpMessageConverter, но вам нужно создать персонализированный сопоставитель, который отображается в руководстве (HibernateAwareObjectMapper), поэтому ваша конфигурация xml должна выглядеть следующим образом:

<mvc:message-converters>
    <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
        <property name="objectMapper">
            <bean class="com.pastelstudios.json.HibernateAwareObjectMapper" />
        </property>
    </bean>
</mvc:message-converters>

Надеюсь, этот ответ будет понятен и поможет кому-то найти решение этой проблемы, любые вопросы не стесняйтесь задавать!

r1ckr
источник
3
Спасибо, что поделились решением. Однако после нескольких попыток я обнаружил, что последняя версия jackson на данный момент ( 2.4.2 ) НЕ работает с Spring 4.0.6. У меня все еще возникает ошибка "нет сеанса". Только когда я понизил версию Джексона до 2.3.4 (или 2.4.2), она начала работать.
Робин
1
Спасибо, что указали MappingJackson2HttpMessageConverter! Некоторое время я искал такой класс.
LanceP
1
Большое спасибо! Я только что протестировал его и его работу со Spring 4.2.4, Jackson 2.7.1-1 и JacksonDatatype 2.7.1 :)
Isthar
1
Это не решает проблему в Spring Data REST.
Alessandro C
1
Как решить ту же проблему, если вместо Spring используется Springboot ?. Я попытался зарегистрировать модуль Hibernate4 (это текущая версия), но это не решило мою проблему.
Ранджит Гампа
34

Начиная с Spring 4.2 и с использованием Spring Boot и javaconfig, зарегистрировать Hibernate4Module теперь так же просто, как добавить это в вашу конфигурацию:

@Bean
public Module datatypeHibernateModule() {
  return new Hibernate4Module();
}

ссылка: https://spring.io/blog/2014/12/02/latest-jackson-integration-improvements-in-spring

Chrismarx
источник
19
Для Hibernate 5 (как и в текущей версии Spring Boot) это так Hibernate5Module. Кроме того, требуется зависимость от<groupId>com.fasterxml.jackson.datatype</groupId><artifactId>jackson-datatype-hibernate5</artifactId>
Zeemee
1
У меня тоже работал на Spring Boot 2.1. Меня начинало сводить с ума.
Van Dame
Непонятно, как это реализовать. Я получаю сообщение об ошибке несовместимых типов.
EarthMind
2
Потрясающе! У меня работал с Spring Boot 2.0.6 и Hibernate 5.
GarouDan
Это было очень полезно. Следует отметить, что регистрации экземпляра класса Hibernate5Module в качестве компонента недостаточно. Если вы хотите применить его к одному экземпляру ObjectMapper, вы можете автоматически подключить модуль к управляемому Spring классу, а затем зарегистрировать его в экземпляре ObjectMapper. @Autowire Module jacksonHibernateModule; ObjectMapper mapper = новый ObjectMapper (); mapper.registerModule (this.jacksonHibernateModule).
gburgalum01
13

Это похоже на принятое решение @rick.

Если вы не хотите трогать существующую конфигурацию конвертеров сообщений, вы можете просто объявить Jackson2ObjectMapperBuilderbean-компонент, например:

@Bean
public Jackson2ObjectMapperBuilder configureObjectMapper() {
    return new Jackson2ObjectMapperBuilder()
        .modulesToInstall(Hibernate4Module.class);
}

Не забудьте добавить следующую зависимость в свой файл Gradle (или Maven):

compile 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate4:2.4.4'

Полезно, если у вас есть приложение, и вы хотите сохранить возможность изменять функции Джексона из application.propertiesфайла.

Луис Беллох
источник
Как мы можем добиться этого (настроить Джексона, чтобы избежать ленивой загрузки объекта) с помощью application.propertiesфайла?
Хему
1
Здесь говорится не об этом. Решение @Luis Belloch предлагает вам создать Jackson2ObjectMapperBuilder, чтобы вы не переопределяли преобразователь сообщений по умолчанию Spring Boot и продолжали использовать значения application.properties для управления свойствами Джексона.
капад 05
извините ... Я новичок, где бы вы реализовали этот метод? При весенней загрузке он использует Spring 5, WebMvcConfigurerAdapter устарел
Эрик Хуанг,
Hibernate4Moduleis legacy
Дмитрий Кальтович
8

В случае Spring Data Rest, хотя решение, опубликованное @ r1ckr, работает, все, что требуется, - это добавить одну из следующих зависимостей в зависимости от вашей версии Hibernate:

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-hibernate4</artifactId>
    <version>${jackson.version}</version>
</dependency>

или же

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-hibernate5</artifactId>
    <version>${jackson.version}</version>
</dependency>

В Spring Data Rest есть класс:

org.springframework.data.rest.webmvc.json.Jackson2DatatypeHelper

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

Однако есть проблема:

Проблема с сериализацией ленивого @ManyToOne

Алан Хэй
источник
Как вы настроите это с помощью весенней загрузки, когда вы не используете Spring Data Rest?
Эрик Хуанг
Недостаточно. Вам тоже нужно@Bean hibernate5Module()
Дмитрий Кальтович
4

Если вы используете XML-конфигурацию и используете <annotation-driven />, я обнаружил, что вам нужно вложиться <message-converters>внутрь, <annotation-driven>как рекомендовано в учетной записи Jackson Github .

Вот так:

<annotation-driven>
  <message-converters>
    <beans:bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
      <beans:property name="objectMapper">
        <beans:bean class="com.pastelstudios.json.HibernateAwareObjectMapper" />
      </beans:property>
    </beans:bean> 
  </message-converters>
</annotation-driven>

`

черное дерево
источник
xml config is legacy
Дмитрий Кальтович
3

Для тех, кто пришел сюда, чтобы найти решение для службы RESTful на основе Apache CXF, конфигурация, которая его исправляет, приведена ниже:

<jaxrs:providers>
    <bean class="com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider">
        <property name="mapper" ref="objectMapper"/>
    </bean>
</jaxrs:providers>

<bean id="objectMapper" class="path.to.your.HibernateAwareObjectMapper"/>

Где HibernateAwareObjectMapper определяется как:

public class HibernateAwareObjectMapper extends ObjectMapper {
    public HibernateAwareObjectMapper() {
        registerModule(new Hibernate5Module());
    }
}

С июня 2016 года требуется следующая зависимость (при условии, что вы используете Hibernate5):

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-hibernate5</artifactId>
    <version>2.7.4</version>
</dependency>  
user2968675
источник
1

Следующее решение предназначено для Spring 4.3 (без загрузки) и Hibernate 5.1, в котором мы перевели все типы fetchtypes fetch=FetchType.LAZYиз вариантов из fetch=FetchType.EAGERсоображений производительности. Сразу мы увидели com.fasterxml.jackson.databind.JsonMappingException: could not initialize proxyисключение из-за проблемы с отложенной загрузкой.

Сначала мы добавляем следующую зависимость maven:

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-hibernate5</artifactId>
</dependency>  

Затем в наш файл конфигурации Java MVC добавляется следующее:

@Configuration
@EnableWebMvc 
public class MvcConfig extends WebMvcConfigurerAdapter {

@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {

    Hibernate5Module h5module = new Hibernate5Module();
    h5module.disable(Hibernate5Module.Feature.USE_TRANSIENT_ANNOTATION);
    h5module.enable(Hibernate5Module.Feature.FORCE_LAZY_LOADING);

    for (HttpMessageConverter<?> mc : converters){
        if (mc instanceof MappingJackson2HttpMessageConverter || mc instanceof MappingJackson2XmlHttpMessageConverter) {
            ((AbstractJackson2HttpMessageConverter) mc).getObjectMapper().registerModule(h5module);
        }
    }
    return;
}

Ноты:

  • Вам необходимо создать и настроить Hibernate5Module, чтобы получить поведение, подобное Джексону, без этого модуля. По умолчанию сделаны несовместимые предположения.

  • У нас WebMvcConfigurerAdapterмного другой конфигурации, и мы хотели избежать другого класса конфигурации, поэтому мы не использовали WebMvcConfigurationSupport#addDefaultHttpMessageConvertersфункцию, о которой говорилось в других сообщениях.

  • WebMvcConfigurerAdapter#configureMessageConvertersотключает всю внутреннюю конфигурацию преобразователей сообщений Spring. Мы предпочли избежать потенциальных проблем, связанных с этим.

  • Использование extendMessageConvertersразрешенного доступа ко всем автоматически настраиваемым классам Джексона без потери конфигурации всех других конвертеров сообщений.

  • С его помощью getObjectMapper#registerModuleмы смогли добавить Hibernate5Moduleк существующим конвертерам.

  • Модуль был добавлен как в процессоры JSON, так и в XML.

Это дополнение решило проблему с гибернацией и отложенной загрузкой, но вызвало остаточную проблему со сгенерированным форматом JSON . Как сообщается в этой проблеме github , модуль отложенной загрузки hibernate-jackson в настоящее время игнорирует аннотацию @JsonUnwrapped, что приводит к потенциальным ошибкам данных. Это происходит независимо от настройки функции принудительной загрузки. Проблема существует с 2016 года.


Запись

Похоже, что, добавив следующее к классам, которые загружаются лениво, встроенный ObjectMapper работает без добавления модуля hibernate5:

@JsonIgnoreProperties(  {"handler","hibernateLazyInitializer"} )
public class Anyclass {
SDW
источник
Решение могло быть намного проще: kotlin @Bean fun hibernate5Module(): Module = Hibernate5Module()
Дмитрий Кальтович
0

Вы можете использовать следующий помощник из проекта Spring Data Rest:

Jackson2DatatypeHelper.configureObjectMapper(objectMapper);
Пшемек Новак
источник
Не о вопросе
Дмитрий Кальтович
0

Я сделал очень простое решение этой проблемы.

@JsonInclude(JsonInclude.Include.NON_EMPTY)
public Set<Pendency> getPendencies() {
    return Hibernate.isInitialized(this.pendencies) ? Collections.unmodifiableSet(this.pendencies) : new HashSet<>();
}

В моем случае я выдавал ошибку, потому что всякий раз, когда я возвращал Pendencies, в качестве хорошей практики я преобразовал его в список, который нельзя изменить, но как он мог или не мог быть ленивым в зависимости от метода, который я использовал для получения экземпляра (с выборкой или без нее), я провожу тест до того, как он был инициализирован Hibernate, и добавляю аннотацию, которая предотвращает сериализацию пустого свойства и решает мою проблему.

Педро Баккини
источник
0

Я целый день пытался решить ту же проблему. Вы можете сделать это, не меняя существующую конфигурацию конвертеров сообщений.

На мой взгляд, самый простой способ решить эту проблему всего за 2 шага с помощью jackson-datatype-hibernate :

Пример котлина (такой же, как java):

  1. Добавить в build.gradle.kts:
implementation("com.fasterxml.jackson.datatype:jackson-datatype-hibernate5:$jacksonHibernate")
  1. Создайте @Bean
   @Bean
   fun hibernate5Module(): Module = Hibernate5Module()

  • Обратите внимание, что Moduleэто com.fasterxml.jackson.databind.Moduleнеjava.util.Module

  • Также хорошей практикой является добавление @JsonBackReference& @JsonManagedReferenceк отношениям @OneToMany& @ManyToOne. @JsonBackReferenceможет быть только 1 в классе.

Дмитрий Кальтович
источник
-1

Хотя этот вопрос немного отличается от этого: при сериализации объекта Hibernate возникает странное исключение Джексона , основная проблема может быть исправлена ​​таким же образом с помощью этого кода:

@Provider
public class MyJacksonJsonProvider extends JacksonJsonProvider {
    public MyJacksonJsonProvider() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
        setMapper(mapper);
    }
}
Невстер
источник
-1

Я пробовал это и работал:

// настраиваемая конфигурация для отложенной загрузки

public static class HibernateLazyInitializerSerializer extends JsonSerializer<JavassistLazyInitializer> {

    @Override
    public void serialize(JavassistLazyInitializer initializer, JsonGenerator jsonGenerator,
            SerializerProvider serializerProvider)
            throws IOException, JsonProcessingException {
        jsonGenerator.writeNull();
    }
}

и настройте маппер:

    mapper = new JacksonMapper();
    SimpleModule simpleModule = new SimpleModule(
            "SimpleModule", new Version(1,0,0,null)
    );
    simpleModule.addSerializer(
            JavassistLazyInitializer.class,
            new HibernateLazyInitializerSerializer()
    );
    mapper.registerModule(simpleModule);
все
источник
-1

Другое решение для весенней загрузки: конфигурация: spring.jpa.open-in-view=true

Bianbian
источник
Это не поможет
Дмитрий Кальтович
-1

Я попробовал полезный ответ @rick , но столкнулся с проблемой, что «хорошо известные модули», такие как jackson-datatype-jsr310, не были автоматически зарегистрированы, несмотря на то, что они были в пути к классам. (Это сообщение в блоге объясняет автоматическую регистрацию.)

Расширяя ответ @ rick, вот вариант с использованием Spring Jackson2ObjectMapperBuilder для создания файлаObjectMapper . Это автоматически регистрирует «известные модули» и устанавливает определенные функции в дополнение к установке Hibernate4Module.

@Configuration
@EnableWebMvc
public class MyWebConfig extends WebMvcConfigurerAdapter {

    // get a configured Hibernate4Module
    // here as an example with a disabled USE_TRANSIENT_ANNOTATION feature
    private Hibernate4Module hibernate4Module() {
        return new Hibernate4Module().disable(Hibernate4Module.Feature.USE_TRANSIENT_ANNOTATION);
    }

    // create the ObjectMapper with Spring's Jackson2ObjectMapperBuilder
    // and passing the hibernate4Module to modulesToInstall()
    private MappingJackson2HttpMessageConverter jacksonMessageConverter(){
        Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder()
                            .modulesToInstall(hibernate4Module());
        return new MappingJackson2HttpMessageConverter(builder.build());
  }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(jacksonMessageConverter());
        super.configureMessageConverters(converters);
    }
}
Маркус Пшайдт
источник
У вас есть новости по этой теме? У меня сейчас проблема. Конечно, когда я добавляю hibernate5Module(потому что я использую hibernate 5), Джексон игнорирует прокси неинициализированных объектов, но кажется, что «известные модули» не регистрируются, несмотря на то, что я использую ваш подход. Я знаю это, потому что некоторые части моего приложения сломаны, и когда я удаляю модуль, они снова работают.
Даниэль Энао
@DanielHenao Вы изучали spring.io/blog/2014/12/02/… ? Я переключился на Hibernate5, используя DelegatingWebMvcConfigurationкласс (вместо WebMvcConfigurerAdapter), предложенный Jackson-Antpath-Filter :@Override public void configureMessageConverters(List<HttpMessageConverter<?>> messageConverters) { messageConverters.add(jacksonMessageConverter()); addDefaultHttpMessageConverters(messageConverters); }
Маркус Пшайдт
1. Hibernate4Moduleэто наследство. 2 SerializationFeature.WRITE_DATES_AS_TIMESTAMPS- не о вопросе
Дмитрий Кальтович
@DmitryKaltovich ad 1) на момент написания не было наследства; объявление 2) ок, снял.
Маркус Пшайдт