-
Java反射
- Java反射是什么?可以做什么?
- Java反射怎么实现?
- Java反射需要注意什么?
- 下期Spi
1. Java反射是什么?可以做什么?
Java反射从功能上来说可以看做为:仅根据类路径来将类实例化,从而操作该类。
例如,假设现在我们知道有一个放在:java.lang.String
。然后我们就可以通过反射,将这个类加载并实例化。
这时候大家或许会有疑问,既然知道这个类在这里,为什么不直接导包然后手动实例化呢?
反射和手动实例化的区别,就是这个类路径。当我们确切的知道类路径时,我们可以轻松的将其实例化。然而在大多数框架中(先让我们站在设计框架的视角),我们其实无从得知需要的类在什么地方。
以Spring为例,Spring如何将类实例化并置于IOC容器中?难道Spring早已预测到我们的包放在哪儿并提前实例化了吗?
事实上当然不可能。我们使用Spring时,为Spring提供了一个包的位置(SpringBoot则是main类所在的包),Spring以此为根递归扫描所有子包,扫描之后,Spring才获取到我们需要置于IOC容器的类路径。
有了类路径就好办了,使用反射就能实例化出来。反射与手动实例化的区别就在这。
实例化之后,还有很多反射的操作,便在代码环节进行介绍。
2. Java反射怎么实现?
接下来是喜闻乐见的代码环节。
我们先准备好一个类用于反射的实验。
package org.jdbc.driver;
public class MyDriver {
private String username;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
类放在org.jdbc.driver
下,类路径则为org.jdbc.driver.MyDriver
。当然自己可以根据个人喜好创建,样例使用这个包名与下期有关,姑且买个关子。
接下来为了方便起见,便在同一个包下写一个main类。
package org.jdbc.driver;
public class Main {
public static void main(String[] args) {
// 获取包名,使用反射技术时,通常这个类路径无法直接获得,而是由使用框架的用户提供
// 此处为了方便起见,所以假设已经获取到了用户提供的类路径
String classPath = "org.jdbc.driver.MyDriver";
try {
// 得到MyDriver的字节码
Class<?> classForMyDriver = Class.forName(classPath);
// 根据获取的字节码将该类实例化,实例化后只能是Object对象
// 因为如果强制转化为MyDriver时,代表我们知道这个类,不符合反射的前提条件
Object myDriver = classForMyDriver.newInstance();
System.out.println(myDriver);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
由此得到一个Object对象。org.jdbc.driver.MyDriver@15db9742
,从这里可以看出确实实例化成功了,但仅仅获取到一个Object对象,有什么意义呢?
对于Spring的IOC容器而言,实例化成功已经完成需求了,实例化之后便可以被用户获取并调用。当然实例化一个对象还需要注入对象,MyDriver
中就有一个值username
,Spring中经常也需要通过注入对象来装配对象。
在Class中,有一个概念叫做Field
。它代表着我们常说的成员变量。
package org.jdbc.driver;
import java.lang.reflect.Field;
public class Main {
public static void main(String[] args) {
String classPath = "org.jdbc.driver.MyDriver";
try {
Class<?> classForMyDriver = Class.forName(classPath);
// 获取声明的成员变量
Field[] declaredFields = classForMyDriver.getDeclaredFields();
for (Field field : declaredFields) {
// 获取成员变量的名称
System.out.println(field.getName());
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
只要获取到字节码Class,我们就可以获取到类的相关信息。包括成员变量,注解(上一节中便是反射获取到的注解),也包括类方法Method
。
类方法获取方式类似:
public static void main(String[] args) {
String classPath = "org.jdbc.driver.MyDriver";
try {
Class<?> classForMyDriver = Class.forName(classPath);
// 获取声明的类方法
Method[] declaredMethods = classForMyDriver.getDeclaredMethods();
for (Method method : declaredMethods) {
System.out.println(method.getName());
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
既然Java中使用了Feild
、Method
代表着成员变量和类方法,也就代表着我们可以操作他们,例如为MyDriver
初始化。
public static void main(String[] args) {
String classPath = "org.jdbc.driver.MyDriver";
try {
Class<?> classForMyDriver = Class.forName(classPath);
Object myDriver = classForMyDriver.newInstance();
// 获取声明的成员变量
Field[] fields = classForMyDriver.getDeclaredFields();
for (Field field : fields) {
// 为myDriver对象的成员变量赋值John
field.set(myDriver, "John");
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
使用Field.set(Object, Object)
方法可以为已经实例化的对象赋值。
为了演示,上述样例代码是存在问题的:
- 不应该强行为所有成员变量赋值为字符串,因为不是所有变量都能赋值为字符串
- 无法得知注入是否成功,没有结果显示。
对于第二个问题,大家可以给MyDriver
添加toString方法打印username。
重点说说第一个问题:
我们都知道Spring注入需要使用注解,那为了什么使用了注解就能实现注入呢?
从上一篇文章中,我们知道Java可以让我们获取到注解的一些描述。那现在我们把反射和注解结合一下,看看触发了什么样的化学反应!
我们修改一下MyDriver
。
public class MyDriver {
// 比较熟练注解的同学可以自己尝试创建一个注解并使用在这
@Resource
private String username;
// 用于对比的无注解成员变量
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String toString() {
return "MyDriver [username=" + username + "]";
}
}
然后回到main
public static void main(String[] args) {
String classPath = "org.jdbc.driver.MyDriver";
try {
Class<?> classForMyDriver = Class.forName(classPath);
Object myDriver = classForMyDriver.newInstance();
// 获取声明的成员变量
Field[] fields = classForMyDriver.getDeclaredFields();
for (Field field : fields) {
// 成员变量上是否有主键
if (field.isAnnotationPresent(Resource.class)) {
System.out.println(field.getName());
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
从这段代码,我们就可以知道,我们需要注入的是username
这个变量。那我们应该向它注入什么数据?
答案来自于Field.getType()
方法。通过这个方法,我们可以获取到username
的类型描述,然后只需要在IOC容器中查找相同的类(父类、子类)即可。
这么些代码下来,我们可以看见,所谓反射,基本围绕着Class
这个类展开操作,我们得到类的Class
后,通过Class
内部的方法可以获取并操作类内的成员变量、方法等,最后再将其实例化,装配。如果想深入的了解Java反射还有什么内容,多尝试Class的不同方法就足够了。文章篇幅有限,姑且介绍到这。
-
Java反射需要注意什么?
Java反射因为可以直接操作对象,甚至进行注入的操作,类的安全性自然是要打折扣的。
其次,使用反射实例化对象效率上不如直接实例化,因此非必要无需使用反射。
反射主要用于框架中,多用于实现微内核+插件化形式。框架设计者无从得知用户提供的类在何处,因此只能通过反射来将用户的类加载进来。了解了Java注解和Java反射之后,我们已经可以自己定义一个简单的自动注入的项目,感兴趣的同学不妨自己尝试一下,算是给自己的巩固作业了。
-
下期Spi
下期准备介绍微内核+插件化的核心思想Spi,会介绍到大家很熟悉的一个类,敬请期待!