注:本文代码基于JDK 11
一、概述
在Java中,注解就是给程序添加一些信息,用字符@开头,这些信息用于修饰它后面紧挨着的其它代码元素,比如类、接口、字段、方法、方法中的参数、构造方法等等,注解可以被编译器、程序运行时和其它工具使用,用于增强或修改程序行为等等。
二、内置注解
2.1、@Override
@Override修饰一个方法,表示该方法不是当前类首先声明的,而是在某个父类或实现的接口中声明的,当前类“重写”了该方法。举个例子,请看如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class Base {
public void action() {
}
}
class Child extends Base {
@Override
public void action(){
System.out.println("child action");
}
@Override
public String toString() {
return "child";
}
}
|
解释一下,Child的action方法重写了父类Base中的action方法,toString方法重写了Object类中的toString方法。
这个注解不写也不会改变这些方法是“重写”的本质,它的作用在于减少一些编程错误,如果方法有Override注解,但没有任何父类或实现的接口声明该方法,则编译器会报错,强制程序员修复该问题。
2.2、@Deprecated
@Deprecated可以修饰的范围很广,包括类、方法、字段、参数等等,它表示对应的代码已经过时了,程序员不应该使用它,不过它是一种警告而不是强制性的,在IDE中会给Deprecated元素加一条删除线以示警告,举个例子,Date中很多方法就过时了,请看如下代码:
1
2
3
4
5
|
@Deprecated
public int getYear()
@Deprecated
public int getMonth()
|
在声明元素为 @Deprecated时,应该用Java文档注释的方式同时说明替代方案,就像Date中的API文档那样,在调用 @Deprecated方法时,应该先考虑其建议的替代方案。
从Java 9开始,@Deprecated多了两个属性:since和forRemoval。since是一个字符串,表示是从哪个版本开始过时的;forRemoval是一个boolean值,表示将来是否会删除。
举个例子,Java 9中Integer的一个构造方法就从版本9开始过时了,请看如下代码:
1
2
3
4
|
@Deprecated(since="9")
public Integer(int value) {
this.value = value;
}
|
2.3、@SuppressWarnings
@SuppressWarnings表示压制Java的编译警告,它有一个必填参数,表示压制哪种类型的警告,它也可以修饰大部分代码元素,在更大范围的修饰也会对内部元素起效,比如在类上的注解会影响到方法,在方法上的注解会影响到代码行,举个例子,调用Date的过期方法,可以这样压制警告,请看如下代码:
1
2
3
4
5
|
@SuppressWarnings({"deprecation", "MagicConstant"})
public static void main(String[] args) {
Date date = new Date(2022, 9, 23);
int year = date.getYear();
}
|
三、创建注解
先学习下注解是怎么定义的,内置注解 @Override就是一个很好的例子,请看如下代码:
1
2
3
4
|
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
|
定义注解与定义接口有点类似,都用了interface,不过注解的interface前多了一个@。另外,它还有两个元注解 @Target和 @Retention,这两个注解专门用于定义注解本身。
@Target表示注解的目标,ElementType是一个枚举,主要可选值如下表所示。
可选值 |
说明 |
TYPE |
类、接口(包括注解),或者枚举 |
FIELD |
字段(包括枚举常量) |
METHOD |
方法 |
PARAMETER |
形式参数 |
CONSTRUCTOR |
构造函数 |
LOCAL_VARIABLE |
本地变量 |
ANNOTATION_TYPE |
注解 |
PACKAGE |
包 |
TYPE_PARAMETER |
类型参数(java 1.8引入) |
TYPE_USE |
使用参数(java 1.8引入) |
MODULE |
模块(java 9引入) |
目标可以有多个,用{}表示,比如 @SuppressWarnings的 @Target就有多个,请看如下代码:
1
2
3
4
|
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE}) @Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
|
如果没有声明 @Target,默认为适用于所有类型。
@Retention表示注解信息保留到什么时候,取值只能有一个,类型为RetentionPolicy,它是一个枚举,主要可选值如下表所示。
可选值 |
说明 |
SOURCE |
只在源代码中保留,编译器将代码编译为字节码文件后就会丢掉 |
CLASS |
保留到字节码文件中,但Java虚拟机将class文件加载到内存时不一定会在内存中保留 |
RUNTIME |
一直保留到运行时 |
如果没有声明 @Retention,则默认为CLASS。
@Override和 @SuppressWarnings都是给编译器用的, 所以 @Retention都是Retention-Policy.SOURCE。
可以为注解定义一些参数,定义的方式是在注解内定义一些方法,比如 @SuppressWarnings内定义的方法value,返回值类型表示参数的类型,这里是String[]。使用 @SuppressWarnings时必须给value提供值,请看如下代码:
1
|
@SuppressWarnings(value = {"deprecation", "MagicConstant"})
|
当只有一个参数,且名称为value时,提供参数值时可以省略"value=",请看如下代码:
1
|
@SuppressWarnings({"deprecation", "MagicConstant"})
|
注解内参数的类型不是什么都可以的,合法的类型有基本类型、String、Class、枚举、注解,以及这些类型的数组。
参数定义时可以使用default指定一个默认值,比如 @Deprecated中的forRemoval(),默认值为false。如果类型为String,默认值可以为"",但不能为null,请看如下代码:
1
2
3
4
5
6
7
|
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE})
public @interface Deprecated {
String since() default "";
boolean forRemoval() default false;
}
|
@Deprecated多了一个元注解 @Documented,它表示注解信息包含到生成的文档中。
与接口和类不同,注解不能继承。不过注解有一个与继承有关的元注解 @Inherited,请看如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
}
@Test
public class Base {
}
public class Child extends Base {
}
public static void main(String[] args) {
// 如果此元素上存在指定类型的注解,则返回true,否则返回false
// 该方法返回的真值相当于:getAnnotation(annotationClass) != null
System.out.println(Child.class.isAnnotationPresent(Test.class));
}
|
Test是一个注解,类Base有该注解,Child继承了Base但没有声明该注解。main方法检查Child类是否有Test注解,输出为true,这是因为Test有注解@Inherited,如果去掉,输出会变成false。
四、查看注解信息
创建了注解,就可以在程序中使用,注解指定的目标,提供需要的参数,但这还是不会影响到程序的运行。要影响程序,我们要先能查看这些信息。我们主要考虑 @Retention为RetentionPolicy.RUNTIME的注解,利用反射机制在运行时进行查看和利用这些信息。
反射相关类Class、Field、Method、Constructor中都有与注解有关的方法,请看如下代码:
1
2
3
4
5
6
7
|
// 如果存在指定类型的该元素的注解,则返回该元素的注解,否则返回null
<T extends Annotation> T getAnnotation(Class<T> annotationClass);
// 返回直接出现在该元素上的注解。此方法忽略继承的注解
Annotation[] getDeclaredAnnotations();
// 方法名带有Annotation的还有很多,请自行查阅...
|
Annotation是一个接口,它表示注解,请看如下代码:
1
2
3
4
5
6
7
|
public interface Annotation {
boolean equals(Object obj);
int hashCode();
String toString();
// 返回真正的注解类型
Class<? extends Annotation> annotationType();
}
|
实际上内部实现时,所有的注解类型都是扩展的Annotation。
对于Method和Contructor,它们都有方法参数,而参数也可以有注解,请看如下代码:
1
|
public Annotation[][] getParameterAnnotations()
|
返回值是一个二维数组,每个参数对应一个一维数组。举个例子,请看如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
int value() default 0;
}
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test2 {
String value() default "";
}
public class Child {
public void getUser(@Test(26) int age, @Test2("MinKin") String name) {
}
}
public static void main(String[] args) {
try {
Method method = Child.class.getMethod("getUser", int.class, String.class);
Annotation[][] annotations = method.getParameterAnnotations();
for (Annotation[] value : annotations) {
for (Annotation annotation : value) {
if (annotation instanceof Test) {
Test test = (Test) annotation;
System.out.println(test.annotationType().getSimpleName() + "; " + test.value());
}else if (annotation instanceof Test2) {
Test2 test2 = (Test2) annotation;
System.out.println(test2.annotationType().getSimpleName() + "; " + test2.value());
}
}
}
}catch(NoSuchMethodException e){
e.printStackTrace();
}
}
|
五、注解的应用
5.1、在运行时反射动态获取注解信息
DI(Dependency Injection)容器是指依赖注入容器,程序员一般不通过直接new创建对象,而是由容器管理对象的创建,对于依赖的服务,也不需要自己管理,而是使用注解表达依赖关系。这么做的好处有很多,代码更为简单,也更为灵活,比如容器可以根据配置返回一个动态代理,实现AOP。
举个简单的DI容器例子,我们引入两个注解:一个是 @Inject,另一个是 @Singleton,先来看下 @Inject,它用来修饰类中字段,表达依赖关系,请看如下代码:
1
2
3
4
5
|
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
}
|
定义两个服务ServiceA与ServiceB,ServiceA依赖于ServiceB,ServiceA使用 @Inject表达对ServiceB的依赖,请看如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class ServiceA {
@Inject
ServiceB serviceB;
public void call() {
serviceB.action();
}
}
public class ServiceB {
public void action() {
System.out.println("ServiceB action");
}
}
|
DI容器的类为ServiceContainer,提供一个方法获取对象的实例,而不是让对象自己new,请看如下代码:
1
|
public static <T> T getInstance(Class<T> cls)
|
ServiceContainer的使用方法,请看如下代码:
1
2
|
ServiceA serviceA = ServiceContainer.getInstance(ServiceA.class);
serviceA.call();
|
ServiceContainer的getInstance方法会创建需要的对象,并配置依赖关系,请看如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public class ServiceContainer {
public static <T> T getInstance(Class<T> cls) {
try {
T t = cls.getDeclaredConstructor().newInstance();
Field[] fields = cls.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Inject.class)) {
if (!field.canAccess(t)) {
field.setAccessible(true);
}
Class<?> fieldCls = field.getType();
field.set(t, getInstance(fieldCls));
}
}
return t;
}catch (InstantiationException | IllegalAccessException |InvocationTargetException |NoSuchMethodException e) {
e.printStackTrace();
}
return null;
}
}
|
上面的代码假定每个类型都有一个public默认构造方法,使用它创建对象,然后查看每个字段,如果有 @Inject注解,就根据字段类型获取该类型的实例,并设置字段的值。
@Singleton用于修饰类,表示类型是单例,请看如下代码:
1
2
3
4
5
|
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Singleton {
}
|
使用 @Singleton来修饰ServiceB,请看如下代码:
1
2
3
4
5
6
|
@Singleton
public class ServiceB {
public void action() {
System.out.println("ServiceB action");
}
}
|
ServiceContainer也需要做修改,首先增加一个静态变量,用来缓存创建过的单例对象,请看如下代码:
1
|
private static Map<Class<?>, Object> instances = new ConcurrentHashMap<>();
|
所以getInstance方法也需要做修改,请看如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public static <T> T getInstance(Class<T> cls) {
try {
boolean singleton = cls.isAnnotationPresent(Singleton.class);
if(!singleton) {
return createInstance(cls);
}
Object obj = instances.get(cls);
if(obj != null) {
return (T) obj;
}
synchronized (cls) {
obj = instances.get(cls);
if(obj == null) {
obj = createInstance(cls);
instances.put(cls, obj);
}
}
return (T) obj;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
|
首先检查类型是否是单例,如果不是就直接调用createInstance创建对象。否则检查缓存,如果有则直接返回,如果没有则调用createInstance创建对象,并放入缓存中。来看下createInstance的实现,请看如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
private static <T> T createInstance(Class<T> cls) throws Exception {
T obj = cls.newInstance();
Field[] fields = cls.getDeclaredFields();
for(Field f : fields) {
if(f.isAnnotationPresent(SimpleInject.class)) {
if(!f.canAccess(obj)) {
f.setAccessible(true);
}
Class<?> fieldCls = f.getType();
f.set(obj, getInstance(fieldCls));
}
}
return obj;
}
|
5.2、APT编译时注解处理器
虽然ButterKnife已经过时了,但仍不失为一个学习APT编译时注解处理器的例子,所以接下来会手写一个简单版的ButterKnife。与此同时,将学习Javac的源码,深入理解注解处理器相关方法的执行过程。
这个分析过程本文就省略了,学习成果在下面地址,可自行学习。
https://github.com/AndDevMK/AnalysisButterKnife