Перейти к основному содержимому

Почему отсутствующие аннотации не вызывают ClassNotFoundException

· 5 мин. чтения

1. Обзор

В этом руководстве мы познакомимся с, казалось бы, странной особенностью языка программирования Java: отсутствие аннотаций не вызовет никаких исключений во время выполнения.

Затем мы копнем глубже, чтобы увидеть, какие причины и правила управляют этим поведением и каковы исключения из таких правил.

2. Быстрое обновление

Начнем со знакомого примера Java. Есть класс A , а есть класс B , который зависит от A :

public class A {
}

public class B {
public static void main(String[] args) {
System.out.println(new A());
}
}

Теперь, если мы скомпилируем эти классы и запустим скомпилированный B , он напечатает для нас сообщение в консоли:

>> javac A.java
>> javac B.java
>> java B
A@d716361

Однако, если мы удалим скомпилированный файл A .class и повторно запустим класс B , мы увидим NoClassDefFoundError ` , вызванную ClassNotFoundException` :

>> rm A.class
>> java B
Exception in thread "main" java.lang.NoClassDefFoundError: A
at B.main(B.java:3)
Caused by: java.lang.ClassNotFoundException: A
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:606)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:168)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
... 1 more

Это происходит потому, что загрузчик классов не смог найти файл класса во время выполнения, хотя он был там во время компиляции . Это нормальное поведение, ожидаемое многими Java-разработчиками.

3. Отсутствующие аннотации

Теперь давайте посмотрим, что происходит с аннотациями при тех же обстоятельствах. Для этого мы изменим класс A на аннотацию:

@Retention(RetentionPolicy.RUNTIME)
public @interface A {
}

Как показано выше, Java сохранит информацию аннотаций во время выполнения. После этого пришло время аннотировать класс B с помощью A :

@A
public class B {
public static void main(String[] args) {
System.out.println("It worked!");
}
}

Далее скомпилируем и запустим эти классы:

>> javac A.java
>> javac B.java
>> java B
It worked!

Итак, мы видим, что B успешно выводит свое сообщение на консоль, что имеет смысл, так как все скомпилировано и соединено вместе очень хорошо.

Теперь давайте удалим файл класса для A :

>> rm A.class
>> java B
It worked!

Как показано выше, хотя файл класса аннотации отсутствует, аннотированный класс работает без каких-либо исключений .

3.1. Аннотация с токенами класса

Чтобы сделать его еще более интересным, давайте представим еще одну аннотацию с атрибутом Class<?> :

@Retention(RetentionPolicy.RUNTIME)
public @interface C {
Class<?> value();
}

Как показано выше, эта аннотация имеет атрибут с именем value с возвращаемым типом Class<?> . В качестве аргумента для этого атрибута давайте добавим еще один пустой класс с именем D :

public class D {
}

Теперь мы собираемся аннотировать класс B этой новой аннотацией:

@A
@C(D.class)
public class B {
public static void main(String[] args) {
System.out.println("It worked!");
}
}

Когда все файлы классов присутствуют, все должно работать нормально. Однако что произойдет, если мы удалим только файл класса D и не будем трогать остальные? Давай выясним:

>> rm D.class
>> java B
It worked!

Как показано выше, несмотря на отсутствие D во время выполнения, все еще работает! Следовательно, кроме аннотаций, во время выполнения также не требуется присутствия маркеров классов, на которые ссылаются, из атрибутов .

3.2. Спецификация языка Java

Итак, мы увидели, что некоторые аннотации с сохранением во время выполнения отсутствовали во время выполнения, но класс с аннотациями работал отлично. Как бы неожиданно это ни звучало, такое поведение на самом деле совершенно нормально в соответствии со спецификацией языка Java, §9.6.4.2 :

Аннотации могут присутствовать только в исходном коде или в бинарной форме класса или интерфейса. Аннотация, представленная в двоичной форме , может быть доступна или недоступна во время выполнения через библиотеки отражения платформы Java SE.

Кроме того, в записи JLS §13.5.7 также говорится:

Добавление или удаление аннотаций не влияет на правильную компоновку бинарных представлений программ на языке программирования Java.

Суть в том, что среда выполнения не генерирует исключений для отсутствующих аннотаций, потому что это позволяет JLS .

3.3. Доступ к отсутствующей аннотации

Давайте изменим класс B таким образом, чтобы он рефлексивно извлекал информацию A :

@A
public class B {
public static void main(String[] args) {
System.out.println(A.class.getSimpleName());
}
}

Если мы их скомпилируем и запустим, все будет хорошо:

>> javac A.java
>> javac B.java
>> java B
A

Теперь, если мы удалим файл класса A и запустим B , мы увидим тот же NoClassDefFoundError , вызванный ClassNotFoundException :

Exception in thread "main" java.lang.NoClassDefFoundError: A
at B.main(B.java:5)
Caused by: java.lang.ClassNotFoundException: A
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:606)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:168)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
... 1 more

Согласно JLS, аннотация не обязательно должна быть доступна во время выполнения. Однако, когда какой-то другой код читает эту аннотацию и что-то с ней делает (например, то, что сделали мы), аннотация должна присутствовать во время выполнения . В противном случае мы бы увидели ClassNotFoundException .

4. Вывод

В этой статье мы увидели, как некоторые аннотации могут отсутствовать во время выполнения, даже если они являются частью двоичного представления класса.

Как обычно, все примеры доступны на GitHub .