问题现象:

由于业务需要对一个老项目(war)进行tomcat7升级tomcat 8, dev/test/reg/stage 升级正常, 业务验证也是OK。生产进行灰度发布后,提示以下错误,找不到业务方法(隐藏业务相关信息)

at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.NoSuchMethodError: com.xxx.yyy.enums.DifficultyEnum.valueOf(Lcom/xxx/yyy/enums/DifficultyValue;)Lcom/xxxx/yyy/enums/DifficultyEnum;
  at com.xxx.yyy.module.question.service.ExamQuestionBiz.getCommonAnalysisData(ExamQuestionBiz.java:960)

分析过程:

1,代码验证

先对业务进行回滚,然后联系开发进行排查。将stage和生产包进行md5值进行比较,确定代码是同一份,开发对包进行反编译,在代码中可以找到对应的方法。

2,环境比较

除生产环境以外,其他环境都是docker容器部署并且运行正常,jdk大版本一致。本打算生产直接迁移docker,开发担心迁移docker有性能影响,希望找到原因。

3, arthas 排查

利用arthas 确认进程是否正确加载,通过命令查看该方法信息

sc -d com.xxx.yyy.enums.DifficultyEnum

  • stage环境 结果
class-info        com.xxx.yyy.enums.DifficultyEnum
code-source       /data/tomcat/webapps/ROOT/WEB-INF/lib/exam-biz-2.1.111.jar
  • 生产环境(隔离机器)结果
class-info        com.xxx.yyy.enums.DifficultyEnum
code-source       /app/tomcat/webapps/ROOT/WEB-INF/lib/exam-service-api-2.1.111.jar

比较结果后发现,两个环境提供同一个方法不是同一个jar文件,开发搜索代码后确认这2个jar文件确实都提供了同样的方法,但是exam-service-api这个包里方法没有业务代码,exam-biz 是正常业务代码。

原因:

网搜索了下资料,其他人遇到过类似问题。具体原因如下:

1,tomcat7会搜索项目 lib/目录下的jar文件,并按照名字排序 ,然后加载。排序后exam-biz 在exam-service-api前面,所以正常。

2,tomcat8会搜索项目 lib/目录下的jar文件,直接进行加载,取消了排序。文件系统返回的结果 exam-service-api 在exam-biz前面,所以业务提示找不到方法,发送异常错误。

解决办法:

  • 临时解决办法

1,修改tomcat context.xml,指定优先加载包

<Resources>
     <PreResources base="/data/tomcat/webapps/ROOT/WEB-INF/lib/exam-biz-2.1.111.jar"
                   className="org.apache.catalina.webresources.JarResourceSet"
                   webAppMount="/WEB-INF/classes"/>
 </Resources>
  • 长期方案

1,规范开发流程,一个项目里不同jar包不能提供相同方法.

扩展讨论:

查阅相关资料的同时,发现这种情况是由于tomcat 使用 file.list() 方法获取项目目录下jar文件时,系统是随机返回结果,并且该方法是调用的系统libc函数readdir_r() 获取的。该函数不同系统和不同的文件系统都会有不同差异。 我们生产环境非容器化部署使用的centos 7.6 xfs格式,docker环境使用centos 7.7 overlay2;

readddir_r() 调用验证

使用 文心一言 提供C代码片段进行验证

  • centos7.6 非docker环境 exam-biz 在 exam-service-api后面,业务异常

tomcat7升级到tomcat8 java.lang.NoSuchMethodError_#include


  • centos 7.7 docker环境 exam-biz 在 exam-service-api前面,业务正常


tomcat7升级到tomcat8 java.lang.NoSuchMethodError_tomcat_02

  • c代码片段

gcc -o list_dir list_dir.c

./list_dir tomcat/webapps/ROOT/WEB-INF/lib

list_dir.c

#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>

int main(int argc, char *argv[]) {
    // 检查是否提供了目录路径作为参数
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <directory_path>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    // 打开指定目录
    DIR *dir = opendir(argv[1]);
    if (dir == NULL) {
        perror("opendir");
        exit(EXIT_FAILURE);
    }

    // 准备读取目录项
    struct dirent entry;
    struct dirent *result;
    int readdir_result;

    // 循环读取目录项,直到读取完毕或发生错误
    do {
        // 重置 result 指针(readdir_r 需要)
        result = NULL;
        // 调用 readdir_r 读取下一个目录项
        readdir_result = readdir_r(dir, &entry, &result);
        // 检查 readdir_r 的返回值
        if (readdir_result != 0) {
            perror("readdir_r");
            closedir(dir);
            exit(EXIT_FAILURE);
        }
        // 如果 result 非空,表示成功读取了一个目录项
        if (result != NULL) {
            printf("Found file: %s\n", result->d_name);
        }
    } while (result != NULL); // 继续读取直到 result 为 NULL

    // 关闭目录流
    closedir(dir);
    return 0;
}

参考文档:

https://www.cnblogs.com/zjdxr-up/p/17139374.html

https://inhann.top/2022/06/28/tomcat_random_lib/