MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java自定义注解的创建与使用

2021-12-201.7k 阅读

Java 自定义注解的创建与使用

在 Java 编程中,注解(Annotation)是一种元数据(Metadata)形式,它为我们在代码中添加额外信息提供了一种便捷的方式。这些信息可以在编译期、运行期被读取并使用,从而实现一些强大的功能,如代码检查、配置管理、依赖注入等。Java 本身提供了一些内置注解,例如 @Override@Deprecated@SuppressWarnings 等,这些注解在特定场景下发挥着重要作用。然而,在实际项目开发中,我们常常需要根据业务需求自定义注解来满足特定的功能需求。下面我们就来详细探讨 Java 自定义注解的创建与使用。

自定义注解的创建

  1. 定义注解格式 在 Java 中,定义一个自定义注解非常简单,只需要使用 @interface 关键字。注解定义的语法形式如下:

    public @interface AnnotationName {
        // 注解元素(成员变量)定义
    }
    

    这里 AnnotationName 是自定义注解的名称,我们可以根据实际需求进行命名。注解内部可以定义一些元素(类似于接口中的方法),这些元素是注解的核心组成部分,它们可以有默认值,也可以在使用注解时进行赋值。

  2. 定义注解元素 注解元素的定义方式和接口中的方法定义类似,但有一些特殊的规则。

    • 基本数据类型、String、Class、枚举类型以及它们的数组类型:这些类型都可以作为注解元素的类型。例如:
    public @interface MyAnnotation {
        int value();
        String name() default "defaultName";
        Class<?> targetClass();
        MyEnum myEnum() default MyEnum.VALUE1;
        String[] tags();
    }
    enum MyEnum {
        VALUE1, VALUE2
    }
    

    在上面的代码中,MyAnnotation 定义了多个注解元素。value 是一个 int 类型的元素,没有默认值,所以在使用注解时必须为其赋值。nameString 类型的元素,有默认值 "defaultName",如果在使用注解时不指定 name 的值,就会使用这个默认值。targetClassClass<?> 类型的元素,同样没有默认值,需要在使用注解时指定。myEnum 是自定义枚举类型 MyEnum 的元素,有默认值 MyEnum.VALUE1tagsString 类型的数组元素,使用注解时需要为其提供数组值。

    • 注解类型:注解元素也可以是另一个注解类型。例如,我们先定义一个子注解 SubAnnotation
    public @interface SubAnnotation {
        String subValue();
    }
    public @interface MainAnnotation {
        SubAnnotation sub();
    }
    

    这里 MainAnnotation 包含一个 SubAnnotation 类型的元素 sub。在使用 MainAnnotation 时,需要同时为 sub 注解的元素赋值。

    • 特殊的元素命名:如果注解中只有一个名为 value 的元素,在使用注解时可以省略 value 名称的显示声明。例如:
    public @interface SingleValueAnnotation {
        int value();
    }
    // 使用时可以这样
    @SingleValueAnnotation(10)
    public class SomeClass {
        // 类的内容
    }
    

    这和 @SingleValueAnnotation(value = 10) 效果是一样的,但更简洁。

  3. 元注解(Meta - Annotation) 元注解是用于注解其他注解的注解,Java 提供了几个重要的元注解,它们分别是 @Retention@Target@Documented@Inherited@Repeatable

    • @Retention@Retention 用于指定注解的保留策略,即注解在什么阶段被保留。它有三个取值:
      • RetentionPolicy.SOURCE:注解只保留在源文件中,编译时会被丢弃,不会存在于字节码文件中。这种注解通常用于一些只在编译期起作用的场景,比如自定义的编译期检查注解。例如,我们定义一个用于编译期检查方法参数是否为空的注解:
      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.PARAMETER)
      public @interface NotNullParameter {
      }
      
      在编译时,我们可以通过自定义的编译器插件来检查带有 @NotNullParameter 注解的方法参数是否为空。
      • RetentionPolicy.CLASS:注解保留在字节码文件中,但在运行期不会被 JVM 读取。许多框架会在编译后处理字节码文件,这种保留策略适用于这些场景。例如,一些代码生成框架可能会在编译后根据注解生成额外的代码。
      • RetentionPolicy.RUNTIME:注解不仅保留在字节码文件中,在运行期还可以通过反射机制读取。这种注解是最常用的,许多依赖注入框架、AOP 框架等都使用这种保留策略的注解。例如,Spring 框架中的 @Component@Autowired 等注解都是 RetentionPolicy.RUNTIME 类型的。
    • @Target@Target 用于指定注解可以应用的目标元素类型。它的取值是 ElementType 枚举中的值,常见的取值有:
      • ElementType.TYPE:可以应用于类、接口(包括注解类型)、枚举。例如,我们定义一个用于标记某个类是数据实体类的注解:
      import java.lang.annotation.ElementType;
      import java.lang.annotation.Retention;
      import java.lang.annotation.RetentionPolicy;
      import java.lang.annotation.Target;
      
      @Retention(RetentionPolicy.RUNTIME)
      @Target(ElementType.TYPE)
      public @interface DataEntity {
      }
      
      然后可以在类上使用这个注解:
      @DataEntity
      public class User {
          private String name;
          // 其他属性和方法
      }
      
      • ElementType.FIELD:可以应用于类的成员变量。比如,我们可以定义一个用于标记数据库表字段的注解:
      import java.lang.annotation.ElementType;
      import java.lang.annotation.Retention;
      import java.lang.annotation.RetentionPolicy;
      import java.lang.annotation.Target;
      
      @Retention(RetentionPolicy.RUNTIME)
      @Target(ElementType.FIELD)
      public @interface DatabaseField {
          String columnName();
      }
      public class Product {
          @DatabaseField(columnName = "product_name")
          private String productName;
          // 其他属性和方法
      }
      
      • ElementType.METHOD:可以应用于方法。例如,我们定义一个用于记录方法执行时间的注解:
      import java.lang.annotation.ElementType;
      import java.lang.annotation.Retention;
      import java.lang.annotation.RetentionPolicy;
      import java.lang.annotation.Target;
      
      @Retention(RetentionPolicy.RUNTIME)
      @Target(ElementType.METHOD)
      public @interface TimeLog {
      }
      public class SomeService {
          @TimeLog
          public void someMethod() {
              // 方法逻辑
          }
      }
      
      • ElementType.PARAMETER:可以应用于方法参数。前面提到的 @NotNullParameter 注解就是应用于方法参数的例子。
      • ElementType.CONSTRUCTOR:可以应用于构造函数。比如,我们可以定义一个用于标记构造函数是主要构造函数的注解:
      import java.lang.annotation.ElementType;
      import java.lang.annotation.Retention;
      import java.lang.annotation.RetentionPolicy;
      import java.lang.annotation.Target;
      
      @Retention(RetentionPolicy.RUNTIME)
      @Target(ElementType.CONSTRUCTOR)
      public @interface PrimaryConstructor {
      }
      public class SomeClass {
          @PrimaryConstructor
          public SomeClass() {
              // 构造函数逻辑
          }
      }
      
      • ElementType.LOCAL_VARIABLE:可以应用于局部变量。虽然这种情况相对较少,但在某些特定场景下可能会有用,比如在局部变量上标记一些调试信息相关的注解。
      • ElementType.ANNOTATION_TYPE:可以应用于注解类型本身。例如,我们可以定义一个元注解,用于标记某个注解是用于安全相关的:
      import java.lang.annotation.ElementType;
      import java.lang.annotation.Retention;
      import java.lang.annotation.RetentionPolicy;
      import java.lang.annotation.Target;
      
      @Retention(RetentionPolicy.RUNTIME)
      @Target(ElementType.ANNOTATION_TYPE)
      public @interface SecurityAnnotation {
      }
      @SecurityAnnotation
      public @interface SecureMethod {
      }
      
      • ElementType.PACKAGE:可以应用于包声明。例如,我们可以定义一个用于标记整个包是用于特定模块的注解:
      import java.lang.annotation.ElementType;
      import java.lang.annotation.Retention;
      import java.lang.annotation.RetentionPolicy;
      import java.lang.annotation.Target;
      
      @Retention(RetentionPolicy.RUNTIME)
      @Target(ElementType.PACKAGE)
      public @interface ModuleAnnotation {
          String moduleName();
      }
      @ModuleAnnotation(moduleName = "user - module")
      package com.example.user;
      
    • @Documented@Documented 用于指定该注解是否会被包含在 Java 文档中。如果一个注解被 @Documented 修饰,那么当我们使用 javadoc 工具生成文档时,该注解及其元素会被包含在文档中。例如:
      import java.lang.annotation.Documented;
      import java.lang.annotation.ElementType;
      import java.lang.annotation.Retention;
      import java.lang.annotation.RetentionPolicy;
      import java.lang.annotation.Target;
      
      @Retention(RetentionPolicy.RUNTIME)
      @Target(ElementType.METHOD)
      @Documented
      public @interface ApiDescription {
          String value();
      }
      public class ApiService {
          @ApiDescription("This method retrieves user information")
          public User getUser(String userId) {
              // 方法逻辑
              return null;
          }
      }
      
      当我们使用 javadoc 生成 ApiService 的文档时,@ApiDescription 注解及其值会被包含在文档中,方便其他开发人员了解该方法的用途。
    • @Inherited@Inherited 用于指定该注解具有继承性。如果一个类被标记了具有 @Inherited 注解的注解,那么它的子类也会自动继承这个注解。例如:
      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;
      
      @Retention(RetentionPolicy.RUNTIME)
      @Target(ElementType.TYPE)
      @Inherited
      public @interface ParentAnnotation {
      }
      @ParentAnnotation
      public class ParentClass {
      }
      public class ChildClass extends ParentClass {
      }
      
      这里 ChildClass 虽然没有直接标记 @ParentAnnotation,但由于 @ParentAnnotation@Inherited 的,所以 ChildClass 也具有 @ParentAnnotation 的语义。不过需要注意的是,@Inherited 只对类继承有效,对于接口实现等情况是无效的。
    • @Repeatable@Repeatable 是 Java 8 引入的元注解,用于指定一个注解可以在同一个目标元素上重复使用。例如,我们定义一个 Tags 注解,它是一个容器注解,用于包含多个 Tag 注解:
      import java.lang.annotation.ElementType;
      import java.lang.annotation.Repeatable;
      import java.lang.annotation.Retention;
      import java.lang.annotation.RetentionPolicy;
      import java.lang.annotation.Target;
      
      @Retention(RetentionPolicy.RUNTIME)
      @Target(ElementType.TYPE)
      public @interface Tags {
          Tag[] value();
      }
      @Retention(RetentionPolicy.RUNTIME)
      @Target(ElementType.TYPE)
      @Repeatable(Tags.class)
      public @interface Tag {
          String name();
      }
      @Tag(name = "tag1")
      @Tag(name = "tag2")
      public class TaggedClass {
      }
      
      TaggedClass 上,我们可以重复使用 @Tag 注解,而 @Tags 作为容器注解,会自动收集这些重复的 @Tag 注解。

自定义注解的使用

  1. 在代码中使用自定义注解 一旦我们定义好了自定义注解,就可以在合适的目标元素上使用它。例如,我们使用前面定义的 @DataEntity 注解来标记一个类:

    @DataEntity
    public class Order {
        private String orderId;
        private double amount;
        // 其他属性和方法
    }
    

    这里 Order 类被标记为 @DataEntity,表示它是一个数据实体类。同样,我们可以使用 @DatabaseField 注解来标记类的成员变量:

    public class Product {
        @DatabaseField(columnName = "product_code")
        private String productCode;
        @DatabaseField(columnName = "product_price")
        private double productPrice;
        // 其他属性和方法
    }
    

    在使用注解时,需要根据注解元素是否有默认值来决定是否为其赋值。如果没有默认值,必须显式赋值;如果有默认值,可以选择使用默认值或者显式赋值。

  2. 在运行期读取注解信息(反射机制) 对于 RetentionPolicy.RUNTIME 类型的注解,我们可以在运行期通过反射机制读取注解的信息,从而实现一些动态的功能。例如,我们定义一个 @Author 注解来标记类的作者信息,并在运行期读取这个信息:

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface Author {
        String name();
        int age();
    }
    @Author(name = "John Doe", age = 30)
    public class MyClass {
        // 类的内容
    }
    public class AnnotationReader {
        public static void main(String[] args) {
            Class<MyClass> myClass = MyClass.class;
            if (myClass.isAnnotationPresent(Author.class)) {
                Author author = myClass.getAnnotation(Author.class);
                System.out.println("Author Name: " + author.name());
                System.out.println("Author Age: " + author.age());
            }
        }
    }
    

    在上面的代码中,AnnotationReader 类通过反射获取 MyClass 类的 @Author 注解,并输出注解中的作者姓名和年龄信息。同样,我们也可以通过反射读取方法、字段等元素上的注解信息。例如,读取方法上的 @TimeLog 注解:

    import java.lang.reflect.Method;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface TimeLog {
    }
    public class SomeService {
        @TimeLog
        public void someMethod() {
            // 方法逻辑
        }
    }
    public class TimeLogReader {
        public static void main(String[] args) throws NoSuchMethodException {
            Class<SomeService> someServiceClass = SomeService.class;
            Method someMethod = someServiceClass.getMethod("someMethod");
            if (someMethod.isAnnotationPresent(TimeLog.class)) {
                System.out.println("This method is marked with @TimeLog");
            }
        }
    }
    

    这里 TimeLogReader 类通过反射获取 SomeService 类的 someMethod 方法,并检查该方法是否被 @TimeLog 注解标记。

  3. 基于自定义注解实现特定功能 自定义注解不仅仅是为了标记信息,更重要的是基于这些注解实现特定的功能。例如,我们可以基于 @TimeLog 注解实现一个方法执行时间记录的功能。这里我们可以使用 AOP(面向切面编程)的思想,通过动态代理来实现:

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface TimeLog {
    }
    public class SomeService {
        @TimeLog
        public void someMethod() {
            System.out.println("Some method is running");
        }
    }
    class TimeLogInvocationHandler implements InvocationHandler {
        private Object target;
        public TimeLogInvocationHandler(Object target) {
            this.target = target;
        }
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            long startTime = System.currentTimeMillis();
            Object result = method.invoke(target, args);
            long endTime = System.currentTimeMillis();
            if (method.isAnnotationPresent(TimeLog.class)) {
                System.out.println("Method " + method.getName() + " executed in " + (endTime - startTime) + " ms");
            }
            return result;
        }
    }
    public class TimeLogProxyFactory {
        public static Object createProxy(Object target) {
            return Proxy.newProxyInstance(
                    target.getClass().getClassLoader(),
                    target.getClass().getInterfaces(),
                    new TimeLogInvocationHandler(target)
            );
        }
    }
    public class Main {
        public static void main(String[] args) {
            SomeService someService = new SomeService();
            SomeService proxy = (SomeService) TimeLogProxyFactory.createProxy(someService);
            proxy.someMethod();
        }
    }
    

    在上述代码中,TimeLogInvocationHandler 实现了 InvocationHandler 接口,在 invoke 方法中,它在方法执行前后记录时间,并在发现方法被 @TimeLog 注解标记时,输出方法的执行时间。TimeLogProxyFactory 用于创建代理对象,Main 类中通过代理对象调用 someMethod 方法,从而实现了基于 @TimeLog 注解的方法执行时间记录功能。

  4. 结合编译期处理自定义注解(APT - Annotation Processing Tool) 除了在运行期处理注解,我们还可以在编译期利用 APT 来处理自定义注解。APT 可以在编译时生成额外的代码或者进行一些代码检查。例如,我们定义一个 @GenerateGetterSetter 注解,用于自动生成类的 getters 和 setters 方法:

    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.TYPE)
    public @interface GenerateGetterSetter {
    }
    @GenerateGetterSetter
    public class User {
        private String name;
        private int age;
    }
    

    然后我们可以编写一个注解处理器:

    import javax.annotation.processing.AbstractProcessor;
    import javax.annotation.processing.RoundEnvironment;
    import javax.annotation.processing.SupportedAnnotationTypes;
    import javax.annotation.processing.SupportedSourceVersion;
    import javax.lang.model.SourceVersion;
    import javax.lang.model.element.Element;
    import javax.lang.model.element.TypeElement;
    import javax.tools.JavaFileObject;
    import java.io.IOException;
    import java.io.Writer;
    import java.util.Set;
    
    @SupportedAnnotationTypes("GenerateGetterSetter")
    @SupportedSourceVersion(SourceVersion.RELEASE_8)
    public class GetterSetterProcessor extends AbstractProcessor {
        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
            for (TypeElement annotation : annotations) {
                Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(annotation);
                for (Element element : elementsAnnotatedWith) {
                    // 生成 getters 和 setters 代码
                    StringBuilder generatedCode = new StringBuilder();
                    generatedCode.append("package ").append(element.getEnclosingElement().toString()).append(";\n");
                    generatedCode.append("public class ").append(element.getSimpleName()).append(" {\n");
                    // 假设这里只处理字段生成 getters 和 setters
                    // 实际应用中需要更复杂的逻辑处理不同类型的字段
                    for (Element enclosedElement : element.getEnclosedElements()) {
                        if (enclosedElement.getKind().isField()) {
                            String fieldName = enclosedElement.getSimpleName().toString();
                            String fieldType = enclosedElement.asType().toString();
                            generatedCode.append("    public ").append(fieldType).append(" get").append(fieldName.substring(0, 1).toUpperCase()).append(fieldName.substring(1)).append("() {\n");
                            generatedCode.append("        return ").append(fieldName).append(";\n");
                            generatedCode.append("    }\n");
                            generatedCode.append("    public void set").append(fieldName.substring(0, 1).toUpperCase()).append(fieldName.substring(1)).append("(").append(fieldType).append(" ").append(fieldName).append(") {\n");
                            generatedCode.append("        this.").append(fieldName).append(" = ").append(fieldName).append(";\n");
                            generatedCode.append("    }\n");
                        }
                    }
                    generatedCode.append("}\n");
                    try {
                        JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(element.getSimpleName() + "Generated");
                        Writer writer = sourceFile.openWriter();
                        writer.write(generatedCode.toString());
                        writer.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            return true;
        }
    }
    

    在这个例子中,GetterSetterProcessor 继承自 AbstractProcessor,通过 process 方法处理被 @GenerateGetterSetter 注解标记的类。它会在编译期生成包含 getters 和 setters 方法的新的 Java 源文件。

通过以上内容,我们详细介绍了 Java 自定义注解的创建与使用,包括注解的定义、元注解的使用、在代码中的使用方式、运行期和编译期的处理等方面。自定义注解在 Java 开发中是一个非常强大的工具,可以帮助我们实现很多灵活且高效的功能,无论是在小型项目还是大型企业级应用中都有着广泛的应用。