前沿:
反射——框架设计的灵魂
使用框架,不会反射也没关系,因为框架已经写好了,并不需要用反射;
开发框架,写框架,需要用到反射;
理解反射,掌握反射,在学习框架,使用框架时,也能使用的更好。
1. 反射概念
-
反射:将类的各个组成部分封装为其他对象,这就是反射机制
- 将类的成员变量封装为Filed对象;
- 将类的构造方法封装为Constructor对象;
- 将类的成员方法封装为Method对象。
-
Java代码在计算机中经历的三个阶段:
- Source 源代码阶段
源代码阶段代码还在硬盘 - Class 类对象阶段
- Runtime 运行时阶段
- Source 源代码阶段
字节码文件Person.class要变成Person对象(Person对象在内存中),即需要把字节码文件Person.class加载进内存后才能有Person对象。所以在第二个阶段需要用类加载器 ClassLoader 把字节码文件加载进内存。
在内存中如何描述字节码文件Person.class?在Java中万物皆对象,所以在内存中会有一个对象来描述这个字节码文件,这个对象即为 Class 类对象。
所有第三阶段的对象,如Person对象都是通过这些 Class 类对象创建出来的。
Java中有一个 Class 类,描述所有字节码文件,这些物理文件的共同特征和行为。
不管是什么字节码,或多或少都有一些:成员变量、构造方法、成员方法。所以Class 类对象有三部分比较重要的东西:成员变量、构造方法、成员方法。这三者是三部分不同的东西,每一部分有每一部分的特征,把这三部分分别封装为不同的对象:将类的成员变量封装为Filed对象;将类的构造方法封装为Constructor对象;将类的成员方法封装为Method对象。
- 反射好处
- 在程序的运行过程中,操作这些对象。
- 可以解耦,降低程序的耦合性,降低程序的紧密程度,来提高程序的可扩展性。
2. 2. 获取字节码Class类对象的三种方式
-
如果Java代码在第一个阶段,意味着Java只有字节码文件,并没有进内存,我们需要手动将它加载进内存,生成字节码Class类对象。
Class.forName(“全限定类名”) :将字节码文件加载进内存,返回Class类对象。
全限定类名=包名.类名
forName(“全限定类名”) 是静态方法,通过类名 Class 可以直接打点调用。 -
如果在第二阶段,已经将字节码文件加载进内存,也就是说这个Class类对象已经有了,那么不需要加载它,只需要获取它即可。
类名.class:通过类名的属性class获取 -
如果在第三阶段,已经有对象,可以通过对象的方法来获取。
对象.getClass()
getClass()方法是在Object类中定义的,被所有的对象继承。
-
结论
同一个字节码文件(XXX.class)在一次程序运行过程中,只会被加载一次,不论通过以上哪一种方式获取的Class类对象都是同一个。 -
三种方式适用场合
- Class.forName(“全限定类名”)
多用于配置文件,将类名定义在配置文件中。读取配置文件,加载类。
因为Class.forName(“全限定类名”)传的是字符串,所以从配置文件中读出来的也是字符串。 - 类名.class
多用于参数的传递。传参,传Class类对象的话,用此方式较为方便。 - 对象.getClass()
多用于对象的获取字节码的方式。有对象了,想获取字节码文件对象,可以用此方式获取。
- Class.forName(“全限定类名”)
3. Class类对象功能
-
获取功能
- 获取成员变量们
- Field[] getFields():获取所有public修饰的成员变量(以下同理)
- Field getField(String name):获取指定名称的 public修饰的成员变量(以下同理)
- Field[] getDeclaredFields():获取所有成员变量,不考虑修饰符(以下同理)
- Field getDeclaredField(String name)
- 获取构造方法们
- Constructor<?>[] getConstructors()
- Constructor getConstructor(类<?>… parameterTypes)
- Constructor getDeclaredConstructor(类<?>… parameterTypes)
- Constructor<?>[] getDeclaredConstructors()
- 获取成员方法们
- Method[] getMethods()
- Method getMethod(String name, 类<?>… parameterTypes)
- 方法具有三要素:方法名、返回值列表、参数列表
- 确定一个方法有两要素:方法名、参数列表
- 因为方法名一样,参数列表不一样
- Method[] getDeclaredMethods()
- Method getDeclaredMethod(String name, 类<?>… parameterTypes)
- 获取全限定类名
- String getName()
- 获取成员变量们
-
成员变量(Field)对象对应的方法
操作:- 设置值
void set(Object obj, Object value) - 获取值
get(Object obj) - 忽略访问权限修饰符的安全检查
setAccessible(true):暴力反射
在反射面前没有是隐私的
Field、Constructor、Method里都有这么一个方法
- 设置值
-
构造方法(Constructor)对象对应的方法
构造器对象用来创建对象:
* T newInstance(Object… initargs)
* 如果使用空参数构造方法创建对象,操作可以简化:Class对象的newInstance方法——Class对象.newInstance()(常用此操作) -
成员方法(Method)对象对应的方法
- 执行方法:
Object invoke(Object obj, Object… args) - 获取方法名称:
String getName():获取方法名
- 执行方法:
4. 案例1
- 需求:写一个"框架",不能改变该类的任何代码的前提下,可以帮我们创建任意类的对象,并且执行其中任意方法
- 实现:
- 配置文件
- 反射
- 步骤:
- 将需要创建的对象的全类名和需要执行的方法定义在配置文件中;
- 在程序中加载并读取配置文件;
- 使用反射技术来加载类文件进内存;
- 创建对象;
- 获取方法对象;
- 执行方法
- 实现:
ReflectTest.java文件:
//1.加载配置文件
//1.1 创建Properties对象
Properties pro = new Properties();
//1.2 加载配置文件,转换为一个集合
//1.2.1 获取class目录下的配置文件
/*
第一步:类名.class:拿到当前类的字节码
第二步:.getClassLoader():获取这个字节码的类加载器
第三步:.getResourceAsStream(filePath):根据类加载器读取指定的配置文件并转换为字节流
*/
ClassLoder classLoder = ReflectTest.class.getClassLoader();//获取了字节码文件对应的类加载器,是由这个类加载器把ReflectTest加载进内存的
/*
URL getResource(String name):获取资源的路径
InputStream getResourceAsStream(String name):获取资源对应的字节流
*/
InputStream is = classLoder.getResourceAsStream("pro.properties");
pro.load(is);
//2.获取配置文件中定义的数据
String className = pro.getProperty("className");//获取配置文件中的加载类的全类名
String methodName = pro.getProperty("methodName");//获取配置文件中的需要执行的方法名称
//3.加载该类进内存(反射)
Class cls = Class.forName(className);
//4.创建对象
Object obj = cls.newInstance();
//5.获取方法对象
Method method = cls.getMethod(methodName);
//6.执行方法
method.invoke(obj);
-
Q:改代码和改配置文件有啥区别?
A:庞大的系统,改Java代码,代码一旦改了,需要重新测试,重新编译,重新上线;
但是仅仅改配置文件的话,配置文件只是一个物理文件,改完之后就完事了。而且改配置文件的方式让程序的扩展性更强一些。 -
将来只要在配置文件中看到有一个地方配了全限定类名,第一时间应该反应过来:用的是反射机制。
5. 案例2:一个创建Bean对象的工厂
- Bean和JavaBean
-
Bean:在计算机英语中,有可重用组件的含义
-
JavaBean:用Java语言编写的可重用组件
- javabean != 实体类
- javabean范围远大于实体类
-
一个创建Bean对象的工厂,它就是创建我们的service和dao对象
- 工厂模式
- 工厂模式优势:
解耦(降低类之间的依赖关系),使我们不用每次重新编译、重新部署,包括重新启动Tomcat。
-
解耦思路
- 第一步:需要一个配置文件配置service和dao
- 配置文件可以是xml也可以是properties(这里采用properties)
- 配置文件的内容:唯一标志=全限定类名(key=value)
- 第二步:通过读取配置文件中配置的内容,反射创建对象
- 第一步:需要一个配置文件配置service和dao
-
单例对象和多例对象
- 单例对象:只被创建一次,从而类中的成员也就只会初始化一次。
- 多例对象:每个对象都是重新初始化的。多例对象被创建多次,执行效率没有单例对象高。
- 我们需要每次都是同一个对象,即单例对象。
.newInstance()会调用默认构造函数创建对象
我们的方法只能.newInstance()一次。如果创建完之后不存起来由于Java的垃圾回收机制,它会在长时间不用时被回收,当下次再用时肯定就没有了。所以.newInstance()用完,对象创建出来之后应该马上存起来。
此案例中,我们定义一个Map,用于存放我们要创建的对象。(如下代码中所示)
bean.properties文件:
accountService=service.impl.AccountServiceImpl
accountDao=dao.impl.AccountDaoImpl
BeanFactory.java文件:
package factory;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* 一个创建Bean对象的工厂
*
* Bean:在计算机英语中,有可重用组件的含义
* JavaBean:用Java语言编写的可重用组件
* javabean != 实体类
* javabean范围远大于实体类
*
* 一个创建Bean对象的工厂 它就是创建我们的service和dao对象的。
*
* 第一个:需要一个配置文件来配置我们的service和dao
* 配置文件的内容:唯一标志=全限定类名(key=value)
* 第二个:通过读取配置文件中配置的内容,反射创建对象
*
* 我们的配置文件可以是xml也可以是properties
*/
public class BeanFactory {
//定义一个Properties对象
private static Properties props;
//定义一个Map,用于存放我们要创建的对象。我们把它称之为容器。
private static Map<String, Object> beans;
//使用静态代码块为Properties对象赋值
static {
try {
//实例化对象
props = new Properties();
//获取properties文件的流对象
InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");//读取配置文件
props.load(in);
//实例化容器
beans = new HashMap<String, Object>();
//取出配置文件中所有的Key
Enumeration keys = props.keys();//返回一个枚举类型
//遍历枚举
while(keys.hasMoreElements()){
//取出每个Key
String key = keys.nextElement().toString();
//根据key获取value
String beanPath = props.getProperty(key);
//反射创建对象
Object value = Class.forName(beanPath).newInstance();
//把key和value存入容器中
beans.put(key, value);
}
// for (String key : beans.keySet()) {
// System.out.println(key);
// System.out.println(beans.get(key));
// }
}catch (Exception e){
throw new ExceptionInInitializerError("初始化properties失败!");
}
}
/**
* 根据Bean的名称获取bean对象
* @param beanName
* @return
*
public static Object getBean(String beanName){
Object bean = null;
try {
String beanPath = props.getProperty(beanName);
// System.out.println("全限定类名:" + beanPath);
// 用反射的方式创建对象
bean = Class.forName(beanPath).newInstance();//每次都会调用默认构造函数创建对象
}catch (Exception e){
e.printStackTrace();
}
return bean;
}*/
/**
* 根据bean的名称获取对象
* @param beanName
* @return
*/
public static Object getBean(String beanName){
return beans.get(beanName);//从map中获取beanName对应的对象
}
}
使用:
xxxServlet.java文件中:
IAccountService as = (IAccountService) BeanFactory.getBean("accountService");
AccountServiceImpl.java文件中:
private IAccountDao accountDao = (IAccountDao) BeanFactory.getBean("accountDao");
6. 案例3:Spring IoC 底层实现
- 核心技术点:XML解析 + 反射
- 具体思路: 配置XML文件,程序读配置文件,把信息读出来,创建对象,装到Map里,再从Map里把对象取出来。
1、根据需求编写 XML 文件,配置需要创建的 bean。
2、编写程序读取 XML 文件(dom4j 解析 XML),获取 bean 相关信息,id、类、属性。
3、根据第 2 步获取到的信息,结合反射机制动态创建对象,同时完成属性的赋值。
4、将创建好的 bean 存入 Map 集合,设置 key - value 映射,key 就是 bean 中 id 值,value 就是 bean 对象。
5、提供方法从 Map 中通过 id 获取到对应的 value。
ApplicationContext 接口
package ioc;
public interface ApplicationContext {
public Object getBean(String id);
}
ClassPathXmlApplicationContext 类
package ioc;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class ClassPathXmlApplicationContext implements ApplicationContext{
private Map<String, Object> ioc = new HashMap<String, Object>();
public ClassPathXmlApplicationContext(String path) {
try {
//1. 将spring.xml转成Document对象
SAXReader reader = new SAXReader();
Document document = reader.read("./src/main/resources/" + path);
//2. dog4j 解析 XML
//2.1 首先获取根节点对象<beans>
Element root = document.getRootElement();
//2.2 迭代获取子节点对象<bean>
//获取一个迭代器
Iterator<Element> iterator = root.elementIterator();
//循环
while(iterator.hasNext()){//若迭代器中有下一个
Element element = iterator.next();
//2.2.1 获取key,即bean中的id属性值
String id = element.attributeValue("id");//把属性名id传进去
//2.2.2 获取全类名,即bean中的class属性值
String className = element.attributeValue("class");//把属性名class传进去
//2.2.3 获去value,即通过全类名,通过反射机制调用无参构造函数创建对象
//2.2.3.1 通过反射机制获取class对象
Class clazz = Class.forName(className);
/*
//2.2.3.2 获取无参构造方法对象
Constructor constructor = clazz.getConstructor();
//2.2.3.3 创建目标对象
Object object = constructor.newInstance();
*/
//2.2.3.2 调用无参构造函数创建目标对象
//如果使用无参数构造方法创建对象,可简化为:Class对象的newInstance方法,即Class对象.newInstance()(常用此操作)
Object object = clazz.newInstance();
//2.2.4 给目标对象赋值
Iterator<Element> beanIter = element.elementIterator();
while(beanIter.hasNext()){
Element property = beanIter.next();
//获取property中name、value、ref的值
String name = property.attributeValue("name");
String valueStr = property.attributeValue("value");
String ref = property.attributeValue("ref");
//通过反射调用目标对象的set方法完成对对象的赋值
//字符串拼接得到set方法名
String methodName = "set" + name.substring(0, 1).toUpperCase() + name.substring(1);
//获取成员变量对象
Field field = clazz.getDeclaredField(name);// 获取private修饰的成员变量
//第一个参数是set方法名,第二个参数是set方法的参数类型
Method method = clazz.getDeclaredMethod(methodName, field.getType());
if(ref == null){//<property name="" value=""/>
//根据成员变量的数据类型将 valueStr 进行转换
Object value = null;
if(field.getType().getName() == "long"){
value = Long.parseLong(valueStr);//包装类方法将String类型转成long类型
}
if(field.getType().getName() == "java.lang.String"){
value = valueStr;
}
if(field.getType().getName() == "int"){
value = Integer.parseInt(valueStr);
}
//执行set方法
method.invoke(object, value);
}else{//<property name="" ref=""/> 依赖注入DI
Object refValue = ioc.get(ref);
//执行set方法
method.invoke(object, refValue);
}
}
//2.2.5 key和value存入map集合
ioc.put(id, object);
}
//System.out.println(ioc);
} catch (DocumentException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
// } catch (NoSuchMethodException e) {
// e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
public Object getBean(String id) {
return ioc.get(id);
}
}
调用 getBean() 方法通过 id 获取到 Map 中对应的 value,也就是对象
package ioc;
import entity.Student;
public class Test {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
Student student = (Student) applicationContext.getBean("student");
System.out.println(student);
}
}