Java基础(十五):注解(Annotation)详解
Java基础系列文章
Java基础(一):初识Java——发展历程、技术体系与JDK环境搭建
Java基础(二):八种基本数据类型详解
Java基础(三):逻辑运算符详解
Java基础(四):位运算符详解
Java基础(五):流程控制全解析——分支(if/switch)和循环(for/while)的深度指南
Java基础(六):数组全面解析
Java基础(七): 面向过程与面向对象、类与对象、成员变量与局部变量、值传递与引用传递、方法重载与方法重写
Java基础(八):封装、继承、多态与关键字this、super详解
Java基础(九):Object核心类深度剖析
Java基础(十):关键字static详解
Java基础(十一):关键字final详解
Java基础(十二):抽象类与接口详解
Java基础(十三):内部类详解
Java基础(十四):枚举类详解
Java基础(十五):注解(Annotation)详解
目录
- 一、什么是注解(Annotation)
- 二、元注解
- 1、@Retention(注解保留策略)
- 1.1、RetentionPolicy.SOURCE(源代码)
- 1.2、RetentionPolicy.CLASS(默认Class文件)
- 1.3、RetentionPolicy.RUNTIME(常用-运行时)
- 2、@Target(注解目标范围)
- 2.1、ElementType.TYPE(类)
- 2.2、ElementType.FIELD(成员变量)
- 2.3、ElementType.METHOD(方法)
- 2.4、ElementType.PARAMETER(参数)
- 2.5、ElementType.CONSTRUCTOR(构造器)
- 2.6、ElementType.LOCAL_VARIABLE(局部变量)
- 2.7、ElementType.ANNOTATION_TYPE(注解的注解)
- 2.8、ElementType.PACKAGE(包)
- 2.9、ElementType.TYPE_PARAMETER(Java8新增-泛型参数)
- 2.10、ElementType.TYPE_USE(Java8新增-类型)
- 2.11、ElementType.MODULE(Java9新增-模块)
- 3、@Documented(文档)
- 4、@Inherited(继承)
- 5、@Repeatable(Java8新增-多次使用)
- 五、自定义注解
- 1. 定义注解
- 2、注解元素类型
- 3、使用自定义注解
- 六、处理注解
- 1、编译时处理
- 1.1、编译时注解处理的作用
- 1.2、编写注解处理器
- 2、运行时处理
一、什么是注解(Annotation)
注解(Annotation)是Java 5引入的一种元数据机制,可以看作是一种特殊的标记,它提供了一种向代码中添加元信息的方式。这些元信息可以在编译时、类加载时或运行时被读取和处理,从而影响程序的行为。
注解本质上是一个接口,它继承自java.lang.annotation.Annotation接口。当我们使用@注解名的形式时,实际上是在创建该注解接口的一个实例。
注解是一种趋势,一定程度上可以说:框架 = 注解 + 反射 + 设计模式。
二、元注解
元注解是用于定义其他注解的注解,Java提供了几个重要的元注解来控制自定义注解的行为。
1、@Retention(注解保留策略)
- 用于指定一个 注解(Annotation)的保留策略(Retention Policy),即该注解在
何时有效、存在于哪个阶段
1.1、RetentionPolicy.SOURCE(源代码)
- 含义:注解仅保留在
源代码级别,编译时会被编译器丢弃,不会包含在生成的字节码文件中 - 用途:一般用于
编译期检查、代码生成等场景@Override和@SuppressWarnings仅在编译阶段被编译器用来检查方法重写或抑制警告,不会保留到字节码或影响运行时lombok就是在编译期扫描@Getter等注解,并生成对应方法,注解不会出现在.class文件中
- 示例:
@Retention(RetentionPolicy.SOURCE) public @interface MySourceAnnotation {}
1.2、RetentionPolicy.CLASS(默认Class文件)
- 含义:注解会被保留到
编译后的字节码文件(.class 文件)中,但在运行时不会被加载到 JVM 中 - 用途:一般用
于字节码处理工具APT(Annotation Processing Tool):在编译期扫描和处理注解,生成额外的代码(如 Dagger、ButterKnife 在早期版本中使用)字节码增强/织入工具:如 ASM、Javassist、AspectJ,在类加载前修改字节码,这些工具可以读取 .class文件中的注解信息,做一些增强操作
- 注意:这是
默认的保留策略,如果不写 @Retention,则默认这个策略 - 示例:
@Retention(RetentionPolicy.CLASS) public @interface MyClassAnnotation {}
1.3、RetentionPolicy.RUNTIME(常用-运行时)
- 含义:注解会保留到
运行时,可以通过反射机制读取注解信息 - 用途:
最常用的策略,通常用于框架(如 Spring、JUnit)、ORM、自定义业务逻辑等场景,实现基于注解的动态行为控制 - 示例:
@Retention(RetentionPolicy.RUNTIME) public @interface MyRuntimeAnnotation {String value() default ""; }
2、@Target(注解目标范围)
- 用于指定自定义注解可以
应用的目标范围,也就是该注解可以用在哪些程序元素(如类、方法、字段等)上
2.1、ElementType.TYPE(类)
- 含义:用于
类、接口、枚举、注解类型本身(也叫注解的注解) - 适用场景:自定义注解用于类级别,比如 ORM 框架中的
@Entity、Spring 中的@Component等 - 示例:
@Target(ElementType.TYPE) public @interface EntityType {String value() default ""; }// 使用 @EntityType("User") public class User { }
2.2、ElementType.FIELD(成员变量)
- 含义:用于
类的字段(成员变量),包括:- 实例字段
- 静态字段
- 枚举常量(因为枚举常量本质也是字段)
- 适用场景:用于字段级别的注解,比如序列化标记、JSON 字段映射、数据库列映射等
- 示例:
@Target(ElementType.FIELD) public @interface NotNull { }// 使用 public class User {@NotNullprivate String name; }
2.3、ElementType.METHOD(方法)
- 含义:用于
类的方法 - 适用场景:方法级别的注解,比如权限控制、日志记录、事务管理、API 接口方法等
- 示例:
@Target(ElementType.METHOD) public @interface LogExecution { }// 使用 public class Service {@LogExecutionpublic void doSomething() {} }
2.4、ElementType.PARAMETER(参数)
- 含义:用于
方法的参数 - 适用场景:参数校验、参数日志、依赖注入等
- 示例:
@Target(ElementType.PARAMETER) public @interface ValidParam { }// 使用 public class Validator {public void check(@ValidParam String input) {} }
2.5、ElementType.CONSTRUCTOR(构造器)
- 含义:用于
类的构造方法 - 适用场景:依赖注入框架(如 Spring)中用于构造器注入、构造方法权限等
- 示例:
@Target(ElementType.CONSTRUCTOR) public @interface Injectable { }// 使用 public class Service {@Injectablepublic Service() {} }
2.6、ElementType.LOCAL_VARIABLE(局部变量)
- 含义:用于
局部变量 - 适用场景:一般用于代码分析、IDE 提示,但运行时通常不可见(因为局部变量信息在编译后往往被丢弃)
- 示例:
@Target(ElementType.LOCAL_VARIABLE) public @interface DebugValue { }
⚠️ 注意:虽然可以标注,但局部变量注解在运行时通常拿不到(受限于 JVM 字节码和类型擦除)
2.7、ElementType.ANNOTATION_TYPE(注解的注解)
- 含义:专门用于
注解类型本身 - 适用场景:用于
“元注解”,即注解的注解,比如你定义一个注解,想让它只能用于其他注解上 - 示例:
@Target(ElementType.ANNOTATION_TYPE) public @interface MetaAnnotation { }
2.8、ElementType.PACKAGE(包)
- 含义:用于
包(package) - 适用场景:包级别的注解,比如包文档、包权限、包配置等
- 使用方式:需要借助
package-info.java文件来标注 - 示例:
// 定义注解 // File: src/main/java/com/example/annotations/PackageInfo.java package com.example.annotations;import java.lang.annotation.*;@Target(ElementType.PACKAGE) @Retention(RetentionPolicy.RUNTIME) public @interface PackageInfo {String author();String version();String description() default ""; }// 使用注解(在 package-info.java中) // File: src/main/java/com/example/package-info.java @PackageInfo(author = "Alice Wang",version = "1.2.0",description = "该包包含与订单处理相关的所有类,如 OrderService, OrderController 等" ) package com.example;// 在运行时获取包注解信息 // File: src/main/java/com/example/annotations/PackageAnnotationReader.java public class PackageAnnotationReader {public static void main(String[] args) {// 获取包对象Package pkg = Package.getPackage("com.example");// 获取包上的 PackageInfo 注解PackageInfo info = pkg.getAnnotation(PackageInfo.class);System.out.println("📦 包名: com.example");System.out.println("👤 作者: " + info.author());System.out.println("🏷️ 版本: " + info.version());System.out.println("📝 描述: " + info.description());} }
⚠️注意:该文件中不要定义任何类!
2.9、ElementType.TYPE_PARAMETER(Java8新增-泛型参数)
- 含义:用于
泛型类型参数,也就是在定义泛型类、泛型方法或泛型接口时,给类型参数(如<T>、<K, V>)添加注解 - 适用场景:用于标注泛型参数,通常结合注解处理器做类型约束或检查
- 示例:
@Target(ElementType.TYPE_PARAMETER) public @interface TypeParam { }// 使用 public class Box<@TypeParam T> { }
2.10、ElementType.TYPE_USE(Java8新增-类型)
- 含义:用于
类型被使用的任何地方- 字段类型(如
private String name;中的String) - 方法参数类型
- 方法返回类型
- 泛型类型参数实例化(如
List<@TypeUse String>) - 强制类型转换类型(如
(@TypeUse String) obj) - 局部变量类型
- 字段类型(如
- 适用场景:类型安全、非空约束、类型注解、框架校验等高级用法
- 示例:
@Target(ElementType.TYPE_USE) public @interface NonNull { }// 使用 @NonNull String name; // 字段类型上使用List<@NonNull String> list; // 泛型类型参数使用
⚠️注意:由于Java的类型擦除 + 反射API限制,
类型参数上的注解(包括TYPE_PARAMETER和TYPE_USE)在运行时通常不可见
2.11、ElementType.MODULE(Java9新增-模块)
- 含义:用于
模块(module-info.java) - 适用场景:模块系统相关注解,比如模块导出、服务声明等元信息标注
- 示例:
@Target(ElementType.MODULE) public @interface ModuleInfo {String name(); }// 在 `module-info.java` 中使用 @ModuleInfo(name = "my.module") module my.module { }
3、@Documented(文档)
- 用于指定:当某个注解被用于一个类/方法/字段等目标上时,该注解应该包含在生成的
JavaDoc API文档中
@Documented
public @interface MyDocumentedAnnotation {// ...
}
4、@Inherited(继承)
- 如果一个类被某个注解标注了,那么它的子类(继承该类)也会“继承”这个注解(可通过反射获取到)
- @Inherited只对
类(Class)有效,对接口、方法、字段、枚举等无效
示例场景:定义一个自定义注解,并测试是否具有继承性
1.定义一个注解,加上 @Inherited
// 定义一个可继承的注解
@Inherited
@Retention(RetentionPolicy.RUNTIME) // 必须设置为 RUNTIME,否则反射看不到
@Target(ElementType.TYPE) // 只能用于类
public @interface InheritableAnnotation {String value() default "这是一个可继承的注解";
}
2.父类使用该注解
// 父类被 @InheritableAnnotation 标注
@InheritableAnnotation("父类被标注了")
public class ParentClass {
}
3.子类继承父类(但子类本身没有标注该注解)
// 子类没有显式使用 @InheritableAnnotation
public class ChildClass extends ParentClass {
}
4.测试:通过反射检查子类是否有该注解
public class InheritanceTest {public static void main(String[] args) {// 检查父类Class<ParentClass> parentClass = ParentClass.class;InheritableAnnotation parentAnnotation = parentClass.getAnnotation(InheritableAnnotation.class);System.out.println("父类是否有 @InheritableAnnotation: " + (parentAnnotation != null)); // true// 检查子类Class<ChildClass> childClass = ChildClass.class;InheritableAnnotation childAnnotation = childClass.getAnnotation(InheritableAnnotation.class);System.out.println("子类是否有 @InheritableAnnotation: " + (childAnnotation != null)); // true}
}
5、@Repeatable(Java8新增-多次使用)
- 表示一个注解可以被
多次使用在同一个目标上
场景:我们想定义一个 @Schedule注解,表示任务调度时间,并允许一个类上标注多个调度时间
1.定义一个 容器注解@Schedules(用来存放多个@Schedule注解)
// 容器注解,用于存放多个 @Schedule
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Schedules {Schedule[] value(); // 必须字段名为 value,且类型是 Schedule 数组
}
⚠️ 注意:
- 这个注解叫
Schedules,它是一个容器,里面可以包含多个@Schedule注解- 它的字段必须是
value(),且类型是Schedule[](即目标注解的数组)- 这是 Java 的强制约定,容器注解必须有一个名为 value()的数组字段来存放可重复的注解
2.定义目标注解 @Schedule,并加上 @Repeatable
// 可重复的注解
@Repeatable(Schedules.class) // 👈 指定它的容器是 Schedules.class
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Schedule {String time(); // 表示调度时间,比如 "10:00"
}
⚠️ 注意:
开发中常常12步骤合并,将容器@Schedules的定义放到@Schedule里面@Repeatable(Schedule.Schedules.class) @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Schedule {String time();@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)@interface Schedules {Schedule[] value();} }
3.使用:在一个类上多次使用 @Schedule
// 现在可以合法地多次使用 @Schedule 注解了!
@Schedule(time = "10:00")
@Schedule(time = "14:00")
@Schedule(time = "18:00")
public class DailyTask {public void execute() {System.out.println("执行日常任务...");}
}
⚠️ 注意:
Java 编译器实际上会把这三个@Schedule注解封装进一个隐藏的 @Schedules容器注解中// Java 编译器实际上会将其转换为类似下面的形式(伪代码): @Schedules({@Schedule(time = "10:00"),@Schedule(time = "14:00"),@Schedule(time = "14:00") }) public class DailyTask { ... }
4.通过反射获取多个 @Schedule注解
public class RepeatableAnnotationDemo {public static void main(String[] args) {Class<DailyTask> taskClass = DailyTask.class;// 方法 1:直接获取所有的 @Schedule 注解(Java 做了适配,可以直接获取)Schedule[] schedules = taskClass.getAnnotationsByType(Schedule.class);for (Schedule schedule : schedules) {System.out.println("调度时间: " + schedule.time());}// 方法 2:也可以获取容器注解 @Schedules(一般不需要手动处理)Schedules container = taskClass.getAnnotation(Schedules.class);if (container != null) {for (Schedule s : container.value()) {System.out.println("[通过容器] 调度时间: " + s.time());}}}
}
五、自定义注解
1. 定义注解
- 使用@interface关键字来定义注解,这实际上定义了一个
特殊的接口
基本语法:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyCustomAnnotation {// 注解元素定义String value() default "默认值";int priority() default 0;String[] tags() default {};
}
2、注解元素类型
- 元素类型只能是
基本类型、String、Class、枚举、注解或这些类型的数组 - 元素可以有默认值,使用
default关键字指定,否则必须为改元素提供值 - 如果注解只有一个元素且名称为
value,在使用时可以省略value=前缀
3、使用自定义注解
1.定义好注解后,就可以在代码中使用它
@MyCustomAnnotation(value = "重要方法", priority = 1, tags = {"业务", "核心"})
public void importantBusinessMethod() {// 业务逻辑
}
2.如果注解元素使用了默认值,可以省略这些元素
@MyCustomAnnotation // 使用所有默认值
public void simpleMethod() {// 简单逻辑
}
3.对于只有一个元素的注解,如果该元素名为"value",在使用时可以省略元素名
public @interface SingleValueAnnotation {String value();
}// 使用时
@SingleValueAnnotation("只需要提供值")
public void method() {}
六、处理注解
1、编译时处理
Java 注解的编译时处理是指在 Java 源代码编译阶段(即.java→ .class的过程中),通过注解处理器对带有特定注解的代码进行分析、验证或生成新的源代码、资源文件等,而无需运行程序。
1.1、编译时注解处理的作用
代码生成:如 Lombok通过注解生成 getter/setter/toString()等方法代码校验:检查代码是否符合某些规范,如注解使用是否正确模板代码生成:如生成 Builder 模式、DAO 层代码、RPC 接口桩代码等减少样板代码:自动生成重复性代码,提高开发效率
1.2、编写注解处理器
1.首先定义一个注解,并指定其保留策略为SOURCE或CLASS(编译时处理一般不用 RUNTIME)
// 该注解只能用于类上
@Target(ElementType.TYPE)
// 仅在源码级别保留,编译后不保留(也可用 CLASS,视需求而定)
@Retention(RetentionPolicy.SOURCE)
public @interface GenerateBuilder {
}
2.注解处理器是核心,你需要继承 javax.annotation.processing.AbstractProcessor,并实现相关方法
// 声明支持的注解类型
@SupportedAnnotationTypes("com.example.GenerateBuilder") // 替换为你的注解全限定名
// 声明支持的 Java 版本
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class GenerateBuilderProcessor extends AbstractProcessor {@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {// 遍历所有被 @GenerateBuilder 注解的元素for (Element element : roundEnv.getElementsAnnotatedWith(GenerateBuilder.class)) {System.out.println("发现 @GenerateBuilder 注解的类: " + element.getSimpleName());// 这里可以生成代码,进行校验等操作// 通常我们会使用 JavaPoet 等库来生成 .java 源文件}return true; // 表示已经处理了这些注解,其他处理器无需再处理}
}
3.注册方式
- 在项目的
resources/META-INF/services/目录下创建文件
src/main/resources/META-INF/services/javax.annotation.processing.Processor
- 文件内容(你的处理器类的
全限定名,每行一个)
com.example.GenerateBuilderProcessor
2、运行时处理
通过Java反射API在运行时处理注解,这是最常见的使用方式。
public class AnnotationProcessor {public static void main(String[] args) throws NoSuchMethodException {// 获取类上的注解Class<MyAnnotatedClass> clazz = MyAnnotatedClass.class;// 检查类是否有特定注解if (clazz.isAnnotationPresent(MyCustomAnnotation.class)) {MyCustomAnnotation classAnnotation = clazz.getAnnotation(MyCustomAnnotation.class);System.out.println("Class annotation: " + classAnnotation);}// 获取方法上的注解Method method = clazz.getMethod("testMethod");if (method.isAnnotationPresent(MyCustomAnnotation.class)) {MyCustomAnnotation methodAnnotation = method.getAnnotation(MyCustomAnnotation.class);System.out.println("Method annotation name: " + methodAnnotation.name());System.out.println("Method annotation priority: " + methodAnnotation.priority());System.out.println("Method annotation tags: " + Arrays.toString(methodAnnotation.tags()));}}
}
