虽然说:重复造轮子是愚蠢的事情,但是对于初学者而言,一切都是在重复。我们不可能一上手就创新,只有在不断重复地练习,熟练运用并理解了前辈们留下的代码及思想,才能结合自己的理解,去创新自己的代码。所以,模拟实现Spring像是在班门弄斧,但是我们真正的意图是更好地理解Spring框架,这样不管是在运用Spring框架时,还是需要自己编写代码处理类似情况时,我们都能有更多的理解及灵感。
首先,我们要对Spring框架有个整体理解。对于初学者或是没有使用过Spring框架的人而言,想一下子理解整个Spring并不是个容易的事情。而本文会通过模拟实现Spring的方式,穷尽我个人的理解及尽力的解释,来帮助初学者了解Spring。下面是对Spring框架的整体介绍,对于不清楚的概念完全可以忽略,当运用到具体的场景时,再做理解。
Spring是J2EE的应用程序框架,是一个轻量级的提供IOC(Inversion Of Control)控制反转和AOP(Aspect Oriented Programming)面向切面编程的容器框架。主要针对JavaBean的生命周期进行管理的轻量级容器,可以单独使用,也可以和Struts框架,Ibatis框架等组合使用。Spring的使用可以降低程序开发的复杂性,并且从简单性,可测试性和松耦合的角度而言,所有Java程序都能从中受益。Spring框架采用分层架构,大约细分为20个模块,主要分为Core Container,Data Access/Integration,Web,AOP,Instrumentation和测试部分。
好了,官话讲完了。Spring的核心是控制反转(IOC)技术和面向切面编程(AOP)技术,也是我们要模拟实现的部分。我们先抛开这两个概念,从另一个角度,也就是如果让我们自己来实现Spring框架的角度,从无到有的一种过程来实现这两种技术。完成以后,自然能够理解这两个概念了。
现在有这么一种需求:我有一个复数类(Complex),它里面存了一个对我非常重要的复数数据,我的程序在很多地方都要用到这个类的数据及里面的方法(思考该怎么做...)。一种解决方案是,把这个类做成单例模式的,每次用到的时候new一个新的实例出来。这样做可以解决,但是有点麻烦,既然是单例模式的,为什么还要一次次的new新的实例呢?可是不new程序又会发生空指针异常。这里给出另一种解决的思想,就是先把这个复数类实例保存到某个地方(容器),当我要用的时候到那里(容器)去取,这样省去了一次次无用的new实例化过程。既然这样,为何不写一段程序,让程序自己去发现我在什么时候用了那个复数类,并把那个复数类实例(提前实例化好的)传过去不就好了。
我们再把以上思想整理一下,并考虑如何实现。首先,我们需要一个能存放某个类实例(Bean)的容器(BeanFactory)。其次,需要一段程序来自动完成在需要的时候把某个类的实例传到要用的地方(注入过程)。其实,这一套过程,就与Spring的控制反转过程很像了,或者说,这就是模拟的Spring的控制反转过程。相信看到这,就能对控制反转的概念有一定理解了。先不去考虑,这样做还能有什么用。我们先来实现它!
首先来做那个容器(BeanFactory),把存到里面的每一个类的实例叫做Bean。这里选择用Map容器来存,以某个类的名字做键,以它的 实例类 (BeanDefinition)做值。如下:
public class BeanDefinition {
//某个类的类型
private Class<?> klass;
//某个类的实例
private Object object;
//是否注入过
private boolean inject;
BeanDefinition() {
this.inject = false;
}
}
下面考虑如何把某个类的实例放到这个Map中。这里给出3种方式,第一种通过XML文件配置方式,通过反射机制得到类的实例。第二种,通过包扫描方式,扫描出带有我们想要存放的类,再反射得到类的实例。第三种,手动添加,用哪个类,new一个实例加进去。下面来介绍以包扫描方式添加。
用包扫描,先要给出注解,来标记哪些类该添加到BeanFactory里。注解如下(模仿了Spring注解):
@Retention(RUNTIME)
@Target(TYPE)
public @interface Component {
String name() default "";
}
扫描指定包下的类并添加到BeanFactory中:
public class BeanFactory {
private static final Map<String, BeanDefinition> beanPool;
private static ParameterDependance pd;
static {
beanPool = new HashMap<String, BeanDefinition>();
pd = new ParameterDependance();
}
public BeanFactory() {
}
public static void scanBeanByPackage(String packageName) {
new PackageScanner() {
@Override
public void dealClass(Class<?> klass) {
//带有Component注解的类才被添加,否则不予处理;
if (klass.isPrimitive()
|| klass.isInterface()
|| klass.isAnnotation()
|| klass.isEnum()
|| klass.isArray()
|| ! klass.isAnnotationPresent(Component.class)) {
return;
}
//扫描到带有Component注解的类,通过反射机制得到类的实例,并添加到beanPool里;
Object object = null;
try {
object = klass.newInstance();
BeanDefinition bd = new BeanDefinition();
bd.setKlass(klass);
bd.setObject(object);
beanPool.put(klass.getName(), bd);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}.packageScanner(packageName);
}
}
(包扫描工具类的具体代码可通过我的其他博客查看)
通过以上程序就可以自动将某个包下的类的实例添加到BeanFactory里了。但是,想要添加某个jar包下类的实例呢?jar包下的类是不能修改的,也就不能加上我们的Component注解,扫描不到,自然添加不进去。这里,给出一种解决方案,也是模拟的Spring的解决方案(并不是源码,可能存在未知的BUG)。首先,要再增加一个注解Bean(并不是实例)。可以参照下面的用法:
@Component
public class Configuration {
public Configuration() {
}
//假设testClass1类是某个jar包下的类,它并没有Component注解;
@Bean
public testClass1 getTestClass1() {
testClass1 t = new testClass1();
return t;
}
//假设Point类是某个jar包下的类,它并没有Component注解;
//并且它的实例化需要Complex类;
@Bean
public Point getPoint(Complex complex) {
Point point = new Point(complex);
return point;
}
@Bean
public testClass3 getTestClass3(testClass4 p) {
testClass3 t = new testClass3(p);
return t;
}
@Bean
public testClass4 getTestClass4(testClass3 p) {
testClass4 t = new testClass4(p);
return t;
}
}
用一个带有Component类专门负责实例化jar包下的类,这样不仅要实例化该类,还要反射执行该类里带有Bean注解的方法,把该方法的返回值(类的实例)添加到BeanFactory中。
但是,上面的处理过程却存在一个BUG。假设,实例化某个类需要其他类做参数,如上面的getPoint方法,需要我们的Complex类的实例。如果BeanFactory中存在,我们可以传过来用。可是如果没有呢?或者先执行了这个方法,Complex类后面才会被扫描并添加到BeanFactory呢?这我们当然希望这个方法不应该失败。再或者像测试类3和测试类4那样相互需要彼此类的实例才能实例化呢?这明显是死锁现象,不能完成的事情。针对所说的种种可能,我们要做相应的处理。
处理的思想:当扫描一个包时,把带有Bean注解的方法收集起来,并不立即反射执行。包扫描完一个包后,检测出那些不需要参数的方法或是参数已存在BenaFactory中的方法,这些方法都是可以成功反射执行的,反射执行,结果添加到BeanFactory中。剩下的方法应该都因缺少必要的参数不能反射执行。但是,每次往BeanFactory中添加了新的实例,或是扫描了里一个包又添加了新的实例后,都有可能满足了之前那些不能反射执行的方法所需的参数。这样需要不断检测哪些方法的参数得到了满足,满足后都要反射执行并添加,并会触发新一轮的检测,直到没有新的实例添加到BeanFactory中。如果这时依然有方法没有得到执行,我们应该抛出异常,即,不允许这样的情况出现。因为一个方法没得到执行,就会缺少一个类的实例,而这个类的实例,不知何时会被使用,到那时就会发生空指针异常,造成不可预知的后果。
具体的实现可以是多样的,为了避免不断地检测及反复地对BeanFactory里的实例的排查,我在具体实现中进行了优化。即,通过建立一个新类专门负责处理这部分情况。把带有Bean注解的方法都可以添加到这个类中,在这个类中进行处理。具体代码如下:
public class ParameterDependance {
private static final Map<Class<?>, List<BeanMethodDefinition>> parameterDependance;
private static final OnReadyBeanMethodDefinition orbmd;
static {
parameterDependance = new HashMap<Class<?>, List<BeanMethodDefinition>>();
orbmd = new OnReadyBeanMethodDefinition();
}
ParameterDependance() {
}
//增加一个方法,由用户自动调用查看是否有没处理的方法或者存在循环依赖;
String checkAllReady() {
if(parameterDependance.size() == 0) {
return "ALLREADY";
}
Set<Class<?>> keySet = parameterDependance.keySet();
List<BeanMethodDefinition> values;
StringBuilder sb = new StringBuilder();
for(Class klass : keySet) {
values = parameterDependance.get(klass);
sb.append("缺少" + klass + "类,无法执行");
for(BeanMethodDefinition bmd : values) {
sb.append(bmd.getMethod().getName());
}
sb.append("方法!\n");
}
return sb.toString();
}
Map<String, BeanDefinition> dealBeanMethodDefiantion(Map<String, BeanDefinition> beanPool) {
Map<String, BeanDefinition> tempBeanPool;
Map<String, BeanDefinition> returnBeanPool;
Collection<BeanDefinition> values;
values = beanPool.values();
dealMatchDependance(values);
tempBeanPool = dealOnReadBeanMethodDefinition(beanPool);
returnBeanPool = tempBeanPool;
while(tempBeanPool != null && parameterDependance.size() != 0) {
values = tempBeanPool.values();
dealMatchDependance(values);
beanPool.putAll(tempBeanPool);
tempBeanPool = dealOnReadBeanMethodDefinition(beanPool);
if(tempBeanPool == null) {
break;
}
returnBeanPool.putAll(tempBeanPool);
}
return returnBeanPool;
}
private void dealMatchDependance(Collection<BeanDefinition> values) {
for(BeanDefinition bd : values) {
matchDependance(bd);
}
}
private Map<String, BeanDefinition> dealOnReadBeanMethodDefinition(Map<String, BeanDefinition> beanPool) {
Map<String, BeanDefinition> tempBeanPool = null;
while(orbmd.hasNext()) {
if(tempBeanPool == null) {
tempBeanPool = new HashMap();
}
BeanMethodDefinition bmd = orbmd.next();
Method method = bmd.getMethod();
Class[] parametersTypes = method.getParameterTypes();
Object[] paras = new Object[parametersTypes.length];
int i = 0;
for(Class klass : parametersTypes) {
BeanDefinition bd = beanPool.get(klass.getName());
paras[i] = bd.getObject();
i++;
}
try {
Object result = method.invoke(bmd.getObject(), paras);
BeanDefinition beanDefination = new BeanDefinition();
beanDefination.setObject(result);
beanDefination.setKlass(result.getClass());
tempBeanPool.put(result.getClass().getName(), beanDefination);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
return tempBeanPool;
}
void matchDependance(BeanDefinition bd) {
List<BeanMethodDefinition> bmdList = parameterDependance.get(bd.getKlass());
if (bmdList == null) {
return;
}
//下面的逻辑很重要,容易出错;
for (BeanMethodDefinition bmd : bmdList) {
int paraCount = bmd.decrease();
if (paraCount == 0) {
orbmd.in(bmd);
}
}
parameterDependance.remove(bd.getKlass(), bmdList);
return;
}
void addDependance(BeanMethodDefinition methodDefinition) {
Method method = methodDefinition.getMethod();
int parameterCount = method.getParameterCount();
if (parameterCount <= 0) {
orbmd.in(methodDefinition);
}
Parameter[] parameters = method.getParameters();
for (Parameter parameter : parameters) {
Class<?> type = parameter.getType();
if (!parameterDependance.containsKey(type)) {
parameterDependance.put(type, new ArrayList<BeanMethodDefinition>());
}
List<BeanMethodDefinition> bmdList = parameterDependance.get(type);
bmdList.add(methodDefinition);
}
}
}
其中,需要另一个容器类,来存储那些可以执行的方法。如下:
public class OnReadyBeanMethodDefinition {
private static final List<BeanMethodDefinition> methodDefinitionList = new LinkedList<BeanMethodDefinition>();
OnReadyBeanMethodDefinition() {
}
void in(BeanMethodDefinition bmd) {
methodDefinitionList.add(bmd);
}
BeanMethodDefinition next() {
return methodDefinitionList.remove(0);
}
boolean hasNext() {
return !methodDefinitionList.isEmpty();
}
}
这里故意设置了那种循环依赖的情况,执行的情况如下:
可以看出,循环依赖时或者有方法没得到执行,会产生异常,终止程序。这种解决方案避免了未知的错误的发生,并能解决包扫描顺序导致的不理想的情况的发生。
当所有的实例都存放到BeanFactory中后,就要检测,何时该把这个实例传到需要它的地方了,这也就是Spring中的注入过程。
这里采用”懒汉模式“来注入,即,使用的时候才注入。
采用加注解的方式,并且要增加一个新的注解(也可以使用XML配置方式):
@Retention(RUNTIME)
@Target(FIELD)
public @interface Autowired {
String name() default "";
}
如下面这个类的Complex和Point成员需要注入:
@Component
public class ClassOne {
@Autowired
private Complex complex;
@Autowired
private Point point;
private String str;
public ClassOne() {
}
@Override
public String toString() {
return "ClassOne [complex=" + complex + ", str=" + str
+ ", point=" + point + "]";
}
只要加上Autowired注解,就可以使用这个成员了,并不需要实例化。这里只是简单的在toString测试一下,主函数及结果如下:
可以看到ClassOne里的Complex成员并不是null,确确实实是从BeanFactory注入了。
注入过程是在BeanFactory中实现的,并且应该考虑到循环注入的情况。即,A类需要注入B类,B类需要注入C类,C类又需要注入A类。
具体实现代码如下:
private void injectProperties(BeanDefinition beanDefinition) throws RuntimeException {
Class<?> klass = beanDefinition.getKlass();
Object object = beanDefinition.getObject();
Field[] fields = klass.getDeclaredFields();
for (Field field : fields) {
if (!field.isAnnotationPresent(Autowired.class)) {
continue;
}
field.setAccessible(true);
//递归注入,重点!;
Object value = getBean(field.getType());
if (value == null) {
throw new HasNoBeanException("类[" + klass.getName()
+ "]的成员[" + field.getName()
+ "]注入失败!");
}
try {
field.set(object, value);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
beanDefinition.setInject(true);
}
@SuppressWarnings("unchecked")
public <T> T getBean(String klassName) throws RuntimeException {
if(pd.checkAllReady() != "ALLREADY") {
throw new HasNoBeanException("\n" + pd.checkAllReady());
}
BeanDefinition bd = getBeanDefinition(klassName);
if (bd == null) {
return null;
}
Object result = bd.getObject();
if (!bd.isInject()) {
bd.setInject(true);
injectProperties(bd);
}
return (T) result;
}
public <T> T getBean(Class<?> klass) throws RuntimeException {
return getBean(klass.getName());
}
以上简单模拟了Spring的控制反转过程,值得指出的是,本文并没有对具体应用场景做介绍。并且Spring的注解更多,功能更丰富,细节处理更到位。有兴趣的可以查看Spring的源代码来学习。
下一篇,我们来着重模拟实现Spring的面向切面编程技术。