在 Java 中,注解(Annotation)是一种元数据形式,它为程序元素(如类、方法、变量等)添加额外的信息,它可以被编译器或运行时环境读取和处理,以实现各种功能。
Java8 对注解处理提供了两点改进:
(1)可重复的注解
(2)可用于类型的注解
在 Java8 之前,注解只能用来标注方法和字段。例如:
// 类初始化时调用 @PostConstruct public void initData() { //... } // 在构造函数之后调用 @Resource("jdbc:derby:sample") private Connection conn;
并且,在同一个元素上同一个注解不能重复使用,例如:
// 在构造函数之后调用 @Resource("jdbc:derby:sample") @Resource("jdbc:derby:sample2") // 非法的 private Connection conn;
但是,我们可以在同一元素上应用不同的注解是可以的,例如:
@JsonIgnore @Resource("jdbc:derby:sample") // ok private Connection conn;
很快,涌现了越来越多使用注解的地方,从而导致了一些不得不需要重复使用相同注解的情况。例如,要表示数据库中的一个复合主键,你需要指定多列:
@Entity @PrimaryKeyJoinColumn(name="ID"), @PrimaryKeyJoinColumn(name="REGION") public class Item { //... }
由于这是不可能做到的,所以这些注解只能被包装到一个父容器注解中,例如:
@Entity // 父容器注解 @PrimaryKeyJoinColumns({ // 子注解 @PrimaryKeyJoinColumn(name="ID"), @PrimaryKeyJoinColumn(name="REGION") }) public class Item { //... }
幸运的是,在 Java8 之后再也不用编写这样丑陋的代码了,可以这样使用:
@Entity @PrimaryKeyJoinColumn(name="ID"), @PrimaryKeyJoinColumn(name="REGION") public class Item { //... }
如果你仅仅想使用重复注解,并且你的框架已经支持重复注解,则知道上述注解知识已经可以了。
但是,对于一个框架开发人员,重复注解的知识点要稍微复杂一点。毕竟,AnnotatedElement 接口有一个方法
会获取类型为 T 的注解(如果有的话)。那么对于拥有同一类型的多个注解来说,该方法应该如何处理呢? 只返回第一个注解? 那样可能会给遗留代码带来意想不到的行为。要解决这个问题,可重复注解必须做到如下两点:
(1)将注解标注为。
(2)提供一个容器注解。
例如:
package com.hxstrive.jdk8.annotation; import java.lang.annotation.Annotation; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Method; /** * JDK8 注解增强 * @author hxstrive.com */ public class AnnotationDemo2 { // 容器注解 @Retention(RetentionPolicy.RUNTIME) static @interface MyTests { // 用来放在容器注解的子注解 AnnotationDemo2.MyTest[] value(); } // 自定义注解 @Repeatable(MyTests.class) // 指定该注解的容器注解 @Retention(RetentionPolicy.RUNTIME) static @interface MyTest { String value() default ""; } @MyTest("value1") @MyTest("value2") public void test() { System.out.println("test"); } public static void main(String[] args) throws Exception { Class<AnnotationDemo2> clazz = AnnotationDemo2.class; Method method = clazz.getMethod("test"); Annotation[] annotations = method.getAnnotations(); for(Annotation annotation : annotations) { System.out.println(annotation); //@com.hxstrive.jdk8.annotation.AnnotationDemo2$MyTests(value=[ // @com.hxstrive.jdk8.annotation.AnnotationDemo2$MyTest(value=value1), // @com.hxstrive.jdk8.annotation.AnnotationDemo2$MyTest(value=value2) //]) } MyTest myTest = method.getAnnotation(MyTest.class); System.out.println(myTest); // null MyTest[] myTests = method.getAnnotationsByType(MyTest.class); for(MyTest my : myTests) { System.out.println(my); } //@com.hxstrive.jdk8.annotation.AnnotationDemo2$MyTest(value=value1) //@com.hxstrive.jdk8.annotation.AnnotationDemo2$MyTest(value=value2) } }
上面示例中,当用户同时提供两个或更多注解时,它们会被自动包装为一个注解。
当在 test() 方法的反射 Method 对象上调用 method.getAnnotation(MyTest.class) 时,会返回 null。这是因为该元素实际上被标注为容器注解 MyTests。
当你实现一个处理可重复注解的方法时,会发现使用 getAnnotationsByType 方法更方便,method.getAnnotationsByType(MyTest.class) 会返回一个包含 MyTest 注解的数组。
在 Java8 之前,注解只能被标注在一个声明上。声明是用来定义某个新名称的代码段。以下是一些声明的例子:
@Entity public class Person { //... } @SuppressWarnings("unchecked") List<Person> people = query.getResultList();
在 Java8 中,你可以在任何类型上标注注解。这对于结合使用检查常见错误的工具非常有用。一个常见的错误是,由于开发人员没有考虑到一个引用可能是 null, 从而抛出了一个 NullPointerException 异常。现在假设你在永远不希望为 null 的变量上标注了 @NonNull 注解,那么工具就可以检查出下面代码是正确的:
private @NonNull List<String> names = new ArrayList<>(); //... names.add("Fred"); // 不可能出现 NullPointerException 异常
当然,工具还应该检测出任何可能会导致 names 变为 null 的语句:
names = null; // 空指针检查程序会将该语句标记为一个错误 names = readNames(); // 如果 readNames 返回一个 @NonNull 字符串,则没问题
到处编写这样的注解似乎很枯燥乏味,但是在实际中,这些工作可以被一些简单的假设来代替,如:Checker 框架。
在上面的例子中,names 变量被声明为 @NonNull。这个注解可能在 Java8 之前就存在了,但是如何表示列表中的元素应该是非 null 的呢? 从逻辑上讲,应该这样表示:
private List<@NonNull String> names;
现在,这类注解可以在 Java8 中合法使用了。
在 JDK8 中,可以通过反射得到参数的名称了,它可以减少注解中的重复代码。以一个普通的 JAX-RS 方法为例:
public Person getEmployee(@PathParam("dept") Long dept, @QueryParam("id") Long id)
在大多数情况下,方法参数的名称都与注解参数相同,或者我们可以将它们刻意统一起来。如果注解处理方法可以读取方法参数的名称,那么我们只需要编写如下代码:
public Person getEmployee(@PathParam Long dept, @QueryParam Long id)
Java8 新提供的类 java.lang.reflect.Parameter 已经使其成为现实。不幸的是,为了获取类文件中的必需信息,你需要使用 javac -parameters SourceFile.java 的方式来编译源代码,例如:
package com.hxstrive.jdk8.reflect; import java.lang.reflect.Method; import java.lang.reflect.Parameter; public class ReflectDemo1 { public void show(String msg) { System.out.println(msg); } public static void main(String[] args) throws Exception { Class<ReflectDemo1> clazz = ReflectDemo1.class; Method show = clazz.getMethod("show", String.class); Parameter[] parameters = show.getParameters(); for(Parameter parameter : parameters) { System.out.println("method: " + show.getName() + ", args: " + parameter.getName()); } } }
使用 javac -parameters -d ./target/classes ./src/main/java/com/hxstrive/jdk8/reflect/ReflectDemo1.java 命令编译上面代码。
然后使用 java com.hxstrive.jdk8.reflect.ReflectDemo1 命令运行示例,如下:
D:\demo_jdk8> javac -parameters -d ./target/classes ./src/main/java/com/hxstrive/jdk8/reflect/ReflectDemo1.java D:\demo_jdk8> cd .\target\classes\ D:\demo_jdk8\target\classes> java com.hxstrive.jdk8.reflect.ReflectDemo1 method: show, args: msg
从输出可以得知,成功获取了 show 方法的 msg 参数命。