在 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 参数命。