避免对未获取的惰性对象进行 Jackson 序列化

Posted

技术标签:

【中文标题】避免对未获取的惰性对象进行 Jackson 序列化【英文标题】:Avoid Jackson serialization on non fetched lazy objects 【发布时间】:2014-03-09 14:55:37 【问题描述】:

我有一个简单的控制器,它返回一个用户对象,这个用户有一个属性坐标,它具有休眠属性 FetchType.LAZY。

当我尝试获取这个用户时,总是要加载所有坐标才能获取用户对象,否则当Jackson尝试序列化用户时会抛出异常:

com.fasterxml.jackson.databind.JsonMappingException: 无法初始化代理 - 没有会话

这是由于 Jackson 试图获取这个未获取的对象。以下是对象:

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-module 的其他答案。但也许可以通过新的杰克逊功能来实现。

我的版本是:

春季 3.2.5 休眠 4.1.7 杰克逊 2.2

提前致谢。

【问题讨论】:

这两个链接中描述的方法对我有用,Spring 3.1, Hibernate 4 and Jackson-Module-Hibernate 和 FasterXML / jackson-datatype-hibernate 谢谢indybee,我一直在看教程,spring 3.2.5已经有一个MappingJackson2HttpMessageConverter,但是我不能用它来避免非获取的惰性对象,我也尝试过实现教程中的一个,但什么都没有…… 您是否收到相同的错误“无法初始化代理”或其他错误? (我在 Spring 3.2.6 和 Hibernate 4.1.7 和 jackson 2.3 的文章中使用了额外的 HibernateAwareObjectMapper) 知道了!!由于版本 3.1.2 Spring 有他自己的 MappingJackson2HttpMessageConverter 并且具有与教程中的几乎相同的行为,但问题是我使用的是 Spring java config 并且我从 javaconfigs 开始。我试图制作一个 @Bean MappingJackson2HttpMessageConverter,而不是将其添加到 Spring 的 HttpMessageConverters 中,也许答案更清楚:) 很高兴它正在工作:) 【参考方案1】:

我终于找到了解决办法!感谢 indybee 提供线索。

教程Spring 3.1, Hibernate 4 and Jackson-Module-Hibernate对Spring 3.1及更早版本有很好的解决方案。但由于 Spring 3.1.2 版本有自己的 MappingJackson2HttpMessageConverter,其功能与教程中的几乎相同,因此我们不需要创建此自定义 HTTPMessageConverter。

使用 javaconfig 我们也不需要创建 HibernateAwareObjectMapper,我们只需将 Hibernate4Module 添加到 Spring 默认的 MappingJackson2HttpMessageConverter已经有了并将其添加到应用程序的 HttpMessageConverters 中,所以我们需要:

    WebMvcConfigurerAdapter 扩展我们的 spring 配置类并覆盖方法 configureMessageConverters

    在该方法上添加 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>

希望这个答案可以理解并帮助某人找到解决此问题的方法,任何问题都可以随时提出!

【讨论】:

感谢分享解决方案。然而,经过几次尝试,我发现目前最新的 jackson (2.4.2) 确实 NOT 与 spring 4.0.6 一起工作。我仍然有“无会话”错误。只有当我将杰克逊版本降级到 2.3.4(或 2.4.2)时,它才开始工作。 感谢您指出 MappingJackson2HttpMessageConverter!我一直在寻找这样的课程。 非常感谢!我刚刚测试了它以及它与 Spring 4.2.4、Jackson 2.7.1-1 和 JacksonDatatype 2.7.1 的配合 :) 这并不能解决 Spring Data REST 中的问题。 如果使用 Springboot 代替 Spring,如何解决相同的问题?我已经尝试注册 Hibernate4 模块(这是我目前拥有的版本),但这并没有解决我的问题。【参考方案2】:

从 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

【讨论】:

对于 Hibernate 5(与当前版本的 Spring Boot 一样),它是 Hibernate5Module。它还需要对&lt;groupId&gt;com.fasterxml.jackson.datatype&lt;/groupId&gt;&lt;artifactId&gt;jackson-datatype-hibernate5&lt;/artifactId&gt; 的依赖 在 Spring Boot 2.1 上也为我工作。开始让我发疯了。 不清楚如何实现。我收到不兼容的类型错误。 太棒了!使用 Spring Boot 2.0.6 和 Hibernate 5 为我工作。 感谢@chrismarx,如果允许的话,我会给你 1337 票赞成这个答案。【参考方案3】:

这类似于@rick 接受的解决方案。

如果您不想修改现有的消息转换器配置,您可以声明一个 Jackson2ObjectMapperBuilder bean,例如:

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

不要忘记将以下依赖项添加到您的 Gradle 文件(或 Maven)中:

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

如果您有一个 spring-boot 应用程序并且希望保留从 application.properties 文件修改 Jackson 功能的能力,这很有用。

【讨论】:

我们如何使用application.properties 文件来实现这一点(配置Jackson 以避免延迟加载对象)? 这不是这里所说的。 @Luis Belloch 的解决方案建议您创建 Jackson2ObjectMapperBuilder,这样您就不会覆盖 spring boots 默认消息转换器并继续使用 application.properties 值来控制 jackson 属性。 对不起...我是新手,你会在哪里实现这个方法?使用spring boot 它使用不推荐使用WebMvcConfigurerAdapter的Spring 5 Hibernate4Module 是旧版【参考方案4】:

在 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

它将在应用程序启动时自动检测并注册模块。

但是有一个问题:

Issue Serializing Lazy @ManyToOne

【讨论】:

不使用 Spring Data Rest 时如何使用 Spring Boot 进行配置? 不够。你还需要@Bean hibernate5Module()【参考方案5】:

我花了一整天的时间试图解决同样的问题。您可以在不更改现有消息转换器配置的情况下执行此操作。

在我看来,在jackson-datatype-hibernate 的帮助下,只需两个步骤即可解决此问题的最简单方法:

kotlin 示例(与 java 相同):

    添加build.gradle.kts:
implementation("com.fasterxml.jackson.datatype:jackson-datatype-hibernate5:$jacksonHibernate")
    创建@Bean
   @Bean
   fun hibernate5Module(): Module = Hibernate5Module()

注意Modulecom.fasterxml.jackson.databind.Module,而不是java.util.Module

另外一个好的做法是将@JsonBackReference & @JsonManagedReference 添加到@OneToMany & @ManyToOne 关系。 @JsonBackReference 在课堂上只能是 1。

【讨论】:

【参考方案6】:

如果您使用 XML 配置并使用&lt;annotation-driven /&gt;,我发现您必须按照Jackson Github account 的建议将&lt;message-converters&gt; 嵌套在&lt;annotation-driven&gt; 中。

像这样:

<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 配置是旧版【参考方案7】:

对于那些来这里寻找基于 Apache CXF 的 RESTful 服务的解决方案的人,修复它的配置如下:

<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 年 6 月起需要以下依赖项(前提是您使用的是 Hibernate5):

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

【讨论】:

【参考方案8】:

以下解决方案适用于 Spring 4.3(非引导)和 Hibernate 5.1,出于性能原因,我们将所有 fetchtypes 从 fetch=FetchType.EAGER 的情况转换为 fetch=FetchType.LAZY。由于延迟加载问题,我们立即看到了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 以获得类似于没有此模块的 Jackson 的行为。默认情况下会做出不兼容的假设。

我们的WebMvcConfigurerAdapter里面有很多其他的配置,我们想避免另一个配置类,这就是为什么我们没有使用其他帖子中提到的WebMvcConfigurationSupport#addDefaultHttpMessageConverters函数。 /p>

WebMvcConfigurerAdapter#configureMessageConverters 禁用 Spring 的所有内部消息转换器配置。我们宁愿避免与此相关的潜在问题。

使用 extendMessageConverters 可以访问所有自动配置的 Jackson 类,而不会丢失所有其他消息转换器的配置。

使用getObjectMapper#registerModule,我们能够将Hibernate5Module 添加到现有转换器中。

该模块已添加到 JSON 和 XML 处理器中

此添加解决了 Hibernate 和延迟加载的问题,但导致生成的 JSON 格式存在残留问题。正如this github issue 中所报告的,hibernate-jackson 延迟加载模块当前忽略了@JsonUnwrapped 注释,从而导致潜在的数据错误。无论强制加载功能设置如何,都会发生这种情况。这个问题从 2016 年就一直存在。


注意

似乎通过将以下内容添加到延迟加载的类中,内置的 ObjectMapper 可以在不添加 hibernate5 模块的情况下工作:

@JsonIgnoreProperties(  "handler","hibernateLazyInitializer" )
public class Anyclass 

【讨论】:

解决方案可能更简单:kotlin @Bean fun hibernate5Module(): Module = Hibernate5Module()【参考方案9】:

您可以使用 Spring Data Rest 项目中的以下帮助程序:

Jackson2DatatypeHelper.configureObjectMapper(objectMapper);

【讨论】:

不是关于这个问题【参考方案10】:

我对这个问题做了一个非常简单的解决方案。

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

在我的情况下,我给出了错误,因为每当我返回一个挂件时,作为一个好习惯,我将它转换为一个无法修改的列表,但它可能会或可能不会懒惰取决于我使用的方法为了获取实例(有或没有 fetch),我在 Hibernate 初始化它之前进行了测试,并添加了防止序列化空属性的注释并解决了我的问题。

【讨论】:

【参考方案11】:

我们无法解决必须为所有属性调用 Hibernate.initialize(objX.getAttributeY()) 的问题。所以在这里我们编写了一个 utils 类来调用所有具有 Entity 注释的属性的初始化方法。 集合仍然必须单独处理。

import org.hibernate.Hibernate;

import javax.persistence.Entity;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class HibernateInitialiseUtil<T> 

    private final List<Method> entityGetterMethods;

    public HibernateInitialiseUtil(Class<T> clazz) 
        this.entityGetterMethods = this.getGettersMethodsOfEntityAttributes(clazz);
    

    public void initializeAllEntityAttributes(T obj) throws InvocationTargetException, IllegalAccessException 
        for (Method getter : this.entityGetterMethods) 
            Hibernate.initialize(getter.invoke(obj));
        
    

    private List<String> getNameOfEntityAttributes(final Class<T> targetClass) 
        Field[] attributes =  targetClass.getDeclaredFields();
        List<String> fields = new ArrayList<>();
        for (Field attribute : attributes) 
            if (attribute.getType().isAnnotationPresent(Entity.class)) 
                fields.add(attribute.getName());
            
        
        return fields;
    

    private List<Method> getGettersMethodsOfEntityAttributes(final Class<T> targetClass) 
        List<Method> resultList = new ArrayList<>();
        for (String attributeName : getNameOfEntityAttributes(targetClass)) 
            try 
                PropertyDescriptor pd = new PropertyDescriptor(attributeName, targetClass);
                Method getter = pd.getReadMethod();
                resultList.add(getter);
             catch (IllegalArgumentException | IntrospectionException e) 
                e.printStackTrace();
            
        
        return Collections.unmodifiableList(resultList);
    

【讨论】:

【参考方案12】:

虽然这个问题与这个问题略有不同:Strange Jackson exception being thrown when serializing Hibernate object,但可以使用以下代码以相同的方式解决根本问题:

@Provider
public class MyJacksonJsonProvider extends JacksonJsonProvider 
    public MyJacksonJsonProvider() 
        ObjectMapper mapper = new ObjectMapper();
        mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
        setMapper(mapper);
    

【讨论】:

【参考方案13】:

我试过了,并且成功了:

//延迟加载的自定义配置

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

【讨论】:

【参考方案14】:

我尝试了@rick's useful answer,但遇到了问题,即“知名模块”(例如jackson-datatype-jsr310)尽管位于类路径中,但并未自动注册。 (这个blog post 解释了自动注册。)

扩展@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(因为我使用的是休眠5)时,杰克逊忽略了未初始化对象的代理,但尽管我使用了你的方法,但似乎“知名模块”没有被注册。我确实知道这一点,因为我的应用程序的某些部分已损坏,当我删除模块时,它们又可以工作了。 @DanielHenao 你研究过spring.io/blog/2014/12/02/…吗?根据Jackson-Antpath-Filter 的建议,我使用DelegatingWebMvcConfiguration 类(而不是WebMvcConfigurerAdapter)切换到Hibernate5:@Override public void configureMessageConverters(List&lt;HttpMessageConverter&lt;?&gt;&gt; messageConverters) messageConverters.add(jacksonMessageConverter()); addDefaultHttpMessageConverters(messageConverters); 1. Hibernate4Module 是旧版。 2 SerializationFeature.WRITE_DATES_AS_TIMESTAMPS - 与问题无关 @DmitryKaltovich ad 1) 在撰写本文时它不是遗产;广告 2) 好的,已删除。【参考方案15】:

spring boot的另一种解决方案: 配置: spring.jpa.open-in-view=true

【讨论】:

这无济于事

以上是关于避免对未获取的惰性对象进行 Jackson 序列化的主要内容,如果未能解决你的问题,请参考以下文章

当jackson序列化json对象时如何防止hibernate5延迟获取?

Jackson - 具有双向关系的实体序列化(避免循环)

JavaScript 惰性实例化代码

Spring Boot (Jackson):如何避免类名被序列化为 JSON

Jackson

Java下利用Jackson进行JSON解析和序列化