需要解决的问题
考虑这么一种场景,传统编码方式中,Service层调用Dao层的时候,一般是在service的实现类中定义一个Dao层接口的成员属性,并直接new一个对象。
public class TransferServiceImpl implements TransferService {
private AccountDao accountDao = new JdbcAccountDaoImpl();
}
此时Service层的TransferServiceImpl 就与Dao层的一个具体的实现类JdbcAccountDaoImpl产生了耦合,如果Dao层的实现要变化(例如从当前使用jdbc实现换成使用mybatis实现),那Service层的调用就需要修改代码。修改点多的时候特别不方便。
最完美的情况是:在TransferServiceImpl 中声明一个AccountDao的对象即可。
解决思路
- 创建对象不再使用直接new的方式,而使用反射
- 将实例化对象的工作使用工程模式处理,因为项目中可能很多地方都要用到实例化对象,工程模式比较合适,也能达到解耦的目的。
实现方式
1. 解决对象实例化的问题
为了避免硬编码,首先将需要实例化的类的全限定名称都配置到xml文件中去,由工厂类从xml配置文件中读取,并通过反射实例化所有配置的对象。
<?xml version="1.0" encoding="UTF-8" ?>
<beans>
<bean id="transferService" class="com.xxx.service.impl.TransferServiceImpl"></bean>
<bean id="accountDao" class="com.xxx.dao.impl.JdbcAccountDaoImpl"></bean>
</beans>
比如配置文件命名为beans.xml,其中每个类建立一个bean标签,标签中给一个id的属性以方便查找,给一个class属性标识出类的全限定名称。
定义一个工程类BeanFactory,它应该具备以下形式:
public class BeanFactory {
// 缓存该工程类提供的所有对象
private static Map<String,Object> map = new HashMap<>();
// 提供获取实例化对象的方法,供外部获取实例化对象用
public static Object getBean(String id) {
return map.get(id);
}
// 完成从xml文件读取所有需要实例化的对象,并实例化。此处先不写,往下继续分析
}
通过dom4j读取解析xml配置文件,获取到所有的bean标签的内容,通过反射和类的全限定名来实例化对象。
通过beans.xml实例化对象代码实现片段:
// 通过类加载器获取配置文件为输入流
InputStream resourceAsStream = BeanFactory.class.getClassLoader().getResourceAsStream("beans.xml");
// 通过dom4j解析xml配置文件
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(resourceAsStream);
// 获取配置文件根节点
Element rootElement = document.getRootElement();
// 获取根节点下的所有bean标签内容
List<Element> list = rootElement.selectNodes("//bean");
// 循环所有的bean标签内容,实例化对象
for (int i = 0; i < list.size(); i++) {
Element element = list.get(i);
String id = element.attributeValue("id");
String clazz = element.attributeValue("class");
// 通过反射实例化对象
Class<?> aClass = Class.forName(clazz);
Object o = aClass.newInstance();
// 将实例化的对象全部放到map中缓存
map.put(id,o);
}
至此,对象的创建问题解决。
2.维护对象依赖关系
此时所有的对象我们已经创建完成了,要做的就是将被依赖的对象实例注入到依赖它的对象中去。首先要在有依赖对象的对象中提供出给被依赖对象赋值的set方法:
public class TransferServiceImpl implements TransferService {
private AccountDao accountDao = new JdbcAccountDaoImpl();
// 提供set方法,供外部注入依赖对象用
public void SetAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
}
其次,就是要知道什么对象依赖了什么对象,这样才能准确的将被依赖对象注入到依赖它的对象中去。这个问题,我们可以通过修改配置文件,增加属性来解决。在有依赖其他对象的bean中,新增property元素来配置依赖对象的信息:
<?xml version="1.0" encoding="UTF-8" ?>
<beans>
<bean id="transferService" class="com.xxx.service.impl.TransferServiceImpl">
<!--新增依赖对象的配置,name是属性名称,给外部调用set方法的时候拼接方法名称用,ref是依赖的bean的id-->
<property name="AccountDao" ref="accountDao"></property>
</bean>
<bean id="accountDao" class="com.xxx.dao.impl.JdbcAccountDaoImpl"></bean>
</beans>
配置文件做了如上改动之后,实例化对象的工厂代码中加上以下操作
List<Element> propertyNodes = rootElement.selectNodes("//property");
for (int i = 0; i < propertyNodes.size(); i++) {
Element element = propertyNodes.get(i);
// 处理property元素
String name = element.attributeValue("name");
String ref = element.attributeValue("ref");
// 获取property标签的父标签,也就是有依赖的bean
String parentId = element.getParent().attributeValue("id");
// 从map中拿到有依赖的bean对象
Object parentObject = map.get(parentId);
// 获取到所有的方法
Method[] methods = parentObject.getClass().getMethods();
// 遍历所有方法,如果有和配置中配置的方法名称一致的,就调用set方法把被依赖的ref的一个实例注入进去
for (int j = 0; j < methods.length; j++) {
Method method = methods[j];
if(("set" + name).equalsIgnoreCase(method.getName())) {
Object propertyObject = map.get(ref);
method.invoke(parentObject,propertyObject);
}
}
map.put(parentId,parentObject);
至此,对象中依赖的其他对象都被注入了一个实例。