注解 Annotation

224 阅读9分钟

Java中的注解(Annotations)是在 JDK 1.5 中引入的,这次引入大大增强了Java语言的元数据功能,使开发者能够以更加简洁和灵活的方式向代码中添加元信息。

⨳ JDK 1.5:首次引入注解机制,包括常见的内置注解和元注解,支持定义自定义注解以及通过反射解析注解。

⨳ JDK 1.6:引入了注解处理器API,用于在编译期间处理注解。这允许开发者创建注解处理器来生成代码、进行验证等。

⨳ JDK 1.8:通过引入JSR 308(类型注解),注解可以用于任何使用类型的地方,如泛型类型参数、局部变量类型、类型转换等.

⨳ ...

元注解

元注解(Meta-Annotations)是用于修饰其他注解的注解。它们定义了自定义注解的行为和应用范围,控制注解的生命周期、目标、可重复性等。

@Retention 注解保留策略

@Retention 指定注解的保留策略,即注解在什么阶段被保留。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    /**
     * Returns the retention policy.
     * @return the retention policy
     */
    RetentionPolicy value();
}

@Retention 注解接受一个 RetentionPolicy 枚举值,来指定注解的保留策略。

RetentionPolicy.SOURCERetentionPolicy.CLASSRetentionPolicy.RUNTIME
注解只在源代码中保留,编译时会被丢弃,不会在字节码中存在注解在编译时保留在字节码中,但不会在运行时可见(默认值)注解在运行时保留,可以通过反射获取注解信息

@Target 注解应用目标

@Target 指定注解可以应用的目标(如类、方法、字段等)。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    /**
     * Returns an array of the kinds of elements an annotation interface
     * can be applied to.
     * @return an array of the kinds of elements an annotation interface
     * can be applied to
     */
    ElementType[] value();
}

@Target 接受一个 ElementType 枚举数组作为参数:

TYPEFIELDMETHODPARAMETERCONSTRUCTORLOCAL_VARIABLEANNOTATION_TYPEPACKAGETYPE_PARAMETERTYPE_USE
类、接口字段方法方法参数构造函数局部变量注解类型包声明泛型类型参数使用类型

其中 ElementType.TYPE_PARAMETERElementType.TYPE_USE 都是 JDK1.8 引入的,特别是ElementType.TYPE_USE 可以让注解应用于任何使用类型的地方,包括泛型、类型转换、实现类或接口等,几乎所有涉及类型的位置都可以使用。

@Inherited 注解是否可以被继承

@Inherited 用于指定某个注解类型是否可以被自动继承。如果一个注解被 @Inherited 修饰,那么当它被应用在一个类上时,这个注解会自动应用于该类的子类。需要注意的是,@Inherited 只对类有效,不适用于接口、方法、字段、参数等。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}

@Documented 注解是否包含在JavaDoc中

@Documented 是一个元注解,用于指定一个自定义注解是否会出现在生成的JavaDoc文档中。换句话说,如果一个注解类型被 @Documented 标记,那么在使用该注解的地方,其注解信息将会包含在JavaDoc生成的API文档中。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}

一些重要的注解(如 @Deprecated@Override)通常希望它们的使用能够被文档化,以便用户清楚地了解它们的影响和作用。

内置注解

Java 提供了一些内置注解(也称为标准注解),这些注解在 Java 语言中已经定义,并且可以直接用于代码中。内置注解通常用于编译时检查、编译器指示或提高代码可读性。

@Override 重写

@Override 表示一个方法是重写了父类或接口中的方法。如果该注解所标注的方法没有正确地重写父类或接口中的方法,编译器会报错。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

@Deprecated 废弃的

@Deprecated 表示某个类、方法、或字段已经不建议使用,可能会在未来版本中被移除。使用被标注为 @Deprecated 的代码时,编译器会发出警告。

@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;
}

@SuppressWarnings 压制警告

@SuppressWarnings 指示编译器忽略特定的警告信息。 参数是一个或多个警告类型的字符串,如 "unchecked""deprecation" 等。

@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    String[] value();
}

...

还有几个常用的内置就不一一举例了。

@SafeVarargs:Java 7 引入,表示使用了泛型可变参数的方法是类型安全的。只能用于 finalstatic 方法,或构造函数。

@FunctionalInterface:Java 8 引入,用于标识一个接口是函数式接口(即只包含一个抽象方法的接口)。如果接口中有多于一个的抽象方法,编译器会报错。

@Native:用于表示一个字段可以在本地代码(如C或C++)中使用,通常与 native 方法结合使用。

可以看到内置注解更多的是“说明性质”的,帮助开发者理解和维护代码,“功能性”不大。

自定义注解

看了这么多内置注解,也了解修饰其他注解的元注解,注解应该不难写出。

注解格式

创建自定义注解的基本步骤如下:

  1. 定义注解: 使用 @interface 关键字定义一个注解类型。
  2. 元注解: 结合使用元注解如 @Retention@Target@Documented@Inherited 来控制注解的行为和作用范围。
  3. 注解属性: 可以在注解中定义属性,类似于接口中的方法。

如下,创建了一个 Author 注解,可以标注在类上,也可以标志在方法上,用来说明是谁在那一天写的代码。

package com.cango.annotation;

import java.lang.annotation.*;

@Documented
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Author {
    String name();
    String date();
}

注意,一旦注解有属性了,使用注解的时候,属性必须有值,当然也可以使用默认值。

属性类型 属性名() default 默认值;

应用注解

应用注解很简单,加到想要加的位置即可,

package com.cango.annotation;

@Author(name = "Cango", date = "2024-08-08")
public class MyClass {
}

因为 @Author 有两个属性,所以需要使用 key=value 的形式给属性赋值,如果属性只有一个,直接写 value 即可。

@Author("Cango")

反射解析注解

还记得《反射篇》讲的 Class、Filed、Method、Constructor 都实现了注解元素 AnnotatedElement 吗?

通过它提供的统一处理注解的能力,很容易就可以在反射时获取注解,解析注解。

Class<MyClass> clazz = MyClass.class;
// 获取类上的注解
if(clazz.isAnnotationPresent(Author.class)){
    Author clazzAuthor = clazz.getAnnotation(Author.class);
    System.out.println(clazz.getSimpleName()+" 是 "+clazzAuthor.name()+" 写的,日期是 "+clazzAuthor.date());

}
// 获取方法上的注解
for (Method method : clazz.getDeclaredMethods()) {
    if (method.isAnnotationPresent(Author.class)) {
        Author methodAuth = method.getAnnotation(Author.class); 
        System.out.println(method.getName()+" 是 "+methodAuth.name()+" 写的,日期是 "+methodAuth.date());
    }
}

输出结果:

MyClassCango 写的,日期是 2024-08-08
sayHello 是 King 写的,日期是 2024-08-10

因为反射可以在程序运行期间获取注解,获取注解的属性值,那根据这些属性值进行不同的行为,是不是就特别方便了。

注解处理器

如果说反射解析注解是应用于程序运行期间,那注解处理器(Annotation Processor)就是应用于程序编译期间。

注解处理器通过扫描源代码中的注解,并根据注解信息生成新的代码或文件,执行编译时任务。

首先,定义一个自定义注解 @BuilderProperty ,用于根据类成员属性生成一个Builder类:

package com.cango.annotation.processor;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface BuilderProperty {
}

注意,Retention 是仅限于源代码中保留的 SOURCE。

自定义注解处理器

注解处理器基于JSR 269规范,通常实现javax.annotation.processing.AbstractProcessor类。

package com.cango.annotation.processor;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.ElementFilter;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.Writer;
import java.util.Set;

// 定义一个BuilderProcessor类,继承自AbstractProcessor
public class BuilderProcessor extends AbstractProcessor {

    // 重写process方法,该方法在注解处理时被调用
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // 遍历所有注解
        for (TypeElement annotation : annotations) {
            // 遍历所有被该注解标注的元素
            for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) {
                // 获取被注解标注的元素的类名
                String className = ((TypeElement) element.getEnclosingElement()).getQualifiedName().toString();
                // 生成Builder类的类名
                String builderClassName = className + "Builder";
                try {
                    // 创建Builder类的Java文件
                    JavaFileObject builderFile = processingEnv.getFiler().createSourceFile(builderClassName);
                    try (Writer writer = builderFile.openWriter()) {
                        // 写入包名
                        writer.write("package " + processingEnv.getElementUtils().getPackageOf(element).getQualifiedName() + ";\n");
                        // 写入Builder类的定义
                        writer.write("public class " + builderClassName.substring(builderClassName.lastIndexOf('.') + 1) + " {\n");

                        // 遍历被注解标注的类的所有字段
                        for (Element field : ElementFilter.fieldsIn(element.getEnclosingElement().getEnclosedElements())) {
                            // 如果字段被BuilderProperty注解标注
                            if (field.getAnnotation(BuilderProperty.class) != null) {
                                // 获取字段名和字段类型
                                String fieldName = field.getSimpleName().toString();
                                String fieldType = field.asType().toString();
                                // 写入字段定义
                                writer.write("    private " + fieldType + " " + fieldName + ";\n");
                                // 写入字段设置方法
                                writer.write("    public " + builderClassName.substring(builderClassName.lastIndexOf('.') + 1) + " " + fieldName + "(" + fieldType + " " + fieldName + ") {\n");
                                writer.write("        this." + fieldName + " = " + fieldName + ";\n");
                                writer.write("        return this;\n");
                                writer.write("    }\n");
                            }
                        }
                        // 写入Builder类的结束符
                        writer.write("}\n");
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return true;
    }
}

这个 BuilderProcessor会在编译期间扫描类中的@BuilderProperty注解,并生成一个Builder类。

注册注解处理器

创建文件 META-INF/services/javax.annotation.processing.Processor,并在其中添加注解处理器的完全限定名:

image.png

这不是不是很像 SPI(Service Provider Interface),文件名为接口,文件内容是实现类,然后通过 ServiceLoader 来加载。

但这就有个问题,SPI 一般用于发现已经编译好的类如数据库驱动 java.sql.Driver,如果 ServiceLoader 尝试加载注解处理器,但这些处理器尚未编译成功,就会导致编译错误。

那怎么办呢?将注解处理器先编译即可。

可以将注解处理器作为一个单独的 Module 来打包。也可以使用 Google 开源的一个小插件 auto-service 即可:

<dependency>
    <groupId>com.google.auto.service</groupId>
    <artifactId>auto-service</artifactId>
    <version>1.1.1</version> 
     <!-- 只在编译时需要 -->
    <scope>provided</scope>
</dependency>

auto-service 可以自动生成META-INF/services 的文件, 也就不需要创建配置文件了。因为编译的时候META-INF/services 还没有配置注解处理器,也就不会加载异常了。

这里采用第二种方式:

@AutoService(Processor.class)
public class BuilderProcessor extends AbstractProcessor 

使用注解处理器

随便创建一个 Person 类,使用 @BuilderProperty 标志在其属性上:

package com.cango.annotation.processor;

public class Person {
    @BuilderProperty
    private String name;
    @BuilderProperty
    private int age;

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

编译项目后,注解处理器将在编译时运行,生成相应的Builder类。以下是生成的PersonBuilder类:

public class PersonBuilder {
    private String name;
    private int age;

    public PersonBuilder name(String name) {
        this.name = name;
        return this;
    }

    public PersonBuilder age(int age) {
        this.age = age;
        return this;
    }
}

现在你应该知道 lombok 是怎么实现的了吧。

总结

使用注解不能局限于如内置注解的“标识”上,还可以简化代码和减少样板代码,如 Spring 框架中的依赖注入注解(@Autowired)可以消除繁琐的setter方法;如 @Transactional注解用于声明事务性操作,而不需要显式地编写事务管理代码。

当然这少不了反射的支持,有一点需要注意,反射和注解处理在运行时会带来一定的性能开销,尤其是在频繁使用反射的情况下。

当然还可以使用注解处理器在编译期间生成代码,这一点了解一下就行。