Unikaj serializacji Jacksona na niedobranych leniwych obiektach

84

Mam prosty kontroler, który zwraca obiekt użytkownika, ten użytkownik ma współrzędne atrybutu, które mają właściwość hibernacji FetchType.LAZY.

Kiedy próbuję uzyskać tego użytkownika, zawsze muszę załadować wszystkie współrzędne, aby uzyskać obiekt użytkownika, w przeciwnym razie, gdy Jackson próbuje serializować, użytkownik zgłasza wyjątek:

com.fasterxml.jackson.databind.JsonMappingException: nie można zainicjować serwera proxy - brak sesji

Wynika to z tego, że Jackson próbuje pobrać ten nietknięty obiekt. Oto obiekty:

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

A kontroler:

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

    User user = userService.getUser(username);

    return user;

}

Czy jest sposób, aby powiedzieć Jacksonowi, aby nie serializował nieobrobionych obiektów? Szukałem innych odpowiedzi opublikowanych 3 lata temu wdrażających moduł jackson-hibernate-module. Ale prawdopodobnie można to osiągnąć dzięki nowej funkcji jackson.

Moje wersje to:

  • Wiosna 3.2.5
  • Hibernacja 4.1.7
  • Jackson 2.2

Z góry dziękuję.

r1ckr
źródło
4
podejście opisane w tych 2 linkach zadziałało dla mnie, Spring 3.1, Hibernate 4 and Jackson-Module-Hibernate and FasterXML / jackson-datatype-hibernate
indybee
Dziękuję indybee, oglądałem samouczek, wiosna 3.2.5 już mam MappingJackson2HttpMessageConverter, ale nie mogę go użyć, aby uniknąć niedostepnego leniwego obiektu, również próbowałem zaimplementować ten w tutorialu, ale nic. ..
r1ckr
1
czy otrzymujesz ten sam błąd „nie można zainicjować serwera proxy” czy coś innego? (użyłem dodatkowego HibernateAwareObjectMapper opisanego w artykułach z Spring 3.2.6 i Hibernate 4.1.7 oraz jackson 2.3)
indybee
1
Rozumiem!! Od wersji 3.1.2 Spring ma swój własny MappingJackson2HttpMessageConverter i zachowuje się prawie tak samo jak ten z samouczka, ale problem polegał na tym, że używam Spring java config i zaczynam od javaconfigs. Próbowałem zrobić @Bean MappingJackson2HttpMessageConverter , zamiast dodawać go do HttpMessageConverters wiosny, może jest jaśniejszy w odpowiedzi :)
r1ckr
cieszę się, że działa :)
indybee

Odpowiedzi:

94

W końcu znalazłem rozwiązanie! dzięki indybee za danie mi wskazówki.

Samouczek Spring 3.1, Hibernate 4 i Jackson-Module-Hibernate mają dobre rozwiązanie dla Spring 3.1 i wcześniejszych wersji. Ale od wersji 3.1.2 Spring ma swój własny MappingJackson2HttpMessageConverter z prawie taką samą funkcjonalnością jak ta w samouczku, więc nie musimy tworzyć tego niestandardowego HTTPMessageConverter.

Dzięki javaconfig nie musimy również tworzyć HibernateAwareObjectMapper , wystarczy dodać Hibernate4Module do domyślnego MappingJackson2HttpMessageConverter, który ma już Spring, i dodać go do HttpMessageConverters aplikacji, więc musimy:

  1. Rozszerz naszą sprężynową klasę konfiguracyjną z WebMvcConfigurerAdapter i zastąp metodę configureMessageConverters .

  2. W tej metodzie dodaj MappingJackson2HttpMessageConverter z Hibernate4Module zarejestrowanym w metodzie previus .

Nasza klasa konfiguracyjna powinna wyglądać następująco:

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

Jeśli masz konfigurację XML, nie musisz też tworzyć własnego MappingJackson2HttpMessageConverter, ale musisz utworzyć spersonalizowany program mapujący, który pojawi się w samouczku (HibernateAwareObjectMapper), więc Twoja konfiguracja xml powinna wyglądać następująco:

<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>

Mam nadzieję, że ta odpowiedź będzie zrozumiała i pomoże komuś znaleźć rozwiązanie tego problemu. Wszelkie pytania zachęcamy do zadawania!

r1ckr
źródło
3
Dzięki za udostępnienie rozwiązania. Jednak po kilku próbach stwierdziłem, że najnowszy jackson w tej chwili ( 2.4.2 ) NIE działa ze sprężyną 4.0.6. Nadal mam błąd „brak sesji”. Dopiero gdy obniżyłem wersję jackson do 2.3.4 (lub 2.4.2), zaczęło działać.
Robin
1
Dziękujemy za wskazanie MappingJackson2HttpMessageConverter! Przez jakiś czas szukałem takiej klasy.
LanceP
1
Dziękuję Ci bardzo! Właśnie go przetestowałem i działa z Spring 4.2.4, Jackson 2.7.1-1 i JacksonDatatype 2.7.1 :)
Isthar
1
Nie rozwiązuje to problemu w Spring Data REST.
Alessandro C
1
Jak rozwiązać ten sam problem, jeśli Springboot jest używany zamiast Spring ?. Próbowałem zarejestrować moduł Hibernate4 (który jest obecnie posiadaną wersją), ale to nie rozwiązało mojego problemu.
ranjith Gampa
34

Począwszy od Spring 4.2 i używając Spring Boot i javaconfig, zarejestrowanie Hibernate4Module jest teraz tak proste, jak dodanie tego do konfiguracji:

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

ref: https://spring.io/blog/2014/12/02/latest-jackson-integration-improvements-in-spring

chrismarx
źródło
19
W przypadku Hibernate 5 (tak jak w bieżącej wersji Spring Boot) tak Hibernate5Module. Dodatkowo potrzebuje zależności od<groupId>com.fasterxml.jackson.datatype</groupId><artifactId>jackson-datatype-hibernate5</artifactId>
Zeemee
1
U mnie też działało na Spring Boot 2.1. Doprowadzało mnie to do szału.
Van Dame
Nie jest jasne, jak to zaimplementować. Otrzymuję błąd niezgodnych typów.
EarthMind
2
Niesamowite! Pracował dla mnie z Spring Boot 2.0.6 i Hibernate 5.
GarouDan
To było bardzo pomocne. Należy zaznaczyć, że zarejestrowanie instancji klasy Hibernate5Module jako komponentu bean nie wystarczy. Jeśli chcesz zastosować go do pojedynczej instancji ObjectMapper, możesz automatycznie podłączyć moduł do klasy zarządzanej przez Spring, a następnie zarejestrować go w instancji ObjectMapper. @Autowire Module jacksonHibernateModule; ObjectMapper mapper = nowy ObjectMapper (); mapper.registerModule (this.jacksonHibernateModule).
gburgalum01
13

Jest to podobne do przyjętego rozwiązania autorstwa @rick.

Jeśli nie chcesz zmieniać istniejącej konfiguracji konwerterów wiadomości, możesz po prostu zadeklarować Jackson2ObjectMapperBuilderbean, taki jak:

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

Nie zapomnij dodać następującej zależności do swojego pliku Gradle (lub Mavena):

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

Przydatne, jeśli masz i chcesz zachować możliwość modyfikowania funkcji Jacksona z application.propertiespliku.

Luis Belloch
źródło
Jak możemy to osiągnąć (konfigurowanie Jacksona, aby uniknąć leniwego ładowania obiektu) za pomocą application.propertiespliku?
hemu
1
Nie o tym tutaj mowa. Rozwiązanie @Luis Belloch sugeruje utworzenie Jackson2ObjectMapperBuilder, aby nie nadpisywać domyślnego konwertera komunikatów podczas rozruchu sprężynowego i nadal używać wartości application.properties do kontrolowania właściwości jackson.
kapad
przepraszam ... jestem nowy, gdzie zaimplementowałbyś tę metodę? Z wiosennym rozruchem Używa Spring 5, która WebMvcConfigurerAdapter jest przestarzała
Eric Huang
Hibernate4Modulejest dziedzictwem
Dmitrij Kaltovich 25.04.2020
8

W przypadku Spring Data Rest, podczas gdy rozwiązanie opublikowane przez @ r1ckr działa, wystarczy dodać jedną z następujących zależności w zależności od wersji Hibernate:

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

lub

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

W ramach Spring Data Rest istnieje klasa:

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

który automatycznie wykryje i zarejestruje Moduł podczas uruchamiania aplikacji.

Jest jednak problem:

Problem serializacji Lazy @ManyToOne

Alan Hay
źródło
Jak to skonfigurować w przypadku rozruchu sprężynowego, gdy nie używasz funkcji Spring Data Rest?
Eric Huang
Niewystarczająco. Potrzebujesz też@Bean hibernate5Module()
Dmitrij Kaltovich
4

Jeśli używasz konfiguracji XML i używasz <annotation-driven />, odkryłem, że musisz zagnieździć się w <message-converters>środku <annotation-driven>zgodnie z zaleceniami na koncie Jackson Github .

Tak jak to:

<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>

`

czarne drewno
źródło
Konfiguracja xml jest starsza
Dmitry Kaltovich
3

Dla tych, którzy przybyli tutaj, szukając rozwiązania dla usługi RESTful opartej na Apache CXF, konfiguracja, która to naprawia, znajduje się poniżej:

<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"/>

Gdzie HibernateAwareObjectMapper definiuje się jako:

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

Następująca zależność jest wymagana od czerwca 2016 r. (Pod warunkiem, że używasz Hibernate5):

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-hibernate5</artifactId>
    <version>2.7.4</version>
</dependency>  
user2968675
źródło
1

Następujące rozwiązanie jest przeznaczone dla Springa 4.3, (nie bootowalnego) i Hibernate 5.1, w których fetch=FetchType.LAZYze względu fetch=FetchType.EAGERna wydajność przenieśliśmy wszystkie typy pobierania z przypadków na. Natychmiast zauważyliśmy com.fasterxml.jackson.databind.JsonMappingException: could not initialize proxywyjątek z powodu problemu z leniwym ładowaniem.

Najpierw dodajemy następującą zależność maven:

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

Następnie do naszego pliku konfiguracyjnego Java MVC dodawany jest następujący plik:

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

Uwagi:

  • Musisz utworzyć i skonfigurować Hibernate5Module, aby uzyskać zachowanie podobne do Jacksona bez tego modułu. Domyślnie przyjmuje niezgodne założenia.

  • Nasz WebMvcConfigurerAdapterzawiera wiele innych konfiguracji i chcieliśmy uniknąć kolejnej klasy konfiguracji, dlatego nie używaliśmy WebMvcConfigurationSupport#addDefaultHttpMessageConvertersfunkcji, do której odwoływaliśmy się w innych postach.

  • WebMvcConfigurerAdapter#configureMessageConverterswyłącza całą wewnętrzną konfigurację Springa konwerterów wiadomości. Woleliśmy uniknąć potencjalnych problemów z tym związanych.

  • Korzystanie z extendMessageConverterswłączonego dostępu do wszystkich automatycznie skonfigurowanych klas Jacksona bez utraty konfiguracji wszystkich innych konwerterów wiadomości.

  • Za pomocą getObjectMapper#registerModulemogliśmy dodać Hibernate5Moduledo istniejących konwerterów.

  • Moduł został dodany do obu procesorów JSON i XML

Ten dodatek rozwiązał problem z hibernacją i leniwym ładowaniem, ale spowodował szczątkowy problem z wygenerowanym formatem JSON . Jak opisano w tym problemie na githubie, moduł leniwego ładowania hibernate-jackson obecnie ignoruje adnotację @JsonUnwrapped, co prowadzi do potencjalnych błędów danych. Dzieje się tak niezależnie od ustawienia funkcji ładowania wymuszonego. Problem istnieje od 2016 roku.


Uwaga

Wygląda na to, że dodając następujące elementy do klas, które są leniwie ładowane, wbudowany ObjectMapper działa bez dodawania modułu hibernate5:

@JsonIgnoreProperties(  {"handler","hibernateLazyInitializer"} )
public class Anyclass {
sdw
źródło
Rozwiązanie mogłoby być dużo prostsze: kotlin @Bean fun hibernate5Module(): Module = Hibernate5Module()
Dmitrij Kaltovich
0

Możesz skorzystać z następującego pomocnika z projektu Spring Data Rest:

Jackson2DatatypeHelper.configureObjectMapper(objectMapper);
Przemek Nowak
źródło
Nie o tym pytaniu
Dmitry Kaltovich 25.04.2020
0

Zrobiłem bardzo proste rozwiązanie tego problemu.

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

W moim przypadku podawałem błąd, ponieważ za każdym razem, gdy zwracałem pendencje, jako dobrą praktykę przekonwertowałem go na listę, której nie można modyfikować, ale jak może lub nie może być leniwy w zależności od metody, której użyłem do uzyskania instancji (z pobieraniem lub bez), wykonuję test przed zainicjowaniem go przez Hibernate i dodaję adnotację, która zapobiega serializacji pustej właściwości, co rozwiązało mój problem.

Pedro Bacchini
źródło
0

Spędziłem cały dzień próbując rozwiązać ten sam problem. Możesz to zrobić bez zmiany istniejącej konfiguracji konwerterów wiadomości.

Moim zdaniem najłatwiejszy sposób rozwiązania tego problemu tylko w 2 krokach z pomocą jackson-datatype-hibernate :

Przykład kotlin (taki sam jak java):

  1. Dodaj w build.gradle.kts:
implementation("com.fasterxml.jackson.datatype:jackson-datatype-hibernate5:$jacksonHibernate")
  1. Stwórz @Bean
   @Bean
   fun hibernate5Module(): Module = Hibernate5Module()

  • Zauważ, że Moduleto com.fasterxml.jackson.databind.Moduleniejava.util.Module

  • Dobrą praktyką jest również dodawanie @JsonBackReference& @JsonManagedReferencedo @OneToMany& @ManyToOnerelacji. @JsonBackReferencemoże być tylko 1 w klasie.

Dmitrij Kaltovich
źródło
-1

Chociaż to pytanie różni się nieco od tego: wyjątek Dziwnego Jacksona jest generowany podczas serializacji obiektu Hibernate , podstawowy problem można naprawić w ten sam sposób za pomocą tego kodu:

@Provider
public class MyJacksonJsonProvider extends JacksonJsonProvider {
    public MyJacksonJsonProvider() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
        setMapper(mapper);
    }
}
nevster
źródło
-1

Próbowałem tego i działałem:

// niestandardowa konfiguracja dla leniwego ładowania

public static class HibernateLazyInitializerSerializer extends JsonSerializer<JavassistLazyInitializer> {

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

i skonfiguruj mappera:

    mapper = new JacksonMapper();
    SimpleModule simpleModule = new SimpleModule(
            "SimpleModule", new Version(1,0,0,null)
    );
    simpleModule.addSerializer(
            JavassistLazyInitializer.class,
            new HibernateLazyInitializerSerializer()
    );
    mapper.registerModule(simpleModule);
ahll
źródło
-1

Inne rozwiązanie dla butów sprężynowych: konfiguracja: spring.jpa.open-in-view=true

bianbian
źródło
To by nie pomogło
Dmitry Kaltovich 25.04.2020
-1

Wypróbowałem użyteczną odpowiedź @ Ricka , ale napotkałem problem polegający na tym, że „dobrze znane moduły”, takie jak jackson-datatype-jsr310 , nie były automatycznie rejestrowane, mimo że znajdowały się na ścieżce klas. (Ten post na blogu opisano automatyczną rejestrację).

Rozwijając odpowiedź @ Ricka, oto odmiana wykorzystująca Jackson2ObjectMapperBuilder Springa do utworzenia pliku ObjectMapper. To automatycznie rejestruje „dobrze znane moduły” i ustawia pewne funkcje oprócz zainstalowania 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);
    }
}
Markus Pscheidt
źródło
Czy masz aktualne informacje na ten temat? Obecnie mam problem. Oczywiście, kiedy dodam hibernate5Module(ponieważ używam hibernacji 5), jackson ignoruje proxy niezainicjalizowanych obiektów, ale wygląda na to, że „dobrze znane moduły” nie są rejestrowane, mimo że używam twojego podejścia. Wiem o tym, ponieważ niektóre części mojej aplikacji są zepsute, a kiedy usuwam Module, znów działają.
Daniel Henao,
@DanielHenao Czy studiowałeś spring.io/blog/2014/12/02/ ... ? Przerzuciłem się na Hibernate5, używając DelegatingWebMvcConfigurationklasy (zamiast WebMvcConfigurerAdapter) zgodnie z sugestią Jackson-Antpath-Filter :@Override public void configureMessageConverters(List<HttpMessageConverter<?>> messageConverters) { messageConverters.add(jacksonMessageConverter()); addDefaultHttpMessageConverters(messageConverters); }
Markus Pscheidt
1. Hibernate4Modulejest dziedzictwem. 2 SerializationFeature.WRITE_DATES_AS_TIMESTAMPS- nie o pytaniu
Dmitry Kaltovich 25.04.2020
@DmitryKaltovich ad 1) nie było to dziedzictwo w momencie pisania; ad 2) ok, usunięte.
Markus Pscheidt