反射
字数: 0 字 时长: 0 分钟
Java 反射
字数: 0 字 时长: 0 分钟
1. Java 反射概述
反射(Reflection)是 Java 提供的一套:
在程序运行期间动态获取类的信息,并操作类、对象、字段、方法和构造方法的机制。
正常编写 Java 代码时,我们通常在编译阶段就已经知道:
User user =
new User();
user.setName(
"kerwim"
);
user.sayHello();这里编译器提前知道:
User 是什么类
setName() 是什么方法
sayHello() 是什么方法而反射允许我们在运行时才决定:
操作哪个类
创建哪个对象
调用哪个方法
读取哪个字段例如:
Class<?> clazz =
Class.forName(
"com.kerwim.User"
);这里类名甚至可以来自:
配置文件
数据库
注解
网络数据
框架扫描结果然后程序再动态操作它。
可以简单理解:
普通 Java
编译期
↓
知道类型
↓
直接调用
反射
运行期
↓
获得 Class
↓
分析类结构
↓
动态创建 / 调用 / 修改1.1 反射能做什么
Java 反射主要可以:
获取类的信息
获取字段
获取方法
获取构造方法
创建对象
读取和修改字段
动态调用方法
读取注解
分析继承关系
分析接口
配合框架完成动态处理例如:
Class<?> clazz =
User.class;可以继续获取:
clazz.getName();
clazz.getDeclaredFields();
clazz.getDeclaredMethods();
clazz.getDeclaredConstructors();
clazz.getAnnotations();所以:
反射的核心入口就是
Class对象。
1.2 为什么需要反射
假设我们现在需要根据配置文件创建对象。
配置:
class=com.kerwim.UserService普通写法必须提前知道:
UserService service =
new UserService();但如果配置改成:
class=com.kerwim.OrderService代码也需要跟着修改。
使用反射:
String className =
"com.kerwim.UserService";
Class<?> clazz =
Class.forName(
className
);
Object object =
clazz
.getDeclaredConstructor()
.newInstance();只要:
className发生变化,就可以创建不同对象。
因此反射特别适合:
框架
IOC 容器
ORM
序列化
插件系统
测试框架
注解处理这些:
代码编写阶段无法完全确定具体类型,需要运行时动态处理的场景。
2. Class 对象
理解反射之前,最重要的是理解:
java.lang.Class每一个被 JVM 加载的类型,在 JVM 中都会存在对应的:
Class对象。
例如:
User.class对应:
User 类型的 Class 对象可以简单理解:
User.java
↓
javac
↓
User.class 字节码
↓
ClassLoader 加载
↓
JVM
↓
Class<User> 对象反射真正操作的入口并不是:
.java 文件也不是直接修改:
.class 字节码文件而是:
通过 JVM 中的
Class对象获取类型的运行时信息。
2.1 一个类对应多少个 Class 对象
在:
同一个 ClassLoader加载范围内,同一个类型对应同一个:
Class对象。
例如:
Class<User> class1 =
User.class;
Class<?> class2 =
new User().getClass();
System.out.println(
class1 == class2
);输出:
true因为:
User.class
new User().getClass()得到的是同一个运行时类型对应的 Class 对象。
不过需要注意:
同名类如果由不同 ClassLoader 加载,在 JVM 看来可以属于不同的类型,并拥有不同的 Class 对象。
这一点后面学习:
类加载机制
双亲委派
Tomcat
插件系统时会更加重要。
3. 获取 Class 对象
Java 中常见的获取 Class 对象方式有三种。
3.1 类名.class
如果编译阶段已经知道类型,可以:
Class<User> clazz =
User.class;这是最直接的方式。
例如:
Class<String> clazz =
String.class;还可以用于基本数据类型:
Class<Integer> intClass =
int.class;数组:
Class<String[]> arrayClass =
String[].class;这种方式:
类型明确
写法简单
没有 ClassNotFoundException3.2 对象.getClass()
如果已经存在对象:
User user =
new User();可以:
Class<?> clazz =
user.getClass();getClass() 定义在:
Object类中。
因为所有普通 Java 对象最终都继承:
Object所以都可以:
object.getClass();例如:
String name =
"kerwim";
Class<?> clazz =
name.getClass();
System.out.println(
clazz.getName()
);输出:
java.lang.String3.3 Class.forName()
如果只有完整类名字符串:
String className =
"com.kerwim.User";可以:
Class<?> clazz =
Class.forName(
className
);这种方式在框架中非常常见。
因为类名可能来自:
配置文件
数据库
扫描结果
插件配置Class.forName() 找不到类时,会抛出:
ClassNotFoundException所以需要:
try {
Class<?> clazz =
Class.forName(
"com.kerwim.User"
);
} catch (
ClassNotFoundException e) {
// 处理异常
}默认情况下:
Class.forName(
"完整类名"
)还会触发类的初始化。
如果只希望加载而暂时不初始化,可以使用:
Class.forName(
className,
false,
classLoader
);3.4 三种方式对比
| 方式 | 使用场景 | 是否需要已存在对象 |
|---|---|---|
User.class | 编译期已经知道类型 | 否 |
user.getClass() | 已经存在对象 | 是 |
Class.forName() | 运行时只有类名字符串 | 否 |
可以简单记:
知道类型
↓
User.class
已有对象
↓
user.getClass()
只有类名
↓
Class.forName()4. 获取类的基本信息
拿到:
Class<?> clazz以后,就可以开始分析类。
假设:
package com.kerwim;
public class User {
}4.1 获取完整类名
clazz.getName();返回:
com.kerwim.User4.2 获取简单类名
clazz.getSimpleName();返回:
User4.3 获取包信息
clazz.getPackageName();返回:
com.kerwim4.4 获取父类
Class<?> superclass =
clazz.getSuperclass();例如:
User extends Person那么:
clazz.getSuperclass();可以得到:
Person对应的 Class 对象。
4.5 获取接口
Class<?>[] interfaces =
clazz.getInterfaces();例如:
public class User
implements Serializable,
Comparable<User> {
}就可以获得对应接口。
4.6 判断类型
常见方法:
clazz.isInterface();
clazz.isEnum();
clazz.isAnnotation();
clazz.isArray();
clazz.isPrimitive();
clazz.isRecord();例如:
System.out.println(
String.class
.isInterface()
);输出:
false5. 获取字段 Field
反射中:
java.lang.reflect.Field用于描述:
字段假设:
public class User {
public Long id;
private String name;
private int age;
}5.1 getFields()
Field[] fields =
clazz.getFields();它主要获取:
当前类以及父类中可访问的
public字段。
例如:
for (Field field : fields) {
System.out.println(
field.getName()
);
}5.2 getDeclaredFields()
Field[] fields =
clazz.getDeclaredFields();它获取:
当前类自己声明的所有字段。
包括:
public
protected
默认权限
private但它:
不会自动包含父类声明的字段所以:
getFields()
↓
public
↓
包含继承来的
getDeclaredFields()
↓
当前类声明的全部字段
↓
不自动向父类查找5.3 获取指定字段
例如:
Field field =
clazz.getDeclaredField(
"name"
);如果找不到:
name会抛出:
NoSuchFieldException5.4 获取字段信息
拿到:
Field field以后可以:
field.getName();
field.getType();
field.getModifiers();
field.getAnnotations();例如:
System.out.println(
field.getName()
);
System.out.println(
field.getType()
);可能输出:
name
class java.lang.String6. 通过反射操作字段
反射不仅能:
看字段还可以:
读取字段值
修改字段值例如:
public class User {
public String name;
}创建对象:
User user =
new User();获得字段:
Field field =
User.class
.getField(
"name"
);设置:
field.set(
user,
"kerwim"
);读取:
Object value =
field.get(user);
System.out.println(value);输出:
kerwim这里:
field.set(
user,
"kerwim"
);可以理解:
user.name = "kerwim"而:
field.get(user)相当于:
读取 user.name6.1 操作 private 字段
如果字段是:
private String name;直接:
field.get(user);通常会受到 Java 访问控制限制。
传统反射代码中经常:
Field field =
User.class
.getDeclaredField(
"name"
);
field.setAccessible(
true
);
field.set(
user,
"kerwim"
);然后:
Object value =
field.get(user);这样可以绕过普通 Java 语言级别的访问检查。
但现代 Java 需要注意:
setAccessible(true)并不意味着任何 private 成员都一定可以无限制访问。
Java 9 引入模块系统以后,强封装可能阻止对其他模块内部成员的深层反射。
某些情况下可能出现:
InaccessibleObjectException所以:
setAccessible(true)应该理解为:
请求取消普通访问检查而不是:
拥有无限权限7. 获取方法 Method
反射中的:
java.lang.reflect.Method用于描述:
方法假设:
public class User {
public void sayHello() {
System.out.println(
"hello"
);
}
private String getName(
String prefix) {
return prefix
+ "kerwim";
}
}7.1 getMethods()
Method[] methods =
clazz.getMethods();主要获取:
当前类以及父类 / 接口中的 public 方法包括:
Object中继承来的:
toString()
hashCode()
getClass()
equals()等方法。
7.2 getDeclaredMethods()
Method[] methods =
clazz.getDeclaredMethods();获取:
当前类自己声明的全部方法。
包括:
public
protected
默认权限
private但不会自动遍历父类方法。
7.3 获取指定方法
无参:
Method method =
clazz.getDeclaredMethod(
"sayHello"
);有参数:
Method method =
clazz.getDeclaredMethod(
"getName",
String.class
);为什么需要传:
String.class?
因为 Java 支持:
方法重载例如:
test();
test(String value);
test(int value);单靠:
方法名无法确定到底是哪一个方法。
所以反射获取方法时需要:
方法名
+
参数类型列表来定位。
8. 通过反射调用方法
获得:
Method以后,可以使用:
invoke()调用。
8.1 调用无参方法
例如:
User user =
new User();
Method method =
User.class
.getDeclaredMethod(
"sayHello"
);
method.invoke(user);相当于:
user.sayHello();8.2 调用有参方法
例如:
Method method =
User.class
.getDeclaredMethod(
"getName",
String.class
);
method.setAccessible(
true
);
Object result =
method.invoke(
user,
"Java:"
);相当于:
user.getName(
"Java:"
);invoke():
method.invoke(
对象,
参数1,
参数2,
...
);其中第一个参数是:
在哪个对象上调用这个方法后面才是真正的方法参数。
8.3 调用 static 方法
如果方法是:
public static void hello() {
}可以:
Method method =
User.class
.getDeclaredMethod(
"hello"
);
method.invoke(
null
);因为:
static 方法属于类不依赖具体对象。
9. 获取构造方法 Constructor
反射中的:
java.lang.reflect.Constructor表示:
构造方法假设:
public class User {
private String name;
public User() {
}
public User(
String name) {
this.name = name;
}
}9.1 获取所有 public 构造方法
Constructor<?>[] constructors =
clazz.getConstructors();9.2 获取当前类声明的全部构造方法
Constructor<?>[] constructors =
clazz.getDeclaredConstructors();包括:
private
protected
默认权限
public9.3 获取指定构造方法
无参:
Constructor<User> constructor =
User.class
.getDeclaredConstructor();有参:
Constructor<User> constructor =
User.class
.getDeclaredConstructor(
String.class
);10. 通过反射创建对象
现代 Java 推荐通过:
Constructor.newInstance()创建对象。
例如:
Constructor<User> constructor =
User.class
.getDeclaredConstructor();
User user =
constructor.newInstance();相当于:
User user =
new User();有参构造:
Constructor<User> constructor =
User.class
.getDeclaredConstructor(
String.class
);
User user =
constructor.newInstance(
"kerwim"
);相当于:
User user =
new User(
"kerwim"
);10.1 不推荐 Class.newInstance()
以前经常看到:
Object object =
clazz.newInstance();这种方式已经被:
Deprecated不推荐使用。
现代 Java 推荐:
clazz
.getDeclaredConstructor()
.newInstance();原因之一是:
异常处理更加清晰
构造方法选择更加明确所以现在应该优先:
getDeclaredConstructor()
.newInstance()11. getXxx 和 getDeclaredXxx 的区别
反射中经常看到两组方法:
getFields()
getDeclaredFields()
getMethods()
getDeclaredMethods()
getConstructors()
getDeclaredConstructors()核心区别可以简单记成:
getXxx
↓
通常强调 public
↓
可能包含继承来的成员
getDeclaredXxx
↓
当前类自己声明
↓
不管访问修饰符
↓
不自动包含父类成员例如字段:
| 方法 | private | public | 父类 public |
|---|---|---|---|
getFields() | ✗ | ✓ | ✓ |
getDeclaredFields() | ✓ | ✓ | ✗ |
方法也是类似逻辑。
因此框架扫描实体类字段时,经常使用:
getDeclaredFields()而如果只关心公共 API,可以使用:
getMethods()12. Modifier 修饰符
通过反射还可以判断:
public
private
protected
static
final
abstract等修饰符。
例如:
int modifiers =
field.getModifiers();再配合:
Modifier判断:
Modifier.isPrivate(
modifiers
);
Modifier.isStatic(
modifiers
);
Modifier.isFinal(
modifiers
);例如:
if (
Modifier.isPrivate(
field.getModifiers()
)
) {
System.out.println(
"这是 private 字段"
);
}所以:
Modifier可以理解为:
解析反射成员修饰符的工具类。
13. 反射获取泛型信息
前面泛型笔记中讲过:
Java 泛型存在类型擦除但并不是:
所有泛型信息在运行时都彻底不存在例如类声明:
public class UserService
implements Service<User> {
}这里:
Service<User>中的泛型声明信息,可以通过反射 API 获取。
例如:
Type[] interfaces =
UserService.class
.getGenericInterfaces();常见反射泛型类型:
Type
ParameterizedType
TypeVariable
WildcardType
GenericArrayType例如:
Type type =
UserService.class
.getGenericInterfaces()[0];
if (
type
instanceof
ParameterizedType parameterizedType
) {
Type[] arguments =
parameterizedType
.getActualTypeArguments();
}因此更准确地说:
类型擦除会擦除对象实例中用于普通类型检查的大部分具体泛型参数,但类文件中某些泛型声明信息仍然可以通过反射读取。
例如不能:
if (
list
instanceof
List<String>
) {
}但可以分析:
字段声明
方法声明
父类声明
接口声明中的泛型签名。
这也是很多框架能够分析:
List<User>
Result<User>
Converter<String, Integer>等声明信息的原因。
14. 反射读取注解
前面 Java 注解.md 中已经讲过:
注解本身只是元数据真正让:
@Retention(
RetentionPolicy.RUNTIME
)注解发挥作用的常见方式之一就是:
反射例如定义:
@Retention(
RetentionPolicy.RUNTIME
)
@Target(
ElementType.FIELD
)
public @interface JsonField {
String value()
default "";
}实体:
public class User {
@JsonField(
"userName"
)
private String name;
}反射:
Field field =
User.class
.getDeclaredField(
"name"
);判断:
boolean present =
field.isAnnotationPresent(
JsonField.class
);读取:
JsonField annotation =
field.getAnnotation(
JsonField.class
);读取参数:
String value =
annotation.value();整个流程:
Class
↓
获取 Field
↓
检查 @JsonField
↓
获取注解对象
↓
读取注解参数
↓
执行对应逻辑所以:
注解
+
反射是一组非常经典的组合。
15. 使用反射做一个简单对象打印器
假设:
public class User {
private Long id;
private String name;
private int age;
public User(
Long id,
String name,
int age) {
this.id = id;
this.name = name;
this.age = age;
}
}希望写一个通用方法:
打印任意对象中的字段可以:
public static void printObject(
Object object)
throws IllegalAccessException {
Class<?> clazz =
object.getClass();
Field[] fields =
clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(
true
);
String name =
field.getName();
Object value =
field.get(object);
System.out.println(
name
+ " = "
+ value
);
}
}调用:
User user =
new User(
1L,
"kerwim",
18
);
printObject(user);输出:
id = 1
name = kerwim
age = 18这个方法完全不知道:
User 有哪些字段它是在:
运行时通过反射扫描出来的。
这就是反射真正强大的地方:
代码可以操作自己在编译阶段并不知道具体结构的对象。
16. 反射与框架
反射真正重要的地方不是:
平时代替 getter / setter而是在各种框架内部。
例如:
Spring
MyBatis
Jackson
Hibernate
JUnit
Mockito都大量依赖类似思想。
16.1 Spring IOC
假设:
@Service
public class UserService {
}Spring 可以:
扫描 Class
↓
发现 @Service
↓
通过反射分析类
↓
寻找构造方法
↓
创建 UserService 对象
↓
放进 IOC 容器如果字段还有:
@Autowired
private UserMapper userMapper;框架又可以:
扫描字段
↓
发现 @Autowired
↓
寻找对应 Bean
↓
完成依赖注入这里本质上就是:
注解
+
反射
+
IOC16.2 MyBatis / ORM
例如数据库查询结果:
id = 1
name = kerwim
age = 18需要转换为:
User框架可以:
获取 User.class
↓
创建 User 对象
↓
读取字段 / Setter
↓
根据字段名映射数据库列
↓
设置属性值底层同样离不开:
反射16.3 Jackson JSON 序列化
对象:
User user;转换:
{
"id": 1,
"name": "kerwim"
}Jackson 需要知道:
对象有哪些字段
有哪些 getter
字段有没有注解
字段类型是什么这些信息都可以通过:
反射获得。
16.4 JUnit
例如:
@Test
public void testCreateUser() {
}JUnit 可以:
扫描测试类
↓
寻找 @Test
↓
获得对应 Method
↓
method.invoke(...)从而自动执行测试方法。
所以:
反射是很多 Java 框架实现“自动化”和“动态化”的基础设施之一。
17. 反射与动态代理
Java 的:
JDK 动态代理也和反射关系非常密切。
例如:
Proxy.newProxyInstance(
classLoader,
interfaces,
invocationHandler
);其中:
InvocationHandler的核心方法:
Object invoke(
Object proxy,
Method method,
Object[] args
)这里直接拿到了:
Method对象。
然后可以:
method.invoke(
target,
args
);最终形成:
调用代理对象
↓
InvocationHandler
↓
获得 Method
↓
前置逻辑
↓
反射调用目标方法
↓
后置逻辑这也是:
AOP
事务
日志
权限
RPC等技术的重要基础之一。
动态代理本身内容比较多,可以后面在:
代理模式 / AOP中单独展开。
18. 反射的缺点
反射非常强大,但不意味着:
所有代码都应该用反射它也有明显缺点。
18.1 可读性降低
普通代码:
user.setName(
"kerwim"
);非常直接。
反射:
Method method =
User.class
.getDeclaredMethod(
"setName",
String.class
);
method.invoke(
user,
"kerwim"
);明显更复杂。
18.2 编译期检查能力下降
普通代码:
user.sayHello();如果方法不存在:
编译阶段直接报错而反射:
clazz.getDeclaredMethod(
"sayHelo"
);字符串写错:
编译阶段可能完全没问题
↓
运行时
↓
NoSuchMethodException也就是说:
一部分错误
↓
从编译期推迟到了运行时18.3 性能存在额外开销
反射需要:
查找成员
访问检查
参数包装
动态调用所以相比普通直接调用通常存在额外开销。
不过现代 JVM 对反射已经进行了大量优化。
因此不应该简单说:
反射性能非常差
绝对不能使用正确理解应该是:
普通业务代码没有必要为了炫技使用反射,但框架和基础设施中合理使用反射完全正常。
真正性能敏感的框架还可能使用:
缓存反射对象
MethodHandle
字节码生成
代码生成进一步优化。
18.4 破坏封装
通过:
setAccessible(true)可能访问:
private 字段
private 方法这会削弱:
private原本提供的封装边界。
所以反射应该:
有明确目的地使用而不是在普通业务代码中随意绕过访问权限。
19. 反射的常见异常
使用反射时经常会遇到一些异常。
19.1 ClassNotFoundException
Class.forName(...)找不到目标类。
19.2 NoSuchFieldException
getDeclaredField(...)找不到字段。
19.3 NoSuchMethodException
getDeclaredMethod(...)
getDeclaredConstructor(...)找不到对应方法或构造方法。
19.4 IllegalAccessException
当前反射操作没有访问权限。
19.5 InstantiationException
某些反射创建对象场景中无法完成实例化。
19.6 InvocationTargetException
通过:
Method.invoke()或者:
Constructor.newInstance()调用目标代码时,如果目标方法本身抛出异常,反射 API 可能使用:
InvocationTargetException进行包装。
例如:
try {
method.invoke(
object
);
} catch (
InvocationTargetException e) {
Throwable cause =
e.getCause();
}真正业务方法抛出的异常通常可以从:
e.getCause()获得。
这一点非常重要:
InvocationTargetException
↓
反射调用产生的包装异常
getCause()
↓
目标方法真正抛出的异常19.7 InaccessibleObjectException
现代 Java 模块强封装下:
setAccessible(true)可能无法突破模块边界。
这时可能出现:
InaccessibleObjectException说明:
反射访问被模块系统限制20. 反射使用原则
反射适合:
框架底层
通用工具
对象映射
序列化
IOC
ORM
插件系统
注解处理
动态代理普通业务代码如果已经明确知道类型:
User user =
new User();就没必要写成:
Class<?> clazz =
User.class;
Object user =
clazz
.getDeclaredConstructor()
.newInstance();所以可以简单记:
类型明确
↓
优先普通 Java 调用
类型运行时才能确定
↓
考虑反射另外,如果需要重复使用反射对象:
Field
Method
Constructor通常可以:
缓存避免每次重复查找。
例如框架经常:
第一次扫描
↓
缓存 Method / Field 元数据
↓
后续重复使用而不是每次请求都重新完整扫描类。
21. 小结
Java 反射的核心入口:
Class获取 Class:
User.class
user.getClass()
Class.forName(...)通过 Class 可以获取:
Field
↓
字段
Method
↓
方法
Constructor
↓
构造方法
Annotation
↓
注解然后可以进行:
读取字段
修改字段
调用方法
创建对象
读取注解
分析类型最重要的一组区别:
getXxx()
↓
通常获取 public
↓
可能包含继承成员
getDeclaredXxx()
↓
当前类自己声明
↓
包含 private
↓
不自动包含父类成员反射和其他 Java 技术之间的关系:
注解
+
反射
↓
读取运行时元数据
反射
+
IOC
↓
动态创建和注入对象
反射
+
ORM
↓
数据库数据映射对象
反射
+
动态代理
↓
AOP / 事务 / 日志整个反射过程可以浓缩成:
获得 Class
↓
分析类结构
↓
获得 Field / Method / Constructor
↓
动态读取 / 调用 / 创建一句话概括:
反射就是让 Java 程序能够在运行时观察并操作类型本身,是 Java 框架实现动态化能力的重要基础。