文章目录
前言
这里是分享 Java 相关内容的专刊,每日一更。
本期将为大家带来以下内容:
- 什么是 Java 注解
- 为什么需要注解
- 注解的类型
- 注解的生命周期
- 注解的工作原理
- 注解继承
什么是 Java 注解
Java 注解(Annotations)就是在代码里加上一些 特殊的标记,这些标记可以告诉编译器、运行时系统或者一些工具,这段代码应该怎么被处理。它们就像是给代码加上了“说明书”,但是这些“说明书”不会直接改变代码的执行效果。
为什么需要注解
在日常编程中,我们写的代码可能会被很多工具处理,比如编译器会检查我们有没有写错,框架(比如 Spring)可能会根据我们的配置来管理对象。注解可以帮助这些工具理解我们的代码,并让这些工具为我们做更多的事情,比如减少我们写配置文件的麻烦、生成一些常见的代码等。
注解的类型
Java 中的注解分为三大类:内置注解、元注解 和 自定义注解。此外,注解根据生命周期还可分为:编译时注解 和 运行时注解。
内置注解
Java 提供了一些常用的注解,用来帮助开发者进行代码检查或优化。以下是最常见的内置注解:
@Override
:标识方法重写。如果一个方法没有正确重写父类的方法,编译器会报错。@Deprecated
:标识某个类或方法已过时,提醒开发者不要使用。编译器会发出警告。@SuppressWarnings
:用于忽略编译器的某些警告。常见参数如"unchecked"
用来忽略未经检查的操作。
元注解
元注解是用来修饰其他注解的注解。常见的元注解有:
@Retention
:指定注解的生命周期(SOURCE、CLASS、RUNTIME)。@Target
:限定注解能被应用于哪些元素(类、方法、字段等)。@Inherited
:允许子类继承父类的注解。@Documented
:将注解包含在 Javadoc 中。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAnnotation {
String value();
}
@Retention
决定注解的保留时间,如 RetentionPolicy.RUNTIME
表示注解在运行时可以通过反射读取。
@Target
限制注解的使用范围,如 ElementType.METHOD
表示注解只能用于方法。
自定义注解
Java 允许开发者定义自己的注解,来适应特殊需求。自定义注解通常有属性,这些属性可以设置默认值或由使用者来赋值。
@interface MyAnnotation {
String name() default "defaultName";
int version() default 1;
}
使用自定义注解:
@MyAnnotation(name = "Test", version = 2)
public void someMethod() {
// 方法内容
}
注解的生命周期
注解的生命周期由 @Retention
元注解决定,注解可以有以下三种保留策略:
SOURCE
:注解只存在于源码中,编译时被丢弃,无法在.class
文件中看到,常用于编译时检查。CLASS
:注解被保存在.class
文件中,但不会加载到 JVM 中,运行时不可见(默认)。RUNTIME
:注解在运行时依然存在,JVM 加载类时不会丢弃,可以通过反射获取和使用注解。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogExecutionTime {
}
上述注解使用了 RetentionPolicy.RUNTIME
,意味着它在运行时依然存在,并可以通过反射机制读取。
注解的工作原理
Java 注解的工作原理主要体现在两个方面:
- 编译时处理(Annotation Processing):注解处理器在编译期间通过扫描和处理源代码上的注解,生成代码或配置。
- 运行时处理(Reflection Processing):注解保留到运行时,通过反射机制获取注解信息,并执行相应的操作。
编译时处理(Annotation Processing)
编译时注解处理器(Annotation Processor) 是一种机制,它允许开发者在代码编译时分析注解并生成额外的代码。这个机制在 Java 5 中引入,并在 Java 6 中通过 javax.annotation.processing
包标准化。编译时处理是通过注解处理器类实现的,这些类会扫描代码中的特定注解,并对其做出相应的响应。
注解处理器的工作机制
编译时注解处理器的工作流程可以分为以下几个步骤:
- 注册注解处理器:注解处理器(
Annotation Processor
)是实现了javax.annotation.processing.Processor
接口的类,处理器通过@SupportedAnnotationTypes
指定处理哪些注解。 - 解析注解:编译器(如
javac
)会在编译期间扫描所有带有注解的元素(类、方法、字段等),并将这些元素及其注解传递给注解处理器。 - 生成代码或报告错误:注解处理器会基于解析的注解信息执行某些操作,例如生成新的 Java 源代码、修改已有代码、生成配置文件或在编译阶段报告错误。
- 多轮处理:注解处理可能需要多轮处理(
rounds
)。当某个注解处理器生成了新的 Java 源代码时,编译器会触发下一轮处理,直到没有新的代码生成为止。
编写自定义注解处理器
下面是一个简单的编译时注解处理器的例子:
Step 1:定义一个注解
// 定义注解
@Retention(RetentionPolicy.SOURCE) // 注解只存在于源码中
@Target(ElementType.METHOD) // 注解可以应用在方法上
public @interface PrintAnnotation {
String value() default "Hello, Annotation Processor!";
}
Step 2:编写注解处理器
// 注册处理器,指定该处理器处理哪些注解
@SupportedAnnotationTypes("PrintAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class PrintAnnotationProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(PrintAnnotation.class)) {
// 获取注解
PrintAnnotation annotation = element.getAnnotation(PrintAnnotation.class);
// 打印注解信息
System.out.println("Processing method: " + element.getSimpleName() + " with message: " + annotation.value());
}
return true; // 表示注解处理完成
}
}
Step 3:使用注解
public class Test {
@PrintAnnotation(value = "Custom message")
public void myMethod() {
System.out.println("Executing myMethod.");
}
}
Step 4:编译和执行
在编译时,注解处理器会捕获 @PrintAnnotation
的使用,并根据注解信息输出处理结果。
多轮处理
注解处理器可以生成新的源文件,如果在第一轮处理生成了新的注解代码,编译器会再次调用注解处理器以处理这些新的注解。这个过程可以发生多轮,直到没有新的注解需要处理为止。
常见的编译时注解应用
Lombok:@Data
、@Getter
、@Setter
等注解,自动生成 getter 和 setter 等方法。
Dagger:依赖注入框架,在编译时生成依赖注入的代码。
AutoValue:Google 的库,用于生成不可变类的代码。
运行时处理(Reflection Processing)
Java 中有一类注解保留到运行时(通过 @Retention(RetentionPolicy.RUNTIME)
标记)。这些注解在程序运行期间可以通过 反射机制 进行处理。反射是 Java 提供的一个非常强大的特性,它允许程序在运行时检查和操作类的结构(包括类、方法、字段等)。
运行时注解的获取
当注解使用 @Retention(RetentionPolicy.RUNTIME)
进行标记时,它将在字节码中保留,并且在运行时仍然存在。开发者可以使用 Java 的反射 API 来获取这些注解,并基于这些注解执行特定逻辑。
例如,Spring 框架在依赖注入(@Autowired
)、事务管理(@Transactional
)等场景中,都会在运行时通过反射处理注解,决定如何处理代码中的逻辑。
通过反射获取注解的例子
下面是一个示例,演示如何在运行时通过反射获取注解信息并处理:
// 定义运行时注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogExecutionTime {
}
// 使用注解的类
public class TestClass {
@LogExecutionTime
public void executeTask() {
System.out.println("Task executed.");
}
}
// 运行时反射处理注解
public class AnnotationProcessor {
public static void main(String[] args) throws Exception {
// 获取 TestClass 类的 Method 对象
Method method = TestClass.class.getMethod("executeTask");
// 检查方法上是否存在 LogExecutionTime 注解
if (method.isAnnotationPresent(LogExecutionTime.class)) {
long startTime = System.currentTimeMillis();
// 调用方法
method.invoke(new TestClass());
long endTime = System.currentTimeMillis();
System.out.println("Execution time: " + (endTime - startTime) + " ms");
}
}
}
反射获取注解信息的步骤
- 获取类、方法或字段的反射对象:通过
Class.getMethod()
、Class.getField()
或Class.getDeclaredMethods()
来获取反射对象。 - 检查是否存在注解:使用
isAnnotationPresent(Class<? extends Annotation> annotationClass)
来判断某个元素上是否标记了特定注解。 - 获取注解实例:使用
getAnnotation(Class<T> annotationClass)
来获取注解实例,之后可以调用注解的属性。 - 执行逻辑:根据获取到的注解信息,在运行时执行相应的逻辑。
编译时和运行时注解处理的比较
特性 | 编译时注解处理 | 运行时注解处理 |
---|---|---|
注解生命周期 | RetentionPolicy.SOURCE 或 CLASS | RetentionPolicy.RUNTIME |
处理时机 | 编译期间 | 程序运行期间 |
典型应用 | 代码生成、编译时校验 | 依赖注入、AOP、事务管理 |
性能影响 | 无运行时开销 | 运行时反射可能影响性能 |
工具 | Annotation Processor | Reflection API |
常见库 | Lombok、Dagger、AutoValue | Spring、Hibernate、JAX-RS |
注解继承
尽管 Java 的注解不能真正被继承,但可以通过将注解标记为 @Inherited
来实现类似的行为。当你给一个类加上 @Inherited
注解时,它的子类也会继承这个注解。不过,注解只会被类继承,不能被方法继承。
示例:
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyInheritedAnnotation {
String value() default "Inherited Annotation";
}
@MyInheritedAnnotation
class ParentClass {}
class ChildClass extends ParentClass {}
当你通过反射获取 ChildClass
的注解时,尽管它没有显式声明 @MyInheritedAnnotation
,但仍然会继承 ParentClass
的注解。
public class Main {
public static void main(String[] args) {
boolean isAnnotated = ChildClass.class.isAnnotationPresent(MyInheritedAnnotation.class);
System.out.println("ChildClass has @MyInheritedAnnotation: " + isAnnotated);
}
}
输出:ChildClass has @MyInheritedAnnotation: true
本期小知识
Java 8 引入了 类型注解(Type Annotations),可以将注解应用在几乎所有类型使用的地方,而不仅仅是类或方法声明上。以下是一些少见但可能有用的例子:
应用在数组中:
String @MyAnnotation [] myArray;
应用在类型转换中:
MyClass obj = (@MyAnnotation MyClass) anotherObject;
应用在构造函数中:
public MyClass() throws @MyAnnotation IOException {
// constructor code
}