web-dev-qa-db-ja.com

別のパッケージのパブリックサブクラスを介してパッケージプライベートクラスのパブリックメソッド参照を使用する場合のIllegalAccessError

昨日、Tomcat8にJava 8 webappをデプロイした後、興味深い問題に直面しました。この問題を解決する方法よりも、なぜそれが起こるのかを理解することに興味があります。しかし、最初から始めましょう。

次のように定義された2つのクラスがあります。

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クラスをpackage-privateではなくpublicに変更します。

  2. somethingクラスのパッケージを「package1」に変更します(つまり、文字通りFooクラスとBarクラスと同じですが、webappで定義されているSomethingクラスとは物理的に異なります)。

  3. 問題のあるコードを実行する前に、Fooのクラスロードを強制します。

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

問題と上記の結果を正当化する明確で技術的な説明を誰かに教えてもらえますか?

アップデート1

3番目のソリューションを試したとき、実際には最初のソリューション(Fooクラスがパッケージprivateではなくpublicであるもの)のcommons.jarを使用していました。すみません。

さらに、私のコメントの1つで指摘されているように、問題のあるコードの直前に、BarクラスとSomethingクラスのクラスローダーをログに記録しようとしました。

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

アップデート2

さて、私はついに謎の1つを解決しました!

私のコメントの1つで、commons.jar。ええと... 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. 「mvncleanpackage」を(Eclipse実行構成として)実行し、Eclipse内からメインを実行すると、すばらしいIllegalAccessError(クール!)が発生します。

  2. maven-> Update project ...を実行し、Eclipse内からメインを実行すると、エラーは発生しません(クールではありません!)。

そこで、コマンドラインに切り替えて、最初のオプションを確認しました。問題のあるコードが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クラスがSomethingに表示されないため( Fooパッケージプライベート)、IllegalAccessErrorがスローされますか?

21
fabriziocucci

Eclipse(Mars、4.5.1)とMaven(Mavenコンパイラプラグインバージョン.5.1、最新の瞬間)。

  • Eclipseからメインをコンパイルして実行する> エラーなし
  • Console/Mavenからコンパイルし、Eclipseからメインを実行します> エラー
  • Console/Mavenからコンパイルし、 _exec:Java_ コンソールから> エラーを介してメインを実行します。
  • Eclipseからコンパイルし、メインを _exec:Java_ コンソールから実行> エラーなし
  • javac(Eclipseなし、Mavenなし、jdk-8u7)を使用してコマンドラインから直接コンパイルし、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)
    _

上記のスタックトレースに注意してください。最初の(Java-8より前の)呼び出しは正常に機能し、2番目の(Java-8ベースの)呼び出しは例外をスローします。

調査の結果、次のリンクが関連していることがわかりました。

  • JDK-8068152バグレポート 、同様の問題について説明し、とりわけ、MavenコンパイラプラグインとJavaに関して次のことに言及しています。

    これは、提供されたMavenプラグインによって引き起こされた問題のように見えます。提供されているmavenプラグイン(「plugin」ディレクトリ内)は「tools.jar」をClassLoader.getSystemClassLoader()に追加し、これが問題を引き起こしています。申し訳ありませんが、javac側で実行できる(または実行する必要がある)ことはあまりありません。

    詳細については、ToolProvider.getSystemJavaCompiler()ClassLoader.getSystemClassLoader()を調べてjavacクラスを見つけます。そこにjavacが見つからない場合は、tools.jarを自動的に見つけようとし、tools.jarのURLClassLoaderを作成して、このクラスローダーを使用してjavacをロードします。このクラスローダーを使用してコンパイルを実行すると、このクラスローダーを使用してクラスがロードされます。ただし、プラグインがtools.jarをClassLoader.getSystemClassLoader()に追加すると、クラスはシステムクラスローダーによってロードされ始めます。 同じパッケージからクラスにアクセスするが、別のクラスローダーによってロードされると、パッケージプライベートアクセスが拒否され、上記のエラーが発生します。これは、ToolProvider.getSystemJavaCompiler()の結果をMavenキャッシュすることでさらに悪化します。そのおかげで、2つのコンパイルの間にプラグインを実行してもエラーが発生します。

    (注:太字は私のものです)

  • 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>
_

以下へ:

_<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コンパイラー、 何も新しいものはありません この場合は異なりますが、それはさらに別の確認です
  • この場合のMaven Javaコンパイラには問題がありますが、Eclipse Javaコンパイラには問題がありません。MavenコンパイラはJDKコンパイラと一貫性があります。したがって、実際には、Mavenコンパイラに影響を与えるJDKのバグである可能性があります。
  • 同じEclipseコンパイラを使用してMavenを作成すると、問題が修正されるか、非表示になります。

参考までに、Maven用のEclipseコンパイラに切り替える前に、次のことも試しましたが、あまり成功しませんでした。

  • Mavenコンパイラプラグインのバージョンを変更します。すべてのバージョンが2.5から.5.1まで
  • JDK-8u25、JDK-8u60、JDK-8u73で試してみる
  • executable オプションを明示的に使用して、EclipseとMavenコンパイラがまったく同じjavacを使用していることを確認します

要約すると、JDKはMavenと一貫性があり、おそらくバグです。私が見つけたいくつかの関連するバグレポートの下に:

  • JDK-8029707継承されたメソッドを呼び出す関数型コンシューマーを使用したIllegalAccessError。修正されないとして修正されました(まったく同じ問題でした)
  • JDK-8141122パッケージへのメソッド参照を使用したIllegalAccessException-pub経由のプライベートクラス。開く(繰り返しますが、まったく同じ問題)
  • JDK-8143647Javacは、IllegalAccessErrorの結果を許可するメソッド参照をコンパイルします。 Java 9で修正されました(同様の問題、Java-8より前のコードは正常に機能し、Java-8スタイルのコードは壊れます)
  • JDK-8138667Java.lang.IllegalAccessError:メソッドにアクセスしようとしました(保護されたメソッドの場合)。オープン(同様の問題、コンパイルは正常ですが、ラムダコードへの不正アクセスのランタイムエラーよりも)。
19
A_Di-Matteo

パッケージcommons.jarpackage2のjarが別のクラスによってロードされた場合-ローダー、それからそれは異なりますランタイムパッケージそしてこの事実は何かFooのパッケージメンバーへのアクセスからのクラス。 JVM仕様の 第5.4.4章 および この素晴らしいトピック を参照してください。

すでに試したことに加えて、もう1つの解決策があると思います:オーバーライドメソッドgetFooinBarクラス

4
nukie