Java注解:给代码贴“小纸条”的修仙秘籍
各位道友,大家好!我是会编程的吕洞宾。今天咱们来聊聊Java里一个神奇的功能——注解(Annotation)。这玩意儿就像给代码贴“小纸条”,告知编译器、工具或者运行时系统一些额外信息。别小看这些“小纸条”,它们可是现代Java开发的“法宝”!

一、注解基础语法:贴“小纸条”的正确姿势
1.1 定义注解:创建自己的“小纸条模板”
注解本质上是一种特殊的接口,用@interface关键字定义。就像这样:
// 定义一个简单的注解
@interface MyAnnotation {
String value() default "default value";
int count() default 0;
}
特征说明:
- 使用@interface关键字:不是class也不是interface,是特殊的注解类型
- 可以包含元素:就像方法声明,但没有方法体
- 可以有默认值:使用default关键字指定默认值
- 元素类型有限制:只能是基本类型、String、Class、枚举、注解或这些类型的数组
1.2 元注解:给“小纸条”贴标签
元注解就是用来注解注解的注解(绕口令来了!)。Java提供了几个内置的元注解:
@Target:指定注解可以用在哪里
import java.lang.annotation.*;
@Target(ElementType.METHOD) // 只能用在方法上
@interface MethodOnly {}
@Target({ElementType.TYPE, ElementType.FIELD}) // 可以用在类和字段上
@interface TypeOrField {}
支持的ElementType值:
- TYPE:类、接口、枚举
- FIELD:字段(包括枚举常量)
- METHOD:方法
- PARAMETER:参数
- CONSTRUCTOR:构造器
- LOCAL_VARIABLE:局部变量
- ANNOTATION_TYPE:注解类型
- PACKAGE:包
- TYPE_PARAMETER:类型参数(Java 8+)
- TYPE_USE:类型使用(Java 8+)
@Retention:指定注解保留到什么时候
@Retention(RetentionPolicy.SOURCE) // 只在源码中保留,编译后丢弃
@interface SourceOnly {}
@Retention(RetentionPolicy.CLASS) // 保留到class文件,但运行时不可用
@interface ClassOnly {}
@Retention(RetentionPolicy.RUNTIME) // 运行时可用,可以通过反射获取
@interface RuntimeAvailable {}
三种保留策略:
- SOURCE:像临时便签,编译完就扔
- CLASS:像写在课本上的笔记,能保存但不能随时查看
- RUNTIME:像随身携带的小抄,随时可用
@Documented:让注解出目前JavaDoc中
@Documented
@interface DocumentedAnnotation {
String description();
}
作用:被这个注解标注的元素,其JavaDoc中会显示这个注解信息。
@Inherited:让子类继承父类的注解
@Inherited
@interface InheritableAnnotation {}
@InheritableAnnotation
class Parent {}
class Child extends Parent {} // Child也会继承@InheritableAnnotation
注意:只对类继承有效,对接口、方法等无效。
@Repeatable:允许重复使用同一个注解
@interface Authors {
Author[] value();
}
@Repeatable(Authors.class)
@interface Author {
String name();
}
@Author(name = "张三")
@Author(name = "李四")
class MultiAuthorClass {}
Java 8新特性:同一个元素上可以多次使用同一个注解。
1.3 使用注解:贴“小纸条”实战
// 定义一个测试注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Test {
String description() default "";
boolean enabled() default true;
}
// 使用注解
class MyTestClass {
@Test(description = "测试加法功能")
public void testAddition() {
System.out.println("Testing addition...");
}
@Test(enabled = false, description = "暂时禁用的测试")
public void disabledTest() {
System.out.println("This won't run");
}
@Test // 使用默认值
public void defaultTest() {
System.out.println("Default test");
}
}
二、编写注解处理器:解读“小纸条”的智慧
2.1 注解元素:小纸条上写什么
注解元素有一些限制:
- 不能有参数:元素声明看起来像方法,但不能有参数
- 不能抛出异常:不能声明throws子句
- 不能是泛型:元素类型不能是类型参数
// 正确的注解定义
@interface GoodAnnotation {
String name(); // OK
int count() default 10; // OK
Class<?> type() default Object.class; // OK
String[] tags() default {}; // OK
}
// 错误的注解定义(编译错误)
@interface BadAnnotation {
// String process(String input); // 错误:不能有参数
// void method() throws Exception; // 错误:不能抛出异常
// List<String> list(); // 错误:不能使用泛型(但Class<?>是允许的)
}
2.2 默认值的限制
默认值必须是编译时常量:
- 基本类型和String的字面量
- Class字面量
- 枚举常量
- 注解
- 以上类型的数组
enum Status { ACTIVE, INACTIVE }
@interface Config {
int timeout() default 1000; // OK:基本类型
String name() default "default"; // OK:String
Class<?> type() default String.class; // OK:Class字面量
Status status() default Status.ACTIVE; // OK:枚举
String[] aliases() default {"a", "b"}; // OK:数组
// Date date() default new Date(); // 错误:不是编译时常量
}
2.3 实现处理器:解读注解的代码
使用反射处理运行时注解
import java.lang.reflect.Method;
// 定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface RunTest {
int priority() default 5;
}
// 使用注解的类
class TestSuite {
@RunTest(priority = 1)
public void highPriorityTest() {
System.out.println("Running high priority test");
}
@RunTest(priority = 3)
public void mediumPriorityTest() {
System.out.println("Running medium priority test");
}
public void noAnnotationMethod() {
System.out.println("No annotation");
}
}
// 注解处理器
class TestRunner {
public static void runTests(Class<?> testClass) throws Exception {
System.out.println("Running tests for: " + testClass.getName());
Object instance = testClass.getDeclaredConstructor().newInstance();
Method[] methods = testClass.getDeclaredMethods();
// 按优先级排序执行
java.util.Arrays.sort(methods, (m1, m2) -> {
RunTest a1 = m1.getAnnotation(RunTest.class);
RunTest a2 = m2.getAnnotation(RunTest.class);
int p1 = a1 != null ? a1.priority() : Integer.MAX_VALUE;
int p2 = a2 != null ? a2.priority() : Integer.MAX_VALUE;
return Integer.compare(p1, p2);
});
for (Method method : methods) {
RunTest annotation = method.getAnnotation(RunTest.class);
if (annotation != null) {
System.out.println("Executing: " + method.getName() +
" [priority: " + annotation.priority() + "]");
method.invoke(instance);
}
}
}
public static void main(String[] args) throws Exception {
runTests(TestSuite.class);
}
}
输出结果:
Running tests for: TestSuite
Executing: highPriorityTest [priority: 1]
Running high priority test
Executing: mediumPriorityTest [priority: 3]
Running medium priority test
处理类级别的注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface DatabaseTable {
String tableName();
boolean audit() default false;
}
@DatabaseTable(tableName = "users", audit = true)
class User {
private String name;
private int age;
// getters and setters...
}
class ORMProcessor {
public static String generateSQL(Class<?> entityClass) {
DatabaseTable table = entityClass.getAnnotation(DatabaseTable.class);
if (table == null) {
throw new IllegalArgumentException("Class not annotated with @DatabaseTable");
}
String tableName = table.tableName();
boolean audit = table.audit();
StringBuilder sql = new StringBuilder();
sql.append("CREATE TABLE ").append(tableName).append(" (
");
sql.append(" id INT PRIMARY KEY AUTO_INCREMENT,
");
// 这里可以添加更多字段...
if (audit) {
sql.append(" created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
");
sql.append(" updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
");
}
sql.append(");");
return sql.toString();
}
public static void main(String[] args) {
System.out.println(generateSQL(User.class));
}
}
三、用javac处理注解:编译时的“小纸条”检查
3.1 最简单的处理器
创建编译时注解处理器需要实现
javax.annotation.processing.Processor接口,但一般继承AbstractProcessor更方便:
// 定义编译时注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE) // 只需要在源码阶段
@interface CheckGetter {}
// 使用注解
class DataClass {
private String name;
@CheckGetter
public String getName() {
return name;
}
// 这个应该有问题,但没有注解
public void setName(String name) {
this.name = name;
}
}
// 注解处理器(简化版概念)
// 实际实现需要继承AbstractProcessor
实际注解处理器实现:
import javax.annotation.processing.*;
import javax.lang.model.*;
import javax.lang.model.element.*;
import javax.tools.*;
import java.util.*;
import java.io.*;
@SupportedAnnotationTypes("CheckGetter")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class CheckGetterProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(CheckGetter.class)) {
if (element.getKind() == ElementKind.METHOD) {
ExecutableElement method = (ExecutableElement) element;
String methodName = method.getSimpleName().toString();
// 检查是否是getter方法(简单检查)
if (!methodName.startsWith("get")) {
processingEnv.getMessager().printMessage(
Diagnostic.Kind.ERROR,
"@CheckGetter should only be used on getter methods",
element
);
}
}
}
return true; // 已处理,不需要其他处理器继续处理
}
}
3.2 使用APT工具处理注解
在实际开发中,我们一般使用注解处理工具(APT)或编译器插件。Maven配置示例:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<annotationProcessors>
<annotationProcessor>
com.example.CheckGetterProcessor
</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>
</plugins>
</build>
四、基于注解的单元测试:@Unit框架示例
4.1 简单的@Unit实现
让我们实现一个简单的基于注解的单元测试框架:
import java.lang.annotation.*;
import java.lang.reflect.*;
// 定义测试注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Test {
String value() default "";
Class<? extends Throwable> expected() default None.class;
class None extends Throwable {
private None() {}
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface BeforeEach {}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface AfterEach {}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface BeforeAll {}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface AfterAll {}
// 测试类示例
class CalculatorTest {
private Calculator calculator;
@BeforeAll
public static void initClass() {
System.out.println("=== CalculatorTest Class Initialized ===");
}
@BeforeEach
public void setUp() {
calculator = new Calculator();
System.out.println("Setup before each test");
}
@Test("测试加法")
public void testAddition() {
int result = calculator.add(2, 3);
if (result != 5) {
throw new AssertionError("2 + 3 should be 5, but got " + result);
}
System.out.println("✓ Addition test passed");
}
@Test("测试减法")
public void testSubtraction() {
int result = calculator.subtract(5, 3);
if (result != 2) {
throw new AssertionError("5 - 3 should be 2, but got " + result);
}
System.out.println("✓ Subtraction test passed");
}
@Test(value = "测试除以零", expected = ArithmeticException.class)
public void testDivideByZero() {
calculator.divide(10, 0); // 应该抛出异常
}
@AfterEach
public void tearDown() {
calculator = null;
System.out.println("Tear down after each test");
}
@AfterAll
public static void cleanupClass() {
System.out.println("=== CalculatorTest Class Cleanup ===");
}
}
// 被测试的类
class Calculator {
public int add(int a, int b) {
return a + b;
}
public int subtract(int a, int b) {
return a - b;
}
public int divide(int a, int b) {
return a / b; // 可能抛出ArithmeticException
}
}
// 测试运行器
class TestRunner {
public static void runTests(Class<?> testClass) throws Exception {
System.out.println("
Running tests for: " + testClass.getSimpleName());
Object testInstance = null;
Method[] methods = testClass.getDeclaredMethods();
// 执行@BeforeAll方法
for (Method method : methods) {
if (method.isAnnotationPresent(BeforeAll.class)) {
method.invoke(null); // 静态方法
}
}
// 对每个@Test方法
for (Method method : methods) {
if (method.isAnnotationPresent(Test.class)) {
Test testAnnotation = method.getAnnotation(Test.class);
// 创建测试实例(如果还没创建)
if (testInstance == null && !Modifier.isStatic(method.getModifiers())) {
testInstance = testClass.getDeclaredConstructor().newInstance();
}
// 执行@BeforeEach
for (Method beforeEach : methods) {
if (beforeEach.isAnnotationPresent(BeforeEach.class)) {
beforeEach.invoke(testInstance);
}
}
// 执行测试
System.out.print("Running: " + testAnnotation.value() + "... ");
try {
if (Modifier.isStatic(method.getModifiers())) {
method.invoke(null);
} else {
method.invoke(testInstance);
}
// 如果期望异常但没有抛出
if (testAnnotation.expected() != Test.None.class) {
System.out.println("FAIL: Expected " +
testAnnotation.expected().getName() +
" but no exception was thrown");
} else {
System.out.println("PASS");
}
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
// 检查是否抛出了期望的异常
if (testAnnotation.expected().isInstance(cause)) {
System.out.println("PASS (expected exception: " +
cause.getClass().getSimpleName() + ")");
} else {
System.out.println("FAIL: Unexpected exception - " + cause);
}
}
// 执行@AfterEach
for (Method afterEach : methods) {
if (afterEach.isAnnotationPresent(AfterEach.class)) {
afterEach.invoke(testInstance);
}
}
}
}
// 执行@AfterAll方法
for (Method method : methods) {
if (method.isAnnotationPresent(AfterAll.class)) {
method.invoke(null); // 静态方法
}
}
System.out.println("All tests completed!
");
}
public static void main(String[] args) throws Exception {
runTests(CalculatorTest.class);
}
}
4.2 在@Unit中使用泛型
// 泛型测试示例
class GenericTest {
@Test("测试泛型列表")
public void testGenericList() {
List<String> list = new ArrayList<>();
list.add("Hello");
list.add("World");
if (list.size() != 2) {
throw new AssertionError("List should have 2 elements");
}
if (!list.get(0).equals("Hello")) {
throw new AssertionError("First element should be 'Hello'");
}
System.out.println("✓ Generic list test passed");
}
@Test("测试泛型方法")
public <T> void testGenericMethod() {
// 泛型方法测试
String result = echo("Test");
if (!result.equals("Test")) {
throw new AssertionError("Echo should return same string");
}
System.out.println("✓ Generic method test passed");
}
private <T> T echo(T input) {
return input;
}
}
// 运行泛型测试
class GenericTestRunner {
public static void main(String[] args) throws Exception {
TestRunner.runTests(GenericTest.class);
}
}
五、注解使用的优缺点总结
优点:
- 减少样板代码:像Lombok这样的库用注解自动生成getter/setter
- 编译时检查:可以在编译时发现错误,而不是运行时
- 代码更清晰:注解让意图更明确,列如@Override
- 框架集成:Spring、Hibernate等框架大量使用注解
- 元数据驱动:可以用注解配置行为,而不是硬编码
缺点:
- 过度使用导致混乱:太多注解会让代码难以阅读
- 隐藏逻辑:注解背后的逻辑可能不透明
- 学习曲线:需要理解每个注解的含义和效果
- 性能开销:运行时注解通过反射访问,可能影响性能
反例:滥用注解
// 反例:过度使用注解,代码意图不清晰
@Transactional(readOnly = true, timeout = 30, isolation = Isolation.READ_COMMITTED)
@Cacheable(value = "users", key = "#id", unless = "#result == null")
@PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
@Validated
@ApiOperation(value = "获取用户", notes = "根据ID获取用户信息")
@GetMapping(path = "/users/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<User> getUser(
@PathVariable @Min(1) Long id,
@RequestHeader("X-Request-ID") String requestId
) {
// 方法体被一堆注解淹没,难以理解
return ResponseEntity.ok(userService.findById(id));
}
问题:注解太多,掩盖了方法的主要逻辑,维护困难。
六、实战提议
- 遵循单一职责:一个注解应该只做一件事
- 提供默认值:让注解使用更简单
- 文档完善:用JavaDoc说明注解的用途和效果
- 合理选择保留策略:不需要运行时访问的用SOURCE或CLASS
- 与工具链集成:思考IDE支持、构建工具集成
总结
注解就像Java代码的“小纸条”,可以给编译器、工具或运行时系统传递额外信息。从简单的标记注解到复杂的元数据注解,它们让Java编程更加声明式和简洁。
记住吕洞宾的忠告:注解如调料,适量提味,过量伤身! 合理使用注解能让代码更清晰、更安全,但滥用会让代码变成“注解汤”,谁也看不懂!
好了,今天的“注解修仙课”就到这里。下次咱们聊聊Java的其他“仙术”,记得点赞关注,咱们下回再见!
© 版权声明
文章版权归作者所有,未经允许请勿转载。
相关文章
暂无评论...