问题
在使用dubbo时,线上出现了如下告警
WARN com.alibaba.com.caucho.hessian.io.SerializerFactory -Hessian/Burlap:
'com.qxz.third.model.dtk.DtkSuperCategoryResponse$SubCategory'
is an unknown class in sun.misc.Launcher$AppClassLoader@681a9515:
java.lang.ClassNotFoundException:com.qxz.third.model.dtk.DtkSuperCategoryResponse$SubCategory
线上的调用链情况,A调用B,B调用C,”com.qxz.third.model.dtk.DtkSuperCategoryResponse$SubCategory“属于C服务暴露的类,在B服务通过spring的BeanUtils进行了转化,假如转化成了X对象,暴露给A,最终A得到X对象。
现在出现了此警告,但并没有影响服务运行,但奇怪的是A服务告警C服务中的类没有找到,但是A服务并没有直接调用C的服务。
此告警会不会有什么隐患,又是如何产生的呢?
问题复现,分析
对于这个问题,根据提示应该和hessian序列化有关系,但序列化之前我们通过BeanUtils进行了一次对象拷贝,初步判定这两个环节中可能出现了问题。
本地环境如下:
test-parent 父工程
test-consumer 消费端
test-provider 提供端
test-provider-api 提供端暴露的接口
使用的类
org.apache.dubbo:dubbo:2.7.7
junit:junit:4.13
com.alibaba:fastjson:1.2.72
警告处找不到的类,都是集合类,我们也建一个包含集合类的pojo类,保持和线上的类一样
test-provider-api中建一个User2类,暴露给test-consumer使用
package com.anjz.provider.model;
import java.io.Serializable;
import java.util.List;
/**
* @author shuai.ding
* @date 2020年09月26日 8:59 下午
*/
public class User2 implements Serializable {
private static final long serialVersionUID = -237774972958788586L;
private Integer userId;
private String userName;
private List<Course> courseList;
//省略set、get方法
public static class Course implements Serializable{
private static final long serialVersionUID = -2407149932112578244L;
private String courseName;
//省略set、get方法
}
}
test-provider中建一个User类,模拟从C模块中获取的类
package com.anjz.provider.thirdModel;
import java.io.Serializable;
import java.util.List;
/**
* @author shuai.ding
* @date 2020年09月26日 8:55 下午
*/
public class User implements Serializable {
private static final long serialVersionUID = -237774972958788586L;
private Integer userId;
private String userName;
private List<Course> courseList;
//省略set、get方法
public static class Course implements Serializable{
private static final long serialVersionUID = -2407149932112578244L;
private String courseName;
//省略set、get方法
}
}
test-provider中建一个UserService类,模拟通过服务获取数据,此处进行了一次pojo的拷贝,User -> User2
package com.anjz.provider.service;
import com.anjz.provider.model.User2;
import com.anjz.provider.thirdModel.User;
import org.springframework.beans.BeanUtils;
import java.util.Collections;
/**
* @author shuai.ding
* @date 2020年09月26日 9:26 下午
*/
public class UserService {
public User2 getUserInfo(){
//从其他提供方获取数据
User user = new User();
user.setUserId(1);
user.setUserName("张三");
User.Course course = new User.Course();
course.setCourseName("数学");
user.setCourseList(Collections.singletonList(course));
User2 user2 = new User2();
BeanUtils.copyProperties(user,user2);
return user2;
}
}
当test-consumer调用test-provider中的UserService.getUserInfo()方法时,test-provider端需要进行序列化,test-consumer端获取数据后,需要反序列化,此处使用dubbo的默认序列化hessian2。
此处将序列化的数据持久化到磁盘,来模拟网络的传输。
package com.anjz.provider;
import com.anjz.provider.model.User2;
import com.anjz.provider.service.UserService;
import org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectOutput;
import org.junit.Test;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* @author shuai.ding
* @date 2020年09月26日 9:32 下午
*/
public class Hessian2UtilsTest {
@Test
public void serialization() throws IOException {
UserService userService = new UserService();
User2 user2 = userService.getUserInfo();
OutputStream os = new FileOutputStream(new File("/Users/dingshuai/Desktop/user2.txt"));
Hessian2ObjectOutput hessian2ObjectOutput = new Hessian2ObjectOutput(os);
hessian2ObjectOutput.writeObject(user2);
hessian2ObjectOutput.flushBuffer();
}
}
test-consumer端反序列化
package com.anjz.consumer;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.anjz.provider.model.User2;
import org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectInput;
import org.junit.Test;
import java.io.*;
/**
* @author shuai.ding
* @date 2020年09月26日 9:32 下午
*/
public class Hessian2UtilsTest {
@Test
public void deserialization() throws IOException {
//hessian2反序列化
InputStream is = new FileInputStream(new File("/Users/dingshuai/Desktop/user2.txt"));
Hessian2ObjectInput hessian2ObjectInput = new Hessian2ObjectInput(is);
Object object = hessian2ObjectInput.readObject();
User2 user2 = (User2)object;
// System.out.println(user2.getCourseList().get(0).getCourseName());
System.out.println(JSON.toJSONString(user2, SerializerFeature.PrettyFormat));
}
}
到此模拟完成一次调用,执行Hessian2UtilsTest.deserialization()方法,得到如下结果:
九月 26, 2020 10:52:00 下午 com.alibaba.com.caucho.hessian.io.SerializerFactory getDeserializer
警告: Hessian/Burlap: 'com.anjz.provider.thirdModel.User$Course' is an unknown class in sun.misc.Launcher$AppClassLoader@18b4aac2:
java.lang.ClassNotFoundException: com.anjz.provider.thirdModel.User$Course
{
"courseList":[
{
"courseName":"数学"
}
],
"userId":1,
"userName":"张三"
}
这个结果和线上的告警是一样的,而且结果正常输出,我们来看看List中存放的到底是什么,将上述代码的注释的一行去掉,执行出现如下错误
java.lang.ClassCastException:
java.util.HashMap cannot be cast to com.anjz.provider.model.User2$Course
这个错误是不是很奇怪,HashMap是从哪里来的?
那我们debug看一下反序列化后的user2
我们看到courseList容器中存在的数据是HashMap,看来Hessian2在进行反序列时,找不到类,直接反序列化成map类型了,同时给出告警,这也是最终结果没有问题的原因。(感兴趣,可以研究下dubbo中的hessian2做了哪些优化,改造)
为什么反序列化时,要去找com.anjz.provider.thirdModel.User$Course类,而不是找com.anjz.provider.Model.User2$Course类呢?还记得持久化到磁盘的类吧?我们来看一下这个文件
看到这,是不是觉得,最终引起这个问题的原因,肯定出在了BeanUtils这个拷贝上,恭喜你,你的感觉是对的。
这个我就直接说原因了,Spring的BeanUtils在进行类copy时,是一种浅拷贝,什么是浅拷贝呢,可能总结两点
1、对于基本类型,直接复制
2、对于引用类型,复制的是引用地址
spring中BeanUtils.copyProperties是通过set、get方法进行复制的(感兴趣的读者,可以去看看源码)
最后我们在看一下执行完BeanUtils.copyProperties后User2中的类的结构
总结
1、dubbo在进行反序列化时,如果找不到对象,会被反序列化成HashMap,给出类找不到的warn,具体的实现可研究com.alibaba.com.caucho.hessian.io.SerializerFactory类。
2、如果项目中出现上述告警,其实还是有隐患的,如再对提供者提供的数据进行处理,会报类型转化异常。
3、在项目中使用Spring中BeanUtils.copyProperties进行copy,特别要注意它的浅拷贝,对于类中集合类属性,建议遍历拷贝,再手动复制,对集合类手动进行一次深拷贝。