Java自定义注解的创建与使用
Java 自定义注解的创建与使用
在 Java 编程中,注解(Annotation)是一种元数据(Metadata)形式,它为我们在代码中添加额外信息提供了一种便捷的方式。这些信息可以在编译期、运行期被读取并使用,从而实现一些强大的功能,如代码检查、配置管理、依赖注入等。Java 本身提供了一些内置注解,例如 @Override
、@Deprecated
、@SuppressWarnings
等,这些注解在特定场景下发挥着重要作用。然而,在实际项目开发中,我们常常需要根据业务需求自定义注解来满足特定的功能需求。下面我们就来详细探讨 Java 自定义注解的创建与使用。
自定义注解的创建
-
定义注解格式 在 Java 中,定义一个自定义注解非常简单,只需要使用
@interface
关键字。注解定义的语法形式如下:public @interface AnnotationName { // 注解元素(成员变量)定义 }
这里
AnnotationName
是自定义注解的名称,我们可以根据实际需求进行命名。注解内部可以定义一些元素(类似于接口中的方法),这些元素是注解的核心组成部分,它们可以有默认值,也可以在使用注解时进行赋值。 -
定义注解元素 注解元素的定义方式和接口中的方法定义类似,但有一些特殊的规则。
- 基本数据类型、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
类型的元素,没有默认值,所以在使用注解时必须为其赋值。name
是String
类型的元素,有默认值"defaultName"
,如果在使用注解时不指定name
的值,就会使用这个默认值。targetClass
是Class<?>
类型的元素,同样没有默认值,需要在使用注解时指定。myEnum
是自定义枚举类型MyEnum
的元素,有默认值MyEnum.VALUE1
。tags
是String
类型的数组元素,使用注解时需要为其提供数组值。- 注解类型:注解元素也可以是另一个注解类型。例如,我们先定义一个子注解
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)
效果是一样的,但更简洁。 -
元注解(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
注解。
- @Retention:
自定义注解的使用
-
在代码中使用自定义注解 一旦我们定义好了自定义注解,就可以在合适的目标元素上使用它。例如,我们使用前面定义的
@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; // 其他属性和方法 }
在使用注解时,需要根据注解元素是否有默认值来决定是否为其赋值。如果没有默认值,必须显式赋值;如果有默认值,可以选择使用默认值或者显式赋值。
-
在运行期读取注解信息(反射机制) 对于
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
注解标记。 -
基于自定义注解实现特定功能 自定义注解不仅仅是为了标记信息,更重要的是基于这些注解实现特定的功能。例如,我们可以基于
@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
注解的方法执行时间记录功能。 -
结合编译期处理自定义注解(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 开发中是一个非常强大的工具,可以帮助我们实现很多灵活且高效的功能,无论是在小型项目还是大型企业级应用中都有着广泛的应用。