Java 反射 (SpringBoot篇)

Java 反射实战:从原理到项目落地

适用场景:Java 后端 · 框架开发 · 面试八股

关键词:Class · Field · Method · Constructor · 动态代理 · 注解解析


前言

写 Spring Boot 时,你一定写过这种代码:

java 复制代码
@RestController
@RequestMapping("/api/books")
public class BookController {
    @Autowired private BookCatalogService service;
    @GetMapping("/{id}") public BookResponse get(@PathVariable Long id) { ... }
}

你有没有想过:Tomcat 收到一个 HTTP 请求后,凭什么能找到 BookController、调用 get 方法、还把路径变量塞进 id 参数? 它在编译期根本不知道你会写哪些 Controller。

答案就两个字:反射

这篇文章从「反射是什么」讲起,把 Class / Field / Method / Constructor 四大核心 API、动态代理、注解解析串起来,最后回到智慧书城 Factory_Test_Demo 看反射在 Spring AI Tool Calling 里的真实落地。


一、反射到底是什么

1.1 一个直觉的定义

普通调用 :编译期就知道调谁。

反射调用:运行期才决定调谁------靠一个「类名字符串」或「Class 对象」去操作它的字段、方法、构造器。

java 复制代码
// 普通调用:编译期就锁定
Book book = new Book();
book.setTitle("Redis");

// 反射调用:运行期才决定调哪个类的哪个方法
Class<?> clazz = Class.forName("com.zx.bookstore.catalog.entity.Book");
Object obj = clazz.getDeclaredConstructor().newInstance();
Method setter = clazz.getMethod("setTitle", String.class);
setter.invoke(obj, "Redis");

两段代码做的事一模一样,区别在于第二段把「类 / 方法 / 参数类型」全部当成运行期数据来处理

1.2 为什么需要它

反射解决的核心矛盾是:框架要在「不知道用户会写什么类」的前提下,调用用户的类

场景 谁用反射 干什么
Spring IoC 容器 扫描 @Componentnew 出 Bean 塞进容器
Spring MVC DispatcherServlet 按 URL 找 @RequestMapping 方法,反射调用
MyBatis SqlSession 把 ResultSet 列反射赋给实体字段
Jackson ObjectMapper 把 JSON 字段反射写入 POJO
JUnit Runner @Test 方法反射执行
Spring AI Tool ChatClient @Tool 方法注册成大模型可调用的函数

没有反射,这些框架都得让用户写一堆「注册代码」,框架的「开箱即用」就无从谈起。

1.3 反射的代价

反射不是免费的:

  • 性能:反射调用要走方法查找、参数装箱、访问检查,比直接调用慢几倍到几十倍(JIT 优化后差距缩小,但仍存在)。
  • 安全 :可以绕过访问修饰符(setAccessible(true)),破坏封装。
  • 可读性:IDE 跳转、编译期类型检查全部失效,重构时容易漏改。

所以原则是:框架用反射,业务代码尽量少用。真要在业务里用,先想想能不能用接口/泛型/枚举替代。


二、四大核心 API

反射的入口是 Class 对象。拿到 Class 之后,就能取 Field(字段)、Method(方法)、Constructor(构造器)。

2.1 拿到 Class 对象的三种方式

java 复制代码
// 1. 类名.class(编译期已知)
Class<Book> c1 = Book.class;

// 2. 实例.getClass()
Book book = new Book();
Class<?> c2 = book.getClass();

// 3. Class.forName(运行期才知道全类名,框架最常用)
Class<?> c3 = Class.forName("com.zx.bookstore.catalog.entity.Book");

三种方式拿到的是同一个 Class 对象 (JVM 里每个类只有一个 Class),c1 == c2 == c3true

Class 对象在类加载阶段由 JVM 创建,存进方法区(JDK 8)/ 元空间(JDK 9+)。它就是「这个类的运行期元数据」。

2.2 Field:操作字段

java 复制代码
Class<Book> clazz = Book.class;

// 取单个字段
Field title = clazz.getDeclaredField("title");
Field id = clazz.getField("status");   // getField 只能拿 public,含继承

// 取所有字段
Field[] all = clazz.getDeclaredFields(); // 本类声明(含 private),不含继承
Field[] pubs = clazz.getFields();         // public,含继承

// 读写实例字段
Book book = new Book();
title.setAccessible(true);   // private 字段必须先放开
title.set(book, "Redis 实战");
Object val = title.get(book); // "Redis 实战"

// 读写静态字段
Field cache = clazz.getDeclaredField("CACHE");
cache.setAccessible(true);
cache.set(null, new HashMap<>());

getDeclaredField vs getField 是高频面试点:

getField / getFields getDeclaredField / getDeclaredFields
范围 只 public 全访问修饰符
继承 含父类 public 字段 只本类声明,不含继承

2.3 Method:调用方法

java 复制代码
Class<Book> clazz = Book.class;

// 取方法(要传参数类型,区分重载)
Method setTitle = clazz.getMethod("setTitle", String.class);
Method getTitle = clazz.getDeclaredMethod("getTitle");

// 调用实例方法
Book book = new Book();
setTitle.invoke(book, "Redis 实战");   // 第一个参数是接收者

// 调用静态方法:接收者传 null
Method valueOf = clazz.getMethod("valueOf", int.class);
Object result = valueOf.invoke(null, 1);

invoke 的两个坑:

  1. 第一个参数是接收者 :实例方法传实例,静态方法传 null
  2. 参数类型必须精确匹配getMethod("setTitle", CharSequence.class) 找不到 setTitle(String),因为反射不做自动重载解析。

2.4 Constructor:创建实例

java 复制代码
Class<Book> clazz = Book.class;

// 无参构造
Book b1 = clazz.getDeclaredConstructor().newInstance();

// 带参构造(要传参数类型)
Constructor<Book> ctor = clazz.getDeclaredConstructor(Long.class, String.class);
Book b2 = ctor.newInstance(1L, "Redis 实战");

// private 构造器(单例破坏)
Constructor<Singleton> sc = Singleton.class.getDeclaredConstructor();
sc.setAccessible(true);
Singleton s = sc.newInstance();  // 绕过 private,单例被破防

newInstance()(已废弃)和 Constructor.newInstance() 的区别:后者能调用带参构造和 private 构造,前者只能无参 public 构造。新代码一律用 Constructor.newInstance()


三、访问控制:setAccessible 的真相

private 字段/方法默认不能反射访问,要先调 setAccessible(true)。但这个方法到底干了什么?

3.1 它不是「修改修饰符」

setAccessible(true) 不会把字段变成 public,它只是告诉 JVM 的访问检查器:「跳过这次访问的权限校验」。

java 复制代码
Field f = Book.class.getDeclaredField("title");
f.setAccessible(true);
// f.getModifiers() 仍是 PRIVATE,修饰符没变
System.out.println(Modifier.isPrivate(f.getModifiers())); // true

3.2 模块化时代的限制(JDK 9+)

JDK 9 引入模块系统后,setAccessible(true) 不再万能:

  • 跨模块访问 JDK 内部类(如 sun.misc.Unsafe)会被 --illegal-access 拦截。
  • 用户自己的模块只要 opens 给反射访问,就还能用。

所以反射访问自己写的实体/Service 没问题 ,但想反射 java.lang.String 内部就会报警告或抛 InaccessibleObjectException

3.3 反射破坏单例的经典案例

java 复制代码
public class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() { return INSTANCE; }
}

// 反射破防
Constructor<Singleton> c = Singleton.class.getDeclaredConstructor();
c.setAccessible(true);
Singleton fake = c.newInstance();   // 不是 INSTANCE

这就是为什么「枚举单例」被推荐------枚举实例由 JVM 保证,反射 Constructor.newInstance() 对枚举会直接抛异常,序列化也安全。


四、动态代理:反射的高阶应用

反射最经典的应用场景是动态代理 :运行期生成一个实现指定接口的代理类,把方法调用转发给 InvocationHandler

4.1 JDK 动态代理

java 复制代码
// 接口
public interface BookService {
    BookResponse getBook(Long id);
}

// 真实实现
public class BookServiceImpl implements BookService {
    public BookResponse getBook(Long id) { return repo.findById(id); }
}

// 处理器:所有方法调用都进 invoke
InvocationHandler handler = (proxy, method, args) -> {
    System.out.println("before " + method.getName());
    Object ret = method.invoke(realImpl, args);  // 反射调真实方法
    System.out.println("after  " + method.getName());
    return ret;
};

BookService proxy = (BookService) Proxy.newProxyInstance(
        BookService.class.getClassLoader(),
        new Class[]{BookService.class},
        handler);
proxy.getBook(1L);  // 打印 before / after

Proxy.newProxyInstance 在运行期用反射生成一个 $Proxy0 类,它实现了你传入的接口,每个方法体里都是「调 handler.invoke」。

4.2 Spring AOP 的本质

Spring AOP 默认就用 JDK 动态代理(目标类实现了接口)或 CGLIB(没实现接口时用字节码生成子类)。你写的 @Transactional@Async@Cacheable,底层全是动态代理 + 反射。

java 复制代码
// @Transactional 的简化逻辑
@Transactional
public void borrowBook(Long userId, Long bookId) { ... }

// 等价于
TransactionProxy.invoke(() -> {
    transactionManager.begin();
    try {
        target.borrowBook(userId, bookId);  // 反射调原方法
        transactionManager.commit();
    } catch (Exception e) {
        transactionManager.rollback();
        throw e;
    }
});

4.3 JDK 代理 vs CGLIB

JDK 动态代理 CGLIB
原理 反射生成接口实现类 字节码生成子类
要求 目标必须实现接口 目标类不能 final
速度 反射调用稍慢 字节码调用快,但生成慢
Spring 默认 有接口用 JDK 无接口用 CGLIB

五、注解解析:反射的另一半

注解本身不做事,注解 + 反射才做事。框架扫描注解、读属性、决定行为。

5.1 定义与读取

java 复制代码
@Retention(RetentionPolicy.RUNTIME)   // 关键:必须 RUNTIME 才能反射读到
@Target(ElementType.METHOD)
public @interface Tool {
    String name();
    String description() default "";
}

// 反射读注解
Method m = BookSearchTool.class.getMethod("searchBooks", String.class);
if (m.isAnnotationPresent(Tool.class)) {
    Tool t = m.getAnnotation(Tool.class);
    System.out.println(t.name() + " - " + t.description());
}

@Retention 三个级别:

  • SOURCE:编译期丢弃(如 @Override),反射读不到。
  • CLASS:写进 class 文件,但 JVM 不加载(默认值),反射读不到。
  • RUNTIME:运行期可反射读,框架注解都用这个

5.2 Spring 怎么用注解 + 反射

java 复制代码
// Spring 扫描 @Component 的简化逻辑
for (Class<?> clazz : scan("com.zx")) {
    if (clazz.isAnnotationPresent(Component.class)) {
        Object bean = clazz.getDeclaredConstructor().newInstance();
        context.register(bean);
    }
}

@Autowired@RequestMapping@Value 全是这套机制。

相关推荐
2301_794461578 小时前
Activiti/BPMN 2.0 的 4 种网关
java·服务器·开发语言
她的男孩8 小时前
低代码只能做单表 CRUD?我们一行代码没写,搭了个完整进销存
java·后端·架构
jctech8 小时前
让「小程序」跑起真正的原生代码:ComboNative 多进程原生小程序运行时 · Alpha 首发
android·设计模式·架构
147API8 小时前
Claude Tag 进入 Slack 后,团队智能体需要哪些任务与审计字段
java·开发语言·数据库
dyonggan9 小时前
IDEA从零搭建SpringCloud Alibaba完整工程
java·spring cloud·intellij-idea
黄焖鸡能干四碗9 小时前
IT数据架构规划设计方案(PPT文件)
大数据·网络·数据库·人工智能·架构·区块链
代码雕刻家9 小时前
编程语法细节
java·c语言·开发语言
SimonKing9 小时前
Agnes AI出桌面版了,可图可视频,免费用
java·后端·程序员
左左右右左右摇晃9 小时前
手写Tomcat原理整理
java·开发语言·笔记·tomcat