Skip to content

元数据卡

  • 前置知识:第7章(面向对象基础)、第8章(继承与多态)
  • 预计时间:70 分钟
  • 核心难度:
  • 阅读模式: 高度专注
  • 完成标志:能读懂 @Override/@Deprecated/@SuppressWarnings;能自定义注解并配合反射读取;能通过反射获取类的方法和字段并动态调用

可选进阶章:注解与反射是框架底层技术,对于编程初学者可跳过。 你只需要知道三个标准注解(@Override/@Deprecated/@SuppressWarnings)的用法即可。

本章分层

  • 必读(最多):@Override/@Deprecated/@SuppressWarnings 的使用场景
  • 选读:注解的定义语法(@interface)、元注解 @Target/@Retention
  • 深水区:反射 API(Class.forNamegetMethodinvoke)——框架原理的前置

本章不会要求你掌握

  • 自定义注解 + 反射处理器的完整实现(框架开发者才需要)
  • Spring/MyBatis 的注解驱动原理
  • 字节码操作库(ASM/Byte Buddy)

你在哪

阿花看你每天都在类上写 @Override 之类的符号,忍不住问:"那个 @ 到底是什么东西?"你张了张嘴,发现自己也说不太清楚。

你在变量村已经是一个熟练的工匠了。你能写类、建接口、搞继承、搞多态——面向对象三板斧滚瓜烂熟。

但有一个问题一直让你不安。

你写了一段代码,加了几个 @Override 注解,IDE 的绿色下划线就没再报错了。可是——这个 @ 符号到底是什么?它是代码吗?它编译到哪里去了?它怎么知道"检查父类"?如果你自己想创建一个类似 @MyAnnotation 的东西,然后写一个工具来读取它——这可能吗?

更狠的问题来了。

老陈师傅的图书馆系统已经做到第 500 个类了。每个类都有一个 saveToDatabase() 方法。某天上面要求:所有类的 saveToDatabase() 方法在调用前必须做日志记录——想一下,你该怎么做?

改 500 个类的 500 个方法?还是写一个工具,自动找到所有带某个标记的方法,然后给它们统一加上行为

"你知道吗,"老陈师傅呷了一口茶,"Java 世界里有两种你还没真正接触的能力。第一种叫注解——给代码贴上标签。第二种叫反射——程序在运行时审视自己。"

"注解是标签,反射是镜子。两者合起来——元编程,也就是写代码的代码。整个 Spring 框架就是建立在这两样东西之上的。"

你的任务

这章分两半。前半段让你明白三件事:

  1. 注解不是什么魔法——它就是贴在类、方法、字段上的"标签"
  2. 标准库给了你三个最常用的标签:@Override(检查重写)、@Deprecated(标记过时)、@SuppressWarnings(按住编译器别叫)
  3. 你也能自己定义标签

后半段是反射——更深入的"元能力":

  1. 程序在运行时查看自己的结构——"我是一个什么类?我有哪些方法?"
  2. 动态调用——"这个方法我知道名字,我能在运行时调用它"
  3. 反射 + 注解 = 框架的基础(包括 Spring)

等你走完这章,你会发现:注解+反射=Java 世界最强大的模式之一。以前你以为框架是黑魔法,现在你将看到它其实就是几个注解定义 + 几个反射方法。

遭遇战 → 获得技能

第一幕:贴标签的艺术——标准注解

先感受一下最简单的注解——不需要自己写代码定义,Java 已经帮你准备好了。

@Override——"我确认我在覆写父类方法"

java
class Animal {
    public void speak() {
        System.out.println("Animal speaks");
    }
}

class Dog extends Animal {
    @Override
    public void speak() {           // 如果没有 @Override,这个方法只是"正好同名"
        System.out.println("Woof!");
    }
    
    @Override
    public void bark() {            // ❌ 编译错误!Animal 没有 bark() 方法
        System.out.println("Bark!");
    }
}

语言:Java 5+ 预期输出:编译报错——method does not override or implement a method from a supertype发生了什么@Override 告诉编译器"我准备覆写父类方法"。如果名字拼错了或者签名不对,编译器直接报错——而不是悄无声息地新建一个方法。

为什么需要它? 没有 @Override 时,你本想说"覆写"却不小心写成了"重载"(参数类型不同),编译器不会报错,你也不会意识到父类的方法根本没被调用。@Override 是你的安全带。

@Deprecated——"这东西老了,别用了"

java
public class OldLibrary {
    
    @Deprecated
    public void oldMethod() {
        System.out.println("这个方法已经过时,请用 newMethod() 替代");
    }
    
    public void newMethod() {
        System.out.println("新方法,强烈推荐");
    }
}

// 使用者:
public class Client {
    public static void main(String[] args) {
        OldLibrary lib = new OldLibrary();
        lib.oldMethod();    // 编译时出现警告:OldLibrary.oldMethod() 已过时
        lib.newMethod();    // OK,没有警告
    }
}

语言:Java 5+ 如何看到效果:编译时终端出现 deprecation warning;IDE 里 oldMethod() 会被画上中划线 发生了什么@Deprecated 告诉使用者"这个元素过时了,考虑使用替代方案"。它不会阻止你使用——但你会收到警告。

Javadoc 版本(更规范的做法——包含替代方案提示):

java
/**
 * 老旧的图书查询方法
 * 
 * @deprecated 从 v2.0 起不再推荐使用。
 * 请使用 {@link #searchByISBN(String)} 替代。
 */
@Deprecated
public Book searchBook(String keyword) {
    return searchLegacy(keyword);
}

public Book searchByISBN(String isbn) {
    return searchEngine(isbn);
}

@SuppressWarnings——"编译器,安静"

java
import java.util.ArrayList;
import java.util.List;

public class SuppressDemo {
    
    @SuppressWarnings("unchecked")
    public void processRawList() {
        List rawList = new ArrayList();    // 原始类型(raw type)——通常会有警告
        rawList.add("hello");               // 传统上会警告 unchecked 操作
        rawList.add(42);
        
        List<String> safeList = (List<String>) rawList;  // unchecked cast
        System.out.println(safeList.get(0));  // hello
    }
    
    @SuppressWarnings("all")                // 压制所有警告——慎用!
    public void ignoreAllWarnings() {
        // 这里所有警告都不会出现
    }
}

语言:Java 5+

常见压制参数

参数值压制什么
"unchecked"未检查的转换(raw type、unchecked cast)
"deprecation"使用了已过时元素
"all"所有警告
"serial"缺少 serialVersionUID(仅对 Serializable 类)
"fallthrough"switch 中忘记 break(fall-through)

使用原则@SuppressWarnings 就像一个"我知道我在做什么"的声明——你确实应该知道。不要为了消灭一个警告而盲目压制所有警告。

🪟 差异窗口

C++ 中的等同概念:

  • [[deprecated]](C++14):等价于 @Deprecated
  • [[nodiscard]](C++17):告诉编译器返回值不应被丢弃——Java 没有完全等同
  • C++ 没有 @Override 的严谨等价物——C++11 的 override 关键字也是写在方法签名后
cpp
// C++11 override
class Dog : public Animal {
    void speak() override final;  // override 关键字
};

Python 中的等同概念:

  • Python 没有强制的 @Override——但 Python 标准库也没有内置的 override 装饰器。社区实现了第三方库(typing 没有 Override 直到 Python 3.12 才加了 typing.override
  • warnings.warn(DeprecationWarning()):等价于 @Deprecated
  • Python 的 @property@staticmethod@classmethod 是 Python 的"注解"——但它们是解释器级别的装饰器,和 Java 的注解完全不同的机制

🧗 进阶内容:自定义注解是框架开发者的工具。 普通应用开发者只需要会用标准注解,不需要自己定义。

第二幕:造自己的标签——自定义注解

标准注解只有几个。但你真正需要的是:我能在自己的代码里创建注解,然后写代码来读它们

先看如何定义一个注解:

java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// 定义一个注解:用于标记需要做日志的方法
@Target(ElementType.METHOD)          // 这个注解只能用在方法上
@Retention(RetentionPolicy.RUNTIME)  // 注解信息保留到运行时(能被反射读取)
public @interface Loggable {
    // 注解的属性(看起来像方法,实际上是配置)
    String level() default "INFO";
    boolean includeParams() default true;
}

语言:Java 5+ 语法拆解

  • @interface —— 不是"接口",是"注解类型"的关键字
  • @Target —— 元注解(注解的注解):声明本注解可以贴在什么上面(方法、类、字段……)
  • @Retention —— 元注解:声明注解的生命周期
  • level() —— 注解的属性,使用时可提供值,有默认值

元注解速查表

元注解说明
@Target注解可以贴在哪儿:METHOD / FIELD / TYPE(类/接口)/ PARAMETER / CONSTRUCTOR / ANNOTATION_TYPE
@Retention注解保留到什么阶段:SOURCE(编译期丢弃,如 @Override)/ CLASS(编译进字节码,运行时不加载)/ RUNTIME(运行时可反射读取)
@Documented注解是否出现在 Javadoc 中
@Inherited子类是否继承父类的该注解

一旦你定义了一个 @Loggable,你就能这样用:

java
public class OrderService {
    
    @Loggable(level = "DEBUG")                         // 只指定 level
    public void placeOrder(String userId, String itemId) {
        System.out.println("下单中...");
    }
    
    @Loggable(level = "ERROR", includeParams = false)   // 指定两个属性
    public void cancelOrder(String orderId) {
        System.out.println("取消订单中...");
    }
    
    @Loggable                                              // 全部使用默认值(level=INFO)
    public void queryOrder(String orderId) {
        System.out.println("查询订单中...");
    }
}

注解属性的数据类型限制

  • 基本类型(int, long, boolean 等)
  • String
  • Class(如 Class<?>
  • 枚举(enum
  • 注解类型
  • 以上类型的数组
java
public @interface AuthorInfo {
    String name();
    int version() default 1;
    String[] reviewers() default {};
    Class<?> relatedEntity() default Void.class;
}

🪟 差异窗口

Python 装饰器 vs Java 注解: Python 的装饰器是可运行的代码——@app.route("/api") 是在导入时被执行并注册路由。而 Java 注解是元数据——它本身不执行任何代码,需要其他代码(通常是反射)来读取它。

python
# Python 装饰器——它在导入时真正执行
from functools import wraps

def loggable(level="INFO"):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(f"[{level}] 调用 {func.__name__}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@loggable(level="DEBUG")
def place_order(user_id, item_id):
    print("下单中...")

这里的 @loggable("DEBUG") 实际执行了一个函数调用——它返回一个包装函数,替换了原来的 place_order。这是真正的行为注入,远比 Java 注解"元数据 + 反射"的组合更直接,但也更侵入——你无法轻易获取原函数。

Python 没有"元注解"的概念——装饰器就是好用的高阶函数。


🧗 进阶内容:反射是框架原理的基石,但日常开发中极少直接使用。 知道"Java 可以在运行时检视自己的类和方法的元信息"就足够了。

第三幕:照镜子——反射 API

注解的定义和使用只是"贴标签"的那一部分。真正的大招是——在运行时读取这些标签

这就是反射。Java 提供的让你在运行时查看和操作类结构的能力。

获取 Class 对象的三种方式

java
public class ReflectionDemo {
    public static void main(String[] args) throws Exception {
        // 方式1:通过对象获取(运行时)
        String str = "hello";
        Class<?> strClass1 = str.getClass();
        
        // 方式2:通过类字面量(编译期确定)
        Class<?> strClass2 = String.class;
        
        // 方式3:通过全限定类名(最灵活——可以写死在配置文件里)
        Class<?> strClass3 = Class.forName("java.lang.String");
        
        // 这三种方式拿到的是同一个 Class 对象
        System.out.println(strClass1 == strClass2);   // true
        System.out.println(strClass2 == strClass3);   // true
        
        // Class 对象能告诉你关于这个类的信息
        System.out.println("类名: " + strClass1.getName());        // java.lang.String
        System.out.println("简单类名: " + strClass1.getSimpleName()); // String
        System.out.println("包名: " + strClass1.getPackage());       // package java.lang
        System.out.println("是否是接口: " + strClass1.isInterface()); // false
        System.out.println("父类: " + strClass1.getSuperclass());    // class java.lang.Object
    }
}

语言:Java 预期输出

true
true
类名: java.lang.String
简单类名: String
包名: package java.lang
是否是接口: false
父类: class java.lang.Object

核心理解Class<?> 是一个描述"类的类"。对于 JVM 加载的每一个类,堆里只有一个对应的 Class 对象。它就像是这个类的"档案袋"——里面装着类名、包名、构造方法、字段、方法……一切信息。

查看一个类的结构——获取方法和字段

java
import java.lang.reflect.Method;
import java.lang.reflect.Field;
import java.util.Arrays;

class Person {
    private String name;
    public int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() { return name; }
    
    private void secretMethod() {
        System.out.println("这是一个私有方法");
    }
}

public class ReflectionInspect {
    public static void main(String[] args) {
        Class<?> clazz = Person.class;
        
        System.out.println("=== 获取所有公有方法(含父类的)===");
        Method[] publicMethods = clazz.getMethods();
        for (Method m : publicMethods) {
            System.out.println("  " + m.getName() + "(" + 
                Arrays.toString(m.getParameterTypes()) + ")");
        }
        
        System.out.println("\n=== 获取所有本类声明的字段(含私有的)===");
        Field[] allFields = clazz.getDeclaredFields();
        for (Field f : allFields) {
            System.out.println("  " + f.getType().getSimpleName() + " " + f.getName());
        }
    }
}

语言:Java 预期输出

=== 获取所有公有方法(含父类的)===
  getName([class [Ljava.lang.Object;])
  wait([long, int])
  wait([long])
  wait([])
  hashCode([])
  equals([class java.lang.Object])
  toString([])
  getClass([])
  notify([])
  notifyAll([])

=== 获取所有本类声明的字段(含私有的)===
  String name
  int age

关键区别

  • getMethods() —— 返回所有 public 方法,包含从父类继承的
  • getDeclaredMethods() —— 返回本类声明的所有方法,不管访问修饰符,但不包含继承的
  • getFields() / getDeclaredFields() —— 同理

🪟 差异窗口

Python 的反射: Python 的反射更直接——一切都是对象,属性和方法就是字典里的键:

python
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    def get_name(self):
        return self.name

p = Person("Alice", 25)

# 获取所有实例属性
print(p.__dict__)        # {'name': 'Alice', 'age': 25}

# 检查是否有某个属性
print(hasattr(p, 'name'))      # True
print(getattr(p, 'name'))      # Alice

# 获取类的方法
print(type(p).__dict__)        # {'__init__': ..., 'get_name': ..., ...}

Python 没有 private 的真正限制——__dict__ 暴露了一切。

C++ 的反射: C++ 没有内置反射——不能运行时获取类的字段列表。这是 Java 和 Python 的优势。C++ 需要用 RTTI(typeid)获取类型信息,但仅限于类型识别,不能枚举字段和方法。


第四幕:动态调用——真正的"元编程"

现在你拿到了一个 Class 对象,知道了类的方法有哪些。下一步更刺激:在不知道类名和方法名的情况下,运行时创建一个对象并调用它的方法

java
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class DynamicInvokeDemo {
    public static void main(String[] args) throws Exception {
        // 情景:我们只知道某个类在配置文件里叫 "java.util.ArrayList"
        // 我们要动态创建它的实例,并调用 add() 方法
        
        // 1. 获取 Class 对象
        Class<?> clazz = Class.forName("java.util.ArrayList");
        
        // 2. 获取无参构造方法,创建实例
        Constructor<?> constructor = clazz.getConstructor();
        Object list = constructor.newInstance();  // 等价于 new ArrayList<>()
        
        // 3. 获取 add 方法(类型签名:add(Object))
        Method addMethod = clazz.getMethod("add", Object.class);
        
        // 4. 动态调用——传入对象和参数
        addMethod.invoke(list, "反射添加的内容");
        addMethod.invoke(list, "第二条");
        
        // 5. 获取 size 方法调用
        Method sizeMethod = clazz.getMethod("size");
        Object sizeResult = sizeMethod.invoke(list);  // 返回 Object 类型
        
        System.out.println("列表大小: " + sizeResult);      // 2
        System.out.println("列表内容: " + list);            // [反射添加的内容, 第二条]
    }
}

语言:Java 预期输出

列表大小: 2
列表内容: [反射添加的内容, 第二条]

关键点

步骤反射 API等价常规代码
获取类Class.forName("java.util.ArrayList")
获取构造方法clazz.getConstructor()new ArrayList()
获取方法clazz.getMethod("add", Object.class)编译期已知
调用方法addMethod.invoke(list, "内容")list.add("内容")
私有方法?clazz.getDeclaredMethod("secret") + method.setAccessible(true)无法访问

调用私有方法——反射的"越权"能力

java
public class PrivateAccessDemo {
    public static void main(String[] args) throws Exception {
        Person p = new Person("Alice", 25);
        Class<?> clazz = p.getClass();
        
        // 获取私有方法
        Method secretMethod = clazz.getDeclaredMethod("secretMethod");
        
        // 没有这行会报 IllegalAccessException
        secretMethod.setAccessible(true);
        
        // 调用
        secretMethod.invoke(p);  // 这是一个私有方法
    }
}

语言:Java 预期输出

这是一个私有方法

setAccessible(true) 的代价:它绕过了 Java 的访问控制——从封装角度看是不安全的。框架(如 Spring、Jackson)会因为"我需要访问所有字段"而使用它,但你的业务代码中应该尽量避免。


第五幕:注解 + 反射 = 框架的基础

现在你把两样东西合在一起——用注解贴标签,用反射读标签做事情。

这是我们之前定义的 @Loggable 注解,配合一个"注解处理器"来使用:

java
import java.lang.reflect.Method;

public class LoggableProcessor {
    
    // 这个方法"处理"一个对象中所有被 @Loggable 标注的方法
    public static void processLoggable(Object obj, String methodName, Object... args) throws Exception {
        Class<?> clazz = obj.getClass();
        
        // 遍历所有方法
        for (Method method : clazz.getDeclaredMethods()) {
            if (method.getName().equals(methodName) && method.isAnnotationPresent(Loggable.class)) {
                // 读取注解
                Loggable loggable = method.getAnnotation(Loggable.class);
                
                // 获取注解中的属性值
                String level = loggable.level();
                boolean includeParams = loggable.includeParams();
                
                // 执行日志逻辑
                System.out.println("[" + level + "] 开始执行 " + methodName);
                if (includeParams && args.length > 0) {
                    System.out.println("  参数: " + java.util.Arrays.toString(args));
                }
                
                // 调用原方法
                method.invoke(obj, args);
                
                System.out.println("[" + level + "] " + methodName + " 执行完毕");
                return;
            }
        }
        
        // 没有注解则直接调用
        Method method = clazz.getMethod(methodName, 
            java.util.stream.Stream.of(args).map(Object::getClass).toArray(Class[]::new));
        method.invoke(obj, args);
    }
    
    public static void main(String[] args) throws Exception {
        OrderService service = new OrderService();
        
        // 通过处理器调用——会自动检查 @Loggable
        processLoggable(service, "placeOrder", "user001", "item999");
        System.out.println("---");
        processLoggable(service, "queryOrder", "order123");
    }
}

语言:Java 预期输出

[DEBUG] 开始执行 placeOrder
  参数: [user001, item999]
下单中...
[DEBUG] placeOrder 执行完毕
---
[INFO] 开始执行 queryOrder
查询订单中...
[INFO] queryOrder 执行完毕

这就是 AOP 的原型:面向切面编程(Aspect-Oriented Programming)。你不需要在每个方法里手动写日志代码——你只贴一个 @Loggable 标签,然后一个"拦截器"自动在所有带这个标签的方法前后执行日志逻辑。

Spring 的原理:Spring 容器启动时,扫描所有类,检查哪些类/方法上有 @Transactional@GetMapping 之类的注解,然后动态创建代理对象,在调用这些方法时插入事务管理、请求路由等逻辑。

Spring 注解快速入门

java
import org.springframework.web.bind.annotation.*;

@RestController                    // 标注这是一个 API 控制器(REST 指 Web 接口的设计风格,暂不深究)
@RequestMapping("/api/books")      // 映射基准 URL 路径
public class BookController {
    
    @Autowired                     // 让 Spring 自动注入依赖(省去 new)
    private BookService bookService;
    
    @GetMapping("/{id}")           // 映射 GET 请求 + URL 路径参数
    public Book getBook(@PathVariable Long id) {
        return bookService.findById(id);
    }
    
    @PostMapping                   // 映射 POST 请求
    public Book createBook(@RequestBody Book book) {
        return bookService.save(book);
    }
    
    @Transactional                 // 这个方法运行在数据库事务中
    public void transferBooks(Long fromId, Long toId) {
        // 如果这里抛出异常,数据库操作会自动回滚
    }
}

语言:Java + Spring 你能看懂的部分:注解是标签。@GetMapping 把 URL 和方法绑定。@Autowired 告诉 Spring "我需要这个对象,你帮我初始化"。它们都是基于"自定义注解 + 反射扫描"实现的——你现在理解了底层原理!

🪟 差异窗口

Python 中 Spring 的等价物(Flask / FastAPI):

python
from flask import Flask, request
app = Flask(__name__)

@app.route("/api/books/<int:id>", methods=["GET"])  # 装饰器:注册路由
def get_book(id):
    return book_service.find_by_id(id)

Python 的装饰器在导入时就执行了路由注册。而 Java 的注解只是元数据,Spring 启动时通过反射扫描——性能更差但更灵活,因为你可以选择在运行时读或不读注解。


常见陷阱

目标:写一个迷你单元测试框架(测试运行器)。

需求

  1. 定义一个 @Test 注解(仅能被放在方法上,保留到运行时)
  2. 定义一个 @BeforeEach 注解——在运行 @Test 方法前自动执行
  3. 写一个 TestRunner 类,接收一个测试类,扫描其中的 @BeforeEach@Test 方法并执行
  4. 统计测试通过/失败个数

提示

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

// 测试示例
public class MyTest {
    @BeforeEach
    public void setup() { System.out.println("准备测试环境"); }
    
    @Test
    public void testAdd() {
        System.out.println("测试加法: 1+1=2");
        // 如果失败,抛 AssertionError
        throw new AssertionError("加法测试失败");
    }
    
    @Test
    public void testSubtract() {
        System.out.println("测试减法: 2-1=1");
    }
}

预期输出

准备测试环境
测试加法: 1+1=2
  ❌ testAdd 失败: 加法测试失败
准备测试环境
测试减法: 2-1=1
  ✅ testSubtract 通过
结果: 1/2 通过

通关挑战

场景:你在一个只能通过网络发送类名的系统上工作——你需要写一个"远程方法调用器",客户端传类名和方法名,服务端动态调用并返回结果。

java
public class RemoteInvoker {
    
    /**
     * 接收远程调用的类全名、静态方法名和参数,动态调用并返回结果
     * 要求:
     * 1. 支持调用本机任意类的静态方法(通过 Class.forName)
     * 2. 方法返回值统一转为 String 返回
     * 3. 如果方法不存在,返回 "ERROR: 方法不存在"
     * 4. 如果方法抛出异常,返回 "ERROR: " + 异常信息
     */
    public static String invoke(String className, String methodName, String... args) {
        // 你的代码在这里
        return "";
    }
    
    // 测试场景
    public static void main(String[] args) {
        // 调用 Math.max(10, 20)
        System.out.println(invoke("java.lang.Math", "max", "10", "20"));   // 期望 "20"
        
        // 调用 Math.sqrt(16)
        System.out.println(invoke("java.lang.Math", "sqrt", "16"));        // 期望 "4.0"
        
        // 调用一个不存在的方法
        System.out.println(invoke("java.lang.Math", "nonexistent"));       // 期望 "ERROR: 方法不存在"
    }
}

进阶要求

  • 支持不同类型参数(Integer.parseInt、Double.parseDouble 等)
  • 支持非静态方法(需要额外传入构造函数参数)

验收标准

完成本章后,你应该能回答:

  • [ ] @Override 的作用是什么?不加它编译会出错吗?
  • [ ] @Deprecated@SuppressWarnings 分别在什么场景使用?
  • [ ] @Target@Retention 是什么?RetentionPolicy.RUNTIMERetentionPolicy.SOURCE 有什么区别?
  • [ ] 如何定义一个带属性的自定义注解?属性类型有哪些限制?
  • [ ] 获取 Class 对象的三种方式分别是什么?
  • [ ] getMethods()getDeclaredMethods() 的区别?
  • [ ] 如何通过反射调用一个私有方法?
  • [ ] setAccessible(true) 做了什么?为什么框架需要它?
  • [ ] 注解 + 反射如何组合使用?能举一个实际的框架例子吗?
  • [ ] Spring 的 @Autowired 大致原理是什么?

常见卡点

"我定义的注解运行时读不到" → 99% 的原因是忘了加 @Retention(RetentionPolicy.RUNTIME)。默认是 RetentionPolicy.CLASS——编译时在字节码里,但 JVM 加载后不保留。

"Class.forName() 抛 ClassNotFoundException" → 检查全限定类名是否写对了——java.util.ArrayList 不是 java.util.ArrayList(别少拼),并且类必须在 classpath 上。

"getMethod 抛 NoSuchMethodException" → 检查方法名是否拼对,并且参数类型是否匹配。getMethod("add", Object.class)getMethod("add", String.class) 是不同的——后者要求方法签名精确匹配。

"invoke 抛 IllegalAccessException" → 方法不是 public。用 getDeclaredMethod() 获取,然后调用 method.setAccessible(true)

"用反射调用静态方法时 invoke 的第一个参数传 null" → 正确。method.invoke(null, args...)——因为静态方法不需要实例。

"为什么 Spring 里我的字段加了 @Autowired 还是 null?" → 反射通过 setAccessible(true) 注入了值,但那个对象必须是 Spring 管理的(由 Spring 容器创建)。自己 new 的对象不会被 Spring 处理。


现在不需要理解

  • 动态代理(Proxy.newProxyInstance:AOP 的底层实现之一——但目前先理解"注解+反射读"就够了
  • ASM / Byte Buddy / Javassist:字节码操作库,在编译后修改 .class 文件——Spring 用到它们,但你不需要现在深究
  • AnnotatedElement 接口体系ClassMethodField 都实现了它——理解 isAnnotationPresent() 就够了
  • MethodHandleVarHandle(Java 7+/9+):反射的高性能替代方案——比传统反射快,但在你碰到性能瓶颈前不急着学
  • 编译期注解处理(APT):在 javac 编译时生成代码——Lombok(@Data)就是通过 APT 实现的。这块偏工具链,代码生成领域
  • Spring AOP 的代理机制(CGLIB / JDK 动态代理):知道"Spring 通过反射 + 代理在注解周围插入逻辑"就可以了

旅人笔记

注解 = 代码的标签。贴上去,告诉别人(编译器、框架、工具)"我是谁"。
@Override 不会改变代码行为——它只是安全网,帮你捕获笔误。
@Deprecated 是善意的提醒——"别用我了,有更好的替代"。
@SuppressWarnings 是"我知道我在做什么"的声明——别滥用。
自定义注解 = @interface + @Target + @Retention。
元注解 = 注解的注解。@Target 说"贴哪",@Retention 说"活多久"。
RUNTIME 保留 = 反射可读。SOURCE 保留 = 编译时丢弃(如 @Override)。
反射 = 程序照镜子——看自己是什么类、有什么方法。

Class 对象 = 类的档案袋。整个 JVM 里每种类只有一个 Class 对象。
getMethods() = 公有方法(含继承)。getDeclaredMethods() = 所有方法(不含继承)。
Method.invoke(对象, 参数) —— 动态调用。私有的需要 setAccessible(true)。
注解 + 反射 = Java 框架的基石 —— Spring、JPA、JUnit 都建立在这个组合上。
注解是设计时的声明,反射是运行时的发现——两者配合,代码就有了"自我意识"。
框架不再神奇了,对吧?
它就是一个循环:扫描类 → 读注解 → 干活。

一句话总结:元编程 = 让代码在运行时知道自己长什么样、贴了什么标签,并据此做出行为。

下一站预告

注解让你给代码贴标签,反射让你在运行时检查代码——但这些都是"单线程世界"里的能力。

下一个大挑战来了。

你的图书管理系统上线了,运行良好。一天,一百个读者同时登录系统查询书目——系统突然变慢了。不是算法问题,是所有人都想读同一个文件。一个线程在写,另一个在读,数据就乱了。

你以前写的所有代码,都假设同一时间只有一个人在执行。现实世界不是这样的——多个操作在同一时刻发生。数据库连接、网络请求、用户操作——它们就像同时涌进大厅的人群,如果没有秩序,就会踩踏。

下一章:【第14章:并发编程基础】——从线程创建到 synchronized,从 volatile 到线程池,你将掌握让多个人同时做事的正确姿势。

Built with VitePress | Software Systems Atlas