【问题标题】:Avoid Jackson serialization on non fetched lazy objects避免对未获取的惰性对象进行 Jackson 序列化
【发布时间】: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

提前致谢。

【问题讨论】:

  • 谢谢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 中,也许答案更清楚:)
  • 很高兴它正在工作:)

标签: java spring hibernate serialization jackson


【解决方案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 中,所以我们需要:

  1. WebMvcConfigurerAdapter 扩展我们的 spring 配置类并覆盖方法 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>

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

【讨论】:

  • 感谢分享解决方案。然而,经过几次尝试,我发现目前最新的 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'

如果您有一个 应用程序并且希望保留从 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 相同):

  1. 添加build.gradle.kts:
implementation("com.fasterxml.jackson.datatype:jackson-datatype-hibernate5:$jacksonHibernate")
  1. 创建@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

              【讨论】:

              • 这无济于事
              猜你喜欢
              • 2014-11-12
              • 1970-01-01
              • 1970-01-01
              • 2019-01-28
              • 2018-09-04
              • 1970-01-01
              • 2014-11-21
              • 2017-10-24
              • 1970-01-01
              相关资源
              最近更新 更多