前言
我们在之前讲解动态代理【JDK动态代理牛在哪里】的博客中完成了对JDK的动态代理的例子,可是这个例子其实十分不灵活。我们构建一个实例对象MyDao jdkDao=(MyDao)Proxy.newProxyInstance (ClassLoader, interfaces, InvocationHandler);
的时候,拿出来的jdkDao
是一个代理对象,这就意味着如果需要使用目标对象就变得非常的麻烦。因为目前为止的例子里目标对象是被写死的,也就意味着要拿出来目标对象就必须要改代码为MyDao jdkDao= new MyDaoImpl();
。如果又用到代理对象,又得改回去。那么我们的代码耦合度可以说非常的高,显然我们自己做的这些小例子对于应用来说还是太拙劣了,那么我们看一下世界上最厉害的Spring框架是怎么做的。更多Spring内容进入【Spring解读系列目录】。
强大的Spring
说到Spring其实可以问一个很好的问题,为什么我们要用Spring?这就要说到工厂方法的好处,工厂方法控制了代理对象的产生过程。目前来说我们的例子里的对象的产生过程是我们自己编码去控制的。那么我们怎么样才能自动的让程序中的bean通过一种依赖关系自动构造出来呢?本篇就将模拟Spring
从xml
中读取数据并解析构造对象,如何构建一个山寨版的基于xml
的Spring IOC
的框架,以更好的了解Spring
原理。
山寨Spring原理
所谓要打败对方就要先融入对方,要想山寨一个Spring的功能同样也要解构Spring给我们提供了什么功能。一个一个针对处理,所谓管中窥豹可见一斑,我们先从一个小点开始,然后一步一步的推理出来Spring的全貌。既然说到了Spring使用的工厂模式,那么我们也可以使用这种模式去控制类的产生过程,比如我们可以用依赖查找MyDao jdkDao = Factory.getBean(“xxx”)
来控制代理类对象jdkDao
的产生。既然能够控制类的产生,于是就有人提出一种思想叫做依赖注入(Dependency Inject)
。这种注入的思想使得程序中对象的产生过程可以通过一个外部的第三方容器产生并注入进来,而对于程序员来说就不需要去关注对象具体生成的是哪个类,具体哪个实现是由外部的容器注入进来的,这就是Spring IOC
的思想。所以我们要山寨的也是这样一个思想,那么我们模拟的到底是什么东西呢?就是要模拟一个类的签名,然后把内容注入进来的一个过程。
准备内容
首先我们还是要把业务类给准备齐了,两个接口UserDao,UserService。两个实现类UserDaoImpl,UserDaoImpl,和只有基本依赖关系的配置文件spring.xml,我们就将会从这个xml中构建依赖关系,并且模拟Spring注入的过程。
public interface UserDao {
public void query();
}
public class UserDaoImpl implements UserDao{
@Override
public void query() {
System.out.println("UserDaoImpl query 1");
}
}
public interface UserService {
public void find();
}
public class UserServiceImpl implements UserService {
private UserDao userDao; // <property name="userDao" ref="dao">
@Override
public void find() {
System.out.println("UserServiceImpl find()");
userDao.query();
}
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
}
我们先分析一下这个逻辑,首先我们的UserService
依赖了UserDao
,如果想要UserService.find()
方法执行业务逻辑,必须先要把UserDao
的实例对象构建出来。按照传统的做法,我们可以直接new
出来一个,但是这样做耦合度就会增大,不利于以后的扩展,所以我们要做的就是开发一个容器,使得UserService
能够和UserDao
关联起来。既然是要山寨Spring,那么我们看看Spring是怎么做的。
关联依赖
Spring提供了xml文件用来注册并管理类,而且可以描述类的关系,基本可以总结为下面这几个要点:
- 哪些类需要Spring来关联,比如你建一个实体类就不需要Spring管理。
- 怎么通知Spring管理类,Spring中就是写bean标签。
- 怎么维护依赖关系(Spring中一般是用setter、constructor做的)
- 怎么体现依赖关系,setter或者constructor
这就意味着我们需要把我们的目标类和要依赖的类都注册到xml中,然后由Spring识别其中的依赖关系,再通过里面构造的依赖关系进行创建。那么就有一个问题:假设我们已经有了UserDao
和UserService
这两个实例,UserDao
怎么传递到UserService
中呢?一般来说手写的话可以用setter方法,或者构造方法。如果把这个传入的过程交给容器,程序员只要配置剩下的就全部交给容器是不是就简化了很多。这个过程就叫做注入
,容器自动完成的这过程就叫做自动注入
,如果其中有依赖就称为依赖注入
,也就是传说中的DI
。这样理解是不是那些难记的概念就活起来了。
所以按照以上的要求,我们就配置了一个匹配的xml配置文件描述两者的关系,因为是山寨Spring,那就按照Spring的语法来。我们写的property就代表setter方法,Spring中是把setter方法的名字简化过来的,我们为了编码方便这里就按照字段的名字来获取。
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean id="dao" class="com.demo.dao.UserDaoImpl"></bean>
<bean id="service" class="com.demo.service.UserServiceImpl">
<property name="userDao" ref="dao"></property> <!--ref关联UserDao-->
</bean>
</beans>
寻找依赖并实例化
话说已经处理好了依赖关系,下面就要开始构建容器了。在预想中,容器应该有下面的功能,后面我们的编码就按照这几点来。
- 识别配置文件。
- 解析xml,并且给配置的类生成实例,处理依赖关系。
- 根据依赖关系返回生成的实例对象给需要依赖的对象使用。
既然要解析xml那就引入相应的包(dom4j)
,然后我们创建一个类BeanFactory
,并且构建我们的容器。Spring底层返回使用的是ConcurrentHashMap
我们简化使用HashMap
作为存储对象的容器。因此看起来很复杂,但是我们要做的其实很简单:
- 实例化对象,并且存到一个map中。
- 找到依赖关系,如果有依赖,就把map已经生成的实例注入进去。
public class BeanFactory {
//map存放类的实例
Map<String, Object> map=new HashMap<>();
/**
* 工厂方法
* @param xml
*/
public BeanFactory(String xml) {
parseXml(xml);
}
/**
* 解析
* @param xml
*/
public void parseXml(String xml){
//拿到根路径
String path=this.getClass().getResource("/").getPath()+xml;
File file=new File(path);
SAXReader reader = new SAXReader();
try {
Document document = reader.read(file);
Element elementRoot=document.getRootElement();
//拿取xml中元素的内容,下面数字代表循环次数。
for (Iterator<Element> itFirst = elementRoot.elementIterator(); itFirst.hasNext();) {
Element elementFirstChild = itFirst.next();
//取到id值:1.dao;2.service
Attribute attribute=elementFirstChild.attribute("id");
String beanName=attribute.getValue();
//取到类全名:1.com.demo.dao.UserDaoImpl;2.com.demo.service.UserServiceImpl
Attribute attribute2=elementFirstChild.attribute("class");
String clazzName=attribute2.getValue();
//获取标签中的类全名,并根据全名创建相应的Class对象
Class clazz=Class.forName(clazzName);
//给每一个类实例化,这里拿到的就是dao的实例,这里调用默认构造方法,
// 所以类里如果重写了构造方法,一定要把默认构造方法显式创建出来。
// UserDao和UserService都是在这里实例化的。
Object object=clazz.newInstance();
//维护依赖关系
// 找到依赖关系:判断是否有属性,然后判断属性是否有对应的property
// 如果有则注入,所以每循环到一个bean就要拿出子标签
for (Iterator<Element> itSecond = elementFirstChild.elementIterator(); itSecond.hasNext();) {
Element elementSecondChild =itSecond.next();
//拿到ref的value,通过value得到map中的对象;
// 拿到name的值,然后根据name的值获取到Field对象;
// 最后通过Field的set方法把对象传递进去
if (elementSecondChild.getName().equals("property")){
//把map中存的UserDao对象拿出来
Object injectObj=map.get(elementSecondChild.attribute("ref").getValue());
//根据名字拿到属性值,这里拿到的就是userDao
String nameValue=elementSecondChild.attribute("name").getValue();
//这里的field就是UserServiceImpl.userDao对象,此时还是null
Field field=clazz.getDeclaredField(nameValue);
//因为属性私有,这里设置为true,就可以操作了
field.setAccessible(true);
//注入,这里object是UserServiceImpl,injectObj是UserDao的实例对象。
// 这里的意思等同于赋值 UserServiceImpl.userDao=UserDao实例对象;
field.set(object,injectObj);
}
}
//这里最终放的是dao=UserDaoImpl对象,service=UserServiceImpl对象
map.put(beanName,object);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取对应的bean
* @param bean
* @return
*/
public Object getBean(String bean){
return map.get(bean);
}
}
山寨版测试
完成以后,构建一个test类,去测试下我们的容器是不是好用.
public class Test {
public static void main(String[] args) {
//获取配置
BeanFactory beanFactory=new BeanFactory("spring.xml");
//拿到bean
UserService service= (UserService) beanFactory.getBean("service");
service.find(); //执行
}
}
执行结果:
UserServiceImpl find()
UserDaoImpl query 1
总结
本篇中我们通过从xml中拿到完整的类名,然后通过类名去实例化实现类和依赖对象。最后由字段把已经实例化的对象注入到实现类中,实现了一个Spring简易的注入功能。通过这一系列的模拟,可以很清楚的看到一个容器是如何通过属性(Spring是通过Setter方法,原理一致)进行注入的,那么下一篇【从山寨Spring中学习Spring IOC原理-XML-Constructor】则是对我们的容器进行修改,使其能够通过构造方法进行注入。