java反射机制是在运行状态中,对于任意一个类, 能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用他的任意一个方法和属性。这种动态获取的信息以及动态调用对象的方法的功能称为JAVA语言的反射机制。
巧妙的利用java中的反射机制,能够帮助我们进行程序开发时达到意想不到的效果,本片博文将对java中的反射机制进行分析以及针对spring中通过配置文件完成bean实例对象的创建原理进行分析。
一:类对象
1. 什么是类对象,java的反射机制是什么意思?如何获取类对象?
java的反射机制,意思很简单,就是通过类对象获取类的信息。
那什么是类对象呢?每一个类型的类都有且仅有一个类对象Class,该类对象中记录了这个类包含了哪些属性、哪些方法、以及有哪些构造方法等信息。那通过类对象,就可以反向获取该类的构造方法,就可以完成实例化,也可以访问类中的方法和属性。完成赋值、调用方法等操作。那类对象如何获取呢?很简单,只需要知道他的类名即可。
获取类对象的三种方法:
①Class a = Class.forName(pojo.Hero);
②Class b = Hero.class;
③Class c = new Hero().getClass;
并且a=b=c,因为在同一个JVM中,或者说在一个ClassLoader中,Hero这个类,有且只有一个类对象。
注意,在获取类对象时,除了Hero.class这种方式,其他的会导致类的属性被初始化,即该类中的静态代码块执行或者静态属性被初始化,可以理解为加载类时完成的事情,并且该初始化过程只会被执行一次。例如在Hero这个类中,添加static静态代码块,控制台输出"初始化",则通过三种方式获取类对象,类中的代码块会被执行一次,并在控制台打印了"初始化"。
2. 通过类对象创建实例化对象、访问属性、访问方法。
㈠通过类对象创建实例化对象:直接看代码,一个类对象可以通过构造方法创建无数个实例化对象。
try {
//Class h = Hero.class;
//Class h = Class.forName("test.pojo.Hero");
Class h = new Hero().getClass(); //获取类对象
Constructor c = h.getConstructor(); //根据类对象获取默认构造器
Hero h1 = (Hero)c.newInstance(); //通过构造器调用构造方法完成实例化
h1.showName(); //通过实例化好的对象即可访问该类中的方法
} catch (Exception e) {
e.printStackTrace();
}
㈡通过类对象访问类属性,修改类属性值
getField和getDeclaredField的区别
这两个方法都是用于获取字段
getField 只能获取public的,包括从父类继承来的字段。
getDeclaredField 可以获取本类所有的字段,包括private的,但是不能获取继承来的字段。 (注: 这里只能获取到private的字段,但并不能访问该private字段的值,除非加上setAccessible(true))
Hero h = new Hero();
h.setPrice("qq");
System.out.println(h.getPrice());
Field f = h.getClass().getDeclaredField("price"); //根据类对象获取该类的price属性
f.set(h, "tt"); //通过获取的属性,给h对象修改这个属性的值
System.out.println(h.getPrice());
㈢通过类对象访问类的方法,并执行该方法。
Hero h = new Hero();
Hero h1 = new Hero();
Method m = Class.forName("test.pojo.Hero").getMethod("setPrice", String.class);//通过类对象获取该类中的方法以及该方法中需要传入的参数对象
//如果没有参数的方法则不需要写第二个参数
System.out.println(h.getPrice());
m.invoke(h, "访问方法"); //通过m对象,访问h对象的setPrice,并传入参数值。此时便对h对象执行了该方法。
m.invoke(h1, "h1");
System.out.println(h.getPrice());
System.out.println(h1.getPrice());
至此,已经完成了通过类对象调用构造方法、调用方法、修改属性值操作了。
接下来,将演示java的反射机制如何使用如何利用反射,以及在哪些场景中使用。
二:读取配置文件
1. 为什么要使用反射?如何利用反射机制
假设有一个场景,有两个不同的类根据业务需要可能需要随时调换,如果通过new的方式,则每次调换时需要修改代码。如果通过反射来完成,我们可以将类全名写在配置文件中,通过IO读取配置文件来决定实例化哪个对象。我们就不需要修改代码了,直接修改配置文件中的类名即可。这只是个小例子,JAVA的反射机制远比这要强大。下面通过读取配置文件的方式完成调用不同的类中的不同方法。
2. 编写配置文件,如下,配置文件中声明需要创建的类名称、需要调用的方法名、参数名、以及该参数的值。
通过读取该配置文件,完全利用反射来完成对象的创建和属性的注入。
①创建两个类文件,service、service1,在这两个类中分别写两个打印的方法,代表两种不同的业务,如果需要创建的是service1的话,则程序需要自动把读取到的name值注入到该对象的name属性中。
public class Service {
public void show(String s) {
System.out.println("this is show1"+s);
}
}
public class Service1 {
public String name;
public void show(String s) {
System.out.println("this is show2"+s);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
②在项目中创建配置文件context.txt文件:
class=example.pojo.Service1
method=show
property=name
name=zhangSan
③编写一个方法,用来读取配置文件,并创建对象。输出需要调用的方法。如果是service1的话,则返回一个对象,该对象中已经被注入好了name值。
public Object getObj() throws Exception {
File f = new File("F:\\project\\fansheORzhujie\\fanshe\\src\\example\\context.txt"); //读取配置文件
Properties springConfig = new Properties();
springConfig.load(new FileInputStream(f)); //加载配置文件
String className1 = (String) springConfig.get("class"); //获取配置文件中class的值
String method1 = (String) springConfig.get("method"); //获取配置文件中method的值
String pro = (String) springConfig.get("property"); //获取配置文件中的属性名
String value = (String) springConfig.get("name"); //获取配置文件中name的属性值
//根据读取的配置文件创建类对象
Class c1 = Class.forName(className1);
//根据类对象创建类实例
Constructor ct1 = c1.getConstructor();
//实例化类对象
Object o1 = ct1.newInstance();
//根据反射读取类对象的方法名,然后具体某个实例化对象执行方法
Method m1 = c1.getMethod(method1, String.class);
m1.invoke(o1, "我是类对象中的方法"); //给这个方法注入参数
//根据配置文件的创建,本例通过反射,读取配置文件中的信息,也完成创建对象(反转控制)和属性的依赖注入
//如果类对象时service1,则判断,获取到该类中的name属性,并将实例化的对象中的name值设置为配置文件中name的值。
if(c1 == Service1.class) {
Field f1 = c1.getDeclaredField(pro);
f1.set(o1, value);
}
return o1;
}
④编写测试类:
public static void main(String[] args) throws Exception {
Test t = new Test();
Object obj = t.getObj();
if(obj.getClass() == Service1.class) {
Service1 s = (Service1) obj;
System.out.println(s.getName());
}
}
⑤输出结果:
⑥这样通过配置文件的方式,如果此时需要切换创建的对象,则只需要修改配置文件中的class值,修改类名,或者需要调用别的方法,则修改方法名。从而达到通过配置文件调控实例化哪个类的对象。或者,通过配置文件实例化一个对象,并给该对象注入参数值的实现。
spring中的bean的创建就是通过java的反射机制来完成的,基本实现过程与本例类似,spring实现时,是通过不断的读取配置文件,将class值,属性名,属性值,实例化成一个一个的object对象,然后将这些对象放到一个map中,key值就是配置文件中的id值或者name值,也就是形成了所谓的IOC容器beanFactory,使用时,直接通过key值获取,然后强转成需要的类型即可直接使用spring帮我们创建好的实例化对象了。接下来我将仿照spring中beanFactory的实现过程进行分析。
三:模拟spring创建beanFactory的实现过程
1. 简介
通过spring的配置文件,配置bean,使用spring类文件实例化bean中配置的对象,在测试类中直接返回使用。本demo中包含一个配置文件:spring.xml、一个spring类文件:applicationContext.java、一个实体类:product.java、一个测试类:text.java。
2.创建实体类与配置文件spring.xml,放在src路径下,设置好bean的属性。
package beans;
public class Product {
public void show() {
System.out.println("我是实例化对象product");
}
}
3. 接下来我们自己创建一个bean工厂,用来生产实例化对象配置文件中配置好的bean,从而达到spring完成创建对象的目的。
①先读取到配置文件spring.xml,并创建一个存放bean的私有静态全局变量map集合。
②然后对该文件进行解析,获取到文件中的跟标签。也就是最外面的那层标签beans,需要导入dom4j-1.6.jar、xml-apis-1.0.b2.jar资源文件
③循环跟标签beans中的bean标签,将每一个bean中的属性值获取出来,根据class属性通过反射创建类对象并完成实例化,然后将这个对象放在map中,key值是读取到的id的值,value值是实例化好的object对象。配置文件中有多少bean标签就会循环创建多少个对象。
④重写该类的构造方法,并传入参数,参数值为配置文件的路径文件名。
⑤编写getBean方法,传入参数值,参数为bean中的id,从而达到通过调用该方法,传入配置的id值,获取到bean工厂中的实例化对象。此方法也是提供给外部访问map的唯一入口。
请看代码:
package spring;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
/**
* spring的配置工厂类文件
* @author lemon
*
*/
public class ApplicationContext {
private static Map beans = new HashMap();
public ApplicationContext(String file) {
buildBeanFactory(file);
}
public void buildBeanFactory(String file) {
//读取配置文件
InputStream in = ApplicationContext.class.getClassLoader().//类的加载容器,把类的实例加载到内存中
getResourceAsStream(file);//这个方法直接从src下读取该文件
//解析文件
//创建解析器对象
SAXReader saxReader = new SAXReader();
Document dom = null;
try {
dom = saxReader.read(in);
} catch (DocumentException e) {
e.printStackTrace();
throw new RuntimeException("解析文件时抛出异常");
}
//解析完了该文件,接下来需要拿到根元素(bean元素)
Element root = dom.getRootElement();
//获取根元素所有的bean元素,并存在放list中,并且将所有的bean标签循环读取,把每一个bean都创建成一个实例化对象
//然后存放在map中,此时,即完成了spring创建bean的过程。
List list = root.elements("bean");
for (Element e : list) {
String className = e.attributeValue("class");
String idName = e.attributeValue("id");
Object obj = null;
try {
obj = Class.forName(className).newInstance();
} catch (Exception e1) {
e1.printStackTrace();
throw new RuntimeException("创建对象时抛出了异常");
}
beans.put(idName, obj);
}
}
/*
* 提供方法,以供外部应用访问并获取map中的值。也就是getBean方法
*/
public Object getBean(String id) {
return beans.get(id);
}
/*
*提供方法,通过调用清初始化spring工厂中的所有值
*/
public void cleanBeans() {
beans.clear();
beans = null;
}
}
4. 编写测试类:
创建spring类,传入配置文件名,调用getBean方法,类型转换至具体类型对象,直接调用对象中的属性和方法即可。
public class Test {
public static void main(String[] args) {
ApplicationContext app = new ApplicationContext("spring.xml");
Product pro = (Product) app.getBean("p");
System.out.println(pro);
pro.show();
}
}
5. 输出结果:
6. 总结:
spring在为我们使用时,内部源码异常复杂,此过程只不过是简化了的实现过程,实则有很多情况需要处理。
在tomcat中使用spring时,tomcat是如何加载spring的?
其实在tomcat的启动过程中,会加载web.xml文件,我们在该文件中配置了监听上下文启动的监听器。在tomcat启动时,该监听器会被加载,然后执行该类中的初始化方法。在初始化时,默认读取默认路径下的默认spring配置文件名applicationContext.xml文件,并且开始加载解析,根据反射机制创建了配置文件中bean的实例化对象,并存在在map集合中,并完成加载其他资源文件等过程。从而spring就跟随者tomcat的启动完成了加载。
org.springframework.web.context.ContextLoaderListener
对于spring的世界,还有许许多多的未知等待我们去探索,在我以后的学习时间里,我会更专注于spring原理的解读。