首先,这种模拟依赖注入所用的知识是反射。
其次,依赖注入有什么好处呢?他可以将创建实例的工作交给第三方完成,而不用交由调用方完成(这一般依靠配置文件来实现),这可以降低类与类之间的耦合度。
第三,仅仅将创建实例的工作交给第三方完成还不够。比如A调用B,A中始终包含一个B类的定义(B b),然后B的实例化由容器完成,将来如果我们需要用C类的功能替换B的功能,则需要修改A类的引用定义(C c),违反开闭原则。因此,对于一个可扩展、易维护的系统,应该尽量面向接口编程,将功能尽量抽象成方法,利用java的多态编程。
一般而言,系统升级时,B类和C类的实现功能是一致的,可能只是底层接口变动(如mysql数据库替换成oracle数据库),或jar包升级,或第三方接口变动,但A类需要的业务功能依然不变。所以系统设计之初,需要定义一个接口,接口中的方法覆盖A类所需的功能。然后B类实现接口I。此时A类调用B类时,可以写成(I i),然后向上转型,依靠容器将B的实例注入A中,类似:I i = new B();。当以后升级时,我们只需要定义一个类C实现接口I,然后修改配置文件,让C类成为I的引用对象即可。
最后,上面的过程其实就是spring的基本功能,面向接口编程,将类之间的实例化调用写在配置文件applicationContext.xml中,然后所有类的实例化由容器完成。
接下来,就开始实现依赖注入:
1.现在又两类类A、B,A类对B类完成调用,需要在A类中写:B b = new B();
我们想像spring一样只定义一个B b,然后生成b的get、set方法就能完成B的实例化,那么需要一个方法,传入A类本身,然后获得以set开头的方法,并调用此方法,完成,传入一个new B()实例,完成B的赋值。方法如下:
public static void diObj(Object obj) {
try {
// 获取本类自定义的所有方法
Method[] ms = obj.getClass().getDeclaredMethods();
for (Method m : ms) {
String mn = m.getName();
// 如果方法名称以set开头
if (mn.startsWith("set")) {
// 截断方法名,获取properties中配置的dao名称
mn = mn.substring(3);
// 将首字母小写
mn = mn.substring(0, 1).toLowerCase() + mn.substring(1);
// 根据peoperties中配置的名称字符串
String fullName = PropertiesUtil.getProperty(mn);
// 通过字符串生成类实例
Object instance = Class.forName(fullName);
// 调用setXXX()方法完成实例的注入
m.invoke(obj, instance);
}
}
} catch (SecurityException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
说明:因为通过字符串生成实例(Class.forName(name)),需要的name参数是类的全路径,所以我们需要将此全路径配置在properties文件中。并通过properties文件操作工具获取。(spring是写在xml中)
2.上面方法有了,如何注入呢?那就需要在每个需要注入的类中调用一下此方法,并将类本身传入。如在A中调用:diObj(this);
3.这种方法有一个问题,就是如果A类中还有其他的set方法,但是我们并不需要注入,就可能抛出异常。因此,我们需要在要自定义一个Annotation类,这个Annotation类标签,放在属性前,或放在set方法前都行(下面的栗子,放在set方法前),反正最终要实现的功能都一样:有Annotation的我们才注入。
1)定义一个Annotation,如下:
<span style="text-indent: 24px; white-space: pre; background-color: rgb(240, 240, 240);">package com.edu.mybatis.common.annotation;</span>
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface DaoDi {
// String abc() default "";
String value() default "";
}
2)修改diObj(Object obj)公共方法:</pre><pre name="code" class="java">public static void diObj(Object obj) {
try {
// 获取本类自定义的所有方法
Method[] ms = obj.getClass().getDeclaredMethods();
for (Method m : ms) {
String mn = m.getName();
// 如果此方法上包含@DaoDi标签,返回true
if (m.isAnnotationPresent(DaoDi.class)) {
DaoDi daoDi = m.getAnnotation(DaoDi.class);
// 获取DaoDi的value值
String value = daoDi.value();
// 如果annotation中传入参数(如:@DaoDi("userDao")),则表示注入userDao
if (value != null && !"".equals(value)) {
mn = value;
}
// 如果annotation不传参数(如:@DaoDi),则表示将setXXX方法的XXX名称最为注入对象
else {
// 截断方法名,获取properties中配置的dao名称
mn = mn.substring(3);
// 将首字母小写
mn = mn.substring(0, 1).toLowerCase() + mn.substring(1);
}
// 根据peoperties中配置的名称字符串
String fullName = PropertiesUtil.getProperty(mn);
// 通过字符串生成类实例
Object instance = Class.forName(fullName);
// 调用setXXX()方法完成dao注入
m.invoke(obj, dao);
}
}
} catch (SecurityException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
3)在setXXX方法上加上Annotation,如:
@DaoDi("addressDao")
public void setAddrDao(IAddressDao addressDao) {
this.addressDao = addressDao;
}
三部完成,代码可以正常工作了。
4.有人可能会觉得这种注入,需要在每个类中初始化时都写上一句diObj(this)是一件非常无趣的事情。那么我们就需要根据具体业务具体分析了。如果是一个基本B/S项目,我们可能会分层,比如dao层、service层、action层等。以dao层为例:
我们本来就需要为dao抽象出一些公共的操作,并封装成一个dao的父类(这样做是必要的,以后更改dao接口也更方便,更灵活)。当业务中需要定义一个dao类时,我们就继承此dao父类。此时机会就出现了,我们将注入语句写在dao父类的构造方法中。那么凡是在dao需要注入的属性,在继承dao父类后,都会通过父类的构造方法完成。
而至于service层、action层也一样,我们需要为该层定义一个父类,并在构造方法中调用注入语句。