文章目录
- 报错内容
- 原因探寻
- 原因及解决方案
报错内容
起因是一段很普通的字符串转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 poolint 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 poolheader = index;}
关于字节码相关知识可以查看之前的文章字节码详解.
通过源码可以看出ClassReader
初始化报错的代码并没有做其他操作,只是要把class
文件对应的常量池读取出来,而读取常量池这个操作也没有任何问题。因为字节码技术保证生成的class
文件需要跨平台使用,达到一次编译,到处运行的效果,所以class
文件的读取解析方式不会因为平台不同而出现字段不同含义的情况。这个不同平台包括windows与Linux操作系统的不同,也包括大小端的不同,class
不管什么样的平台编译,都只会以大端形式存储。
也就是说,只要class
正常编译后,都是可以按照大端顺序通过字节顺序读取出来,那上面的报错就只能是class
文件格式被修改导致的。
原因及解决方案
开发者本地环境和内网环境之所以没有报错,是因为使用的都是原始的class文件,而为了保证代码安全性,公司运维会在拉取项目jar包时对jar包进行加密,运行时加上-agent解密保证项目本身可以稳定运行。但是对于第三方直接拉取class
二进制并按照原始顺序去解析的行为就不支持了,因为加密行为是公司层面为了杜绝代码外泄而进行的,所以不会因为这个报错而选择不加密。
目前最简单的解决方案是:通过修改代码,json
转换时确保FastJson
不会走asm
相关读取class
文件的逻辑,比如先将String
转成JsonObject
对象,再读取对象相关属性赋值到自己的类中,或者保证要转换的java
对象有默认构造函数,如例子中的PojoTest
类,加上默认构造函数后便不会再报错。