【问题标题】:Disabling compile-time dependency checking when compiling Java classes编译 Java 类时禁用编译时依赖项检查
【发布时间】:2010-12-04 23:52:29
【问题描述】:

考虑以下两个 Java 类:

a.) class Test { void foo(Object foobar) { } }

b.) class Test { void foo(pkg.not.in.classpath.FooBar foobar) { } }

此外,假设在类路径中找不到pkg.not.in.classpath.FooBar

第一个类可以使用标准 javac 编译。

但是,第二个类不会编译,javac 会给你错误消息"package pkg.not.in.classpath does not exist"

错误消息在一般情况下很好,因为检查您的依赖项允许编译器告诉您是否有某些方法参数错误等。

虽然在编译时检查依赖关系很好且很有帮助,但 AFAIK 并不严格需要在上面的示例中生成 Java 类文件。

  1. 您能否举个例子,如果不执行编译时依赖性检查,在技术上不可能生成有效的 Java 类文件?

  2. 您知道有什么方法可以指示 javac 或任何其他 Java 编译器跳过编译时依赖性检查吗?

请确保您的答案解决了这两个问题。

【问题讨论】:

  • 唯一(有效的)解决方法是查找MethodHandle,将其存储在static final 字段中,并在MH 上使用invokeExact() 和正确的类型提示。如果您需要的方法不止几种,那当然是相当乏味的。

标签: java jvm bytecode javac


【解决方案1】:

我看不出你如何在不破坏 java 类型检查的情况下允许这样做。你将如何在你的方法中使用你的引用对象?为了扩展您的示例,

class test {
   void foo (pkg.not.in.classpath.FooBar foobar) { 
       foobar.foobarMethod(); //what does the compiler do here?
  } 
}

如果您在某些情况下必须编译(并调用方法)在库上工作的东西上,您无法访问最接近的方法是通过反射获取方法,类似(从内存中调用的方法,可能不准确)

 void foo(Object suspectedFoobar)
     {
       try{
        Method m = suspectedFoobar.getClass().getMethod("foobarMethod");
        m.invoke(suspectedFoobar);
       }
       ...
     }

不过,我真的看不出这样做的意义。您能否提供有关您要解决的问题的更多信息?

【讨论】:

  • 编译器会愉快地生成类文件。 foob​​arMethod() 是否存在是一个运行时问题。
  • "您能提供更多关于您要解决的问题的信息吗?"我不是想解决任何问题。我想知道我的两个问题的答案,以便更好地理解 JVM。
  • @knorv - “如果 foobarMethod() 存在与否是一个运行时问题。” - 不,这不对。至少在 Java 中不是。
  • Nate:您是否暗示在编译时跳过依赖项检查是不可能的?请更具体。
【解决方案2】:

我不认为有这样的方法 - 编译器需要知道参数的类,以便创建适当的字节码。如果找不到 Foobar 类,则无法编译 Test 类。

请注意,虽然您的两个类在功能上是等效的,因为您并没有真正使用该参数,但它们并不相同,并且在编译时会产生不同的字节码。

所以你的前提 - 在这种情况下编译器不需要找到要编译的类 - 是不正确的。

编辑 - 您的评论似乎在问“编译器不能忽略这一事实并生成 无论如何都合适的字节码吗?”

答案是不——不能。 According to the Java Language Specification,方法签名必须采用类型,elsewhere defined 可以在编译时解析。

这意味着虽然创建一个可以满足您要求的编译器在机械上非常简单,但它会违反 JLS,因此从技术上讲不会是 Java 编译器。此外,规避编译时安全对我来说听起来不是一个很好的卖点...... :-)

【讨论】:

  • 与“java/lang/Object”被替换为“pkg/not/in/classpath/FooBar”相比,生成的字节码受到的影响更大吗?请更具体。
  • 我认为示例代码是一种特殊情况,因为该对象实际上并没有被使用(没有调用任何方法,它没有在任何地方用作参数),所以在这种情况下编译器可能 能够在不知道类的情况下编译它。但我想不出这种特殊情况有什么用处。
  • 你能给我这个在技术上不是Java编译器的简单编译器吗?我花了太多时间试图让javac 开心,而没有足够的时间完成软件开发中涉及的所有其他任务。
【解决方案3】:

提取界面

pkg.in.classpath.IFooBar

制作FooBar implements IFooBar

class Test { void foo(pkg.in.classpath.IFooBar foobar) {} }

您的测试类将被编译。只需使用工厂和配置在运行时插入正确的实现,即FooBar。寻找一些IOC 容器

【讨论】:

  • 如果不参考原始的 FooBar,如何让 Foobar 实现 IFooBar?即使方法签名相同,编译器也不会接受。
  • Boris:谢谢,但我不是在寻找解决办法。您如何看待提出的两个问题?
【解决方案4】:

你能举出任何例子,如果不执行编译时依赖检查,在技术上是不可能生成有效的 Java 类文件的吗?

考虑这段代码:

public class GotDeps {
  public static void main(String[] args) {
    int i = 1;
    Dep.foo(i);
  }
}

如果目标方法有签名public static void foo(int n),那么会生成这些指令:

public static void main(java.lang.String[]);
  Code:
   0:   iconst_1
   1:   istore_1
   2:   iload_1
   3:   invokestatic    #16; //Method Dep.foo:(I)V
   6:   return

如果目标方法具有签名public static void foo(long n),则int 将在方法调用之前提升为long

public static void main(java.lang.String[]);
  Code:
   0:   iconst_1
   1:   istore_1
   2:   iload_1
   3:   i2l
   4:   invokestatic    #16; //Method Dep.foo:(J)V
   7:   return

在这种情况下,将无法生成调用指令或如何填充类常量池中由数字 16 引用的CONSTANT_Methodref_info 结构。有关详细信息,请参阅 VM 规范中的class file format .

【讨论】:

  • 很好的答案!直截了当并有明确的证据(与 SO 上通常看到的挥手相反 :-))。
  • @knorv:你能证明 SO 上有人在挥手还是只是在挥手? ;-)
  • Joachim:不,这是非常主观的,因此无法证明。-)
  • @knorv:这看起来像是在承认挥手...... Pot 先生 :-)
  • 是否可以从类编译、反编译生成存根方法,然后在没有依赖关系的情况下对其进行编译?
【解决方案5】:

您唯一能做的就是使用一些字节码操作将其转换为更具体的类型。

Java 语法中没有使用pkg.not.in.classpath.FooBar 来区分这一点:

 package pkg.not.in.classpath;
 public class FooBar { }

从此:

 package pkg.not.in.classpath;
 class FooBar { }

所以只有你说在那里使用 FooBar 是合法的。

包作用域类和源代码中的内部类之间也存在歧义:

class pkg {
    static class not {
        static class in {
            static class classpath {
                static class FooBar {}
            }
        }
    }
}

内部类在源代码中也称为pkg.not.in.classpath.FooBar,但在类文件中将称为pkg$not$in$classpath$FooBar,而不是pkg/not/in/classpath/FooBar。如果不在类路径中查找,javac 无法分辨出您的意思。

【讨论】:

  • 你是绝对正确的内部类。但是关于“公共”与“受保护” - 这不会改变字节码,因此理论上可以推迟到在运行时进行检查。还是我错过了什么?
  • 是的,它可以推迟到以后 - IIRC 在加载它时验证类时它会失败。
【解决方案6】:

Java 在设计上会进行编译时依赖性检查,并且不仅使用它来确定类型,还使用它来确定重载时的方法调用。我不知道有什么办法。

可以做的(例如 JDBC 驱动程序)是通过使用反射来延迟依赖检查。您可以从Class.forName 获取类,而编译器在编译时不知道该类。然而,一般来说,这意味着将代码写入接口,并在运行时加载实现该接口的类。

【讨论】:

    【解决方案7】:

    编译一个类而不查看它所依赖的类的类型签名将违反 JLS。没有符合标准的 Java 编译器允许您这样做。

    但是……可以做一些类似的事情。具体来说,如果我们有一个类 A 和一个依赖于 A 的类 B,则可以这样做:

    1. 编译A.java
    2. 针对 A.class 编译 B.java。
    3. 编辑 A.java 以不兼容的方式对其进行更改。
    4. 编译 A.java,替换旧的 A.class。
    5. 使用 B.class 和新的(不兼容的)A.class 运行 Java 应用程序。

    如果您这样做,当类加载器注意到签名不兼容时,应用程序将失败并返回 IncompatibleClassChangeError

    实际上,这说明了为什么编译忽略依赖项是一个坏主意。如果您使用不一致的字节码文件运行应用程序,(仅)将报告检测到的第一个不一致。因此,如果您有很多不一致之处,您将需要多次运行您的应用程序来“检测”它们。实际上,如果应用程序或其任何依赖项中存在任何类的动态加载(例如使用Class.forName()),那么其中一些问题可能不会立即出现。

    总而言之,在编译时忽略依赖项的代价是 Java 开发速度变慢,Java 应用程序可靠性降低。

    【讨论】:

      【解决方案8】:

      我创建了两个类:CallerCallee

      public class Caller {
          public void doSomething( Callee callee) {
              callee.doSomething();
          }
      
          public void doSame(Callee callee) {
              callee.doSomething();
          }
      
          public void doSomethingElse(Callee callee) {
              callee.doSomethingElse();
          }
      }
      
      public class Callee {
          public void doSomething() {
          }
          public void doSomethingElse() {
          }
      }
      

      我编译了这些类,然后用javap -c Callee > Callee.bcjavap -c Caller > Caller.bc 将它们反汇编。这产生了以下结果:

      Compiled from "Caller.java"
      public class Caller extends java.lang.Object{
      public Caller();
      Code:
      0: aload_0
      1: invokespecial #1; //Method java/lang/Object."<init>":()V
      4: return
      
      public void doSomething(Callee);
      Code:
      0: aload_1
      1: invokevirtual #2; //Method Callee.doSomething:()V
      4: return
      
      public void doSame(Callee);
      Code:
      0: aload_1
      1: invokevirtual #2; //Method Callee.doSomething:()V
      4: return
      
      public void doSomethingElse(Callee);
      Code:
      0: aload_1
      1: invokevirtual #3; //Method Callee.doSomethingElse:()V
      4: return
      
      }
      
      Compiled from "Callee.java"
      public class Callee extends java.lang.Object{
      public Callee();
      Code:
      0: aload_0
      1: invokespecial #1; //Method java/lang/Object."<init>":()V
      4: return
      
      public void doSomething();
      Code:
      0: return
      
      public void doSomethingElse();
      Code:
      0: return
      
      }
      

      编译器为对“被调用者”的方法调用生成了一个方法签名和一个类型安全的invokevirtual 调用——它知道这里调用的是什么类和什么方法。如果该类不可用,编译器将如何生成方法签名或“invokevirtual”?

      有一个 JSR (JSR 292) 可以添加支持动态调用的“invokedynamic”操作码,但是 JVM 目前不支持。

      【讨论】:

        猜你喜欢
        • 2011-09-29
        • 2016-04-05
        • 1970-01-01
        • 1970-01-01
        • 2014-04-05
        • 1970-01-01
        • 2019-05-03
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多