目录
一、什么是反射
二、反射的主要用途?
三、什么情况下使用反射
四、反射有什么优点?
1、增加程序的灵活性
2、避免将固有的逻辑程序写死到代码里
3、提高代码的复用率
4、支持动态代理和动态配置
5、支持自动化测试和代码生成
6、自由度高,可以无视访问权限限制
五、反射的缺点
六、反射实现
1、三种获取Class对象的方式
2、通过JSON字符串进行反射
3、Class相关操作
4、constructor反射调用构造方法
5、getConstructors()和getDeclaredConstructors()的区别
6、反射成员变量Field
7、反射调用方法
8、反射调用静态方法
9、模仿mybatis执行sql反射
一、什么是反射
反射(Reflection)是一种 Java 程序运行期间的动态技术,可以在运行时(runtime )检查、修改其自身结构或行为。通过反射,程序可以访问、检测和修改它自己的类、对象、方法、属性等成员。
二、反射的主要用途?
动态加载类:程序可以在运行时动态地加载类库中的类;
动态创建对象:反射可以基于类的信息,程序可以在运行时,动态创建对象实例;
调用方法:反射可以根据方法名称,程序可以在运行时,动态地调用对象的方法(即使方法在编写程序时还没有定义)
访问成员变量:反射可以根据成员变量名称,程序可以在运行时,访问和修改成员变量(反射可以访问私有成员变量)
运行时类型信息:反射允许程序在运行时,查询对象的类型信息,这对于编写通用的代码和库非常有用;
- spring 框架使用反射来自动装配组件,实现依赖注入;
- MyBatis 框架使用反射来创建 resultType 对象,封装数据查询结果;
三、什么情况下使用反射
Java中的反射机制是指在程序运行时动态获取类的信息以及使用该信息来创建、操作和销毁对象的能力。下面是一些Java中使用反射的情况:
- 动态加载类:可以使用类加载器动态加载要使用的类,而不是在编译期间声明对该类的依赖关系。
- 获取类的信息:通过反射可以获取一个类的属性、方法、构造函数等信息,甚至可以获取注解和泛型信息。
- 通过名称调用方法或访问属性:使用反射可以根据方法/属性名称动态地调用/访问对应的方法/属性,这使得编写通用代码更加容易。
- 动态代理:使用反射可以实现动态代理,即在运行时动态地创建一个实现某个接口的代理类,从而实现一些特殊的功能,如事务处理等。
四、反射有什么优点?
1、增加程序的灵活性
反射允许程序在运行时动态地访问和操作类的属性和方法,这意味着开发者可以根据需要,在程序执行过程中改变程序的行为,而无需在编译时硬编码这些行为。这种灵活性使得程序能够适应不同的运行环境或需求变化。
2、避免将固有的逻辑程序写死到代码里
通过反射,开发者可以将一些决策逻辑或行为延迟到运行时进行,而不是在编译时就确定下来。这有助于减少代码中的硬编码,提高代码的可维护性和可扩展性。
3、提高代码的复用率
反射使得开发者可以编写一些通用的函数或方法,这些函数或方法能够动态地处理不同类型的对象或方法。这有助于提高代码的复用率,减少重复代码的编写。
4、支持动态代理和动态配置
通过反射,开发者可以在运行时动态地创建代理对象,从而实现对目标对象的代理。这种机制在 AOP(面向切面编程)等领域中非常有用。此外,反射还支持在运行时读取和修改程序的配置参数,从而实现动态配置。
5、支持自动化测试和代码生成
反射技术可以应用于白动化测试中,通过动态地创建测试用例来验证程序的行为。同时,反射还可以用于在运行时生成程序代码,这在某些需要动态构建程序结构的场景中非常有用。
6、自由度高,可以无视访问权限限制
在某些情况下,反射可以绕过 ava 等语言的访问控制机制,直接访问私有成员。虽然这可能会带来一些安全风险,但在某些特定的应用场景中,这种能力是非常有用的;
五、反射的缺点
性能开销:反射操作通常比直接代码调用更慢,因为涉及到额外的检查和解步骤
安全问题:如果允许程序修改私有成员或调用私有方法,可能会破坏封装性和安全性。
代码可读性和可维护性:过度使用反射可能会使代码难以理解和维护,因为它隐藏了类型之间的依赖关系。
六、反射实现
首先是定义了一个实体类Document:
public class Document {
private String name;
private int favCount;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getFavCount() {
return favCount;
}
public void setFavCount(int favCount) {
this.favCount = favCount;
}
public Document() {
super();
}
public Document(String name, int favCount) {
super();
this.name = name;
this.favCount = favCount;
}
@Override
public String toString() {
return "Document [name=" + name + ", favCount=" + favCount + "]";
}
private Document(String name) {
super();
this.name = name;
}
public Document(int favCount) {
super();
this.favCount = favCount;
}
public static void dosth() {
System.out.println("这是一个dosth静态方法");
}
}
1、三种获取Class对象的方式
public static void main(String[] args) throws ClassNotFoundException {
// 方式1:通过类名
Class stringClass1 = String.class;
// 方式2:通过Class类的forName()方法
Class stringClass2 = Class.forName("java.lang.String");
// 方式3:通过对象调用getClass()方法
Class stringClass3 = "".getClass();
System.out.println(stringClass1.hashCode());
System.out.println(stringClass2.hashCode());
System.out.println(stringClass3.hashCode());
}
Class对象是 Java 反射机制的一部分,表示运行时的类或接口信息。一个类在 JVM 中只有一个 Class 对象,因此无论通过哪种方式获取该类的 Class 对象,结果都是相同的。
首先第一种是通过类名后加.class来获取class对象。第二种通过Class类的forName方法,根据类的全限定名获取Class对象。第三种是通过一个对象调用getClass方法获取该对象所属类的 Class 对象。
2、通过JSON字符串进行反射
public static void main(String[] args) {
String json = "{\"name\":\"长安荔枝\",\"favCount\":234}";
// 方法定义
// public static T parseObject(String json,Class jsonClass)
Document doc = JSON.parseObject(json,Document.class);
System.out.println(doc.getName());
System.out.println(doc.getFavCount());
}
这里导入了fastjson的jar包,获取到了JSON类来调用对应的方法,把json字符串传递给了parseObject方法,它是一个静态的方法,它将 JSON 字符串解析为指定类型的 Java 对象。第一个参数是传入的json字符串,后面是要解析的对象类型。最后转换成功后打印对象的两个属性。
执行结果:
3、Class相关操作
public static void main(String[] args) throws ClassNotFoundException {
Class clz = Class.forName("java.util.HashMap");
// 获取类名
System.out.println("完全限定名:" + clz.getName());
System.out.println("简单的类名:" + clz.getSimpleName());
System.out.println();
// 获取包名
System.out.println("package包名:" + clz.getPackage().getName());
System.out.println();
// 获取成员变量
Field[] fieldArray = clz.getDeclaredFields();
System.out.println("成员变量字段");
for (Field field : fieldArray) {
System.out.println(field);
}
System.out.println();
// 获取成员方法
Method[] methodArray = clz.getDeclaredMethods();
System.out.println("成员方法");
for (Method method : methodArray) {
System.out.println(method);
}
}
调用类名后通过调用.getName()返回类的完全限定名,比如java.util.HashMap。
通过.getSimpleName()返回类的简单名(不包含包名的类名),比如HashMap。
通过.getPackage().getName()返回类所在包的名称,比如java.util。
通过.getDeclaredMethods()方法返回类中所有的成员方法,包括私有、保护、默认(包)和公共方法,最后打印所有对象信息。
执行结果:
4、constructor反射调用构造方法
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException {
Class clz = Class.forName("com.apesource.demo01.Document");
// 方式1:直接通过Class对象,调用newInstance()方法
Object objx = clz.newInstance(); // 相当于在执行无参构造方法
// 方式2:通过构造器(构造方法)
// 无参构造方法
Constructor constructor1 = clz.getDeclaredConstructor(); // 获取无参构造方法
System.out.println(constructor1);
Object obj1 = constructor1.newInstance();
// 有参构造方法
Constructor constructor2 = clz.getDeclaredConstructor(String.class); // 获取有参构造方法
System.out.println(constructor2);
Object obj2 = constructor2.newInstance("两京十五日");
Constructor constructor3 = clz.getDeclaredConstructor(int.class); // 获取有参构造方法
System.out.println(constructor2);
Object obj3 = constructor3.newInstance(11);
Constructor constructor4 = clz.getDeclaredConstructor(String.class,int.class); // 获取有参构造方法
System.out.println(constructor2);
Object obj4 = constructor4.newInstance("两京十五日",11);
System.out.println(objx);
System.out.println(obj1);
System.out.println(obj2);
System.out.println(obj3);
System.out.println(obj4);
}
首先获取Class对象,通过调用newInstance()方法直接创建类的实例。通过调用这个方法会默认调用无参构造方法。如果没有无参或者默认无参就会抛出异常。
然后通过调用getDeclaredConstructor()获取无参构造方法,并创建 Constructor 对象,如果里面传入了参数就会返回对应的有参无参 Constructor 对象。然后再通过.newInstance()方法调用对应的构造方法,有参数则传入参数,无参数就无需传参,然后根据构造方法创造对应的实体类对象(这里是指Document对象)。
执行结果:
5、getConstructors()和getDeclaredConstructors()的区别
public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, ClassNotFoundException {
Class clz = Class.forName("com.apesource.demo01.Document");
// 获取一组构造器
Constructor[] constructorArray1 = clz.getConstructors(); // public
Constructor[] constructorArray2 = clz.getDeclaredConstructors(); // public、private
// 获取指定构造器
Constructor constructor = clz.getDeclaredConstructor(String.class);
System.out.println(constructor);
// 调用私有构造器,必须设置它的访问权限
constructor.setAccessible(true);
// 调用构造器,创建对象
Object obj = constructor.newInstance("长安三万里");
System.out.println(obj);
}
这里调用不同的方法获取Constructor对象数组,通过getConstructors()方法返回类的所有public构造器数组,意味着只能获取那些在外部可见的构造器,比如public等。
通过getDeclaredConstructors()方法返回类的所有构造器数组,包括 public、protected、default(包级私有)、和 private 构造器,无论它们的访问修饰符如何。
虽然getConstructors()能获取私有的构造方法,但是任然不对其进行调用操作,所以这里可以用setAccessible(true)方法来绕过 Java 访问控制检查,使其可以被调用。
6、反射成员变量Field
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchFieldException, SecurityException {
// 硬编码的方式
Document doc1 = new Document();
// doc1.name = "";
// doc1.favCount = 64;
// 反射的方式
Class clz = Class.forName("com.apesource.demo01.Document"); // 获取类型信息
Object obj = clz.newInstance(); // 创建对象
// 获取指定名称的成员变量
Field nameField = clz.getDeclaredField("name");
Field favCountField = clz.getDeclaredField("favCount");
// 访问私有的成员变量,必须设置权限
nameField.setAccessible(true);
favCountField.setAccessible(true);
// 使用成员变量,将指定数据存入对象中
nameField.set(obj, "长安十二时辰");
favCountField.setInt(obj, 128);
System.out.println(obj);
}
这里通过getDeclaredField("属性名")获取类中名为 name 的字段(成员变量),即使这个字段是私有的。
由于 name 和 favCount 是私有字段,直接访问会被 Java 的访问控制机制阻止。通过 setAccessible(true)可以绕过访问控制,使得可以访问和修改这些私有字段。
然后通过set方法,这个方法可以传与对应的字段相同类型的参数,value 的类型必须能够赋值给 name 字段,使用 nameField 字段对象将 "长安十二时辰" 的值设置到 obj 对象的 name 字段中。setInt方法是可以设置int属性,使用 favCountField 字段对象将 128 的值设置到 obj 对象的 favCount 字段中。最后打印对象。
7、反射调用方法
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException {
// 硬编码的方式
// Document doc1 = new Document();
// doc1.setName("海底两万里");
// doc1.setFavCount(10025);
// 反射的方式
Class clz = Class.forName("com.apesource.demo01.Document");
Object doc1 = clz.newInstance(); // 创建对象
// 获取指定名称和参数类型的方法
Method setNameMethod = clz.getMethod("setName", String.class);
Method setFavCountMethod = clz.getMethod("setFavCount",int.class);
// 执行方法
// 相当于doc1.setName("海底两万里");
setNameMethod.invoke(doc1, "海底两万里");
// doc1.setFavCount(10025);
setFavCountMethod.invoke(doc1, 10025);
System.out.println(doc1);
}
这里通过getMethod方法,获取 Document 类中名为 setName 且参数类型为 String 的 public 方法的 Method 对象。后面的方法过程相同。
然后再通过invoke方法,相当于调用 doc1.setName("海底两万里"),将 name 字段设置为 "海底两万里"。后面的调用过程相同。
最后输出结果。
8、反射调用静态方法
public static void main(String[] args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException, ClassNotFoundException {
// 硬编码的方式
// 调用静态方法
// Document.dosth();
String ret1 = String.format("HashMap的默认初始容量是%d,加载因子默认是%.2f", 16, 0.75f);
System.out.println(ret1);
// 反射的方式
Class clz1 = Class.forName("com.apesource.demo01.Document");
Method dosthMethod = clz1.getMethod("dosth");
dosthMethod.invoke(null);
Class clz2 = String.class;
Method formatMehtod = clz2.getMethod("format", String.class, Object[].class);
String ret2 = formatMehtod.invoke(null, "HashMap的默认初始容量是%d,加载因子默认是%.2f", new Object[] { 16, 0.75f }).toString();
System.out.println(ret2);
}
还是一样的通过getMethod方法来获取dosth静态方法的Method对象,因为dosth是静态方法,所以第一个参数可以传 null。这相当于 Document.dosth()。然后调用的format方法时,传入的是有参,第一个参数写为null,后面的参数正常来写。
最后输出结果。
9、模仿mybatis执行sql反射
首先我们定义一个实体类Subject:
public class Subject {
private int id;
private String title;
private String url;
private boolean playable;
private boolean isNew;
private double rate;
private String cover;
private String type;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public boolean isPlayable() {
return playable;
}
public void setPlayable(boolean playable) {
this.playable = playable;
}
public boolean isNew() {
return isNew;
}
public void setNew(boolean isNew) {
this.isNew = isNew;
}
public double getRate() {
return rate;
}
public void setRate(double rate) {
this.rate = rate;
}
public String getCover() {
return cover;
}
public void setCover(String cover) {
this.cover = cover;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
@Override
public String toString() {
return "Subject [id=" + id + ", title=" + title + ", url=" + url + ", playable=" + playable + ", isNew=" + isNew
+ ", rate=" + rate + ", cover=" + cover + ", type=" + type + "]";
}
}
然后我们在sql.properties配置文件中写入sql查询语句:
sql = select id,title,url,playable,is_new As isNew,rate,cover,type from subject
我们要创建一个sql执行器:
// SQL执行器
public class SQLHandler {
private static final String JDBC_URL = "jdbc:mysql://localhost:3306/mybatis_demo?serverTimezone=GMT";
private static final String DB_USER = "root";
private static final String DB_PASS = "123456";
前三个定义的静态成员常量是JDBC_URL、DB_USER 和 DB_PASS 是连接数据库所需的参数。分别是指定了 MySQL 数据库的连接 URL、数据库的用户名和密码。
然后是通过上面三个静态常量创建好的连接对象来创建创建 PreparedStatement 对象以执行 SQL 查询。ResultSet 对象用于存储查询结果。
// 执行
public static <T> List<T> execute(String sql, Class<T> resultType) throws NoSuchFieldException, SecurityException {
try {
// 创建Connection链接对象
Connection con = DriverManager.getConnection(JDBC_URL, DB_USER, DB_PASS);
// 创建PerparedStatement执行对象
PreparedStatement pst = con.prepareStatement(sql);
// 执行SQL语句,获取结果集
ResultSet rs = pst.executeQuery();
通过.getMetaData()方法获取所有刚才提取到的结果集的字段名称。然后创建一个用于保存查询出来字段结果的List集合。
// 获取结果集的字段名称
ResultSetMetaData resultSetMetaData = rs.getMetaData();
// 创建一个用于保存查询结果的List集合
List<T> queryList = new ArrayList<T>();
建立一个循环,每次都回去到结果集的每一行,通过反射创建 resultType 类的一个新实例。
while (rs.next()) {
// 每行数据,对应一个数据对象(反射方式创建)
Object resultData = resultType.newInstance();
因为一行数据都包含了所有字段,要完成所有字段的注入,才能完成对象的注入,完成实例化创建加载。所以这里进行遍历刚才提取到的所有字段,通过getColumnLabel(i)提取到对应的字段名,然后根据字段名调用getDeclaredField()获取 resultType 类中对应字段的 Field 对象。最后在设置是允许访问私有字段。
// 按照字段名称,获取字段别名
for (int i = 1; i <= resultSetMetaData.getColumnCount(); i++) {
// 获取当前字段名
String columnLabel = resultSetMetaData.getColumnLabel(i);
// 根据字段名称,获取成绩变量对象
Field field = resultType.getDeclaredField(columnLabel);
field.setAccessible(true);
然后是根据当前字段的类型来进行判断注入,通过.getColumnType(i)获取当前的字段类型,再根据.getColumnType(i)返回类型枚举来进行判断,如果是Types.INTEGER的话就是匹配获取数据类型为int数据设置到 resultData 对象中。Types.DOUBLE就是匹配获取数据类型为double数据设置到 resultData 对象中。如果其他类型的话就直接设置字符串值。如果字段是布尔类型,则需要额外处理,判断是否为true或者false,如果是则按照布尔类型来注入,如果是String类型,则正常注入。
// 判断当前字段的类型
// 根据字段名称,获取当前行中的指定数据
switch (resultSetMetaData.getColumnType(i)) {
case Types.INTEGER:
field.setInt(resultData, rs.getInt(columnLabel));
break;
case Types.DOUBLE:
field.setDouble(resultData, rs.getDouble(columnLabel));
break;
default:
String value = rs.getString(columnLabel);
if ("true".equals(value) || "false".equals(value)) {
field.setBoolean(resultData, Boolean.valueOf(value));
} else {
field.set(resultData, value);
}
}
最后将填充好的 resultData 对象添加到 queryList 中, 捕获 InstantiationException、IllegalAccessException 和 SQLException 异常,打印错误堆栈并返回空列表。
接着就新建一个Test测试类进行测试:
public class Test {
public static void main(String[] args) throws IOException, NoSuchFieldException, SecurityException {
// 创建Properties对象
Properties props = new Properties();
// 读取执行的配置文件
InputStream in = Test.class.getResourceAsStream("sql.properties");
props.load(in);
String sql = props.getProperty("sql");
List<Subject> lsub = SQLHandler.execute(sql, Subject.class);
System.out.println("查询结果数量:" + lsub.size());
for(Subject Subject : lsub) {
System.out.println(Subject);
}
// List<City> lcity = SQLHandler.execute(sql, City.class);
// System.out.println("查询结果数量:" + lcity.size());
// for(City city : lcity) {
// System.out.println(city);
// }
}
}
首先是创建一个Properties对象,Properties是一个用于存储键值对的类,通常用于读取配置文件中的数据。
然后通过getResourceAsStream()方法从sql.properties文件中读取数据,再将配置文件内容加载到Properties对象中。在上面我已经将properties文件展示出来了。
接着通过getProperty()方法到sql.properties文件读取键为sql的SQL查询语句。然后通过之前定义的 SQLHandler 类执行 SQL 查询,并将结果映射到 Subject 类的对象中,结果以 List<Subject> 的形式返回。
最后输出查询结果的数量和每个 Subject 对象的内容。