Java基础系列文章
Java基础(一):初识Java------发展历程、技术体系与JDK环境搭建
Java基础(五):流程控制全解析------分支(if/switch)和循环(for/while)的深度指南
Java基础(七): 面向过程与面向对象、类与对象、成员变量与局部变量、值传递与引用传递、方法重载与方法重写
Java基础(八):封装、继承、多态与关键字this、super详解
目录
- 一、什么是注解(Annotation)
- 二、元注解
-
- 1、@Retention(注解保留策略)
- 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新增-多次使用)
- 五、自定义注解
- 六、处理注解
一、什么是注解(Annotation)
** 注解(Annotation)是Java 5引入的一种元数据机制,可以看作是一种特殊的标记,它提供了一种向代码中添加元信息的方式。这些元信息可以在编译时、类加载时或运行时被读取和处理,从而影响程序的行为。**
注解本质上是一个接口,它继承自java.lang.annotation.Annotation接口。当我们使用@注解名的形式时,实际上是在创建该注解接口的一个实例。
注解是一种趋势,一定程度上可以说:框架 = 注解 + 反射 + 设计模式。
二、元注解
元注解是用于定义其他注解的注解,Java提供了几个重要的元注解来控制自定义注解的行为。
1、@Retention(注解保留策略)
- 用于指定一个 注解(Annotation)的保留策略(Retention Policy),即该注解在
何时有效、存在于哪个阶段
1.1、RetentionPolicy.SOURCE(源代码)
-
含义:注解仅保留在
源代码级别,编译时会被编译器丢弃,不会包含在生成的字节码文件中 -
用途:一般用于
编译期检查、代码生成等场景@Override和@SuppressWarnings仅在编译阶段被编译器用来检查方法重写或抑制警告,不会保留到字节码或影响运行时lombok就是在编译期扫描@Getter等注解,并生成对应方法,注解不会出现在.class文件中
-
示例:
java@Retention(RetentionPolicy.SOURCE) public @interface MySourceAnnotation {}
1.2、RetentionPolicy.CLASS(默认Class文件)
-
含义:注解会被保留到
编译后的字节码文件(.class 文件)中,但在运行时不会被加载到 JVM 中 -
用途:一般用
于字节码处理工具APT(Annotation Processing Tool):在编译期扫描和处理注解,生成额外的代码(如 Dagger、ButterKnife 在早期版本中使用)字节码增强/织入工具:如 ASM、Javassist、AspectJ,在类加载前修改字节码,这些工具可以读取 .class文件中的注解信息,做一些增强操作
-
注意:这是
默认的保留策略,如果不写 @Retention,则默认这个策略 -
示例:
java@Retention(RetentionPolicy.CLASS) public @interface MyClassAnnotation {}
1.3、RetentionPolicy.RUNTIME(常用-运行时)
-
含义:注解会保留到
运行时,可以通过反射机制读取注解信息 -
用途:
最常用的策略,通常用于框架(如 Spring、JUnit)、ORM、自定义业务逻辑等场景,实现基于注解的动态行为控制 -
示例:
java@Retention(RetentionPolicy.RUNTIME) public @interface MyRuntimeAnnotation { String value() default ""; }
2、@Target(注解目标范围)
- 用于指定自定义注解可以
应用的目标范围,也就是该注解可以用在哪些程序元素(如类、方法、字段等)上
2.1、ElementType.TYPE(类)
-
含义:用于
类、接口、枚举、注解类型本身(也叫注解的注解) -
适用场景:自定义注解用于类级别,比如 ORM 框架中的
@Entity、Spring 中的@Component等 -
示例:
java@Target(ElementType.TYPE) public @interface EntityType { String value() default ""; } // 使用 @EntityType("User") public class User { }
2.2、ElementType.FIELD(成员变量)
-
含义:用于
类的字段(成员变量),包括:- 实例字段
- 静态字段
- 枚举常量(因为枚举常量本质也是字段)
-
适用场景:用于字段级别的注解,比如序列化标记、JSON 字段映射、数据库列映射等
-
示例:
java@Target(ElementType.FIELD) public @interface NotNull { } // 使用 public class User { @NotNull private String name; }
2.3、ElementType.METHOD(方法)
-
含义:用于
类的方法 -
适用场景:方法级别的注解,比如权限控制、日志记录、事务管理、API 接口方法等
-
示例:
java@Target(ElementType.METHOD) public @interface LogExecution { } // 使用 public class Service { @LogExecution public void doSomething() { } }
2.4、ElementType.PARAMETER(参数)
-
含义:用于
方法的参数 -
适用场景:参数校验、参数日志、依赖注入等
-
示例:
java@Target(ElementType.PARAMETER) public @interface ValidParam { } // 使用 public class Validator { public void check(@ValidParam String input) { } }
2.5、ElementType.CONSTRUCTOR(构造器)
-
含义:用于
类的构造方法 -
适用场景:依赖注入框架(如 Spring)中用于构造器注入、构造方法权限等
-
示例:
java@Target(ElementType.CONSTRUCTOR) public @interface Injectable { } // 使用 public class Service { @Injectable public Service() { } }
2.6、ElementType.LOCAL_VARIABLE(局部变量)
-
含义:用于
局部变量 -
适用场景:一般用于代码分析、IDE 提示,但运行时通常不可见(因为局部变量信息在编译后往往被丢弃)
-
示例:
java@Target(ElementType.LOCAL_VARIABLE) public @interface DebugValue { }
⚠️ 注意:虽然可以标注,但局部变量注解在运行时通常拿不到(受限于 JVM 字节码和类型擦除)
2.7、ElementType.ANNOTATION_TYPE(注解的注解)
-
含义:专门用于
注解类型本身 -
适用场景:用于
"元注解",即注解的注解,比如你定义一个注解,想让它只能用于其他注解上 -
示例:
java@Target(ElementType.ANNOTATION_TYPE) public @interface MetaAnnotation { }
2.8、ElementType.PACKAGE(包)
-
含义:用于
包(package) -
适用场景:包级别的注解,比如包文档、包权限、包配置等
-
使用方式:需要借助
package-info.java文件来标注 -
示例:
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>)添加注解 -
适用场景:用于标注泛型参数,通常结合注解处理器做类型约束或检查
-
示例:
java@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) - 局部变量类型
- 字段类型(如
-
适用场景:类型安全、非空约束、类型注解、框架校验等高级用法
-
示例:
java@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) -
适用场景:模块系统相关注解,比如模块导出、服务声明等元信息标注
-
示例:
java@Target(ElementType.MODULE) public @interface ModuleInfo { String name(); } // 在 `module-info.java` 中使用 @ModuleInfo(name = "my.module") module my.module { }
3、@Documented(文档)
- 用于指定:当某个注解被用于一个类/方法/字段等目标上时,该注解应该包含在生成的
JavaDoc API文档中
java
@Documented
public @interface MyDocumentedAnnotation {
// ...
}
4、@Inherited(继承)
- 如果一个类被某个注解标注了,那么它的子类(继承该类)也会"继承"这个注解(可通过反射获取到)
- @Inherited只对
类(Class)有效,对接口、方法、字段、枚举等无效
示例场景:定义一个自定义注解,并测试是否具有继承性
1.定义一个注解,加上 @Inherited
java
// 定义一个可继承的注解
@Inherited
@Retention(RetentionPolicy.RUNTIME) // 必须设置为 RUNTIME,否则反射看不到
@Target(ElementType.TYPE) // 只能用于类
public @interface InheritableAnnotation {
String value() default "这是一个可继承的注解";
}
2.父类使用该注解
java
// 父类被 @InheritableAnnotation 标注
@InheritableAnnotation("父类被标注了")
public class ParentClass {
}
3.子类继承父类(但子类本身没有标注该注解)
java
// 子类没有显式使用 @InheritableAnnotation
public class ChildClass extends ParentClass {
}
4.测试:通过反射检查子类是否有该注解
java
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注解)
java
// 容器注解,用于存放多个 @Schedule
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Schedules {
Schedule[] value(); // 必须字段名为 value,且类型是 Schedule 数组
}
⚠️ 注意:
- 这个注解叫
Schedules,它是一个容器,里面可以包含多个@Schedule注解- 它的字段必须是
value(),且类型是Schedule[](即目标注解的数组)- 这是 Java 的强制约定,容器注解必须有一个名为 value()的数组字段来存放可重复的注解
2.定义目标注解 @Schedule,并加上 @Repeatable
java
// 可重复的注解
@Repeatable(Schedules.class) // 👈 指定它的容器是 Schedules.class
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Schedule {
String time(); // 表示调度时间,比如 "10:00"
}
⚠️ 注意:
开发中常常12步骤合并,将容器@Schedules的定义放到@Schedule里面
java@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
java
// 现在可以合法地多次使用 @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// Java 编译器实际上会将其转换为类似下面的形式(伪代码): @Schedules({ @Schedule(time = "10:00"), @Schedule(time = "14:00"), @Schedule(time = "14:00") }) public class DailyTask { ... }
4.通过反射获取多个 @Schedule注解
java
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关键字来定义注解,这实际上定义了一个
特殊的接口
基本语法:
java
@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.定义好注解后,就可以在代码中使用它
java
@MyCustomAnnotation(value = "重要方法", priority = 1, tags = {"业务", "核心"})
public void importantBusinessMethod() {
// 业务逻辑
}
2.如果注解元素使用了默认值,可以省略这些元素
java
@MyCustomAnnotation // 使用所有默认值
public void simpleMethod() {
// 简单逻辑
}
3.对于只有一个元素的注解,如果该元素名为"value",在使用时可以省略元素名
java
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)
java
// 该注解只能用于类上
@Target(ElementType.TYPE)
// 仅在源码级别保留,编译后不保留(也可用 CLASS,视需求而定)
@Retention(RetentionPolicy.SOURCE)
public @interface GenerateBuilder {
}
2.注解处理器是核心,你需要继承 javax.annotation.processing.AbstractProcessor,并实现相关方法
java
// 声明支持的注解类型
@SupportedAnnotationTypes("com.example.GenerateBuilder") // 替换为你的注解全限定名
// 声明支持的 Java 版本
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class GenerateBuilderProcessor extends AbstractProcessor {
@Override
public 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/目录下创建文件
java
src/main/resources/META-INF/services/javax.annotation.processing.Processor
- 文件内容(你的处理器类的
全限定名,每行一个)
java
com.example.GenerateBuilderProcessor
2、运行时处理
** 通过Java反射API在运行时处理注解,这是最常见的使用方式。**
java
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()));
}
}
}