問題與分析
某日忽然發現在用postman測試數據時報錯如下:
com.fasterxml.jackson.databind.JsonMappingException: could not initialize proxy [com.cbxsoftware.cbx.attachment.entity.RefAttachment#c109ec36e60c4a89a10eabc72416d984] - no Session (through reference chain: com.cbxsoftware.cbx.sampletracker.elasticsearch.entity.SampleTrackerDetailEstc["sampleTracker"]->com.cbxsoftware.cbx.sampletracker.elasticsearch.entity.SampleTrackerEstc["sampleTracker"]->com.cbxsoftware.cbx.sampletracker.entity.SampleTracker["item"]->com.cbxsoftware.cbx.item.entity.RefItem["image"]->com.cbxsoftware.cbx.image.entity.RefImage["propFormat"]->com.cbxsoftware.cbx.attachment.entity.RefAttachment$HibernateProxy$uNA5RwMT["revision"])
at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:394)
at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:353)
at com.fasterxml.jackson.databind.ser.std.StdSerializer.wrapAndThrow(StdSerializer.java:316)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:727)
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155)
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:727)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719)
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155)
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:727)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719)
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155)
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:727)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719)
at com.fasterxml.jackson.databind.ser.impl.UnwrappingBeanSerializer.serialize(UnwrappingBeanSerializer.java:120)
at com.fasterxml.jackson.databind.ser.impl.UnwrappingBeanPropertyWriter.serializeAsField(UnwrappingBeanPropertyWriter.java:127)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719)
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155)
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:727)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719)
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155)
at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider._serialize(DefaultSerializerProvider.java:480)
at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:319)
at com.fasterxml.jackson.databind.ObjectMapper._configAndWriteValue(ObjectMapper.java:3905)
at com.fasterxml.jackson.databind.ObjectMapper.writeValueAsString(ObjectMapper.java:3219)
...
Caused by: org.hibernate.LazyInitializationException: could not initialize proxy [com.cbxsoftware.cbx.attachment.entity.RefAttachment#c109ec36e60c4a89a10eabc72416d984] - no Session
at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:169)
at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:309)
at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:45)
at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:95)
at com.cbxsoftware.cbx.attachment.entity.RefAttachment$HibernateProxy$uNA5RwMT.getRevision(Unknown Source)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:688)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719)
... 128 common frames omitted
報錯很明顯,是由於hibernate的懶加載引起的。項目使用的是SpringBoot框架,JPA默認使用的是hibernate的實現,而hibernate的懶加載機制其實就是延遲加載對象,如果沒有在session關閉前使用到對象里除id以外的屬性時,就只會返回一個沒有初始化過的包含了id的代理類。很多時候,這個代理類會引發上述的異常。
簡單說一下為什么會觸發懶加載異常,首先hibernate開啟一個session(會話),然后開啟transaction(事務),接着發出sql找回數據並組裝成pojo(或者說entity、model),這時候如果pojo里有懶加載的對象,並不會去發出sql查詢db,而是直接返回一個懶加載的代理對象,這個對象里只有id。如果接下來沒有其他的操作去訪問這個代理對象除了id以外的屬性,就不會去初始化這個代理對象,也就不會去發出sql查找db。接着事務提交,session關閉。如果這時候再去訪問代理對象除了id以外的屬性時,就會報上述的懶加載異常,原因是這時候已經沒有session了,無法初始化懶加載的代理對象。
解決方法一
如果是spring繼承的hibernate,根據上述的原因,可以延長session的生命周期,但是這里用的是SpringBoot的JPA,處理方法不同,需要在application.properties
配置下懶加載相關的東西:
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
進行該配置后,可以在session關閉時也能另外開啟一個新的session和事務來訪問db以取回懶加載對象的數據。
解決方法二
因為該懶加載異常是缺少session導致的,那么可以通過在方法前添加事務注解@Transactional
的方式來解決,只要事務沒有提交,session就不會關閉,自然就不會出現上述的懶加載異常。不過由於該事務注解是用Spring AOP實現的,存在着一些坑,比如類內直接調用無效或者對非public方法無效等,需要多加注意。
當使用了上述兩種方法后,發現不再觸發LazyInitializationException
,但是卻發生了另一個新的異常InvalidDefinitionException
:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: com.cbxsoftware.cbx.item.elasticsearch.entity.ItemEstc["mainEntity"]->com.cbxsoftware.cbx.item.entity.Item["image"]->com.cbxsoftware.cbx.image.entity.RefImage["propFormat"]->com.cbxsoftware.cbx.attachment.entity.RefAttachment$HibernateProxy$vTKSYzrN["hibernateLazyInitializer"])
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:77)
at com.fasterxml.jackson.databind.SerializerProvider.reportBadDefinition(SerializerProvider.java:1191)
at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:313)
at com.fasterxml.jackson.databind.ser.impl.UnknownSerializer.failForEmpty(UnknownSerializer.java:71)
at com.fasterxml.jackson.databind.ser.impl.UnknownSerializer.serialize(UnknownSerializer.java:33)
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:727)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719)
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155)
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:727)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719)
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155)
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:727)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719)
at com.fasterxml.jackson.databind.ser.impl.UnwrappingBeanSerializer.serialize(UnwrappingBeanSerializer.java:120)
at com.fasterxml.jackson.databind.ser.impl.UnwrappingBeanPropertyWriter.serializeAsField(UnwrappingBeanPropertyWriter.java:127)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719)
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155)
at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider._serialize(DefaultSerializerProvider.java:480)
at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:319)
at com.fasterxml.jackson.databind.ObjectMapper._configAndWriteValue(ObjectMapper.java:3905)
at com.fasterxml.jackson.databind.ObjectMapper.writeValueAsString(ObjectMapper.java:3219)
...
這個異常是由於hibernate在代理類里添加了一個屬性hibernateLazyInitializer
,當該對象轉換成json的時候就會報錯。解決方法是將該屬性過濾掉,可以在對應的類名或者公共類前加上如下注解:
@JsonIgnoreProperties(value = { "hibernateLazyInitializer" })
源碼分析
因為對懶加載異常的發生有些好奇,所以看了下hibernate的源碼,這里簡單分析下,另外我看的是兩個源碼包如下:
spring-orm-5.1.5.RELEASE.jar
hibernate-core-5.3.7.Final.jar
首先是關於spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
的配置,前面半截是因為JPA集成了hibernate的配置,所以在hibernate中,這個配置應該是hibernate.enable_lazy_load_no_trans=true
。
在hibernate的一個常量接口org.hibernate.cfg.AvailableSettings
中定義了各種配置常量,其中就包括上述這個配置:
String ENABLE_LAZY_LOAD_NO_TRANS = "hibernate.enable_lazy_load_no_trans";
在啟動項目的時候會讀取配置文件,將其解析為一個HashMap<K,V>
,這些參數在newEntityManagerFactoryBuilderImpl
的時候被使用到,上面的常量會在org.hibernate.boot.internal.SessionFactoryOptionsBuilder
里被拿來初始化:
this.initializeLazyStateOutsideTransactions = cfgService.getSetting( ENABLE_LAZY_LOAD_NO_TRANS, BOOLEAN, false );
因為在配置文件里配置了該變量的值為true,所以這里在初始化的時候就會把initializeLazyStateOutsideTransactions
的值設置為true。該變量由一個方法來判斷其值是否為true:
@Override
public boolean isInitializeLazyStateOutsideTransactionsEnabled() {
return initializeLazyStateOutsideTransactions;
}
接着在組裝pojo時,會為懶加載對象創建對應的代理對象,當需要獲取該代理對象除id以外的屬性時,就會調用AbstractLazyInitializer#initialize()
進行初始化,邏輯如下:
@Override
public final void initialize() throws HibernateException {
if ( !initialized ) {
if ( allowLoadOutsideTransaction ) {
permissiveInitialization();
}
else if ( session == null ) {
throw new LazyInitializationException( "could not initialize proxy [" + entityName + "#" + id + "] - no Session" );
}
else if ( !session.isOpen() ) {
throw new LazyInitializationException( "could not initialize proxy [" + entityName + "#" + id + "] - the owning Session was closed" );
}
else if ( !session.isConnected() ) {
throw new LazyInitializationException( "could not initialize proxy [" + entityName + "#" + id + "] - the owning Session is disconnected" );
}
else {
target = session.immediateLoad( entityName, id );
initialized = true;
checkTargetState(session);
}
}
else {
checkTargetState(session);
}
}
如果在配置文件中設置了spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
,那么上述的allowLoadOutsideTransaction
變量值就為true,則可以進入permissiveInitialization()
方法另起session和事務,最終避免懶加載異常LazyInitializationException
。如果沒有配置該參數,那么就會由於session已關閉(即為null)而拋出LazyInitializationException
。
參考鏈接
- springboot jpa 解決延遲加載問題
- No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor
- springboot集成jpa返回Json報錯 com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
- Hibernate和Spring整合出現懶加載異常:org.hibernate.LazyInitializationException: could not initialize proxy - no Session