反射这个东西,确实很抽象。你可以先不要从"反射"这个词理解,它不是"光的反射",而是:
程序在运行时,反过来观察、分析、操作自己。
普通代码是:
java
User user = new User();
user.setName("张三");
你在写代码时就已经知道:
text
我要创建 User
我要调用 setName
而反射是:
java
Class<?> clazz = Class.forName("com.demo.User");
Object obj = clazz.getDeclaredConstructor().newInstance();
Method method = clazz.getMethod("setName", String.class);
method.invoke(obj, "张三");
这里的意思是:
text
运行时才知道类名是 User
运行时才找到 setName 方法
运行时才调用这个方法
这就是反射。
1. 反射到底解决什么问题?
一句话:
当你写代码的时候,不确定未来要操作哪个类、哪个方法、哪个字段,就可以用反射。
普通代码适合"写死"的场景:
java
User user = new User();
但很多框架不能写死。
比如 Spring Boot 不可能提前知道你会写哪些类:
java
@Service
public class UserService {
}
Spring 框架的作者不认识你的 UserService,但它启动时却能:
text
扫描到 UserService
发现它有 @Service 注解
创建 UserService 对象
放进 Spring 容器
它靠的就是反射。
2. 你可以把反射理解成"运行时查户口"
假设有一个类:
java
public class User {
private String name;
public void sayHello() {
System.out.println("hello");
}
}
正常情况下,你要使用它:
java
User user = new User();
user.sayHello();
这是"直接调用"。
反射则是:
java
Class<?> clazz = User.class;
System.out.println(clazz.getName());
Method[] methods = clazz.getDeclaredMethods();
Field[] fields = clazz.getDeclaredFields();
意思是:
text
这个类叫什么名字?
它有哪些字段?
它有哪些方法?
它有哪些构造方法?
它有没有某个注解?
所以反射最基础的作用是:运行时获取类的信息。
3. 反射不只是"看",还能"操作"
反射可以做几件事:
text
1. 获取类的信息
2. 创建对象
3. 获取字段
4. 修改字段
5. 获取方法
6. 调用方法
7. 读取注解
比如:
java
Class<?> clazz = User.class;
// 创建对象
Object obj = clazz.getDeclaredConstructor().newInstance();
// 获取方法
Method method = clazz.getDeclaredMethod("sayHello");
// 调用方法
method.invoke(obj);
这就等价于:
java
User user = new User();
user.sayHello();
区别是:反射版本可以在运行时动态决定。
4. 为什么框架特别喜欢用反射?
因为框架的代码是提前写好的,但你的业务类是后来写的。
比如 Spring 的作者写框架时,不知道你会写:
java
UserController
OrderService
CouponMapper
TrainService
但 Spring 启动的时候,需要把这些类都找出来、创建对象、注入依赖。
比如你写:
java
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
}
Spring 大概会干这些事:
text
1. 扫描 class 文件
2. 找到带 @Service 的类
3. 通过反射创建 UserService 对象
4. 发现 userMapper 字段上有 @Autowired
5. 通过反射给 userMapper 赋值
6. 把 UserService 放入容器
这就是你平时用 Spring 时"不 new 对象,Spring 却能帮你创建对象"的底层原因之一。
5. MyBatis 也大量用反射
比如你写一个实体类:
java
public class User {
private Long id;
private String name;
}
数据库查出来:
text
id = 1
name = 张三
MyBatis 要把数据库结果变成 Java 对象:
java
User user = new User();
user.setId(1L);
user.setName("张三");
但 MyBatis 框架作者不认识你的 User 类。
所以它会通过反射:
text
发现 User 有 id 字段
发现 User 有 name 字段
创建 User 对象
把数据库里的 id 填进去
把数据库里的 name 填进去
所以你会发现,很多框架要求实体类有:
java
无参构造方法
getter/setter
字段名和数据库列名对应
这些都和反射、对象映射有关。
6. JSON 转对象也靠反射
比如前端传来 JSON:
json
{
"id": 1,
"name": "张三"
}
后端接收:
java
@PostMapping("/user")
public void addUser(@RequestBody User user) {
}
Spring/Jackson 会帮你把 JSON 变成 User 对象。
它大概做的是:
text
看到 JSON 里有 id
找到 User 的 id 字段或者 setId 方法
把 1 填进去
看到 JSON 里有 name
找到 User 的 name 字段或者 setName 方法
把 张三 填进去
这背后也离不开反射。
7. 注解为什么能生效?也靠反射
比如:
java
@GetMapping("/hello")
public String hello() {
return "hello";
}
这个注解本身不会自动执行任何逻辑。
真正做事的是 Spring。
Spring 启动时通过反射扫描:
text
哪个类上有 @RestController?
哪个方法上有 @GetMapping?
@GetMapping 里面的路径是多少?
然后建立映射关系:
text
/hello -> hello() 方法
所以注解不是魔法。
注解本质上是"标记",反射负责"读取标记"。
8. 一个非常关键的理解
你可以这样记:
注解负责贴标签,反射负责读取标签,框架负责根据标签做事情。
比如:
java
@Service
public class UserService {
}
@Service 只是一个标签。
Spring 通过反射发现这个标签,然后说:
text
哦,这个类要交给我管理。
再比如:
java
@Autowired
private UserMapper userMapper;
@Autowired 也是标签。
Spring 通过反射发现这个字段,然后说:
text
哦,这个字段要自动注入对象。
9. 反射和普通调用的区别
普通调用:
java
User user = new User();
user.sayHello();
特点:
text
写代码时就确定了类和方法
速度快
类型安全
代码清晰
反射调用:
java
Class<?> clazz = Class.forName("com.demo.User");
Object obj = clazz.getDeclaredConstructor().newInstance();
Method method = clazz.getDeclaredMethod("sayHello");
method.invoke(obj);
特点:
text
运行时才确定类和方法
更灵活
性能稍差
代码复杂
容易出错
所以你自己平时业务开发里,不要随便用反射。
但是框架底层非常需要它。
10. 反射的典型使用场景
常见场景有这些:
text
Spring 创建 Bean、依赖注入
MyBatis 结果集映射
Jackson / Fastjson JSON 转对象
JUnit 扫描 @Test 方法
Lombok/IDE/插件分析类结构
RPC 框架动态调用方法
ORM 框架对象和数据库表映射
配置文件里写类名,然后运行时加载类
比如配置文件里写:
properties
className=com.demo.UserService
程序运行时读取这个字符串,然后创建对象:
java
Class<?> clazz = Class.forName("com.demo.UserService");
Object obj = clazz.getDeclaredConstructor().newInstance();
这就是反射的典型价值:用字符串控制程序加载哪个类。
11. 为什么叫"反射"?
正常情况是:
text
代码 -> 操作对象
比如:
java
user.sayHello();
反射是:
text
对象/类 -> 反过来告诉代码:我有哪些字段、方法、注解
也就是程序在运行时"观察自己"。
所以叫 reflection。
你可以简单理解成:
普通调用是我知道你是谁,所以调用你。反射是我不知道你是谁,但我可以运行时查出来你是谁,然后调用你。
12. 别的语言有反射吗?
有。
很多语言都有类似能力,只是名字和用法不同。
Java 有反射
Java 的反射比较正式,核心类有:
java
Class
Method
Field
Constructor
Annotation
比如:
java
User.class
clazz.getDeclaredMethods()
method.invoke()
Python 也有类似反射
Python 里更自然,因为它本身就是动态语言。
比如:
python
class User:
def say_hello(self):
print("hello")
user = User()
method = getattr(user, "say_hello")
method()
这里:
python
getattr(user, "say_hello")
就是根据字符串拿到方法,然后调用。
Python 还可以:
python
hasattr(user, "name")
setattr(user, "name", "张三")
dir(user)
这些都类似反射。
JavaScript 也有
JavaScript 也可以动态访问属性和方法:
javascript
const user = {
name: "张三",
sayHello: function() {
console.log("hello");
}
};
user["sayHello"]();
也可以:
javascript
user["name"] = "李四";
JavaScript 甚至还有:
javascript
Reflect
Proxy
这些机制。
C# 也有反射
C# 和 Java 很像,也有 Reflection:
csharp
Type type = typeof(User);
MethodInfo method = type.GetMethod("SayHello");
C++ 传统上反射比较弱
C++ 本身传统反射能力不如 Java、C#。
它有 RTTI,可以知道一些运行时类型信息,但不像 Java 那样方便地拿字段、方法、注解。
不过现代 C++ 也一直在补这方面能力,很多框架会用宏、模板、代码生成来模拟反射。
13. Java 为什么特别强调反射?
因为 Java 是静态语言。
正常 Java 代码里,类型在编译期就确定了:
java
User user = new User();
编译器知道 user 是 User 类型。
但框架需要动态能力:
text
我不知道你未来会写什么类
但我运行时要能找到它、创建它、调用它
于是 Java 提供了反射。
所以 Java 反射本质上是在静态语言里提供一部分动态能力。
14. 用一个生活例子理解
普通调用像这样:
text
你提前知道要找张三。
你直接说:张三,过来。
反射像这样:
text
你不知道具体是谁。
你手里只有一个名字字符串:"张三"。
你去名单里查有没有张三。
查到了之后,再让他过来。
代码对应:
java
Class<?> clazz = Class.forName("com.demo.ZhangSan");
Object obj = clazz.getDeclaredConstructor().newInstance();
15. 反射的缺点
反射不是万能的,它有代价:
1. 性能比直接调用差
直接调用:
java
user.sayHello();
反射调用:
java
method.invoke(user);
反射需要查找方法、做权限检查、封装调用,通常更慢。
不过现代 JVM 对反射做了很多优化,一般框架使用是可以接受的。
2. 破坏封装
比如 private 字段本来不想让外部访问:
java
private String password;
但反射可以:
java
field.setAccessible(true);
field.set(obj, "123456");
这就绕过了正常的访问控制。
3. 编译期不安全
普通代码如果方法名写错:
java
user.sayHelloo();
编译器直接报错。
但反射里:
java
clazz.getDeclaredMethod("sayHelloo");
编译器不报错,运行时才报错。
所以反射代码更容易出现运行时异常。
16. 你学 Java 时应该掌握到什么程度?
作为后端开发,不需要一开始就把反射源码研究得特别深。
你先掌握这几个点就够了:
text
1. 反射是运行时操作类、字段、方法、注解的能力
2. Spring、MyBatis、Jackson 这些框架大量使用反射
3. 注解本身只是标记,框架通过反射读取注解
4. 反射可以动态创建对象、调用方法、修改字段
5. 反射灵活但性能差一些,也更容易出运行时错误
17. 最小代码例子
你可以直接看这个例子。
User 类
java
public class User {
private String name;
public User() {
}
public void setName(String name) {
this.name = name;
}
public void sayHello() {
System.out.println("hello, " + name);
}
}
反射调用
java
public class ReflectDemo {
public static void main(String[] args) throws Exception {
// 1. 获取 Class 对象
Class<?> clazz = Class.forName("User");
// 2. 创建对象
Object obj = clazz.getDeclaredConstructor().newInstance();
// 3. 获取 setName 方法
Method setNameMethod = clazz.getDeclaredMethod("setName", String.class);
// 4. 调用 setName
setNameMethod.invoke(obj, "张三");
// 5. 获取 sayHello 方法
Method sayHelloMethod = clazz.getDeclaredMethod("sayHello");
// 6. 调用 sayHello
sayHelloMethod.invoke(obj);
}
}
它等价于:
java
User user = new User();
user.setName("张三");
user.sayHello();
但是反射版本的特点是:类名、方法名可以从配置文件、注解、网络数据、框架扫描中动态获得。
18. 最核心的一句话
你可以这样记:
反射就是 Java 在运行时"看见类的内部结构,并动态创建对象、调用方法、修改字段、读取注解"的能力。它主要不是给普通业务代码天天用的,而是给 Spring、MyBatis、JSON 框架、测试框架这些底层框架用的。
更直白一点:
text
你平时写业务:直接 new,直接调用方法。
框架写底层:不知道用户会写什么类,所以用反射动态操作。
所以你现在学反射,重点不是"以后业务里天天写反射",而是为了理解:
text
为什么 Spring 能自动创建 Bean
为什么 @Autowired 能注入
为什么 @GetMapping 能生效
为什么 MyBatis 能把查询结果变成对象
为什么 JSON 能自动转成 Java 对象
这些背后,都有反射的影子。
JSON 如何转为 Java 对象
java
@PostMapping("/user")
public void addUser(@RequestBody User user) {
}
假设前端发的是:
json
{
"id": 1,
"name": "张三",
"age": 18
}
后端有一个类:
java
public class User {
private Long id;
private String name;
private Integer age;
public User() {
}
public void setId(Long id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setAge(Integer age) {
this.age = age;
}
}
你看到的现象是:
java
@RequestBody User user
然后 Spring 自动把 JSON 变成了 User 对象。
这中间主要是 Spring MVC + Jackson + 反射 一起完成的。
1. 请求先到 DispatcherServlet
当前端请求:
http
POST /user
Content-Type: application/json
{
"id": 1,
"name": "张三",
"age": 18
}
Spring Boot 里的核心入口是 DispatcherServlet。
它大概会做这件事:
text
收到 POST /user 请求
↓
查找哪个 Controller 方法能处理这个请求
↓
找到 addUser(@RequestBody User user)
也就是说,Spring 先根据:
java
@PostMapping("/user")
找到这个方法:
java
public void addUser(@RequestBody User user)
2. Spring 发现参数上有 @RequestBody
然后 Spring 会分析方法参数:
java
public void addUser(@RequestBody User user)
它发现:
text
这个参数上有 @RequestBody
于是 Spring 明白:
这个
User user不是从 URL 参数里拿,也不是从路径变量里拿,而是从 HTTP 请求体 body 里拿。
也就是从这里拿:
json
{
"id": 1,
"name": "张三",
"age": 18
}
所以 @RequestBody 的核心作用是:
告诉 Spring:请把请求体里的 JSON 读取出来,然后转换成 Java 对象。
3. Spring 选择一个消息转换器
Spring 自己不直接手写 JSON 解析,它会找一个工具来做。
在 Spring MVC 里,这类工具叫:
text
HttpMessageConverter
你可以理解成:
HTTP 请求体和 Java 对象之间的转换器。
常见转换器有:
text
StringHttpMessageConverter:处理普通字符串
MappingJackson2HttpMessageConverter:处理 JSON
FormHttpMessageConverter:处理表单
ByteArrayHttpMessageConverter:处理字节数组
因为你的请求头通常是:
http
Content-Type: application/json
所以 Spring 会选择:
text
MappingJackson2HttpMessageConverter
这个转换器底层用的就是 Jackson。
4. Jackson 开始解析 JSON
Jackson 拿到请求体字符串:
json
{
"id": 1,
"name": "张三",
"age": 18
}
然后它知道目标类型是:
java
User.class
因为你的方法参数写的是:
java
@RequestBody User user
所以 Spring 会告诉 Jackson:
text
请把这段 JSON 转成 User 类型
Jackson 不是随便转,它会按字段名匹配。
JSON 里有:
json
"id": 1
Java 里有:
java
private Long id;
JSON 里有:
json
"name": "张三"
Java 里有:
java
private String name;
JSON 里有:
json
"age": 18
Java 里有:
java
private Integer age;
于是 Jackson 开始做映射。
5. Jackson 通过反射创建 User 对象
普通代码里你会这样创建:
java
User user = new User();
但 Jackson 框架作者不认识你的 User 类。
所以它不能提前写死:
java
new User()
它只能通过反射动态创建:
java
User.class.getDeclaredConstructor().newInstance();
意思是:
text
找到 User 类
找到 User 的无参构造方法
创建一个 User 对象
所以很多时候,实体类最好保留无参构造方法:
java
public User() {
}
如果没有无参构造,Jackson 可能不知道怎么创建对象,除非你额外配置构造器注解,比如 @JsonCreator。
6. Jackson 通过反射给字段赋值
创建出空对象以后,现在对象大概是这样:
java
User user = new User();
user.id = null;
user.name = null;
user.age = null;
然后 Jackson 要把 JSON 的值塞进去。
它可能通过 setter 方法:
java
user.setId(1L);
user.setName("张三");
user.setAge(18);
但 Jackson 框架不可能提前知道你有什么 setter。
所以它通过反射查找:
text
User 类里有没有 setId 方法?
User 类里有没有 setName 方法?
User 类里有没有 setAge 方法?
找到以后,动态调用:
java
setIdMethod.invoke(user, 1L);
setNameMethod.invoke(user, "张三");
setAgeMethod.invoke(user, 18);
也可以理解成:
text
JSON 字段 id → Java 的 setId()
JSON 字段 name → Java 的 setName()
JSON 字段 age → Java 的 setAge()
所以你会发现:
java
private String name;
public void setName(String name) {
this.name = name;
}
这种命名很重要。
7. 类型转换也会处理
JSON 里的数字:
json
"id": 1
本质上不是 Java 的 Long,它只是 JSON 数字。
Jackson 看到 Java 字段是:
java
private Long id;
就会尝试把 JSON 数字转成 Long。
类似地:
json
"age": 18
会转成:
java
Integer age
如果 JSON 是:
json
"age": "abc"
但是 Java 里是:
java
private Integer age;
这时候就会转换失败。
然后你可能会看到类似错误:
text
HttpMessageNotReadableException
意思是:
请求体读不懂,JSON 无法正确转换成 Java 对象。
8. 转换完成后,Spring 调用你的方法
Jackson 把对象构造好以后,Spring 才真正调用你的 Controller 方法。
也就是说,最终类似于:
java
User user = objectMapper.readValue(jsonString, User.class);
controller.addUser(user);
你的方法执行时,user 已经有值了:
java
@PostMapping("/user")
public void addUser(@RequestBody User user) {
System.out.println(user);
}
此时对象大概是:
java
User{
id = 1,
name = "张三",
age = 18
}
9. 整体流程串起来
完整流程大概是:
text
前端发送 POST /user
请求体是 JSON
↓
DispatcherServlet 接收到请求
↓
根据 @PostMapping("/user") 找到 addUser 方法
↓
发现参数 User user 上有 @RequestBody
↓
读取 HTTP 请求体
↓
根据 Content-Type: application/json 选择 JSON 转换器
↓
MappingJackson2HttpMessageConverter 调用 Jackson
↓
Jackson 解析 JSON 字符串
↓
通过反射创建 User 对象
↓
根据 JSON 字段名匹配 Java 属性名
↓
通过 setter 或字段反射赋值
↓
得到完整 User 对象
↓
Spring 调用 addUser(user)
你可以把它想成:
text
HTTP JSON 字符串
↓
Spring 发现 @RequestBody
↓
Jackson 转换
↓
Java User 对象
10. 用伪代码模拟一下
Spring/Jackson 背后大概可以理解成这样:
java
String json = request.getBody();
Class<?> targetClass = User.class;
// 创建对象
Object obj = targetClass.getDeclaredConstructor().newInstance();
// 解析 JSON
Map<String, Object> map = parseJson(json);
// 遍历 JSON 字段
for (Map.Entry<String, Object> entry : map.entrySet()) {
String fieldName = entry.getKey();
Object value = entry.getValue();
// id -> setId
// name -> setName
String methodName = "set" + capitalize(fieldName);
Method method = findSetter(targetClass, methodName);
method.invoke(obj, value);
}
User user = (User) obj;
addUser(user);
真实源码比这个复杂很多,但核心思想就是这个。
11. 字段名必须完全一样吗?
默认情况下,JSON 字段名和 Java 属性名最好一致。
比如:
json
{
"userName": "张三"
}
对应:
java
private String userName;
然后 setter 是:
java
public void setUserName(String userName) {
this.userName = userName;
}
如果前端传的是下划线:
json
{
"user_name": "张三"
}
Java 里是:
java
private String userName;
默认不一定能自动匹配,除非你配置了命名策略,或者加注解:
java
@JsonProperty("user_name")
private String userName;
意思是:
text
JSON 里的 user_name 对应 Java 里的 userName
12. 为什么 private 字段也能赋值?
你可能会疑惑:
java
private String name;
既然是 private,Jackson 怎么能赋值?
通常有两种方式。
第一种是通过 public setter:
java
public void setName(String name) {
this.name = name;
}
这不算直接访问 private 字段,而是正常调用公开方法。
第二种是 Jackson 可以通过反射强行访问字段:
java
field.setAccessible(true);
field.set(user, "张三");
这就绕过了 private 限制。
不过普通 Java 业务代码不建议这么干,但框架底层经常会这么做。
13. 如果没有 setter 可以吗?
看情况。
比如:
java
public class User {
private Long id;
private String name;
}
没有 setter,Jackson 默认可能无法正常赋值,具体取决于配置、字段可见性、构造器等。
推荐新手阶段先写成这样:
java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Long id;
private String name;
private Integer age;
}
这里 Lombok 的:
java
@Data
会生成 getter/setter。
java
@NoArgsConstructor
会生成无参构造方法。
这样 Jackson 反序列化最省心。
14. 如果 JSON 多传了字段怎么办?
前端传:
json
{
"id": 1,
"name": "张三",
"age": 18,
"address": "南京"
}
但 Java 类没有:
java
private String address;
默认情况下,Spring Boot/Jackson 通常会忽略未知字段,具体也跟配置有关。
如果配置严格模式,也可能报错。
可以通过注解控制:
java
@JsonIgnoreProperties(ignoreUnknown = true)
public class User {
}
意思是:
text
JSON 里多出来的字段不用管
15. 如果 JSON 少传字段怎么办?
前端传:
json
{
"name": "张三"
}
Java 类是:
java
public class User {
private Long id;
private String name;
private Integer age;
}
那么最后对象大概是:
java
User{
id = null,
name = "张三",
age = null
}
少传的字段一般就是默认值。
对象类型默认是:
text
null
基本类型默认是:
text
int -> 0
boolean -> false
所以接口 DTO 里通常更推荐用包装类型:
java
Integer age;
Long id;
而不是:
java
int age;
long id;
因为 null 能表示"前端没传"。
16. @RequestBody 和 @RequestParam 的区别
这个也很容易混。
@RequestBody
读请求体 JSON:
http
POST /user
Content-Type: application/json
{
"name": "张三",
"age": 18
}
后端:
java
@PostMapping("/user")
public void addUser(@RequestBody User user) {
}
@RequestParam
读 URL 参数或表单参数:
http
POST /user?name=张三&age=18
后端:
java
@PostMapping("/user")
public void addUser(@RequestParam String name,
@RequestParam Integer age) {
}
所以:
text
@RequestBody:从请求体 JSON 里取
@RequestParam:从 URL 参数或者表单参数里取
17. Controller 方法里为什么直接就是 User?
因为 Spring 在调用你的方法之前,已经把参数准备好了。
你写的是:
java
public void addUser(@RequestBody User user)
Spring 实际上会先做:
java
User user = jackson把JSON转出来;
然后再调用:
java
addUser(user);
所以你不是"直接拿到了 JSON",而是拿到了 Spring 帮你转换后的对象。
18. 最核心理解
你可以这样记:
@RequestBody User user的意思是:Spring 读取 HTTP 请求体中的 JSON,然后用 Jackson 把 JSON 解析成User对象,再把这个对象作为参数传给你的 Controller 方法。
再直白一点:
text
前端传 JSON 字符串
Spring 负责接收请求
@RequestBody 表示从请求体读数据
Jackson 负责 JSON ↔ Java 对象转换
反射负责创建对象、找字段、调 setter、赋值
最后你的方法拿到完整 User 对象
所以这个过程不是 Java 语言天然自动完成的,而是 Spring Boot 自动配置好了 Jackson 转换器,帮你做了这件事。