Java 反射机制
反射概述
Java 反射是指程序在运行期间获取类的结构信息,并动态创建对象、访问字段、调用方法或调用构造器的机制。
通常情况下,代码在编译时就已经明确要使用的类型。使用反射后,程序可以在运行时根据类名或配置文件决定加载哪个类,因此反射常用于框架、依赖注入、对象映射和通用工具类。
类的加载与 Class 对象
JVM 加载一个类后,会为该类型创建唯一的 Class 对象。Class 对象保存了类名、字段、方法、构造器、注解等结构信息,是使用反射的入口。
获取 Class 对象
通过 Class.forName() 获取
try {
Class<?> personClass = Class.forName("com.hyxy.Person");
System.out.println(personClass.getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
这种方式通过完整类名加载类,适合类名来自配置文件,或编译时不能直接引用目标类型的场景。
通过类字面量获取
Class<Person> personClass = Person.class;
编译时已经知道目标类型时,使用 类型.class 最直接,也不会抛出 ClassNotFoundException。
通过对象获取
Person person = new Person();
Class<?> personClass = person.getClass();
已经持有对象,但需要获取其运行时类型时,可以调用 getClass()。
public static void printType(Object object) {
Class<?> objectClass = object.getClass();
System.out.println(objectClass.getName());
}
Class 类的常用方法
| 方法 | 作用 |
|---|---|
getName() | 获取完整类名,包括包名 |
getSimpleName() | 获取不包含包名的类名 |
getDeclaredField(String name) | 获取当前类中指定名称的字段 |
getDeclaredFields() | 获取当前类声明的全部字段 |
getDeclaredMethod(String name, Class<?>... parameterTypes) | 根据方法名和参数类型获取方法 |
getDeclaredMethods() | 获取当前类声明的全部方法 |
getDeclaredConstructor(Class<?>... parameterTypes) | 根据参数类型获取构造器 |
getDeclaredConstructors() | 获取当前类声明的全部构造器 |
Class.newInstance() 已经过时。创建对象时,应先获得构造器,再调用构造器的 newInstance()。
Object object = personClass.getDeclaredConstructor().newInstance();
使用 Field 操作字段
每个 Field 对象代表类中声明的一个字段。通过 Field 可以获得字段名称、字段类型,并对指定对象的字段进行读取或赋值。
常用方法如下:
| 方法 | 作用 |
|---|---|
getName() | 获取字段名 |
getType() | 获取字段类型 |
setAccessible(true) | 允许反射访问非公有字段 |
set(Object object, Object value) | 为指定对象的字段赋值 |
get(Object object) | 读取指定对象的字段值 |
下面的示例创建一个 Student 对象,并通过反射修改其 name 字段:
package com.hyxy;
import java.lang.reflect.Field;
public class FieldTest {
public static void main(String[] args) {
try {
Class<?> studentClass = Class.forName("com.hyxy.Student");
Object student = studentClass.getDeclaredConstructor().newInstance();
Field nameField = studentClass.getDeclaredField("name");
nameField.setAccessible(true);
nameField.set(student, "Tom");
Object name = nameField.get(student);
System.out.println(name);
} catch (ReflectiveOperationException e) {
e.printStackTrace();
}
}
}getDeclaredField() 能获取当前类声明的字段,包括私有字段,但不会自动向父类查找。访问私有字段时通常需要调用 setAccessible(true)。
使用 Method 调用方法
每个 Method 对象代表类中的一个方法。调用 invoke() 时,第一个参数是方法所属的对象,后续参数是调用该方法需要传入的实参。
package com.hyxy;
import java.lang.reflect.Method;
public class MethodTest {
public static void main(String[] args) {
try {
Class<?> studentClass = Class.forName("com.hyxy.Student");
Object student = studentClass.getDeclaredConstructor().newInstance();
Method method1 = studentClass.getDeclaredMethod("study");
Method method2 = studentClass.getDeclaredMethod(
"study",
String.class,
int.class
);
method1.setAccessible(true);
method2.setAccessible(true);
method1.invoke(student);
method2.invoke(student, "Java", 24);
} catch (ReflectiveOperationException e) {
e.printStackTrace();
}
}
}查找重载方法时,不仅要提供方法名,还要提供准确的参数类型列表。调用方法过程中,目标方法内部抛出的异常会被包装为 InvocationTargetException。
对于静态方法,调用 invoke() 时第一个参数可以传入 null。
method.invoke(null, "参数");
使用 Constructor 创建对象
每个 Constructor 对象代表类中的一个构造器。通过参数类型可以获得指定构造器,再调用 newInstance() 创建对象。
package com.hyxy;
import java.lang.reflect.Constructor;
public class ConstructorTest {
public static void main(String[] args) {
try {
Class<Student> studentClass = Student.class;
Constructor<Student> constructor = studentClass.getDeclaredConstructor(
String.class,
int.class,
int.class
);
constructor.setAccessible(true);
Student student = constructor.newInstance("Tom", 20, 110011);
System.out.println(
student.getName() + "," +
student.getAge() + "," +
student.getNo()
);
} catch (ReflectiveOperationException e) {
e.printStackTrace();
}
}
}反射实现通用 DAO
基本思路
传统 DAO 通常为每张表分别编写增、删、改、查代码。通用 DAO 可以通过反射读取实体类的类名和字段信息,再动态生成 SQL。
一种简单的约定是:
- 实体类名对应表名。
- 实体类字段名对应列名。
- 主键字段名由调用者传入。
- 自增主键在插入时不作为占位符参数传入。
真实项目中,类名和表名、字段名和列名往往不完全相同,因此通常还会结合注解完成映射。
通用新增方法示例
package com.hyxy.dao;
import org.apache.commons.dbutils.QueryRunner;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
public class CommonDao {
public int insert(Object entity, String primaryKeyName, boolean autoIncrement) {
Class<?> entityClass = entity.getClass();
String tableName = entityClass.getSimpleName();
Field[] fields = entityClass.getDeclaredFields();
List<String> columnNames = new ArrayList<>();
List<String> placeholders = new ArrayList<>();
List<Object> parameterValues = new ArrayList<>();
for (Field field : fields) {
String fieldName = field.getName();
if (autoIncrement && fieldName.equals(primaryKeyName)) {
continue;
}
field.setAccessible(true);
columnNames.add(fieldName);
placeholders.add("?");
try {
parameterValues.add(field.get(entity));
} catch (IllegalAccessException e) {
throw new RuntimeException("读取字段失败:" + fieldName, e);
}
}
String sql = "insert into " + tableName +
"(" + String.join(",", columnNames) + ") values(" +
String.join(",", placeholders) + ")";
QueryRunner queryRunner = new QueryRunner();
try (Connection connection = DBUtil.getConnection()) {
return queryRunner.update(
connection,
sql,
parameterValues.toArray()
);
} catch (SQLException e) {
throw new RuntimeException("执行新增操作失败", e);
}
}
}这个示例展示了反射和 JDBC 封装的基本组合方式,但仍有以下限制:
- 实体类名必须与表名一致。
- 字段名必须与列名一致。
- 字段顺序依赖反射返回结果,不应将其作为数据库固定顺序使用。
- 真实项目还需要处理主键、继承字段、静态字段、事务和异常体系。
- 表名与列名不能使用预编译占位符,因此必须来自可信的映射信息,不能直接使用用户输入。
反射使用注意事项
反射提高了程序的动态性,但也会降低代码的可读性,并绕过部分编译期类型检查。使用时应遵循以下原则:
- 能直接调用时,优先使用普通 Java 代码。
- 对反射入口进行类型和参数校验。
- 不要把未经校验的用户输入直接作为类名、方法名、表名或列名。
- 对
ReflectiveOperationException进行统一处理。 - 在模块化 Java 项目中,访问非公有成员还可能受到模块访问规则限制。
喜欢的话,留下你的评论吧~