注:本文代码基于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";
    }
}

解释一下,Childaction方法重写了父类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多了两个属性:sinceforRemovalsince是一个字符串,表示是从哪个版本开始过时的;forRemoval是一个boolean值,表示将来是否会删除。

举个例子,Java 9Integer的一个构造方法就从版本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"})

注解内参数的类型不是什么都可以的,合法的类型有基本类型、StringClass、枚举、注解,以及这些类型的数组。

参数定义时可以使用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

四、查看注解信息

创建了注解,就可以在程序中使用,注解指定的目标,提供需要的参数,但这还是不会影响到程序的运行。要影响程序,我们要先能查看这些信息。我们主要考虑 @RetentionRetentionPolicy.RUNTIME的注解,利用反射机制在运行时进行查看和利用这些信息。

反射相关类ClassFieldMethodConstructor中都有与注解有关的方法,请看如下代码:

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

对于MethodContructor,它们都有方法参数,而参数也可以有注解,请看如下代码:

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、在运行时反射动态获取注解信息

DIDependency Injection)容器是指依赖注入容器,程序员一般不通过直接new创建对象,而是由容器管理对象的创建,对于依赖的服务,也不需要自己管理,而是使用注解表达依赖关系。这么做的好处有很多,代码更为简单,也更为灵活,比如容器可以根据配置返回一个动态代理,实现AOP

举个简单的DI容器例子,我们引入两个注解:一个是 @Inject,另一个是 @Singleton,先来看下 @Inject,它用来修饰类中字段,表达依赖关系,请看如下代码:

1
2
3
4
5
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {

}

定义两个服务ServiceAServiceBServiceA依赖于ServiceBServiceA使用 @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();

ServiceContainergetInstance方法会创建需要的对象,并配置依赖关系,请看如下代码:

 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