Skip to content

第13章:注解、反射与元编程

本章讲 Java 里的"元编程"——让代码能"看"和"标记"其他代码。

📌 代码说明:标注"可复制运行"的代码块包含完整的 main 方法。没标注的是概念示意片段。

一个故事开头

你从第8章开始就在写 @Override。你大概知道它是"告诉编译器我在重写父类方法"。但你有没想过——

@Override 到底是什么?它不是 Java 的关键字,那为什么 Java 编译器认识它?

答案是:@Override 是一个注解(Annotation)。注解就像贴在代码上的便利贴——上面写着"这段代码有什么属性",编译器或者运行环境可以读到这个便利贴,然后做相应的事。

顺着这个思路往下想:如果我能自己写便利贴,写一个 @LogExecutionTime 贴在方法上,让程序自动记录每个方法的执行时间——这不就是"元编程"了吗?


1. JDK 内置的三个注解

java
// @Override——告诉编译器"我在重写父类方法"
// 写错方法名时编译器直接报错,而不是默默当成新方法
@Override
void make() { ... }

// @Deprecated——标记这个方法"过时了,别用了"
// 谁调用它编译时会收到警告
@Deprecated
public void oldMethod() { ... }

// @SuppressWarnings——"我知道这里有警告,别烦我"
@SuppressWarnings("unchecked")
public void doSomething() { ... }

2. 定义自己的注解

最基本的注解

java
import java.lang.annotation.*;

// @interface 定义注解——注意不是 interface
public @interface Loggable {
}

// 使用
@Loggable
public void makeTea() { ... }

带参数的注解

java
public @interface Author {
    String name();           // 注解参数——像方法声明
    String date();
    String description() default "无描述";  // 默认值
}

// 使用
@Author(name = "团子", date = "2026-06-22")
public class MilkTeaOrderSystem {
    // ...
}

// 只有一个参数时习惯叫 value
public @interface Version {
    int value();  // 如果有且仅有一个参数叫 value,使用时可省略参数名
}

@Version(2)      // 等价于 @Version(value = 2)
public class SomeClass { }

元注解——注解的注解

定义注解时,你要告诉 Java 这个注解贴在哪里、什么时候读取、能不能重复贴。

java
import java.lang.annotation.*;

@Target(ElementType.METHOD)          // 这个注解只能贴在方法上
@Retention(RetentionPolicy.RUNTIME)  // 运行时可以通过反射读取
public @interface LogExecutionTime {
}
元注解作用常用值
@Target这个注解能贴在哪METHODTYPE(类)、FIELDPARAMETER
@Retention保留到什么阶段SOURCE(编译完丢弃)、CLASS(类文件里有)、RUNTIME(运行时也能读)

3. 反射——让代码"看"自己

反射(Reflection)就是程序运行时查看自己和操作自己的能力

java
// 可复制运行,保存为 ReflectionDemo.java
import java.lang.reflect.*;

public class ReflectionDemo {
    public static void main(String[] args) throws Exception {
        // 获取 String 类的 Class 对象
        Class<?> clazz = String.class;
        
        System.out.println("类名: " + clazz.getName());
        System.out.println("父类: " + clazz.getSuperclass().getName());
        
        System.out.println("\n方法列表:");
        for (Method m : clazz.getMethods()) {
            System.out.println("  " + m.getName() + "()");
            if (m.getParameterCount() > 0) {
                System.out.print("    参数: ");
                for (Class<?> p : m.getParameterTypes()) {
                    System.out.print(p.getSimpleName() + " ");
                }
                System.out.println();
            }
        }
    }
}

输出(节选):

类名: java.lang.String
父类: java.lang.Object
方法列表:
  length()
  isEmpty()
  charAt()
   参数: int
  ...

💥 拆了它:用反射绕过 private

java
// 可复制运行,保存为 PrivateAccess.java
import java.lang.reflect.*;

class BankAccount {
    private double balance = 1000;
    
    private void secretMethod() {
        System.out.println("你发现了我!余额: " + balance);
    }
}

public class PrivateAccess {
    public static void main(String[] args) throws Exception {
        BankAccount account = new BankAccount();
        
        // 获取私有字段并修改
        Field balanceField = BankAccount.class.getDeclaredField("balance");
        balanceField.setAccessible(true);  // 取消 Java 的访问检查
        double oldBalance = (double) balanceField.get(account);
        System.out.println("原始余额: " + oldBalance);
        balanceField.set(account, 999999.99);
        System.out.println("新余额: " + balanceField.get(account));
        
        // 调用私有方法
        Method secret = BankAccount.class.getDeclaredMethod("secretMethod");
        secret.setAccessible(true);
        secret.invoke(account);  // 你发现了我!余额: 999999.99
    }
}

反射可以破坏封装。 日常开发中不要这样做——它破坏了代码的本来意图。但框架(Spring、Hibernate)广泛用这个机制来做依赖注入、ORM 映射等。


4. 注解 + 反射——让注解真正干活

光有注解只是贴了便利贴。要让它产生效果,必须配合反射去读取它。

java
// 可复制运行,保存为 AnnotationProcessor.java
import java.lang.annotation.*;
import java.lang.reflect.*;

// 定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface LogExecutionTime {
}

class TeaMaker {
    @LogExecutionTime
    public void makeTea() throws InterruptedException {
        System.out.println("制作奶茶中...");
        Thread.sleep(500);  // 模拟制作耗时
    }
    
    public void cleanUp() {
        System.out.println("清理台面");
    }
}

public class AnnotationProcessor {
    public static void main(String[] args) throws Exception {
        TeaMaker maker = new TeaMaker();
        
        // 遍历 TeaMaker 的所有方法
        for (Method method : TeaMaker.class.getDeclaredMethods()) {
            // 检查方法上有没有 @LogExecutionTime
            if (method.isAnnotationPresent(LogExecutionTime.class)) {
                System.out.println("执行 " + method.getName() + "...");
                long start = System.nanoTime();
                method.invoke(maker);
                long end = System.nanoTime();
                long ms = (end - start) / 1_000_000;
                System.out.println(method.getName() + " 耗时: " + ms + "ms");
            } else {
                System.out.println("普通调用 " + method.getName());
                method.invoke(maker);
            }
        }
    }
}

输出:

执行 makeTea...
制作奶茶中...
makeTea 耗时: ~500ms
普通调用 cleanUp...
清理台面

这个模式就是 Spring 的雏形:

  1. 定义注解(@Autowired@Transactional
  2. 贴到类或方法上
  3. 框架在启动时用反射扫描注解,做对应的处理

5. 实际世界里的注解

你在 Spring 等项目里看到的注解,本质上就是这样工作的:

java
// Spring Boot 中你以后会写的代码
@SpringBootApplication
@RestController
public class MyController {
    
    @Autowired
    private OrderService orderService;
    
    @GetMapping("/orders")
    public List<Order> getOrders() {
        return orderService.findAll();
    }
}

Spring 在启动时做的事情:

  1. 用反射扫描所有类,发现有 @RestController 的类——注册为 HTTP 控制器
  2. 发现 @Autowired 字段——自动注入对应的对象
  3. 发现 @GetMapping 方法——注册路由

注解只是标记。真正做事的是读注解的框架代码。


本章小结

  1. 注解 = 代码上的便利贴——给类、方法、字段打标签
  2. JDK 内置@Override@Deprecated@SuppressWarnings
  3. 自定义注解:用 @interface,可以带参数和默认值
  4. 元注解@Target(贴在哪)、@Retention(保留多久)
  5. 反射:运行时查看类的结构(方法、字段、注解)
  6. 反射 + 注解 = 元编程:框架读取注解,自动执行逻辑
  7. 实际应用:Spring 的 @Autowired@GetMapping

✅ 验收标准

完成本章后,你应该能:

  • [ ] 使用常用内置注解(@Override, @Deprecated 等)
  • [ ] 定义自己的注解,包括元注解配置
  • [ ] 通过反射获取类的方法/字段/注解信息
  • [ ] 注解 + 反射结合实现简单逻辑
  • [ ] 理解 Spring 注解的基本原理

📌 常见卡点

  • 反射能访问私有成员但会导致封装破坏
  • 注解没有继承性——子类不继承父类注解
  • 反射性能开销不小——别在频繁调用的路径上用
  • 未设置 @Retention(RUNTIME) 的注解在运行时读不到

🔜 现在不需要理解

  • InvocationHandler 和动态代理
  • ClassLoader 自定义加载器
  • 编译期注解处理(APT)——框架开发者才需要


🧪 练习

1. 定义注解:定义一个 @Important 注解,有一个 String level() 参数,默认值为 "HIGH"。用 @Retention(RUNTIME)@Target(METHOD)

2. 读取注解:写一个类,有三个方法——两个贴上 @Important(不同级别),一个不贴。用反射遍历方法,打印出所有 @Important 的方法名和级别。

3. 反射调用:用反射调用一个私有方法 private void secret(),打印它的输出。


下一篇

第14章 并发编程基础

用 ❤️ 构建 | Software Systems Atlas