背景

从ECS在迁移到容器之后,迁移了一下代码部署的位置,以及Tomcat 从 7 升级到 8,业务开始频繁出现NoSuchMethodError。

Tomcat7和Tomcat8加载的区别和相同点

Tomcat 8.0 :class loader how to work in tomcat8
Tomcat 7.0 :class load how to work in tomcat7

结论

加载class的方式都是一样的,但是lib的排序方式发生了变化,导致读取class发生了不一致. 从而引发NoSuchMethodError

相同点

1、读取class的方式,伪代码如下:

  1. //jarFiles ===> WEB-INF/lib 下的lib文件
  2. for (i = 0; (entry == null) && (i < jarFilesLength); i++) {
  3. jarEntry = jarFiles[i].getJarEntry(jarEntryPath);
  4. if (jarEntry != null) {
  5. //tomcat biz
  6. //返回 ResourceEntry
  7. }
  8. }

谁先找到class文件谁先加载,问题就出现在jarFiles的排列顺序
jarFiles的是直接读取的WEB-INF/lib下的文件,那么读取的顺序就决定了加载的顺序

Tomcat7 读取顺序代码如下: FileDirContext.java (在Tomcat8中被重构掉)

  1. protected List<NamingEntry> list(File file) {
  2. String[] names = file.list();
  3. Arrays.sort(names);//这里进行了一次排序,得出的结果和我们在WEB-INF/lib下执行ll命令一致
  4. //剩下的省略掉
  5. }

Tomcat8 读取顺序代码如下:DirResourceSet.java

  1. @Override
  2. public String[] list(String path) {
  3. String webAppMount = getWebAppMount();
  4. if (path.startsWith(webAppMount)) {
  5. File f = file(path.substring(webAppMount.length()), true);
  6. if (f == null) {
  7. return EMPTY_STRING_ARRAY;
  8. }
  9. //读取文件,然而没有了排序. 完全无序,具体的读取全看native的实现,这里我不清楚
  10. String[] result = f.list();
  11. if (result == null) {
  12. return EMPTY_STRING_ARRAY;
  13. } else {
  14. return result;
  15. }
  16. } else {
  17. //省略部分
  18. return EMPTY_STRING_ARRAY;
  19. }
  20. }

既然没有了顺序,那么我们就不知道他会从哪一个jar中读取,但是还是有个规律的,如果n次发布的jar包都没有变化,那么n从读取的顺序也是没有变化的.

从上述结论定制NoSuchMethodError

1、打包jar包3份,分别是

  1. -rw-r--r-- 1 admin wheel 46K 2 16 08:19 safe-common-0.1.0-SNAPSHOT.jar 1
  2. -rw-r--r-- 1 admin wheel 46K 2 16 08:08 safe-common-1.0-SNAPSHOT.jar 2
  3. -rw-r--r-- 1 admin wheel 46K 2 15 21:37 safe-common-2.0-SNAPSHOT.jar 3

1和3 代码一致,比2多出一个方法.

  1. public class NoSuchMethodTest {
  2. public NoSuchMethodTest() {
  3. }
  4. //jar包1,2,3中都有
  5. public static void handle(String test) {
  6. System.out.println("handle===>" + test);
  7. }
  8. //jar包1,3中才有
  9. public static void handle(String test, String test2) {
  10. System.out.println("2.0===>handle===>" + test + ":" + test2);
  11. }
  12. }

调用代码

  1. @RequestMapping("zz")
  2. @ResponseBody
  3. public String xx() {
  4. try {
  5. NoSuchMethodTest.handle("123", "123");
  6. } catch (Throwable e) {
  7. return e.getMessage();
  8. }
  9. return "zz";
  10. }

测试1

将2和3直接放到WEB-INF/lib下,调用

调用结果如下,必然发生NoSuchMethodError

➜ lib curl 127.0.0.1:18080/zz.json com.raycloud.safe.group.api.such.NoSuchMethodTest.handle(Ljava/lang/String;Ljava/lang/String;)V%

测试2

将1、2、3号jar包都放入,因为在tomcat7下排列顺序为为1、2、3号jar包依次排列,所以没有问题

在而Tomcat8下则跟file.list的读取顺序有关.

其他链接

https://bz.apache.org/bugzilla/show_bug.cgi?id=57129

总结

世界是科学的