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

fab*_*cci 21 java maven-3 maven java-8 maven-compiler-plugin

昨天我在Tomcat 8上部署我的Java 8 webapp后遇到了一个有趣的问题.我不想如何解决这个问题,而是更了解为什么会发生这种情况.但是,让我们从头开始.

我有两个类定义如下:

Foo.java

package package1;

abstract class Foo {

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

}
Run Code Online (Sandbox Code Playgroud)

Bar.java

package package1;

public class Bar extends Foo {

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

}
Run Code Online (Sandbox Code Playgroud)

正如你所看到的,它们在同一个包中,并最终在同一个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);

    ...

}
Run Code Online (Sandbox Code Playgroud)

当它被执行时,我得到:

java.lang.IllegalAccessError: tried to access class package1.Foo from class package2.Something
Run Code Online (Sandbox Code Playgroud)

玩耍和试验我能够通过三种方式避免错误:

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

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

  3. 在执行违规代码之前强制加载Foo:

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

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

更新1

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

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

WebappClassLoader
context: my-web-app
delegate: false
----------> Parent Classloader:
java.net.URLClassLoader@681a9515
Run Code Online (Sandbox Code Playgroud)

更新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>
Run Code Online (Sandbox Code Playgroud)
  1. 如果我执行"mvn clean package"(作为Eclipse Run Configuration)并从Eclipse中运行main,我会得到精彩的IllegalAccessError(很酷!);

  2. 如果我执行Maven - >更新项目...并从Eclipse中运行main我没有得到任何错误(不酷!).

所以我切换到命令行,我确认了第一个选项:无论违规代码是在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

    }

}
Run Code Online (Sandbox Code Playgroud)

我将要在这里亵渎,所以忍受我:可能是Bar :: getFoo方法引用简单地"解析"到Foo :: getFoo方法引用,因为Foo类在Something中不可见(正在Foo包私有),IllegalAccessError被抛出?

A_D*_*teo 19

我能够在Eclipse(Mars,4.5.1)和使用Maven(Maven Compiler Plugin版本3.5.1,目前最新版本)的命令行中重现相同的问题.

  • 从Eclipse> No Error编译并运行main
  • 从console/Maven编译并从Eclipse> Error运行main
  • 从console/Maven编译并从控制台运行主要通道exec:java> 错误
  • 从Eclipse编译并exec:java从控制台运行主要通道> 无错误
  • 直接从命令行编译javac(没有Eclipse,没有Maven,jdk-8u73)并直接从命令行运行java> Error

    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)
    
    Run Code Online (Sandbox Code Playgroud)

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

经过一番调查,我发现相关的以下链接:

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

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

    在更多细节中,ToolProvider.getSystemJavaCompiler()将研究ClassLoader.getSystemClassLoader()查找javac类.如果它在那里找不到javac,它会尝试自动找到tools.jar,并URLClassLoader为tools.jar 创建一个,使用这个类加载器加载javac.当使用此类加载器运行编译时,它使用此类加载器加载类.但是,当插件将tools.jar添加到其中时ClassLoader.getSystemClassLoader(),类将开始由系统类加载器加载.当从同一个包访问一个类但由另一个类加载器加载时,拒绝了包私有访问,从而导致上述错误.由于maven缓存结果,使得这更糟糕ToolProvider.getSystemJavaCompiler(),因为在两个编译之间运行插件仍会导致错误.

    (注意:大胆是我的)

  • Maven编译器插件 - 使用非Javac编译器,描述如何将不同的编译器插入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>
Run Code Online (Sandbox Code Playgroud)

以下内容:

<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>
Run Code Online (Sandbox Code Playgroud)

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

事实上,这导致了以下结论:

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

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

  • 更改Maven编译器插件版本,每个版本从2.53.5.1
  • 尝试使用JDK-8u25,JDK-8u60,JDK-8u73
  • 确保Eclipse和Maven Compiler使用完全相同的javac,显式使用该executable选项

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

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

  • 请注意,此错误已在JDK8u102中修复,另请参阅[此答案](http://stackoverflow.com/a/39120603/2711488) (2认同)

nuk*_*kie 4

如果包commons.jar和带有package2 的jar由另一个类加载器加载,那么它是不同的运行时包,并且这一事实阻止Something类的方法访问Foo的包成员。请参阅JVM 规范第 5.4.4 章和这个很棒的主题

我认为除了您已经尝试过的解决方案之外,还有一种解决方案:重写Bar类中的方法getFoo