通过另一个包的公共子类使用包私有类的公共方法引用时出现IllegalAccessError

时间:2016-03-19 10:30:22

标签: java maven java-8 maven-3 maven-compiler-plugin

昨天我在Tomcat 8上部署我的Java 8 webapp后遇到了一个有趣的问题。与其解决这个问题的方法不同,我更感兴趣的是理解为什么会这样。但是,让我们从头开始。

我有两个类定义如下:

Foo.java

package package1;

abstract class Foo {

    public String getFoo() {
        return "foo";
    }

}

Bar.java

package package1;

public class Bar extends Foo {

    public String getBar() {
        return "bar";
    }

}

正如您所看到的,它们位于同一个包中,并最终位于同一个jar中,我们称之为 commons.jar 。这个jar是我的webapp的依赖(即在我的webapp的pom.xml中定义为依赖)。

在我的webapp中,有一段代码可以:

package package2;

public class Something {

    ...

    Bar[] sortedBars = bars.stream()
                           .sorted(Comparator.comparing(Bar::getBar)
                                             .thenComparing(Bar::getFoo))
                           .toArray(Bar[]::new);

    ...

}

当它被执行时我得到:

java.lang.IllegalAccessError: tried to access class package1.Foo from class package2.Something

玩弄并尝试我能够以两种方式避免错误:

  1. 将Foo类更改为public而不是package-private;

  2. 将Something类的包更改为“package1”(即字面上与Foo和Bar类相同,但物理上不同的是webapp中定义的Something类);

  3. 在执行有问题的代码之前强制加载Foo:

    try {
        Class<?> fooClass = Class.forName("package1.Foo");
    } catch (ClassNotFoundException e) { }
    

  4. 有人可以给我一个明确的技术解释,证明问题和上述结果是正确的吗?

    更新1

    当我尝试第三个解决方案时,我实际上正在使用第一个解决方案的commons.jar(Foo类是public而不是package private)。我很抱歉。

    此外,正如我的一条评论所指出的那样,我试图在违规代码之前记录Bar类和Something类的类加载器,两者的结果是:

    WebappClassLoader
    context: my-web-app
    delegate: false
    ----------> Parent Classloader:
    java.net.URLClassLoader@681a9515
    

    更新2

    好的,我终于解开了其中一个谜团!

    在我的一条评论中,我说我无法通过从与 commons.jar 的Foo和Bar不同的包中创建的简单主要执行违规代码来复制问题。 。嗯...... Eclipse(4.5.2)和Maven(3.3.3)在这里欺骗了我!

    用这个简单的pom:

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <groupId>my.test</groupId>
        <artifactId>commons</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <maven.compiler.source>1.8</maven.compiler.source>
            <maven.compiler.target>1.8</maven.compiler.target>
        </properties>
    
    </project>
    
    1. 如果我执行“mvn clean package”(作为Eclipse Run Configuration)并从Eclipse中运行main,我会得到精彩的IllegalAccessError(很酷!);

    2. 如果我执行Maven - &gt;更新项目......并在Eclipse中运行main我没有收到任何错误(不酷!)。

    3. 所以我切换到了命令行并确认了第一个选项:无论违规代码是在webapp中还是在jar中,都会始终出现错误。尼斯!

      然后,我能够进一步简化Something类并发现一些有趣的东西:

      package package2;
      
      import java.util.stream.Stream;
      import package1.Bar;
      
      public class Something {
      
          public static void main(String[] args) {
      
              System.out.println(new Bar().getFoo());
              // "foo"
      
              Stream.of(new Bar()).map(Bar::getFoo).forEach(System.out::println);
              // IllegalAccessError
      
          }
      
      }
      

      我要在这里亵渎神明,所以请耐心等待:可能是Bar :: getFoo方法引用只是“解析”到Foo :: getFoo方法引用,因为Foo类在有什么东西(Foo包私有),抛出了IllegalAccessError?

2 个答案:

答案 0 :(得分:19)

我能够在Eclipse(Mars, 4.5.1 )和使用Maven(Maven编译器插件版本 3.5.1 )的命令行中重现相同的问题。目前最新的。)

  • 从Eclipse编译并运行main&gt; 没有错误
  • 从console / Maven编译并从Eclipse运行main&gt;的错误
  • 从console / Maven编译并从控制台&gt;运行主要exec:java错误
  • 从Eclipse编译并从控制台&gt;运行主要通过exec:java 没有错误
  • 直接从命令行编译javac(没有Eclipse,没有Maven, jdk-8u73 )并直接从命令行运行java&gt;的错误

    foo
    Exception in thread "main" java.lang.IllegalAccessError: tried to access class com.sample.package1.Foo from class com.sample.package2.Main   
    at com.sample.package2.Main.lambda$MR$main$getFoo$e8593739$1(Main.java:14)   
    at com.sample.package2.Main$$Lambda$1/2055281021.apply(Unknown Source)   
    at java.util.stream.ReferencePipeline$3$1.accept(Unknown Source)   
    at java.util.stream.Streams$StreamBuilderImpl.forEachRemaining(Unknown Source)   
    at java.util.stream.AbstractPipeline.copyInto(Unknown Source)   
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(Unknown Source)   
    at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(Unknown Source)   
    at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(Unknown Source)   
    at java.util.stream.AbstractPipeline.evaluate(Unknown Source)   
    at java.util.stream.ReferencePipeline.forEach(Unknown Source)   
    at com.sample.package2.Main.main(Main.java:14)
    

注意上面的堆栈跟踪,第一个(pre-java-8)调用工作正常,而第二个(基于java-8)引发异常。

经过一番调查后,我发现了以下相关信息:

  • JDK-8068152 bug report,描述了类似的问题,最重要的是,提到了有关Maven编译器插件和Java的以下内容:

      

    这看起来像是由提供的maven插件引起的问题。提供的maven插件(在“插件”目录中)将“tools.jar”添加到ClassLoader.getSystemClassLoader(),这就是触发问题。我真的没有看到那些可能(或应该)在javac方面做的事,抱歉。

         

    更详细信息,ToolProvider.getSystemJavaCompiler()会查看ClassLoader.getSystemClassLoader()以查找javac类。如果在那里找不到javac,它会尝试自动查找tools.jar,并为tools.jar创建URLClassLoader,使用此类加载器加载javac。当使用此类加载器运行编译时,它使用此类加载器加载类。但是,当插件将tools.jar添加到ClassLoader.getSystemClassLoader()时,类将开始由系统类加载器加载。 当从同一个包访问一个类但由另一个类加载器加载时,拒绝了包私有访问,从而导致上述错误。 maven缓存ToolProvider.getSystemJavaCompiler()的结果会使情况变得更糟,这要归功于在两个编辑之间运行插件仍然会导致错误。

    (注意:粗体是我的)

  • Maven Compiler Plugin - Using Non-Javac Compilers,描述了如何将不同的编译器插入Maven编译器插件并使用它。

所以,只需从下面的配置切换:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.5.1</version>
    <configuration>
        <source>1.8</source>
        <target>1.8</target>
    </configuration>
</plugin>

以下内容:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.5.1</version>
    <configuration>
        <source>1.8</source>
        <target>1.8</target>
        <compilerId>eclipse</compilerId>
    </configuration>
    <dependencies>
        <dependency>
            <groupId>org.codehaus.plexus</groupId>
            <artifactId>plexus-compiler-eclipse</artifactId>
            <version>2.7</version>
        </dependency>
    </dependencies>
</plugin>

针对相同的代码修复了问题,不再有IllegalAccessError。但是这样做,我们实际上在这个上下文中删除了Maven和Eclipse之间的差异(使用Eclipse编译器制作Maven),所以这是一种正常的结果。

确实,这导致了以下结论:

  • 在这种情况下,Eclipse Java编译器与Maven Java编译器不同,nothing new,但这是另一个确认
  • 在这种情况下,Maven Java编译器存在问题,而Eclipse Java编译器则没有。 Maven编译器与JDK编译器一致。所以它可能实际上是JDK对Maven编译器产生影响的错误。
  • 使用相同的Eclipse编译器制作Maven可以解决问题,或者隐藏

作为参考,在切换到Maven的eclipse编译器之前,我还尝试了以下方面没有太大成功:

  • 更改Maven编译器插件版本,每个版本从 2.5 直到 3.5.1
  • 尝试使用JDK-8u25,JDK-8u60,JDK-8u73
  • 确保Eclipse和Maven编译器使用完全相同的javac,明确使用executable选项

总而言之,JDK与Maven一致,而且很可能是一个bug。下面我发现了一些相关的错误报告:

  • JDK-8029707使用功能消费者调用继承方法的IllegalAccessError。固定为无法修复(这是完全相同的问题)
  • JDK-8141122 IllegalAccessException使用对pub-private类的方法引用,通过pub 。打开(再次,完全相同的问题)
  • JDK-8143647 Javac编译方法引用,允许结果出现IllegalAccessError 。在Java 9中修复(类似的问题,pre-java-8代码可以正常工作,java-8样式代码会中断)
  • JDK-8138667 java.lang.IllegalAccessError:尝试访问方法(对于受保护的方法)。打开(类似的问题,编译正常,但运行时错误,因为非法访问lambda代码)。

答案 1 :(得分:4)

如果包 commons.jar 和jar包含 package2 由另一个类加载器加载,那么它是不同的运行时包并且这个事实阻止 Something 类的方法访问 Foo 的包成员。请参阅JVM规范的chapter 5.4.4this awesome topic

我认为除了您已尝试的内容之外还有一个解决方案:在 Bar 类中重写方法 getFoo

相关问题