java 注解机制详解

166 阅读13分钟

1. 注解机制介绍

Java 注解机制是 Java 语言中的一项重要功能,它允许开发者在代码中添加元数据,这些元数据可以在编译时、类加载时或运行时被处理。注解是一种特殊的接口,使用 @interface 关键字来定义,通常用于提供额外的信息而不直接改变程序的逻辑。

2. 注解的作用

2.1.1. 提供元数据

注解可以为代码元素(类、方法、字段等)添加元数据。这些元数据可以描述代码的行为、约束、配置等,而不直接影响代码逻辑。

@Deprecated // 表示这个方法已被弃用
public void oldMethod() {
    // 旧的实现
}

2.1.2. 增强代码可读性

通过使用注解,可以提高代码的可读性和自说明性。注解能够清晰地表达特定的意图或特性,使得代码更易于理解和维护。

@Override // 表示这个方法重写了父类的方法
public String toString() {
    return "Custom toString method";
}

2.1.3. 生成代码

注解可以用于代码生成工具(如注解处理器)自动生成代码。这在很多现代框架和库中非常常见,比如依赖注入框架和 ORM 框架。

@Entity // Hibernate 注解,用于将类映射到数据库表
public class User {
    @Id // 表示字段是主键
    private Long id;

    @Column(name = "username") // 映射字段到数据库列
    private String username;
}

2.1.4. 编译时检查

注解可以与注解处理器结合使用,以在编译时执行检查,确保代码符合特定规则或约定。这有助于减少运行时错误并提高代码质量。

@NonNull // 自定义注解,用于检查方法参数是否不能为空
public void setUser(@NonNull User user) {
    this.user = user;
}

2.1.5. 运行时处理

某些注解在运行时可以通过反射机制进行处理。这使得在运行时动态地改变行为成为可能,例如依赖注入、事务管理等。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Transactional {
    String value() default "default";
}

public class Service {
    @Transactional
    public void performAction() {
        // 执行事务操作
    }
}

2.1.6. 定义业务逻辑

注解可以用于定义和简化业务逻辑,特别是在使用框架时,例如 Spring 框架的注解用于定义 Bean、配置事务等。

@Component // 表示这个类是一个 Spring Bean
public class MyService {
    @Autowired // 自动注入依赖
    private MyRepository myRepository;

    @Transactional // 事务管理
    public void execute() {
        // 业务逻辑
    }
}

2.1.7. 自动化测试

测试框架如 JUnit 使用注解来标记测试方法和测试类,从而简化测试的编写和执行。

@RunWith(JUnit4.class) // 指定测试运行器
public class MyTests {

    @Test // 标记一个测试方法
    public void testMethod() {
        // 测试逻辑
    }

    @Before // 在每个测试方法前执行
    public void setUp() {
        // 设置测试环境
    }

    @After // 在每个测试方法后执行
    public void tearDown() {
        // 清理测试环境
    }
}

Java 注解的作用不仅限于提供元数据和增强代码的可读性,它们还在编译时检查、代码生成、运行时处理以及定义业务逻辑等方面发挥着重要作用。通过合理使用注解,可以大大提高开发效率和代码质量。

3. java 注解分类

  • 元注解
  • java 内置注解
  • 自定义注解

3.1. 元注解

在JDK 1.5中提供了4个标准的元注解:@Target@Retention@Documented@Inherited, 在JDK 1.8中提供了两个元注解 @Repeatable@Native

3.1.1. @Target

Target 注解的作用是:描述注解的使用范围(即:被修饰的注解可以用在什么地方) 。

注解可以用于修饰 packagestypes(类、接口、枚举、注解类)、类成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。

@Target 注解的默认值是 ElementType.TYPE,意味着如果没有显式指定,注解将默认为只应用于类、接口、或枚举类型上。

public enum ElementType {
 
    TYPE, // 类、接口、枚举类
 
    FIELD, // 成员变量(包括:枚举常量)
 
    METHOD, // 成员方法
 
    PARAMETER, // 方法参数
 
    CONSTRUCTOR, // 构造方法
 
    LOCAL_VARIABLE, // 局部变量
 
    ANNOTATION_TYPE, // 注解类
 
    PACKAGE, // 可用于修饰:包
 
    TYPE_PARAMETER, // 类型参数,JDK 1.8 新增
 
    TYPE_USE // 使用类型的任何地方,JDK 1.8 新增
 
}

3.1.2. @Retention & @RetentionTarget

Reteniton 注解的作用是:描述注解保留的时间范围(即:被描述的注解在它所修饰的类中可以被保留到何时)

Reteniton 注解用来限定那些被它所注解的注解类在注解到其他类上以后,可被保留到何时,一共有三种策略,定义在 RetentionPolicy 枚举中。这个注解在运行时可以通过反射获取到。

public enum RetentionPolicy {
    
    SOURCE,    // 源文件保留
    
    CLASS,       // 编译期保留,默认值
    
    RUNTIME   // 运行期保留,可通过反射去获取注解信息
}

3.1.3. @Documented

Documented注解的作用是:描述在使用 javadoc 工具为类生成帮助文档时是否要保留其注解信息。

@Documented 是一个元注解,用于指示某个注解应该包含在 Javadoc 文档中。也就是说,如果一个注解被 @Documented 标记,那么当使用该注解的类、方法或字段被文档化时,注解的信息也会被包括在生成的文档中。

@Documented元注解仅仅影响注解本身是否在文档中可见,并不会影响注解所标记的代码元素(类、方法等)是否在文档中可见。要使得注解所标记的代码元素在文档中可见,需要在注解所标记的元素上添加合适的文档注释。

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface MyDocumentedAnnotation {
    String value() default "default";
}

3.1.4. @Inherited

Inherited 注解的作用:被它修饰的 Annotation 将具有继承性。如果某个类使用了被 @Inherited 修饰的Annotation,则其子类将自动具有该注解。

注意:

  • 继承: 只有在注解定义了 @Inherited,其子类才会继承这个注解。如果注解没有标记 @Inherited,则子类不会自动获得父类的注解。
  • 保留策略: @Inherited 注解的注解通常会使用 @Retention(RetentionPolicy.RUNTIME),以确保在运行时可以访问注解。

3.1.5. @Repeatable (Java8)

@Repeatable重复注解,允许在同一申明类型(类,属性,或方法)的多次使用同一个注解,每次应用的注解实例都是独立的。

使用方式:

要使一个注解可重复,你需要做以下几步:

  1. 定义一个“容器”注解,这个注解将包含所有重复注解的集合。
  2. 使用 @Repeatable 注解 在目标注解上,指定刚刚定义的容器注解。

示例 1: 定义和使用 @Repeatable

javaCopy Codeimport java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// 1. 定义一个容器注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Tags {
    Tag[] value();
}

// 2. 使用 @Repeatable 标记目标注解
@Repeatable(Tags.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Tag {
    String value();
}

// 3. 使用重复注解
@Tag("Science")
@Tag("Technology")
public class MyClass {
}

在这个示例中,Tag 注解可以重复使用于 MyClass 类上。容器注解 Tags 用于汇集所有 Tag 注解的实例。

主要点

  • 定义容器注解: 容器注解必须使用 @Retention(RetentionPolicy.RUNTIME),并且定义一个 value 方法返回一个 Tag 数组(即重复注解的数组)。
  • 标记目标注解: 目标注解必须使用 @Repeatable 注解,指定容器注解作为参数。
  • 访问注解: 使用反射可以访问到所有的重复注解实例。以下是如何获取重复注解的示例:

示例 2: 访问重复注解

javaCopy Codeimport java.lang.annotation.Annotation;

public class Test {
    public static void main(String[] args) {
        // 获取 MyClass 上的所有 Tag 注解
        Tag[] tags = MyClass.class.getAnnotationsByType(Tag.class);
        for (Tag tag : tags) {
            System.out.println(tag.value());
        }
    }
}

在这个示例中,getAnnotationsByType(Tag.class) 会返回 MyClass 上的所有 Tag 注解实例,你可以遍历这些实例并获取注解的值。

示例场景:

示例 3: 使用 @Repeatable 在方法上

javaCopy Code@Repeatable(Tags.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Permission {
    String value();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Permissions {
    Permission[] value();
}

public class AccessControl {
    @Permission("read")
    @Permission("write")
    public void performAction() {
    }
}

在这个示例中,Permission 注解可以重复应用于 performAction 方法上,容器注解 Permissions 用于包含所有 Permission 注解实例。

3.1.6. @Native (Java8)

使用 @Native 注解修饰成员变量,则表示这个变量可以被本地代码引用,常常被代码生成工具使用。

注解的特点:

  • 定义: @Native 注解本身并没有直接的逻辑功能,它只是一个标记,帮助开发者理解某些字段或方法与本地代码的关系。
  • 文档作用: 在 Java 代码中,@Native 注解的主要作用是文档化,使得代码的意图更明确。这对于维护和理解与本地代码交互的部分尤其有用。
  • 使用限制: @Native 注解不会影响编译器或 JVM 的行为。它只用于标记,并不会导致任何编译时或运行时的行为变化。

使用场景:

  • 跨语言交互: 当你在 Java 代码中使用本地方法(Native Methods)时,@Native 注解可以帮助文档化这些方法和字段的本地依赖。
  • 代码维护: 在大型项目中,使用 @Native 注解可以帮助开发者更容易地识别哪些字段和方法是与本地代码交互的,便于维护和调试

3.2. java 内置注解

Java 1.5开始自带的标准注解,包括@Override@Deprecated@SuppressWarnings

3.2.1. @Override

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

这个注解可以被用来修饰方法,并且它只在编译时有效,在编译后的class文件中便不再存在。

用于标记一个方法是从超类中重写的方法,告诉编译器被修饰的方法是重写的父类的中的相同签名的方法,编译器会对此做出检查,若发现父类中不存在这个方法或是存在的方法签名不同,则会报错。

3.2.2. @Deprecated

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}

@Deprecated 是 Java 中的一个注解,用于标记某个元素(如类、方法或字段)已经过时,不建议使用,并且可能在将来的版本中被删除。这个注解主要用于提醒开发者,不再使用某些过时的代码,并鼓励使用替代方案。

3.2.3. @SuppressWarnings

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    String[] value();
}

它能够修饰的程序元素包括类型、属性、方法、参数、构造器、局部变量,只能存活在源码时,取值为String[]。它的作用是告诉编译器忽略指定的警告信息,它可以取的值如下所示:

参数作用原描述
all抑制所有警告to suppress all warnings
boxing抑制装箱、拆箱操作时候的警告to suppress warnings relative to boxing/unboxing operations
cast抑制映射相关的警告to suppress warnings relative to cast operations
dep-ann抑制启用注释的警告to suppress warnings relative to deprecated annotation
deprecation抑制过期方法警告to suppress warnings relative to deprecation
fallthrough抑制确在switch中缺失breaks的警告to suppress warnings relative to missing breaks in switch statements
finally抑制finally模块没有返回的警告to suppress warnings relative to finally block that don’t return
hiding抑制与隐藏变数的区域变数相关的警告to suppress warnings relative to locals that hide variable()
incomplete-switch忽略没有完整的switch语句to suppress warnings relative to missing entries in a switch statement (enum case)
nls忽略非nls格式的字符to suppress warnings relative to non-nls string literals
null忽略对null的操作to suppress warnings relative to null analysis
rawtype使用generics时忽略没有指定相应的类型to suppress warnings relative to un-specific types when using
restriction抑制与使用不建议或禁止参照相关的警告to suppress warnings relative to usage of discouraged or
serial忽略在serializable类中没有声明serialVersionUID变量to suppress warnings relative to missing serialVersionUID field for a serializable class
static-access抑制不正确的静态访问方式警告to suppress warnings relative to incorrect static access
synthetic-access抑制子类没有按最优方法访问内部类的警告to suppress warnings relative to unoptimized access from inner classes
unchecked抑制没有进行类型检查操作的警告to suppress warnings relative to unchecked operations
unqualified-field-access抑制没有权限访问的域的警告to suppress warnings relative to field access unqualified
unused抑制没被使用过的代码的警告to suppress warnings relative to unused code

3.3. 注解与反射接口

AnnotatedElement 接口是所有程序元素(ClassMethodConstructor)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象之后,程序就可以调用该对象的方法来访问Annotation信息。

  1. boolean isAnnotationPresent(Class<?extends Annotation> annotationClass)

判断该程序元素上是否包含指定类型的注解,存在则返回true,否则返回false。注意:此方法会忽略注解对应的注解容器。

  1. <T extends Annotation> T getAnnotation(Class<T> annotationClass)

返回该程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回null

  1. Annotation[] getAnnotations()

返回该程序元素上存在的所有注解,若没有注解,返回长度为0的数组。

  1. <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass)

返回该程序元素上存在的、指定类型的注解数组。没有注解对应类型的注解时,返回长度为0的数组。该方法的调用者可以随意修改返回的数组,而不会对其他调用者返回的数组产生任何影响。getAnnotationsByType方法与 getAnnotation的区别在于,getAnnotationsByType会检测注解对应的重复注解容器。若程序元素为类,当前类上找不到注解,且该注解为可继承的,则会去父类上检测对应的注解。

  1. <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass)

返回直接存在于此元素上的所有注解。与此接口中的其他方法不同,该方法将忽略继承的注释。如果没有注释直接存在于此元素上,则返回null

  1. <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass)

返回直接存在于此元素上的所有注解。与此接口中的其他方法不同,该方法将忽略继承的注释

  1. Annotation[] getDeclaredAnnotations()

返回直接存在于此元素上的所有注解及注解对应的重复注解容器。与此接口中的其他方法不同,该方法将忽略继承的注解。如果没有注释直接存在于此元素上,则返回长度为零的一个数组。该方法的调用者可以随意修改返回的数组,而不会对其他调用者返回的数组产生任何影响。

4. 自定义注解

4.1. 定义注解

package annotation;

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

/**
 * @Author: Julbreeze
 * @Date: 2024/8/29 13:42
 * @Version: v1.0.0
 * @Description: TODO
 **/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyMethodAnnotation {
    public String title() default "";
    public String description() default "";
}

4.2. 使用注解

package annotation;

import java.io.FileNotFoundException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

/**
 * @Author: Julbreeze
 * @Date: 2024/8/29 13:48
 * @Version: v1.0.0
 * @Description: TODO
 **/
public class TestMethodAnnotation {

    @Override
    @MyMethodAnnotation(title = "toStringMethod", description = "override toString method")
    public String toString() {
        return "Override toString method";
    }

    @Deprecated
    @MyMethodAnnotation(title = "old static method", description = "deprecated old static method")
    public static void oldMethod() {
        System.out.println("old method, don't use it.");
    }

    @SuppressWarnings({"unchecked", "deprecation"})
    @MyMethodAnnotation(title = "test method", description = "suppress warning static method")
    public static void genericsTest() throws FileNotFoundException {
        List l = new ArrayList();
        l.add("abc");
        oldMethod();
    }


    // ----> 反射获取注解信息
    public static void main(String[] args) {
        try {
            // 获取所有methods
            Method[] methods = TestMethodAnnotation.class.getClassLoader()
            .loadClass(("annotation.TestMethodAnnotation"))
            .getMethods();

            // 遍历
            for (Method method : methods) {
                // 方法上是否有MyMethodAnnotation注解
                if (method.isAnnotationPresent(MyMethodAnnotation.class)) {
                    try {
                        // 获取并遍历方法上的所有注解
                        for (Annotation anno : method.getDeclaredAnnotations()) {
                            System.out.println("Annotation in Method '"
                                               + method + "' : " + anno);
                        }

                        // 获取MyMethodAnnotation对象信息
                        MyMethodAnnotation methodAnno = method
                        .getAnnotation(MyMethodAnnotation.class);

                        System.out.println(methodAnno.title());

                        System.out.println("--------------------");
                    } catch (Throwable ex) {
                        ex.printStackTrace();
                    }
                }
            }
        } catch (SecurityException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

}