Java-注解详解


1️⃣ 注解的含义

注解(Annotation)是Java语言中一种特殊的修饰符,它可以用于类、方法、参数、变量、构造器以及包声明中,用于为Java代码提供元数据。相对于其他修饰符如publicfinal等,注解并不直接影响代码的语义,但却能被某些工具软件(如编译器、框架)所读取和利用。

注解的主要作用

  1. 编译检查 - 如@Override放在方法前,如果该方法不是重写父类的方法,则编译器会发出警告。
  2. 代码分析 - 通过代码里标识的注解,程序可以在编译时进行一些基于注解的处理。注解信息和JAVA的反射功能在一起时会使得程序的功能更加强大。
  3. 编译时动态处理 - 如常见的Java框架Spring、Hibernate、JUnit等,会在编译时读取注解的信息,然后根据注解的信息进行一些其他处理。
  4. 生成额外的文件 - 如Javadoc工具会根据源码中的注解来生成API文档。

注解的基本语法

注解的声明类似于接口的声明,但是前面多了一个@符号:

public @interface MyAnnotation {
    String value() default "";  //定义value属性
}

使用注解:

@MyAnnotation(value="This is my custom annotation")
public class MyClass {
    // class body
}

内置注解

Java提供了一些预定义的注解,如:

  • @Override: 限定重写父类方法。
  • @Deprecated: 表示某个程序元素(如方法)已经过时。
  • @SuppressWarnings: 告诉编译器忽略指定的警告。

元注解

用于注解其他注解的注解称为元注解。Java提供了以下几种元注解:

  • @Target: 表明该注解可以被应用于什么地方(如方法、类、字段等)。
  • @Retention: 表明该注解的生命周期(仅源代码、编译期、运行期)。
  • @Documented: 表示使用该注解的元素应被Javadoc或其他工具文档化。
  • @Inherited: 表示该注解可以被子类继承。

注解为Java提供了一种元级编程的方式,它允许开发者在不修改代码逻辑的前提下,向源码中添加一些额外的信息,这些信息可以被编译器或其他工具所读取并使用。


2️⃣ 注解分类

  • 由编译器使用的注解

    • 含义:这类注解不会被编译进入 .class 文件,在编译后它们就被编译器丢弃了。
    • 示例:
      • @Override:此注解让编译器检查该方法是否正确地实现了覆写。
      • @SuppressWarnings:此注解告诉编译器忽略此处代码产生的警告。
  • 由工具处理 .class 文件使用的注解

    • 含义:有些工具在加载 class 的时候,会对 class 文件做动态修改,以实现一些特殊的功能。这类注解会被编译进入 .class 文件,但在加载完成后并不会存在于内存中。这类注解主要被一些底层库使用,一般我们不需要自己处理。
    • 示例:可以参考 lombok
  • 在程序运行期能够读取的注解

    • 含义:这些注解在加载后会一直存在于 JVM 中,是最常用的注解。
    • 示例:配置了 @PostConstruct 的方法会在调用构造方法后自动被调用。这是 Java 代码通过读取该注解实现的功能,JVM 本身并不会识别该注解。

3️⃣ 元注解(java.lang.annotation下的注解:用来定义注解的)

Java 的注解为代码添加了额外的元数据,这些元数据在编译或运行时都可以被访问。为了定义注解的属性和行为,Java 提供了以下一系列的元注解:


@Retention

  • 描述:确定注解的生命周期,即何时可用。
  • 用途:通知开发者注解在何时应被使用或访问。
  • 示例
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MyAnnotation { }
    

@Target

  • 描述:指定注解可以应用的源码元素。
  • 用途:避免注解被误用在不适当的地方,限制注解的适用范围。
  • 示例
    @Target(ElementType.METHOD)
    public @interface MyMethodAnnotation { }
    

@Inherited

  • 描述:允许子类继承父类上的注解。
  • 用途:当需要子类自动继承父类的注解时使用。
  • 示例
    @Inherited
    public @interface Inheritable { }
    

@Documented

  • 描述:注解信息会被包含在 Javadoc 中。
  • 用途:确保开发者在查看 API 文档时,可以看到注解的信息。
  • 示例
    @Documented
    public @interface DocumentedAnnotation { }
    

@Repeatable (Java 8 新增)

  • 描述:允许同一个注解在同一声明上使用多次。
  • 用途:在 Java 8 之前,一个注解在同一位置只能使用一次,此元注解允许多次使用同一个注解。
  • 示例
    public @interface RepeatedValues {
        Value[] value();
    }
    
    @Repeatable(RepeatedValues.class)
    public @interface Value {
        String info() default "";
    }
    

4️⃣ JDK 内置注解(都属于由编译器使用的注解)

java.lang 包中,JDK 提供了五个基本注解:


@Override

  • 描述:用于指定子类中的方法重写了父类的方法。
  • 用法
    • 告知编译器我们意图重写父类方法。
    • 如果子类的方法与父类方法签名不匹配,编译器会报错。
  • 适用范围:只能用于方法。

@Deprecated

  • 描述:用于标记已过时的程序元素,如类、方法等。
  • 用法
    • 提醒开发者不建议使用该程序元素。
    • 使用被此注解标记的元素时,编译器会给出警告。

@SuppressWarnings

  • 描述:用于抑制编译器产生警告。
  • 用法:告知编译器忽略特定的警告。
  • 常见参数
    • deprecation:使用已过时的类或方法。
    • unchecked:未经检查的类型转换。
    • fallthroughswitch 语句缺少 break
    • path:类或源文件路径不存在。
    • serial:可序列化类缺少 serialVersionUID
    • finallyfinally 语句未正常完成。
    • all:上述所有情况。

@SafeVarargs

  • 描述:抑制堆污染警告。
  • 特点:JDK 7 新增,用于标记可能产生堆污染的方法或构造函数。

@FunctionalInterface (Java 8 新增)

  • 描述:标记一个接口为函数式接口。
  • 定义:接口中只有一个抽象方法(但可以有多个 defaultstatic 方法)。

注意事项

  • value 特权:如果注解的 value 成员变量是唯一需要赋值的,可以省略 name=value 的格式,直接在注解的括号中指定值。

  • 推荐使用 @Override:在意图重写父类方法的每个方法上使用 @Override,帮助编译器及时捕获可能的错误。


5️⃣ 自定义注解

在 Java 中,注解是一种为代码添加元数据的方式。当我们使用 @interface 关键字定义新的注解时,背后的一些细节会由编译器自动完成,例如自动继承 java.lang.annotation.Annotation 接口。

注解的定义规则:

  1. 在定义新注解时,不能继承其他注解或接口。
  2. @interface 关键字用于声明一个新的注解。
  3. 在注解内,每个方法都代表一个配置参数。
    • 方法名为参数名。
    • 返回值类型定义了参数的类型。
    • 方法可以没有参数。
    • 可以使用 default 关键字为参数设定默认值。

注解参数的支持类型:

  • 基本数据类型:如 int, float, boolean, byte, double, char, long, short
  • String
  • Class
  • 枚举类型 (Enum)
  • 其他注解 (Annotation)
  • 以上所有类型的数组形式
package annotation.custom;

// 导入必要的注解处理类
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// 表明这个注解是可以被子类继承的
@Inherited
// 指明该注解的生命周期,此处指定为RUNTIME,表示注解在运行时仍然存在,并可以通过反射机制读取
@Retention(RetentionPolicy.RUNTIME)
// 指明该注解可以被应用于方法和类型(类、接口、枚举、注解)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Tag {
    // 定义一个名为name的元素,它的默认值为"undefined"
    String name() default "undefined";

    // 定义一个名为description的元素,使用该注解时,这个元素是必须要设置的,因为它没有默认值
    String description();
}
// 代表所有的注解类型
public interface Annotation {

    // 用于比较两个注解是否相等。注意:自定义的注解会有编译器生成的实现,所以使用者不需要手动实现此方法。
    boolean equals(Object obj);

    // 返回该注解的哈希码。同样,这个方法会由编译器自动生成其实现。
    int hashCode();

    // 返回该注解的字符串表示形式。这也是由编译器自动生成的方法。
    String toString();

    // 返回表示此注解的注解类型的Class对象。它可以用于在运行时获取注解的信息。
    Class<? extends Annotation> annotationType();
}

注意: Java 使用 Annotation 接口来代表程序元素前面的注解,该接口是所有 Annotation 类型的父接口。 自定义的注解继承了 Annotation 这个接口,因此自定义注解中包含了 Annotation 接口中所有的方法


6️⃣ 注解的提取

Java在java.lang.reflect包内提供了强大的反射机制,其中AnnotatedElement接口扮演了关键角色,代表那些可以携带注解的程序元素。

AnnotatedElement接口

  • 定义:此接口表示可以接受注解的程序元素。

  • 实现类

    • AccessibleObject:是FieldMethodConstructor对象的基类。
    • Executable:是MethodConstructor对象的共有基类。
    • Method:表示类或接口中的某个方法。
    • Constructor:提供某个类的构造方法的信息。
    • Field:表示类或接口中的某个字段。
    • Class:代表应用程序中的类和接口。
    • Package:提供有关Java包的版本信息。
    • Parameter:提供关于方法参数的信息,Java 8 新增。
  • 功能:通过这些类,Java提供了一套丰富的API用于获取运行时的注解信息。

如何使用

只有当一个注解被定义为运行时注解(即使用@Retention(RetentionPolicy.RUNTIME)标注)时,它才会在运行时被JVM读取。这意味着,通过反射,我们可以读取类、方法或字段上的这些注解,并据此执行某些操作。例如,通过反射获得某个类的Method对象后,可以调用该对象的getAnnotations()方法来获取所有的注解。

方法描述

  • isAnnotationPresent(Class annotationType):

    • 描述:此方法用于判断指定的对象是否应用了某个注解。
    • 应用:主要用于方便地访问标记注解。
  • getAnnotations():

    • 描述:返回作用于指定对象的所有注解。
    • 注意:如果没有,则返回长度为0的数组。
  • getDeclaredAnnotations():

    • 描述:返回直接作用于指定对象的所有注解。
    • 注意:如果不存在,则返回长度为0的数组。此方法忽略继承的注解。
  • getAnnotation(Class annotationClass):

    • 描述:返回指定对象上的指定类型的注解。
    • 注意:如果注解不存在,则返回null。
  • getDeclaredAnnotation(Class annotationClass):

    • 描述:返回直接作用于指定对象的指定类型的注解。
    • 注意:如果注解不存在,则返回null。此方法忽略继承的注解。
  • getAnnotationsByType(Class annotationClass):

    • 描述:返回指定对象上的指定类型的所有注解。
    • 注意:如果不存在,则返回长度为0的数组。此方法检测其参数是否为可重复的注解类型。
  • getDeclaredAnnotationsByType(Class annotationClass):

    • 描述:返回直接作用于指定对象的指定类型的所有注解。
    • 注意:如果不存在,则返回长度为0的数组。此方法检测其参数是否为可重复的注解类型,并忽略继承的注解。

注意: 只有当定义 Annotation 时使用了 @Retention(RetentionPolicy.RUNTIME) 修饰, JVM
才会在装载 class 文件时提取保存在 class 文件中的 Annotation, 该 Annotation 才会在运行时可见。否则class 文件的注解信息在执行过程中将不可用, 从而也就不能从中得到任何和注解有关的数据。


7️⃣ 注解处理器


注解处理器简介

  1. 定义:注解处理器是用于在编译时扫描、处理源代码中的注解的工具。它可以分析注解并生成额外的源代码或其他文件。
  2. 历史
    • Java 5:首次引入注解,但处理注解的API不够成熟。需要使用一个独立的工具apt(Annotation Processor Tool)和com.sun.mirror包中的Mirror API来处理注解。
    • Java 6:通过JSR 269,注解处理器被标准化并纳入到Java标准库中。apt工具被集成到javac编译工具中。Java 6提供了一个通用功能的抽象类javax.annotation.processing.AbstractProcessorjavax.lang.model包来支持注解处理。

如何工作

  1. 在编译阶段,javac会扫描每个源文件中的注解。
  2. 对于每个找到的注解,javac会查询是否有已注册的注解处理器可以处理它。
  3. 如果有,javac将调用该处理器来处理注解。处理器可以分析注解、读取注解的值,并根据这些值生成额外的源代码或其他文件。

注解处理器的用途

  1. 自动化代码生成:允许开发者定义规则和模式,然后让机器自动为我们生成代码,避免了人为错误和重复劳动。
  2. 编译时检查:通过注解处理器,我们可以在编译时进行更多的检查,确保代码的质量和准确性。
  3. 性能:由于大部分工作在编译时完成,运行时的性能开销被最小化。
  4. 代码整洁:自动生成的代码可以分离到另外的文件中,保持主要业务逻辑的代码清晰和整洁。

应用场景

  1. 类属性自动赋值

    • 自动注入对象或值,例如Spring框架中的@Autowired@Value
  2. 验证对象属性完整性

    • 例如,使用Java Bean Validation API(如@NotNull, @Size, @Min, @Max等)进行对象属性验证。
  3. 代替配置文件功能

    • 通过注解配置,如Spring的@Component, @Service, @Repository, @Controller等。
  4. 生成文档

    • Javadoc工具使用注解来生成API文档。
  5. 编译时检查

    • 例如,@Override用于告诉编译器子类中的某个方法是否正确地重写了父类中的方法。
  6. 代码生成

    • 编译时注解处理器可以用于生成额外的源代码。
  7. 运行时处理

    • 通过反射API在运行时读取注解信息,如Spring AOP、JUnit等。
  8. APT(Annotation Processing Tool)

    • 处理和提取注解的工具或代码。例如,Lombok库使用注解处理器在编译时生成getter、setter和其他常见方法。

AbstractProcessor

AbstractProcessor 是一个提供便捷实现注解处理器功能的抽象类,实现了 Processor 接口,都位于 javax.annotation.processing 包中。

自定义注解处理器的主要步骤:

  1. 实现 Processor 接口: 通过继承 AbstractProcessor 类来实现自定义的注解处理器。主要的工作在于实现 process 方法,进行你想要完成的注解处理功能。

    init() 方法中,你可以获得:

    • Elements: 一个处理 Element 的工具类。
    • Types: 用来处理 TypeMirror 的工具类。
    • Filer: 用于创建文件。

    在注解处理过程中,会扫描所有的 Java 源文件。每个部分都是一个特定类型的 Element,代表程序的元素,例如包、类或者方法。主要的子类有:

    • PackageElement: 代表一个包。
    • TypeElement: 代表一个类或接口。
    • VariableElement: 表示变量,如成员变量、枚举常量、方法或构造方法参数等。
    • ExecutableElement: 表示类或接口的方法、构造方法。
    • TypeParameterElement: 表示类型参数。
  2. 注册注解处理器: 有两种方法:

    • 手动注册: 在项目的 resources/META-INF/services 目录下新建文件 javax.annotation.processing.Processor,内容是处理器的全称。
    • 自动注册: 使用 Google 提供的库,添加 com.google.auto.service:auto-service 依赖,并在处理器上添加 @AutoService(Processor.class) 注解。这会自动在 META-INF 目录下生成配置信息文件。

注意:在 Java 6 及以上版本中,可以使用 @SupportedAnnotationTypes 注解和 @SupportedSourceVersion 注解来分别替代 getSupportedAnnotationTypes() 方法和 getSupportedSourceVersion() 方法。

自定义注解处理器都需要继承于 AbstractProcessor,如下所示:

/**
 * 自定义注解处理器
 */
public class CustomProcessor extends AbstractProcessor {
 
    private Filer mFiler;
    private Messager mMessager;
    private Elements mElementUtils;
 
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mFiler = processingEnvironment.getFiler();
        mMessager = processingEnvironment.getMessager();
        mElementUtils = processingEnvironment.getElementUtils();
    }
 
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotataions = new LinkedHashSet<String>();
        annotataions.add(Tag.class.getCanonicalName());
        return annotataions;
    }
 
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
 
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
        Set<? extends Element> tagElements = roundEnvironment.getElementsAnnotatedWith(Tag.class);
        for (Element element : tagElements) {
            // 1.获取包名
            PackageElement packageElement = mElementUtils.getPackageOf(element);
            String pkName = packageElement.getQualifiedName().toString();
            printMessage(String.format("package = %s", pkName));
            // 2.获取包装类类型
            TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
            String enclosingName = enclosingElement.getQualifiedName().toString();
            printMessage(String.format("enclosindClass = %s", enclosingName));
            // 3.获取注解的成员变量名
            String tagFiledName = element.getSimpleName().toString();
            // 4.获取注解的成员变量类型
            String tagFiledClassType = element.asType().toString();
            // 5.获取注解元数据
            Tag tag = element.getAnnotation(Tag.class);
            String name = tag.name();
            printMessage(String.format("%s %s = %s", tagFiledClassType, tagFiledName, name));
            // 6.生成文件
            createFile(enclosingElement, tagFiledClassType, tagFiledName, name);
            return true;
        }
        return false;
    }
 
    private void createFile(TypeElement enclosingElement, String tagFiledClassType, String tagFiledName, String name) {
        String pkName = mElementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();
        try {
            JavaFileObject javaFileObject = mFiler.createSourceFile(pkName + ".Tag");
            Writer writer = javaFileObject.openWriter();
            writer.write(generateCode(pkName, tagFiledClassType, tagFiledName, name));
            writer.flush();
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
 
    private void printMessage(String msg) {
        mMessager.printMessage(Diagnostic.Kind.NOTE, msg);
    }
 
    private String generateCode(String pkName, String tagFiledClassType, String tagFiledName, String name) {
        StringBuilder builder = new StringBuilder();
        builder.append("package " + pkName + ";\n\n");
        builder.append("//Auto generated by apt,do not modify!!\n\n");
        builder.append("public class Tag { \n\n");
        builder.append("public static void main(String[] args){ \n");
        String info = String.format("%s %s = %s", tagFiledClassType, tagFiledName, name);
        builder.append("System.out.println(\"" + info + "\");\n");
        builder.append("}\n");
        builder.append("}");
        return builder.toString();
    }
 
}

你可能感兴趣的:(Java-基础,java,开发语言)