Ответ 1
После перехода на Java SE 8 Final Specification я могу ответить на мои вопросы.
(1) В ответ на мой первый вопрос
Есть ли способ аннотировать лямбда-выражение, подобное аннотации соответствующий анонимный класс, поэтому можно получить ожидаемый "Hello World" в примере выше?
Нет.
При аннотации Class Instance Creation Expression (§15.9)
анонимного типа аннотация будет сохранена в файле класса либо для расширяющегося интерфейса, либо для расширяющегося класса анонимного типа.
Для следующей анонимной аннотации интерфейса
Consumer<String> c = new @MyTypeAnnotation("Hello ") Consumer<String>() {
@Override
public void accept(String str) {
System.out.println(str);
}
};
аннотация типа может быть доступна во время выполнения, вызывая Class#getAnnotatedInterfaces()
:
MyTypeAnnotation a = c.getClass().getAnnotatedInterfaces()[0].getAnnotation(MyTypeAnnotation.class);
Если вы создаете анонимный класс с пустым телом, как это:
class MyClass implements Consumer<String>{
@Override
public void accept(String str) {
System.out.println(str);
}
}
Consumer<String> c = new @MyTypeAnnotation("Hello ") MyClass(){/*empty body!*/};
аннотация типа можно также получить во время выполнения, вызвав Class#getAnnotatedSuperclass()
:
MyTypeAnnotation a = c.getClass().getAnnotatedSuperclass().getAnnotation(MyTypeAnnotation.class);
Этот вид аннотаций типа не возможен для лямбда-выражений.
На стороне примечания такой вид аннотации также невозможен для обычных экземпляров создания экземпляра класса следующим образом:
Consumer<String> c = new @MyTypeAnnotation("Hello ") MyClass();
В этом случае аннотация типа будет храниться в структуре method_info метода, где это выражение произошло, а не как аннотации самого типа (или любой из его супер-типов).
Это различие важно, поскольку аннотации, хранящиеся в методе_info, не будут доступны во время выполнения API отражения Java. Если посмотреть на сгенерированный код байта с ASM, то разница выглядит следующим образом:
Тип Аннотации к созданию экземпляра анонимного интерфейса:
@Java8Example$MyTypeAnnotation(value="Hello ") : CLASS_EXTENDS 0, null
// access flags 0x0
INNERCLASS Java8Example$1
Тип Аннотации к созданию экземпляра обычного класса:
NEW Java8Example$MyClass
@Java8Example$MyTypeAnnotation(value="Hello ") : NEW, null
В первом случае аннотация связана с внутренним классом, во втором случае аннотация связана с выражением создания экземпляра внутри кода байтов методов.
(2) В ответ на комментарий от @assylias
Вы также можете попробовать (@MyTypeAnnotation ( "Hello" ) String s) → System.out.println(s), хотя мне не удалось получить доступ к значение аннотации...
Да, это действительно возможно в соответствии со спецификацией Java 8. Но в настоящее время невозможно получить аннотации типов формальных параметров лямбда-выражений через API отражения Java, что, скорее всего, связано с этой ошибкой JDK: Type Annotations Cleanup. Кроме того, компилятор Eclipse еще не сохраняет соответствующий атрибут Runtime [In] VisibleTypeAnnotations в файле класса - соответствующая ошибка найдена здесь: Имена и аннотации параметров Lambda не попадают в класс файлы.
(3) В ответ на мой второй вопрос
В примере, где я произвел лямбда-выражение и аннотировал casted type: есть ли способ получить этот экземпляр аннотации во время выполнения или такая аннотация всегда неявно ограничивается RetentionPolicy.SOURCE?
При аннотировании типа выраженного выражения эта информация также сохраняется в структуре method_info файла класса. То же самое верно для других возможных местоположений аннотаций типа внутри кода метода, такого как, например, if(c instanceof @MyTypeAnnotation Consumer)
. В настоящее время нет публичного API отражения Java для доступа к этим аннотациям кода. Но поскольку они хранятся в файле класса, по крайней мере потенциально возможно получить к ним доступ во время выполнения - например, путем чтения байтового кода класса с внешней библиотекой, например ASM.
На самом деле, мне удалось получить пример "Hello World", работающий с литым выражением типа
testTypeAnnotation(list,(@MyTypeAnnotation("Hello ") Consumer<String>) (p -> System.out.println(p)));
путем анализа байтового кода вызывающих методов с использованием ASM. Но код очень взломан и неэффективен, и, вероятно, в производственном коде нельзя делать что-то подобное. Во всяком случае, только для полноты, вот полный рабочий пример "Hello World":
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.TypePath;
import org.objectweb.asm.TypeReference;
public class Java8Example {
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE_USE)
public @interface MyTypeAnnotation {
public String value();
}
public static void main(String[] args) {
List<String> list = Arrays.asList("World!", "Type Annotations!");
testTypeAnnotation(list, new @MyTypeAnnotation("Hello ") Consumer<String>() {
@Override
public void accept(String str) {
System.out.println(str);
}
});
list = Arrays.asList("Type-Cast Annotations!");
testTypeAnnotation(list,(@MyTypeAnnotation("Hello ") Consumer<String>) (p -> System.out.println(p)));
}
public static void testTypeAnnotation(List<String> list, Consumer<String> consumer){
MyTypeAnnotation annotation = null;
for (AnnotatedType t : consumer.getClass().getAnnotatedInterfaces()) {
annotation = t.getAnnotation(MyTypeAnnotation.class);
if (annotation != null) {
break;
}
}
if (annotation == null) {
// search for annotated parameter instead
loop: for (Method method : consumer.getClass().getMethods()) {
for (AnnotatedType t : method.getAnnotatedParameterTypes()) {
annotation = t.getAnnotation(MyTypeAnnotation.class);
if (annotation != null) {
break loop;
}
}
}
}
if (annotation == null) {
annotation = findCastAnnotation();
}
for (String str : list) {
if (annotation != null) {
System.out.print(annotation.value());
}
consumer.accept(str);
}
}
private static MyTypeAnnotation findCastAnnotation() {
// foundException gets thrown, when the cast annotation is found or the search ends.
// The found annotation will then be stored at foundAnnotation[0]
final RuntimeException foundException = new RuntimeException();
MyTypeAnnotation[] foundAnnotation = new MyTypeAnnotation[1];
try {
// (1) find the calling method
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
StackTraceElement previous = null;
for (int i = 0; i < stackTraceElements.length; i++) {
if (stackTraceElements[i].getMethodName().equals("testTypeAnnotation")) {
previous = stackTraceElements[i+1];
}
}
if (previous == null) {
// shouldn't happen
return null;
}
final String callingClassName = previous.getClassName();
final String callingMethodName = previous.getMethodName();
final int callingLineNumber = previous.getLineNumber();
// (2) read and visit the calling class
ClassReader cr = new ClassReader(callingClassName);
cr.accept(new ClassVisitor(Opcodes.ASM5) {
@Override
public MethodVisitor visitMethod(int access, String name,String desc, String signature, String[] exceptions) {
if (name.equals(callingMethodName)) {
// (3) visit the calling method
return new MethodVisitor(Opcodes.ASM5) {
int lineNumber;
String type;
public void visitLineNumber(int line, Label start) {
this.lineNumber = line;
};
public void visitTypeInsn(int opcode, String type) {
if (opcode == Opcodes.CHECKCAST) {
this.type = type;
} else{
this.type = null;
}
};
public AnnotationVisitor visitInsnAnnotation(int typeRef, TypePath typePath, String desc, boolean visible) {
if (lineNumber == callingLineNumber) {
// (4) visit the annotation, if this is the calling line number AND the annotation is
// of type MyTypeAnnotation AND it was a cast expression to "java.util.function.Consumer"
if (desc.endsWith("Java8Example$MyTypeAnnotation;") && this.type != null && this.type.equals("java/util/function/Consumer")) {
TypeReference reference = new TypeReference(typeRef);
if (reference.getSort() == TypeReference.CAST) {
return new AnnotationVisitor(Opcodes.ASM5) {
public void visit(String name, final Object value) {
if (name.equals("value")) {
// Heureka! - we found the Cast Annotation
foundAnnotation[0] = new MyTypeAnnotation() {
@Override
public Class<? extends Annotation> annotationType() {
return MyTypeAnnotation.class;
}
@Override
public String value() {
return value.toString();
}
};
// stop search (Annotation found)
throw foundException;
}
};
};
}
}
} else if (lineNumber > callingLineNumber) {
// stop search (Annotation not found)
throw foundException;
}
return null;
};
};
}
return null;
}
}, 0);
} catch (Exception e) {
if (foundException == e) {
return foundAnnotation[0];
} else{
e.printStackTrace();
}
}
return null;
}
}