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

时间:2014-02-11 17:07:14

标签: java spring hibernate serialization jackson

我有一个返回User对象的简单控制器,该用户的属性坐标具有hibernate属性FetchType.LAZY。

当我尝试获取此用户时,我总是必须加载所有坐标以获取用户对象,否则当Jackson尝试序列化时,用户会抛出异常:

  

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-module。但可能是通过新的杰克逊功能实现的。

我的版本是:

  • Spring 3.2.5
  • Hibernate 4.1.7
  • Jackson 2.2

提前致谢。

15 个答案:

答案 0 :(得分:90)

我终于找到了解决方案!感谢indybee给我一个线索。

教程Spring 3.1, Hibernate 4 and Jackson-Module-Hibernate为Spring 3.1及早期版本提供了一个很好的解决方案。但是,自3.1.2版本以来,Spring拥有自己的 MappingJackson2HttpMessageConverter ,其功能几乎与教程中的相同,因此我们不需要创建此自定义HTTPMessageConverter。

使用javaconfig我们也不需要创建 HibernateAwareObjectMapper ,我们只需要将 Hibernate4Module 添加到Spring的默认 MappingJackson2HttpMessageConverter 中已经将它添加到应用程序的HttpMessageConverters中,因此我们需要:

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

  2. 在该方法中添加 MappingJackson2HttpMessageConverter ,并在previus方法中注册 Hibernate4Module

  3. 我们的配置类应如下所示:

    @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配置应如下所示: / p>

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

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

答案 1 :(得分:26)

从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

答案 2 :(得分:14)

这类似于@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功能的能力,这很有用。

答案 3 :(得分:6)

在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

答案 4 :(得分:5)

如果您使用XML配置并使用<annotation-driven />,我发现您必须按照Jackson Github account的建议在<message-converters>内嵌套<annotation-driven>

像这样:

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

`

答案 5 :(得分:3)

对于那些希望找到基于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>  

答案 6 :(得分:1)

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

Jackson2DatatypeHelper.configureObjectMapper(objectMapper);

答案 7 :(得分: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可以访问所有自动配置的Jackson类,而不会丢失所有其他消息转换器的配置。

  • 使用getObjectMapper#registerModule,我们可以将Hibernate5Module添加到现有的转换器中。

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

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


注意

看来,通过将以下内容添加到延迟加载的类中,内置的ObjectMapper可以工作而无需添加hibernate5模块:

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

答案 8 :(得分:0)

我尝试了@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()
            .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .modulesToInstall(hibernate4Module());
        return new MappingJackson2HttpMessageConverter(builder.build());
  }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(jacksonMessageConverter());
        super.configureMessageConverters(converters);
    }
}

答案 9 :(得分:0)

虽然这个问题与此问题略有不同: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);
    }
}

答案 10 :(得分:0)

我试过这个,然后工作:

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

public static class HibernateLazyInitializerSerializer extends JsonSerializer<JavassistLazyInitializer> {

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

并配置mapper:

    mapper = new JacksonMapper();
    SimpleModule simpleModule = new SimpleModule(
            "SimpleModule", new Version(1,0,0,null)
    );
    simpleModule.addSerializer(
            JavassistLazyInitializer.class,
            new HibernateLazyInitializerSerializer()
    );
    mapper.registerModule(simpleModule);

答案 11 :(得分:0)

我为这个问题提供了一个非常简单的解决方案。

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

在我的情况下,我给出了错误,因为每当我返回未决时,作为一种好的做法,我都会将其转换为无法修改的列表,但是根据我使用的方法,它可能会或可能不会变得很懒要获取实例(带有或不带有获取),我在Hibernate初始化它之前先进行测试,并添加注释,以防止序列化一个空属性并解决了我的问题。

答案 12 :(得分:0)

我整天都在尝试解决相同的问题。您可以在不更改现有消息转换器配置的情况下完成此操作。

我认为,在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个班级。

答案 13 :(得分:0)

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

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

答案 14 :(得分:-1)

弹簧靴的另一种解决方案: 组态: spring.jpa.open-in-view=true