文章目录
- 报错内容
- 原因探寻
- 原因及解决方案
报错内容
起因是一段很普通的字符串转Java对象的代码,在本地和内网测试都没有问题,偏偏外网一跑就报错,错误如下:
报错的代码特别简单,涉及到公司代码这里用测试代码演示,就是将Json字符串转成java对象,示例代码:
List<PojoTest> list = JSONObject.parseObject(json, new TypeReference<>() {});
PojoTest pojo = JSONObject.parseObject(json, PojoTest.class);
PojoTest
就是一个特别简单的类:
public class PojoTest {
private long id;
private int sn;
private int num;
public PojoTest(long id)
{
this.id = id;
}
public boolean isGood()
{
return id > 100;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public int getSn() {
return sn;
}
public void setSn(int sn) {
this.sn = sn;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
}
一个没有默认构造函数的简单对象.
原因探寻
翻看错误日志,可以找到最终报错的代码是ASM
试图读取一个class源文件,执行ClassReader
构造函数,报了数组越界,ClassReader
构造函数源码如下:
public ClassReader(InputStream is) throws IOException {
{
//is是class文件的二进制流
//这个大括号内的代码是把二进制流读取到this.b的byte数组内
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
for (; ; ) {
int len = is.read(buf);
if (len == -1) {
break;
}
if (len > 0) {
out.write(buf, 0, len);
}
}
is.close();
this.b = out.toByteArray();
}
//items数组存放的是Class的常量池
items = new int[readUnsignedShort(8)];
int n = items.length;
strings = new String[n];
// parses the constant pool
int max = 0;
int index = 10;
try {
//这个for循环就是根据class文件的二进制数组读取常量池并且存放到items数组中
for (int i = 1; i < n; ++i) {
items[i] = index + 1;
int size;
switch (b[index]) {//报错的就是这一行,index过大导致数组越界
case 9: // FIELD:
case 10: // METH:
case 11: //IMETH:
case 3: //INT:
case 4: //FLOAT:
case 18: //INVOKEDYN:
case 12: //NAME_TYPE:
size = 5;
break;
case 5: //LONG:
case 6: //DOUBLE:
size = 9;
++i;
break;
case 15: //MHANDLE:
size = 4;
break;
case 1: //UTF8:
size = 3 + readUnsignedShort(index + 1);
if (size > max) {
max = size;
}
break;
// case HamConstants.CLASS:
// case HamConstants.STR:
default:
size = 3;
break;
}
index += size;
}
} catch (Exception e) {
System.out.println("加载class报错,className:");
throw e;
}
maxStringLength = max;
// the class header information starts just after the constant pool
header = index;
}
关于字节码相关知识可以查看之前的文章字节码详解.
通过源码可以看出ClassReader
初始化报错的代码并没有做其他操作,只是要把class
文件对应的常量池读取出来,而读取常量池这个操作也没有任何问题。因为字节码技术保证生成的class
文件需要跨平台使用,达到一次编译,到处运行的效果,所以class
文件的读取解析方式不会因为平台不同而出现字段不同含义的情况。这个不同平台包括windows与Linux操作系统的不同,也包括大小端的不同,class
不管什么样的平台编译,都只会以大端形式存储。
也就是说,只要class
正常编译后,都是可以按照大端顺序通过字节顺序读取出来,那上面的报错就只能是class
文件格式被修改导致的。
原因及解决方案
开发者本地环境和内网环境之所以没有报错,是因为使用的都是原始的class文件,而为了保证代码安全性,公司运维会在拉取项目jar包时对jar包进行加密,运行时加上-agent解密保证项目本身可以稳定运行。但是对于第三方直接拉取class
二进制并按照原始顺序去解析的行为就不支持了,因为加密行为是公司层面为了杜绝代码外泄而进行的,所以不会因为这个报错而选择不加密。
目前最简单的解决方案是:通过修改代码,json
转换时确保FastJson
不会走asm
相关读取class
文件的逻辑,比如先将String
转成JsonObject
对象,再读取对象相关属性赋值到自己的类中,或者保证要转换的java
对象有默认构造函数,如例子中的PojoTest
类,加上默认构造函数后便不会再报错。