JAVA找出哪个类import了不存在的类

JAVA找出哪个类import了不存在的类

1. 背景

在JAVA中一个类A,import 另外的一个类B.然后在容器启动时,只会提示B类不存在,不会出现任何A类相关的信息

Tomcat中错误信息如下,测试代码使用org.slf4j.Logger说明 ,部分错误信息如下

复制代码
at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.NoClassDefFoundError: Lorg/slf4j/Logger;
at java.lang.Class.getDeclaredFields0(Native Method)
at java.lang.Class.privateGetDeclaredFields(Class.java:2583)
... 10 more
Caused by: java.lang.ClassNotFoundException: org.slf4j.Logger
at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1360)
at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1182)
... 23 more

2. 日志能解决吗

在容器TOMCAT为类WebappClassLoaderBase开启详细日志,在/conf/logging.properties 添加如下日志

复制代码
org.apache.catalina.loader.WebappClassLoaderBase.level = FINE

加上日志以后,输出内容如下

复制代码
org.apache.catalina.loader.WebappClassLoaderBase.loadClass   Loading class from local repository
org.apache.catalina.loader.WebappClassLoaderBase.loadClass loadClass(dxxs.BTestClass, false)
org.apache.catalina.loader.WebappClassLoaderBase.loadClass   Searching local repositories
org.apache.catalina.loader.WebappClassLoaderBase.findClass     findClass(dxxs.BTestClass)
org.apache.catalina.loader.WebappClassLoaderBase.loadClass   Loading class from local repository
org.apache.catalina.loader.WebappClassLoaderBase.loadClass loadClass(org.slf4j.Logger, false)
org.apache.catalina.loader.WebappClassLoaderBase.loadClass   Searching local repositories
org.apache.catalina.loader.WebappClassLoaderBase.findClass     findClass(org.slf4j.Logger)
org.apache.catalina.loader.WebappClassLoaderBase.findClass     --> Returning ClassNotFoundException

从日志输出上来看,只是知道 dxxs.BTestClass 后就加载 org.slf4j.Logger,但并不能确定就是 dxxs.BTestClass 导致的

3. 分析字节码能解决吗

通过日志分析,感觉上是可以,但是不能确定.所以为了确定,采用 Java Agent 方式分析,当前加载的class是否引用了当前环境不存在的class

在TOMCAT启动时,配置参数,在启动时,注入编写jar

复制代码
set JAVA_OPTS=%JAVA_OPTS% -javaagent:"%CATALINA_HOME%\lib\class-load-monitor-agent.jar"="%CATALINA_HOME%\conf\agent-config.properties"

再次启动后,对应输出内容如下

复制代码
[ClassLoadMonitor] ==========================================
[ClassLoadMonitor] FOUND REFERENCE TO MISSING CLASS:
[ClassLoadMonitor] Source class: dxxs.BTestClass
[ClassLoadMonitor] Missing class: org.slf4j.Logger
[ClassLoadMonitor] ClassLoader: ParallelWebappClassLoader
  context: test
  delegate: false
----------> Parent Classloader:
java.net.URLClassLoader@87aac27

[ClassLoadMonitor] Thread: localhost-startStop-1

这次从日志中,可以明确知道 dxxs.BTestClass 引用了类org.slf4j.Logger,而当前环境中却不存在 org.slf4j.Logger.由此问题解决

4. 如何实现

构建配置,实现javaagent,需要在MANIFEST.MF文件中配置Premain-ClassAgent-Class,所有当前pom.xml构建配置信息如下

xml 复制代码
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>3.2.0</version>
    <configuration>
        <archive>
            <manifestEntries>
                <Premain-Class>com.example.ClassLoadMonitorAgent</Premain-Class>
                <Agent-Class>com.example.ClassLoadMonitorAgent</Agent-Class>
                <Can-Redefine-Classes>true</Can-Redefine-Classes>
                <Can-Retransform-Classes>true</Can-Retransform-Classes>
            </manifestEntries>
        </archive>
    </configuration>
</plugin>

这个代码中实现两个方法

复制代码
public static void premain(String agentArgs, Instrumentation inst);
public static void agentmain(String agentArgs, Instrumentation inst)

添加类加载转换器

复制代码
inst.addTransformer(new ClassFileTransformer() {
 @Override
  public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
  ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
      if (className != null && classfileBuffer != null) {
          String fullClassName = className.replace('/', '.');
          if (!checkedClasses.contains(fullClassName)) {
              checkedClasses.add(fullClassName);             
              analyzeBytecode(fullClassName, classfileBuffer, loader);
          }
      }
      return classfileBuffer;
  }
}, true);

核心分析类分析代码 analyzeBytecode

关于常量池信息参考,可以查看地址https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-4.html#jvms-4.4

复制代码
private static void analyzeBytecode(String className, byte[] bytecode, ClassLoader loader) {
    try {
        DataInputStream dis = new DataInputStream(new ByteArrayInputStream(bytecode));
        int magic = dis.readInt();
        if (magic != 0xCAFEBABE) {
            return; 
        }
        dis.readShort(); 
        dis.readShort(); 
        int constantPoolCount = dis.readShort();
        String[] utf8Pool = new String[constantPoolCount];
        int[] classRefIndices = new int[constantPoolCount]; 
        int classRefCount = 0;
        for (int i = 1; i < constantPoolCount; i++) {
            byte tag = dis.readByte();
            switch (tag) {
                case 1: // UTF8
                    utf8Pool[i] = dis.readUTF();
                    break;
                case 7: // Class
                    int nameIndex = dis.readShort();
                    classRefIndices[classRefCount++] = nameIndex;
                    break;
                case 8: // String
                    dis.readShort();
                    break;
                case 9:  // Fieldref
                case 10: // Methodref
                case 11: // InterfaceMethodref
                case 12: // NameAndType
                    dis.readShort();
                    dis.readShort();
                    break;
                case 3: // Integer
                case 4: // Float
                    dis.readInt();
                    break;
                case 5: // Long
                case 6: // Double
                    dis.readLong();
                    i++;
                    break;
                case 15: // MethodHandle
                    dis.readByte();
                    dis.readShort();
                    break;
                case 16: // MethodType
                    dis.readShort();
                    break;
                case 18: // InvokeDynamic
                    dis.readShort();
                    dis.readShort();
                    break;
                default:
                    break;
            }
        }
        for (int i = 0; i < classRefCount; i++) {
            int utf8Index = classRefIndices[i];
            if (utf8Index > 0 && utf8Index < constantPoolCount && utf8Pool[utf8Index] != null) {
                // 小游戏 地心侠士
                // 获取到当前class文件中引用类名
                String referencedClass = utf8Pool[utf8Index].replace('/', '.');
                // className 配置是要检查的的类名
                checkReference(className, referencedClass, loader);
            }
        }
        // 小游戏 地心侠士
        // 检查class文件,其他地方是否包含丢失的class
        scanForClassReferences(className, bytecode, loader);
    } catch (Exception e) {
        
    }
}

private static void scanForClassReferences(String className, byte[] bytecode, ClassLoader loader) {
    // 公众号: 小满小慢
    for (String missingClass : missingClasses) {
        String internalName = missingClass.replace('.', '/');
        String descriptorFormat = "L" + internalName + ";";
        if (containsClassReference(bytecode, internalName) || 
            containsClassReference(bytecode, descriptorFormat)) {
            logClassReference(className, missingClass, loader);
        }
    }
}

如何使用?

  1. 生成配置文件 agent-config.properties
    配置内容如下:

    复制代码
    # 小游戏 地心侠士
    # 公众号 小满小漫 精心制作
    # 配置需要监控的不存在的类,多个类用逗号或分号分隔
    missingClasses=org.slf4j.Logger
  2. 修改启动参数
    在catalina.bat文件中添加,agent配置信息,修改内容如下

    bash 复制代码
    set JAVA_OPTS=%JAVA_OPTS% -javaagent:"%CATALINA_HOME%\lib\class-load-monitor-agent-1.0-SNAPSHOT.jar"="%CATALINA_HOME%\lib\agent-config.properties"

总结

需要找缺失类有那个类导致的情况不多见.但是要查找,确实很费时间.在十多年JAVA斗智斗勇的过程中,这也是头一次遇到.以往提示找不到某个class,找到这个class对应jar往对应的环境一放就可以.主要这次是排查一个历史项目代码,当时日志文件直接使用log4j.后来项目升级了,而客户环境class没有升级.导致在客户客开代码在开发环境完全没问题,切换客户环境就报错奇怪的现象,class-load-monitor-agent-1.0-SNAPSHOT.jar,如果需要,请关注公众号[小满小慢]回复javanotfound获取下载地址

原文地址: https://mp.weixin.qq.com/s/YCbyaj4tPzjc22JJ5317VQ