先说说坑
JSON.toString在序列化对象时,默认通过的是get*()方法来查找属性,而不是具体某个属性,同时回忽略transient注解的属性。
测试案例如下
public class FastJsonTest {
public static void main(String[] args) {
Person person = new Person();
person.setBirth(new Date());
System.out.println(JSON.toJSONString(person));
}
public static class Person{
private Integer age =123;
private transient Date birth;
public Date getBirth() {
return birth;
}
public void setBirth(Date birth) {
this.birth = birth;
}
public String getName(){
return "scj";
}
}
}
输出
{"name":"scj"}
问题发生
最近在迭代一个老项目,升级中间件框架版本(不升级不给打包部署)后,在项目启动的时候居然抛出以下异常
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.masaike.yama.platform.domain.model.product.ProductServiceEntity.properties, could not initialize proxy - no Session
at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:582)
at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:201)
at org.hibernate.collection.internal.AbstractPersistentCollection.readSize(AbstractPersistentCollection.java:145)
at org.hibernate.collection.internal.PersistentBag.size(PersistentBag.java:261)
at com.masaike.yama.platform.infrastructure.converter.ProductServiceConverterImpl.productServicePropertyVOListToProductServicePropertyDTOList(ProductServiceConverterImpl.java:139)
at com.masaike.yama.platform.infrastructure.converter.ProductServiceConverterImpl.map(ProductServiceConverterImpl.java:50)
at com.masaike.yama.platform.infrastructure.converter.ProductServiceConverterImpl.entityListToDTOList(ProductServiceConverterImpl.java:32)
at com.masaike.yama.platform.query.ProductServiceQueryServiceImpl.getAllProductService(ProductServiceQueryServiceImpl.java:43)
at com.alibaba.fastjson.serializer.ASMSerializer_12_ProductServiceQueryServiceImpl.write(Unknown Source)
at com.alibaba.fastjson.serializer.JSONSerializer.writeWithFieldName(JSONSerializer.java:333)
at com.alibaba.fastjson.serializer.ASMSerializer_1_InterfaceInfo.write(Unknown Source)
at com.alibaba.fastjson.serializer.JSONSerializer.write(JSONSerializer.java:285)
at com.alibaba.fastjson.JSON.toJSONString(JSON.java:745)
at com.alibaba.fastjson.JSON.toJSONString(JSON.java:683)
at com.alibaba.fastjson.JSON.toJSONString(JSON.java:648)
at com.alibaba.dubbo.config.masaikehttp.ExportedInterfaceManager.addInterface(ExportedInterfaceManager.java:104)//关键点
at com.alibaba.dubbo.config.ServiceConfig.doExport(ServiceConfig.java:321)
at com.alibaba.dubbo.config.ServiceConfig.export(ServiceConfig.java:218)
at com.alibaba.dubbo.config.spring.ServiceBean.onApplicationEvent(ServiceBean.java:123)
at com.alibaba.dubbo.config.spring.ServiceBean.onApplicationEvent(ServiceBean.java:49)
at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:172)
at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:165)
at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:139)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:400)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:354)
at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:886)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.finishRefresh(ServletWebServerApplicationContext.java:161)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:551)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:140)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:386)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:307)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1242)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1230)
at com.masaike.yama.bootstrap.BootStrapApplication.main(BootStrapApplication.java:36)
这个一个使用JPA时常见问题:延迟加载的时候session不存在
关于延迟加载no-session问题,可以看如何解决JPA延迟加载no Session报错
从日志定位到抛出异常的方法为
@Override
@Transactional(rollbackFor = Exception.class)
public List getAllXX() {
List result = xXQueryRepository.findAll();
//下面的converter会触发延迟加载
return XXConverter.INSTANCE.entityListToDTOList(result);
}
这边存在两个迷惑性行为
- 启动的时候怎么调用了getAllXX方法
- getAllXX我加了@Transactional,理论上是有session的
问题排查
精简上面的异常栈
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.masaike.yama.platform.domain.model.product.ProductServiceEntity.properties, could not initialize proxy - no Session
at com.masaike.yama.platform.infrastructure.converter.ProductServiceConverterImpl.productServicePropertyVOListToProductServicePropertyDTOList(ProductServiceConverterImpl.java:139)
at com.masaike.yama.platform.infrastructure.converter.ProductServiceConverterImpl.map(ProductServiceConverterImpl.java:50)
at com.masaike.yama.platform.infrastructure.converter.ProductServiceConverterImpl.entityListToDTOList(ProductServiceConverterImpl.java:32)
at com.masaike.yama.platform.query.ProductServiceQueryServiceImpl.getAllProductService(ProductServiceQueryServiceImpl.java:43)
at com.alibaba.fastjson.JSON.toJSONString(JSON.java:648)
at com.alibaba.dubbo.config.masaikehttp.ExportedInterfaceManager.addInterface(ExportedInterfaceManager.java:104)//关键点
at com.alibaba.dubbo.config.ServiceConfig.doExport(ServiceConfig.java:321)
at com.alibaba.dubbo.config.ServiceConfig.export(ServiceConfig.java:218)
可以复盘出问题发生的现场
dubbo服务进行export的时候调用了ExportedInterfaceManager.addInterface方法,而在addInterface方法中调用的JSON.toJSONString方法触发了ProductServiceQueryServiceImpl.getAllProductService方法
在看了ExportedInterfaceManager.addInterface源码之后,问题的起因浮出水面
ExportedInterfaceManager这个类是用来针对接口暴露http服务时收集元数据使用
public synchronized void addInterface(Class> interfaceCls, Object obj) {
//如果是代理类获取代理类对象
obj = getObjectTarget(obj);//获取原始对象
//...
InterfaceInfo interfaceInfo = new InterfaceInfo();
interfaceInfo.setInterfaceName(interfaceName);
interfaceInfo.setRef(obj);//致命之处
//...
logger.info(String.format("start to addInterface into interfaceMap,interfaceName[%s],interfaceInfo[%s]",interfaceName, JSON.toJSONString(interfaceInfo)));//致命之处
// add interface info to map
interfaceMap.put(interfaceName, interfaceInfo);
}
对于第一个问题,InterfaceInfo的ref指向ProductServiceQueryServiceImpl,在打印日志的时候,JSON.toJSONString触发了ProductServiceQueryServiceImpl中的get方法
而对于第二个问题,obj = getObjectTarget(obj);
这段代码会获取代理的原始对象,导致事务失效。
问题危害
抛开获取原始对象这个逻辑不说,这个bug的致命之处在于,他会调用所暴露dubbo接口中所有get*()
格式的方法
问题解决
解决方式很简单,有以下两种
- 不要序列化ref(加fastjson注解或字段加transient)
- 去掉打印日志逻辑
在反馈这个问题后,中间件团队的改动如下