一、环境概述
1、此次项目使用的环境: Jboss 4.2.3.GA,JDK1.7.0_79,Eclipse Mars.2 版本,Maven3.3.9。
2、项目发布采用增量部署的方式,即版本发布时,只部署修改过的类或jsp文件,这样可以提高版本的稳定性,降低未知的发布风险。
二、打包过程
采用增量打包的方式,只将有修改的类编译的class文件放在生成的war包里,再通过jar命令将增量的war文件压缩到运行环境的war包内。
三、出现的状况
1、研发环境(Eclipse环境下)正常使用。
2、测试环境(增量包部署后)出现了如下异常:
Exception in thread "main" java.lang.NoSuchMethodError: com.hy.method.MethodModify.update(Lcom/hy/truelicense/LicenseBean;)V
at com.hy.method.MethodClient.main(MethodClient.java:9)
3、检查修改类反编译的结果,发现方法已经是最新版本的类编译生成的class文件。
四、问题模拟重现及追溯
为了重现该问题,单独写了两个非常小的demo类:MethodModify和MethodClient ,如下:
方法声明示例类MethodModify 代码(修改前):
package com.hy.method;
public class MethodModify {
public void update(String str) {
System.out.println(str);
}
}
方法声明示例类MethodModify 代码(修改后):
package com.hy.method;
public class MethodModify {
public int update(String str) {
System.out.println(str);
return str.hashCode();
}
}
方法调用示例类MethodClient代码:
package com.hy.method;
public class MethodClient {
public static void main(String[] args) {
MethodModify mm = new MethodModify();
mm.update( "hello");
}
}
MethodModify这个类修改方法签名,update方法原有的返回值是void,现改为int,查看该类的class文件,使用命令javap -verbose MethodModify.class或javap -c MethodModify.class
针对比较大的class文件,可以把查看的字节码内容输出到txt文件当中,方便阅读,搜索,命令如下:javap -verbose MethodModify.class > MethodModify.txt
示例类MethodModify取修改后的字节码片断:
public int update(com.hy.truelicense.LicenseBean);
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_1
4: invokevirtual #22 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
7: iconst_1
8: ireturn
LineNumberTable:
line 8: 0
line 9: 7
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/hy/method/MethodModify;
0 9 1 bean Lcom/hy/truelicense/LicenseBean;
可以看到方法声明返回值类型为int,说明该类的最新修改已经编译成class文件了。
方法调用示例类MethodClient取字节码片断:
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: new #16 // class com/hy/method/MethodModify
3: dup
4: invokespecial #18 // Method com/hy/method/MethodModify."<init>":()V
7: astore_1
8: aload_1
9: new #19 // class com/hy/truelicense/LicenseBean
12: dup
13: invokespecial #21 // Method com/hy/truelicense/LicenseBean."<init>":()V
16: invokevirtual #22 // Method com/hy/method/MethodModify.update:(Lcom/hy/truelicense/LicenseBean;)V
19: return
LineNumberTable:
line 8: 0
line 9: 8
line 10: 19
LocalVariableTable:
Start Length Slot Name Signature
0 20 0 args [Ljava/lang/String;
8 12 1 mm Lcom/hy/method/MethodModify;
阅读字节码可以发现,调用类字节码调用update方法的关键行“Method com/hy/method/MethodModify.update:(Lcom/hy/truelicense/LicenseBean;)V ,标识是个“V”,表示void,即要找的是返回值为void的update方法,而新改的方法声明类,update方法返回值类型是int,方法签名不一致,所以会出现
java.lang.NoSuchMethodError: com.hy.method.MethodModify.update(Lcom/hy/truelicense/LicenseBean;)V。
以上可以说明3个问题:
1、修改的java类更新成了最新的class,但调用该方法的class类未更新,导致方法不一致。
2、打增量包的方式并不能保证所有关联的class都是最新的。
3、Eclipse一般会刷新所有的class类,比如使用maven命令clean package等,或是Eclipse自带的project—>clean操作,都会清空之前生成的class,然后重新编译生成一套新的class,所以研发环境是正常的,但测试环境会抛出异常,根本原因是部分依赖的class未更新,还是使用方法签名修改之前编译好的class,与最新的方法不一致,因而会出现java.lang.NoSuchMethodError异常。
五、解决办法
在Eclise上搜索所有使用到了MethodModify类的update方法的类,将这些类最新编译的class作为增量包的一部分部署到测试环境中。
六、经验总结:
探讨一下什么情况会出现class不同步的现象?分析一下
1、修改的类有新增的方法,若调用类有使用到新的方法,会修改调用类的代码,此调用类编译的class也会作为增量包的一部分,不会出现class不同步的现象。
2、修改的类有删除的方法或成员变量,若调用类也删除了该方法的调用,同理会出现在增量包中,若未涉及到此方法,不会受影响,此场景也不会出现class不同步的现象。
3、修改了方法的签名,比如返回值由void变成int,调用类有使用这个方法的,编译环境不会摄氏,极有可能会导致class不同步。
根据以上探讨的结果可知,以后打增量包要注意,若出现修改方法签名又不会直接导致编译失败的,要多注意一下class的同步问题。
另外,javap工具对此次诊断问题很有帮助。