ClassNotFoundException 即使我不使用它 class

ClassNotFoundException even though I don't use that class

我调用了下一个 class 的方法 m1,但我的 class 路径中没有 class PKCSException 据说我不需要它:

package otropack;

import iaik.pkcs.PKCSException;
import iaik.security.provider.IAIK;

public class Test {

    public static void m1(){
        System.out.println("Method 1");
    }
    
    public static void m2() {
        try {
            IAIK.addAsProvider(false);
            System.out.println("Method 2"); 
            throw new PKCSException();
        }catch(PKCSException e) {
            e.printStackTrace();
        }
    }
}

为什么我在调用Test.m1() 时得到ClassNotFountException?我调用了一个不使用 class

的方法
Caused by: java.lang.ClassNotFoundException: iaik.pkcs.PKCSException

如果我将 PKCSException 替换为 Exception,它运行良好,不会因为 class 路径中没有 class IAIK 而给我一个错误。我也没有 class IAIK。

非常感谢!

当 class 加载程序加载您的测试 class 时,它将尝试查找所有相关 class 的定义。在您的代码中,它将因 ClassNotFoundException 而失败,因为您缺少 classes.

这是一个非常有趣的问题。答案是验证者。

让我们来做实验!

class Test {
    public static void main(String[] args) {
        m1();
    }

    public static void m1() { System.out.println("M1"); }

    public static void neverCalled() throws Exception {
        Exception e = new Ex1();
        // throw e; [1]
        // w1(e); [2]
        // w2(e); [3]
        // w3(e); [3]
    }

    private static void w1(Object x) {}
    private static void w2(Exception x) {}
    private static void w3(Serializable x) {}
}

class Ex1 implements Exception extends Exception {}

然后在命令行上:

> javac Test.java; rm Ex1.class; java Test

这个有效

然而,取消注释 [1] 并失败。取消注释 [2] 并且它有效。取消注释 [3] 并且失败。取消注释 [4] 即可。

什么。这。翻转?

答案是,如果你做任何有趣的事情,它都会失败。特别是,'throw it' 或 'pass it as argument' 算在内,但 'pass it as argument where the argument type is either j.l.Object or any interface' 不算。这很奇怪。变量的类型是什么一点也不重要。写Ex1 e = new Ex1();Exception e = new Ex1();甚至Object e = new Ex1();,都没有区别。

此外,如果我们删除Ex1.class,而是在其中放置一个打印'initializing!'的静态初始化程序,它从不打印。这是 JVM 保证。

那么反过来又发生了什么?

是验证者。 运行 这与 java -noverify Test 并且它将成功 运行 (打印 M1 所有情况 ;如果需要,取消注释所有 4 行。

所以,这是怎么回事?

验证者正在尝试验证由 javac 生成的堆栈帧。 javac 或多或少地在生成的字节码中添加了一些注释,以使 class 验证器的工作更轻松(这是检查执行字节码是否不可能导致核心转储或任何安全敏感的重要代码的事情错误)。验证这些注释是否正确然后分析字节码是否没有堆损坏失败比推测没有注释不存在堆损坏更容易。

但是,作为 检查 这些注释的一部分,它需要知道 Ex1 是什么,并且假设 Ex1 不存在,您会得到 NoClass 错误生成任何堆栈帧注释的时刻,它告诉验证者堆栈将在其中某处有一个 Ex1 实例。因此,问题归结为:VM 何时生成堆栈帧注释,这是一个奇怪的注释,但与上述代码段完全匹配:throw 并且作为非对象、非接口调用的参数。其他情况没有。

您可以从交易中得到一个实际的验证器错误。真的很简单:

> nano Test.java
class Test {
    public static void main(String[] args) {
        Ex1 e = new Ex1();
        m(e);
    }

    public static void m(Exception e) {}
}

> nano E1.java
class E1 extends Exception {}

> javac *.java
> nano E1.java
class E1 extends Thread {} // extend something else
> javac E1.java; # recompile _JUST_ E1
> java Test
VerifierError!

你可以得出的一般结论很简单:

在 class 中任何地方使用的任何类型都必须 在 运行 时出现,否则只能在 运行 方法中使用类型 [ A] 从未调用过,并且 [B] 没有出现在任何堆栈框架注释中 - 将避免错误,并且这两种情况几乎永远不会发生。但是,class 初始化仅在代码实际为 运行 时才会发生。所以,如果你想要 属性 编写代码,即使从未真正执行过,也不会因为引用不存在的 classes 而导致任何问题, 你可以这样做,但前提是您将此代码隔离在它自己的 class:

package otropack;

import iaik.pkcs.PKCSException;
import iaik.security.provider.IAIK;

public class Test {

    public static void m1(){
        System.out.println("Method 1");
    }
    
    public static void m2() {
        Container.m2();
    }

    private static class Container {
      public static void m2() {
          try {
              IAIK.addAsProvider(false);
              System.out.println("Method 2"); 
              throw new PKCSException();
          }catch(PKCSException e) {
             e.printStackTrace();
          }
      }
    }
}

解决您的问题。完全没有 class 加载错误。