昨天我在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
玩弄并尝试我能够以两种方式避免错误:
将Foo类更改为public而不是package-private;
将Something类的包更改为“package1”(即字面上与Foo和Bar类相同,但物理上不同的是webapp中定义的Something类);
在执行有问题的代码之前强制加载Foo:
try {
Class<?> fooClass = Class.forName("package1.Foo");
} catch (ClassNotFoundException e) { }
有人可以给我一个明确的技术解释,证明问题和上述结果是正确的吗?
当我尝试第三个解决方案时,我实际上正在使用第一个解决方案的commons.jar(Foo类是public而不是package private)。我很抱歉。
此外,正如我的一条评论所指出的那样,我试图在违规代码之前记录Bar类和Something类的类加载器,两者的结果是:
WebappClassLoader
context: my-web-app
delegate: false
----------> Parent Classloader:
java.net.URLClassLoader@681a9515
好的,我终于解开了其中一个谜团!
在我的一条评论中,我说我无法通过从与 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>
如果我执行“mvn clean package”(作为Eclipse Run Configuration)并从Eclipse中运行main,我会得到精彩的IllegalAccessError(很酷!);
如果我执行Maven - &gt;更新项目......并在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
}
}
我要在这里亵渎神明,所以请耐心等待:可能是Bar :: getFoo方法引用只是“解析”到Foo :: getFoo方法引用,因为Foo类在有什么东西(Foo包私有),抛出了IllegalAccessError?
答案 0 :(得分:19)
我能够在Eclipse(Mars, 4.5.1 )和使用Maven(Maven编译器插件版本 3.5.1 )的命令行中重现相同的问题。目前最新的。)
exec:java
的错误 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),所以这是一种正常的结果。
确实,这导致了以下结论:
作为参考,在切换到Maven的eclipse编译器之前,我还尝试了以下方面没有太大成功:
executable
选项总而言之,JDK与Maven一致,而且很可能是一个bug。下面我发现了一些相关的错误报告:
答案 1 :(得分:4)
如果包 commons.jar 和jar包含 package2 由另一个类加载器加载,那么它是不同的运行时包并且这个事实阻止 Something 类的方法访问 Foo 的包成员。请参阅JVM规范的chapter 5.4.4和this awesome topic。
我认为除了您已尝试的内容之外还有一个解决方案:在 Bar 类中重写方法 getFoo