Может ли тип аннотации определять статические методы? - PullRequest
0 голосов
/ 12 июня 2018

Я разработал фреймворк и соответствующий API, который включает в себя видимую во время выполнения аннотацию.API также предоставляет некоторые вспомогательные методы, предназначенные для использования клиентом на объектах, классы которых имеют эту аннотацию.Понятно, что помощники тесно связаны с аннотацией, но важно, чтобы их внутренние компоненты были инкапсулированы от клиента.Вспомогательные методы в настоящее время предоставляются через статический внутренний класс внутри типа аннотации ...

@Target(TYPE)
@Retention(RUNTIME)
public @interface MyAnnotation {
   // ... annotation elements, e.g. `int xyz();` ...

   public static final class Introspection {
       public static Foo helper(Object mightHaveMyAnnotation) {
           /* ... uses MyAnnotation.xyz() if annotation is present ... */
      }
   }
}

... но помощники могут так же легко существовать в каком-либо другом служебном классе верхнего уровня.В любом случае обеспечивается необходимый объем инкапсуляции из клиентского кода, но оба несут дополнительные расходы на поддержание совершенно отдельного типа, предотвращают их создание, поскольку все полезные методы являются статическими и т. Д.

Когда Java 8 ввела статические методы вТипы интерфейса Java (см. JLS 9.4 ), эта функция рекламировалась как обеспечивающая возможность ...

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

- из Java Tutorials Методы интерфейса по умолчанию

Это использовалось в библиотеках JDK для предоставления реализаций, таких как List.of(...), Set.of(...) и т. Д., Тогда как ранее такие методы были отнесены к отдельному служебному классу, например java.util.Collections.Размещая служебные методы в связанных с ними интерфейсах, он улучшает их обнаруживаемость и удаляет, возможно, ненужные типы вспомогательных классов из домена API.

Поскольку я являюсь текущей JVM представление байт-кода для типов аннотаций очень тесно связано с обычными интерфейсами , мне было интересно, будут ли аннотации также поддерживать статические методы.Когда я переместил помощников в тип аннотации, такой как:

@Target(TYPE)
@Retention(RUNTIME)
public @interface MyAnnotation {
   // ... annotation elements ...

   public static Foo helper(Object mightHaveMyAnnotation) { /* ... */ }
}

... Я был немного удивлен, что javac пожаловался на следующие ошибки во время компиляции:

Среда выполнения OpenJDK 18.3 (сборка 10 + 46)

  • Статический модификатор здесь не разрешен
  • элементы в объявлениях типа аннотации не могут объявлять формальные параметры
  • интерфейс абстрактных методов не может иметь тела

Очевидно, что язык Java в настоящее время не позволяет этого.Возможно, существуют веские причины для его запрета или, как ранее предполагалось для статических методов интерфейса , «не было веских причин для этого; согласованность недостаточно убедительна для изменения статусаquo ".

В частности, , а не цель этого вопроса -" , почему не работает? "или " должен язык поддерживать его?", чтобы избежать основанных на мнении ответов.

JVM - это мощная технология, которая во многих отношениях более гибкая, чем то, что допускаетсяЯзык Java.В то же время язык Java продолжает развиваться, и сегодняшний ответ может устареть завтра.С пониманием того, что такая мощность должна использоваться с большой осторожностью ...

Технически возможно ли инкапсулировать статическое поведение непосредственно в типе аннотации и как?

1 Ответ

0 голосов
/ 12 июня 2018

Технически возможно сделать это в JVM и взаимодействовать со стандартным кодом Java, но он имеет важные предостережения:

  1. Java-совместимый исходный код, согласноJLS, не может определять статические методы в типах аннотаций.
  2. Исходный код Java может использовать такие методы , если они существуют , в том числе при компиляции-время и во время выполнения с помощью отражения.
  3. Возможно, тематическую аннотацию необходимо поместить в отдельный модуль компиляции, чтобы ее двоичный класс был доступен для IDE и javac при обработке кода.
  4. Это было проверено на OpenJDK 10 HotSpot, но существует вероятность того, что наблюдаемое поведение может зависеть от внутренних деталей, которые могут быть изменены в более поздних выпусках.
  5. Тщательно рассмотрите влияние на долгосрочное обслуживание исовместимость, прежде чем принять этот подход.

Проверка концепции прошла успешно с использованием механизма, которыйнапрямую манипулирует байт-кодом JVM.

Механизм достаточно прост.Используйте альтернативный язык или инструмент манипулирования байт-кодом (например, ASM), который создаст файл JVM *.class, который (1) соответствует функции и внешнему виду аннотации legal Java (language), и(2) также содержит требуемую реализацию метода с набором модификаторов доступа static.Этот файл класса может быть скомпилирован отдельно и упакован в JAR-файл или непосредственно помещен в путь к классам, после чего он станет доступным для вашего другого нормального Java-кода.

Следующие шаги создадут рабочий байт-код, соответствующий следующему не совсем легально Тип аннотации Java, который определяет простую strlen статическую функцию для простоты в POC:

@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {

    String value();

    // not legal in Java, through at least JDK 10:
    public static int strlen(java.lang.String str) {
        return str.length(); // boring!
    }
}

Сначала установите класс аннотации с "нормальным"«value() параметр в виде строки без значения по умолчанию:

import static org.objectweb.asm.Opcodes.*;
import java.util.*;
import org.objectweb.asm.*;
import org.objectweb.asm.tree.*;

/* ... */

final String fqcn = "com.example.MyAnnotation";
final String methodName = "strlen";
final String methodDesc = "(Ljava/lang/String;)I"; // int function(String)

ClassNode cn = new ClassNode(ASM6);
cn.version = V1_8; // Java 8
cn.access = ACC_SYNTHETIC | ACC_PUBLIC | ACC_INTERFACE | ACC_ABSTRACT | ACC_ANNOTATION;
cn.name = fqcn.replace(".", "/");
cn.superName = "java/lang/Object";
cn.interfaces = Arrays.asList("java/lang/annotation/Annotation");

// String value();
cn.methods.add(
    new MethodNode(
        ASM6, ACC_PUBLIC | ACC_ABSTRACT, "value", "()Ljava.lang.String;", null, null));

При необходимости добавьте аннотацию с помощью @Retention(RUNTIME), если необходимо:

AnnotationNode runtimeRetention = new AnnotationNode(ASM6, "Ljava/lang/annotation/Retention;");
runtimeRetention.values = Arrays.asList(
    "value", // parameter name; related value follows immediately next:
    new String[] { "Ljava/lang/annotation/RetentionPolicy;", "RUNTIME" } // enum type & value
);
cn.visibleAnnotations = Arrays.asList(runtimeRetention);

Далее добавьте желаемое static method:

MethodNode method = new MethodNode(ASM6, 0, methodName, methodDesc, null, null);
method.access = ACC_PUBLIC | ACC_STATIC;
method.annotationDefault = Integer.MIN_VALUE; // see notes
AbstractInsnNode invokeStringLength =
    new MethodInsnNode(INVOKEVIRTUAL, "java/lang/String", "length", "()I", false);
method.instructions.add(new IntInsnNode(ALOAD, 0)); // push String method arg
method.instructions.add(invokeStringLength);        // invoke .length()
method.instructions.add(new InsnNode(IRETURN));     // return an int value
method.maxLocals = 1;
method.maxStack = 1;
cn.methods.add(method);

Наконец, выведите байт-код JVM для этой аннотации в файл *.class на пути к классам или загрузите его непосредственно в память с помощью пользовательского ClassLoader (не показан):

ClassWriter cw = new ClassWriter(0);
cn.accept(cw);
byte[] bytecode = cw.toByteArray();

Примечания:

  1. Это требует генерации байт-кода версии 52 (Java 8) или новее и будет работать только в JVM, поддерживающих эту версию.
  2. Аннотации имеют java.lang.Object в качестве своего супертипа, и они реализуют java.lang.annotation.Annotation интерфейс.
  3. Два null параметра для конструкторов MethodNode предназначены для обобщенных типов и объявленыисключения, ни один из них не используется в этом примере.
  4. HotJpot OpenJDK 10 требуется установка MethodNode.annotationDefault в ненулевое значение (соответствующего типа) в статическом методе, даже если установка /переопределение strlen никогда не будет возможным, если применяет аннотацию к другому элементу.Это серая область для этого подхода, являющегося «легальным».Похоже, что верификатор байт-кода HS игнорирует флаг ACC_STATIC и предполагает, что все определенные методы являются обычными элементами аннотаций.
...