Подтвердить что ты не робот

IllegalAccessError при использовании общедоступной ссылки метода класса private-private через публичный подкласс из другого пакета

Вчера я столкнулся с интересной проблемой после развертывания своего Java 8 webapp на Tomcat 8. Вместо того, чтобы решить эту проблему, мне больше интересно понять, почему это происходит. Но пусть начнется с самого начала.

У меня есть два класса, которые определены следующим образом:

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";
    }

}

Как вы можете видеть, они находятся в одном пакете и, в конечном итоге, попадают в одну и ту же банку, позвольте называть ее 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

Играя и экспериментируя, я смог избежать ошибки в three двумя способами:

  • изменение класса Foo для публичного, а не для пакета-private;

  • изменение пакета класса Something как "package1" (т.е. буквально такое же, как классы Foo и Bar, но физически разные - это класс Something, определенный в webapp);

  • принудительно загружает класс Foo перед выполнением кода нарушения:

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

Может ли кто-нибудь дать мне четкое, техническое объяснение, которое оправдывает проблему и приведенные выше результаты?

Обновление 1

Когда я попробовал третье решение, я фактически использовал commons.jar первого (тот, где класс Foo является общедоступным, а не private). Мне очень жаль.

Кроме того, как указано в одном из моих комментариев, я попытался зарегистрировать загрузчик классов класса Bar и Something прямо перед кодом нарушения, и результат для обоих был:

WebappClassLoader
context: my-web-app
delegate: false
----------> Parent Classloader:
[email protected]

Обновление 2

Хорошо, я наконец решил одну из тайн!

В одном из моих комментариев я сказал, что не смог реплицировать проблему, выполнив код нарушения с простого основного, созданного в другом пакете, чем Foo и Bar of 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>
  • если я выполняю "mvn clean package" (как Eclipse Run Configuration) и запускаю main из Eclipse, я получаю замечательный IllegalAccessError (cool!);

  • если я выполнил Maven → Проект обновления... и запустил main из Eclipse. Я не получаю никакой ошибки (не круто!).

Итак, я переключился на командную строку, и я подтвердил первый вариант: ошибка постоянно появляется независимо от того, находится ли код нарушения в webapp или в банке. Ницца!

Затем я смог еще больше упростить класс 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?

4b9b3361

Ответ 1

Я смог воспроизвести ту же самую проблему с компиляцией в Eclipse (Mars, 4.5.1) и из командной строки с помощью Maven (версия плагина Maven Compiler 3.5.1 последний на данный момент).

  • Компиляция и запуск main из Eclipse > Без ошибок
  • Компиляция из консоли /Maven и запуск main из Eclipse > Ошибка
  • Компиляция из консоли /Maven и запуск main через exec:java из консоли > Ошибка
  • Компиляция из Eclipse и запуск main через exec:java из консоли > Без ошибок
  • Компилирование из командной строки непосредственно с помощью javac (без Eclipse, no Maven, jdk-8u73) и запуск из командной строки непосредственно с помощью java > Ошибка

    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, описывающий подобную проблему и, прежде всего, упоминание о плагине Maven Compiler и Java:

    Это похоже на проблему, вызванную предоставленным плагином maven. Предоставленный плагин maven (в каталоге "plugin" ) добавляет "tools.jar" к ClassLoader.getSystemClassLoader(), и это вызывает проблему. Я действительно не вижу многого, что могло бы (или должно) быть сделано на стороне джавака, извините.

    Более подробно ToolProvider.getSystemJavaCompiler() рассмотрит ClassLoader.getSystemClassLoader(), чтобы найти классы javac. Если он не находит javac там, он пытается автоматически найти tools.jar и создает URLClassLoader для tools.jar, загружая javac с помощью этого загрузчика классов. Когда компиляция выполняется с использованием этого загрузчика классов, он загружает классы, используя этот загрузчик классов. Но тогда, когда плагины добавят tools.jar в ClassLoader.getSystemClassLoader(), классы начнут загружаться системным загрузчиком классов. И закрывается доступ к частному доступу при доступе к классу из одного пакета, но загружается другим загрузчиком классов, что приводит к вышеуказанной ошибке. Это усугубляется тем, что maven кэширует результаты ToolProvider.getSystemJavaCompiler(), благодаря которым запуск плагина между двумя компиляциями по-прежнему приводит к ошибке.

    (ПРИМЕЧАНИЕ: полужирный - мой)

  • Плагин Maven Compiler - Использование компиляторов Non-Javac, описывающий, как вы можете подключить другой компилятор к Maven Compiler 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>
    </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>

Исправлена ​​проблема, no IllegalAccessError для того же кода. Но при этом мы фактически удалили разницу между Maven и Eclipse в этом контексте (делая Maven с помощью компилятора Eclipse), поэтому это был обычный результат.

Таким образом, это приводит к следующим выводам:

  • В этом случае компилятор Eclipse Java отличается от Maven Java Compiler, ничего нового, но это еще одно подтверждение
  • У компилятора Maven Java в этом случае есть проблема, в то время как у компилятора Eclipse Java нет. Компилятор Maven совместим с компилятором JDK. Таким образом, на самом деле это может быть ошибка JDK, влияющая на компилятор Maven.
  • Создание Maven с использованием того же компилятора Eclipse устраняет проблему или скрывает ее.

Для справки, я пробовал также следующее без особого успеха, прежде чем переключиться на компилятор eclipse для Maven:

  • Изменение версии плагина Maven Compiler, каждая версия от 2,5 до 3.5.1
  • Попытка с JDK-8u25, JDK-8u60, JDK-8u73
  • Убедитесь, что Eclipse и Maven Compiler используют тот же javac, явно используя параметр executable

Подводя итог, JDK согласован с Maven, и это, скорее всего, ошибка. Ниже некоторых связанных сообщений об ошибках я обнаружил:

  • JDK-8029707: IllegalAccessError с использованием метода унаследованного пользовательского вызова. Исправлено как "Не исправить" (это была точно такая же проблема).
  • JDK-8141122: IllegalAccessException с использованием ссылки метода на пакет-частный класс через pub. Открыть (опять же, точно такая же проблема)
  • JDK-8143647: Ссылка на метод компиляции Javac, которая позволяет результаты в IllegalAccessError. Исправлено в Java 9 (аналогичная проблема, код pre-java-8 будет работать нормально, код стиля java-8 сломается)
  • JDK-8138667: java.lang.IllegalAccessError: попытался получить доступ к методу (для защищенный метод). Open (аналогичная проблема, компиляция отлично, но не ошибка времени выполнения для незаконного доступа к лямбда-коду).

Ответ 2

Если пакеты commons.jar и jar с пакетом2 загружаются другим загрузчиком классов, то это разные пакеты времени выполнения, и этот факт предотвращает доступ методов класса Something к членам пакета Foo. См. глава 5.4.4 спецификации JVM и этот удивительный вопрос.

Я думаю, что есть еще одно решение в дополнение к тому, что вы уже пробовали: переопределить метод getFoo в классе Bar