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 .