深入理解Java的Spring框架中的IOC容器
spring ioc的原型
spring框架的基础核心和起点毫无疑问就是ioc,ioc作为spring容器提供的核心技术,成功完成了依赖的反转:从主类的对依赖的主动管理反转为了spring容器对依赖的全局控制。
这样做的好处是什么呢?
当然就是所谓的“解耦”了,可以使得程序的各模块之间的关系更为独立,只需要spring控制这些模块之间的依赖关系并在容器启动和初始化的过程中将依据这些依赖关系创建、管理和维护这些模块就好,如果需要改变模块间的依赖关系的话,甚至都不需要改变程序代码,只需要将更改的依赖关系进行修改即可,spring会在再次启动和初始化容器的过程中使得这些新的依赖关系重新建立符合新需求的模块,在这个过程中,需要注意的是代码本身不需要体现对于模块具体依赖情形的声明而只需要定义其所需模块的接口,所以这是一种典型的面向接口思想,同时最好将依赖关系以配置文件或者注解的形式表述出来,相关的spring处理类会根据这些外部的配置文件组装模块,或者扫描注解调用内部的注解处理器组装模块,以此完成ioc的过程。
ioc的目的是称为di的依赖注入,通过ioc技术,最终容器将帮助我们完成模块间的依赖注入。
另外,最终的一点是,在spring ioc的过程中,我们必须始终清楚以上这条主线,即时语法和类的结构再复杂,但是其作用和目的都是一样的:就是通过依赖描述的配置文件这一装配“图纸”去完成模块的“组装”,复杂的语法只是完成这一目的的手段罢了。
所谓的ioc原型,为了展示最简单的ioc原理图,我们不妨做一个完全简单的原型来说明这个过程:
首先是我们定义的几个模块,包括主模块和两个接口定义的依赖模块:
class mainmodule{ private dependmodulea modulea; private dependmoduleb moduleb; public dependmodulea getmodulea() { return modulea; } public void setmodulea(dependmodulea modulea) { this.modulea = modulea; } public dependmoduleb getmoduleb() { return moduleb; } public void setmoduleb(dependmoduleb moduleb) { this.moduleb = moduleb; } } interface dependmodulea{ public void funcfrommodulea(); } interface dependmoduleb{ public void funcfrommoduleb(); } class dependmoduleaimpl implements dependmodulea{ @override public void funcfrommodulea() { system.out.println("this is func from module a"); } } class dependmodulebimpl implements dependmoduleb{ @override public void funcfrommoduleb() { system.out.println("this is func from module b"); } }
如果我们不采用ioc,而是依靠主模块本身去控制其依赖模块的创建,那么会是这样的:
public class simpleiocdemo { public static void main(string[] args) throws classnotfoundexception { mainmodule mainmodule = new mainmodule(); mainmodule.setmodulea(new dependmoduleaimpl()); mainmodule.setmoduleb(new dependmodulebimpl()); mainmodule.getmodulea().funcfrommodulea(); mainmodule.getmoduleb().funcfrommoduleb(); } }
这是我们经过简化定义的ioc容器原型,容器在启动后初始化的时候会读取用户写入的配置文件,这里我们以简单的properties配置文件为例,只有当用户调取getbean方法的时候才会真正地按照配置文件组装加载相应的bean,在我们定义的容器原型内部维护着一个用于保存装配好的bean 的map,如果在其中有满足要求的bean的话就不需要再新建了:
class simpleioccontainer{ private properties properties = new properties(); private map<string, object> modulemap = new hashmap<>(); { try { properties.load(new fileinputstream(new file("simpleioc.properties"))); } catch (exception e) { e.printstacktrace(); } } public object getbean(string modulename) throws classnotfoundexception { object instanceobj; if(modulemap.get(modulename)!=null){ system.out.println("return old bean"); return modulemap.get(modulename); } system.out.println("create new bean"); string fullclassname = properties.getproperty(modulename); if(fullclassname == null) throw new classnotfoundexception(); else{ class<? extends object> clazz = class.forname(fullclassname); try { instanceobj = clazz.newinstance(); instanceobj = buildattachedmodules(modulename,instanceobj); modulemap.put(modulename, instanceobj); return instanceobj; } catch (instantiationexception e) { e.printstacktrace(); } catch (illegalaccessexception e) { e.printstacktrace(); } } return null; } private object buildattachedmodules(string modulename , object instanceobj) { set<string> propertieskeys = properties.stringpropertynames(); field[] fields = instanceobj.getclass().getdeclaredfields(); for (string key : propertieskeys) { if(key.contains(modulename)&&!key.equals(modulename)){ try { class<? extends object> clazz = class.forname(properties.getproperty(properties.getproperty(key))); for (field field : fields) { if(field.gettype().isassignablefrom(clazz)) field.set(instanceobj, clazz.newinstance()); } } catch (exception e) { e.printstacktrace(); } } } return instanceobj; } }
这是我们使用properties配置文件写成的依赖关系配置文件,这个配置文件是我们装配模块的“图纸”,这里的语法个是完全是我们定义的,在真实的spring ioc容器中,为了表达更为复杂的依赖逻辑,会使用更为发达的xml格式配置文件或者更新的注解配置,依靠注解处理器来完成图纸的解析:
mainmodule=com.rocking.demo.mainmodule mainmodule.modulea=modulea mainmodule.moduleb=moduleb modulea=com.rocking.demo.dependmoduleaimpl moduleb=com.rocking.demo.dependmodulebimpl
这是测试代码,可以看到的是我们可以完整的通过我们定义的ioc容器获取到符合要求的模块,同时也可以发现我们定义的容器可以为我们维护这些bean,当有bean已经组装创建出来之后就不需要再创建了。
public class simpleiocdemo { public static void main(string[] args) throws classnotfoundexception { simpleioccontainer container = new simpleioccontainer(); dependmodulea modulea = (dependmodulea) container.getbean("modulea"); modulea.funcfrommodulea(); dependmoduleb moduleb = (dependmoduleb) container.getbean("moduleb"); moduleb.funcfrommoduleb(); mainmodule mainmodule = (mainmodule) container.getbean("mainmodule"); mainmodule.getmodulea().funcfrommodulea(); mainmodule.getmoduleb().funcfrommoduleb(); container.getbean("mainmodule"); } }
这就是我依据ioc的基本思想创建的ioc容器原型,spring ioc虽然语法复杂,但是说到底完成的任务在核心上都是一样的,所谓的“万变不离其宗”。
spring ioc 的具体过程
上回展示了ioc的大致实现的原型,那么在spring框架中具体是怎么实现这个容器根据metadata元信息配置加载pojo的过程的呢?在整个spring ioc容器的工作过程中有很多地方是设计地相当灵活的,供给使用者很多空间去完成自己的任务,而不是一味地只是完成容器的机械过程。
这是整个ioc容器工作过程的过程图:
1、容器启动阶段
(1)加载配置文件信息
(2)解析配置文件信息
(3)装配beandefinition
(4)后处理
首先配置文件或者注解等元信息和javabean的类信息被加载到ioc容器中,容器读取到xml格式的配置文件,这个配置文件是使用者声明的依赖关系和装配中需要特别关注的地方,是装配bean的早期“外部图纸”,容器中的解析引擎可以把我们写入的文本形式的字符元信息解析成容器内部可以识别的beandefinition,可以把beandefinition理解成为类似反射机制的类结构,这个通过对javabean和配置文件进行分析得到的beandefinition获取了组装一个符合要求的javabean的基本结构,如果需要除了beandefinition之后还要对这个beandefinition再做修改的话则执行这个后处理,后处理一般是通过spring框架内的beanfactorypostprocessor处理的。
我们仍然使用上次使用过的例子来说明这个beandefinition的运作原理:有三个bean,主模块mainmodule和依赖模块dependmodulea,dependmoduleb,前者依赖后面两个模块构成,在配置文件里我们一般会这么进行依赖的声明:
<?xml version="1.0" encoding="utf-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/xmlschema-instance" xsi:schemalocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"> <bean id="mainmodule" class="com.rocking.demo.mainmodule"> <property name="modulea"> <ref bean="modulea"/> </property> <property name="moduleb"> <ref bean="moduleb"/> </property> </bean> <bean id="modulea" class="com.rocking.demo.dependmoduleaimpl"></bean> <bean id="moduleb" class="com.rocking.demo.dependmodulebimpl"></bean> </beans>
这是我们的程序演示一个标准的beanfactory容器(spring ioc容器的实现之一)对上面配置文件的装配:
class mainmodule { private dependmodulea modulea; private dependmoduleb moduleb; public dependmodulea getmodulea() { return modulea; } public void setmodulea(dependmodulea modulea) { this.modulea = modulea; } public dependmoduleb getmoduleb() { return moduleb; } public void setmoduleb(dependmoduleb moduleb) { this.moduleb = moduleb; } } interface dependmodulea { public void funcfrommodulea(); } interface dependmoduleb { public void funcfrommoduleb(); } class dependmoduleaimpl implements dependmodulea { @override public void funcfrommodulea() { system.out.println("this is func from module a"); } } class dependmodulebimpl implements dependmoduleb { @override public void funcfrommoduleb() { system.out.println("this is func from module b"); } } public class simpleiocdemo { public static void main(string[] args) throws classnotfoundexception { defaultlistablebeanfactory beanfactory = new defaultlistablebeanfactory(); xmlbeandefinitionreader reader = new xmlbeandefinitionreader(beanfactory); reader.loadbeandefinitions("beans.xml"); mainmodule mainmodule = (mainmodule) beanfactory.getbean("mainmodule"); mainmodule.getmodulea().funcfrommodulea(); mainmodule.getmoduleb().funcfrommoduleb(); } }
这里我们的配置文件和javabean被加载读取并被解析,这里的beandefinition生成使用过程掩藏在其中,这是实际上在ioc内部发生的大致过程:
public class simpleiocdemo { public static void main(string[] args) throws classnotfoundexception { defaultlistablebeanfactory beanfactory = new defaultlistablebeanfactory(); abstractbeandefinition mainmodule = new rootbeandefinition(mainmodule.class); abstractbeandefinition modulea = new rootbeandefinition(dependmoduleaimpl.class); abstractbeandefinition moduleb = new rootbeandefinition(dependmodulebimpl.class); beanfactory.registerbeandefinition("mainmodule", mainmodule); beanfactory.registerbeandefinition("modulea", modulea); beanfactory.registerbeandefinition("moduleb", moduleb); mutablepropertyvalues propertyvalues = new mutablepropertyvalues(); propertyvalues.add("modulea", modulea); propertyvalues.add("moduleb", moduleb); mainmodule.setpropertyvalues(propertyvalues); mainmodule module = (mainmodule) beanfactory.getbean("mainmodule"); module.getmodulea().funcfrommodulea(); module.getmoduleb().funcfrommoduleb(); } }
对xml的元信息进行加载读取后,ioc解析引擎会将其中提到的模块依据其真实类型创建成beandefinition,这个beandefinition可以看成是一种反射或者代理的过程,目的是为了让ioc容器清楚以后要创建的实例对象的bean结构,然后将这些bean结构注册到beanfactory中去,之后将主模块的依赖以setter注入的形式加入到主模块的属性中去,(这一点要看主模块提供的是setter方法还是初始化方法),这个过程结束后注册完所有“图纸”上规定的bean的definition后,beanfactory就已经成型。之后只要调用getbean方法即可将符合要求的bean生产出来,这是下一阶段的过程,我们之后再说。
在将beandefinition这一“图纸”上的信息注册到beanfactory完毕后,我们仍然可以对已经注册完的beandefinition进行改动的操作,这就是我们前面提到的spring为使用者设计的灵活的地方之一,不是说所有的过程不可控,而是在很多地方留了很多使用者可以发挥的余地。具体的办法是使用beanfactory处理器beanfactorypostprocessor来介入对beanfactory的处理以进一步改写我们需要修改的beandefinition部分。这个过程对应流程里的“后处理”过程。
以常见的处理器之一:属性占位符配置处理器为例,就是在已经构建完成已注册完毕的beanfactory之后再对它处理,以使得beandefinition相应属性里的内容修改为配置处理器指定配置文件里的信息:
defaultlistablebeanfactory beanfactory = new defaultlistablebeanfactory(); xmlbeandefinitionreader reader = new xmlbeandefinitionreader(beanfactory); reader.loadbeandefinitions( new classpathresource( "beans.xml")); propertyplaceholderconfigurer configurer = new propertyplaceholderconfigurer(); configurer.setlocation( new classpathresource( "about.properties")); configurer.postprocessbeanfactory( beanfactory);
beanfactorypostprocessor将对beanfactory处理,处理的结果就是把beandefinition中定义的某些属性改成beanfactorypostprocessor定义位置处的某些信息。
2、bean 实例化阶段
有了经过处理的beandefinition的“内部图纸”的指导下,容器可以进一步把beandefifnition通过反射或cglib动态字节码生产的方式化为存在于内存中的活化实例对象,再将beandefinition规定的依赖对象通过setter注入或者初始化注入的方式装配进新创建的实例对象中,这里是实实在在地将依赖对象的引用赋给需要依赖的对象属性中。
但是这里需要注意的是创建的实例不仅仅是一个简单的bean定义的实例,而是一个经过spring包装的beanwrapper实例,这里为什么要采用beanwrapper的方式来包装bean呢?是因为beanwrapper提供了统一访问bean属性的接口,在创建完了基本的bean的框架后要对其中的属性进行设置,每个bean的setter方法都不一样,所以如果直接用反射设置的话会非常复杂,所以spring提供这种包装来简化属性设置:
beanwrapper beanwrapper = new beanwrapperimpl(class.forname("com.rocking.demo.mainmodule")); beanwrapper.setpropertyvalue( "modulea", class.forname("com.rocking.demo.depmoduleaimpl").newinstance()); beanwrapper.setpropertyvalue( "moduleb", class.forname("com.rocking.demo.depmodulebimpl").newinstance()); mainmodule mainmodule= (mainmodule) beanwrapper.getwrappedinstance(); mainmodule.getmodulea().funcfroma(); mainmodule.getmoduleb().funcfromb();
以上的过程展示了在spring内部,通过获取类的反射容器了解将来包装的实例bean的结构并作出包装,使用统一的属性设置方法setpropertyvalue来对这个包装的实例设置属性,最后得到的bean实例通过getwrappedinstance拿到,可以发现已经成功将其属性赋值。
这个时候的bean实例其实已经完全可以使用了,但是spring同样在实例化阶段也为我们准备了灵活的策略以完成使用者对这个阶段的介入,和容器启动阶段的beanfactorypostprocessor控制beandefinition类似,在实例化阶段,spring提供了beanpostprocessor处理器来对已经装配好的实例进行操作,以完成可能需要的改动:、
这里举个例子来说明,定义一个beanpostprocessor的实现类,实现其中的方法postprocessafterinitialization和postprocessbeforeinitialization来定义对在bean实例装配之后和之前分别进行的操作,在beanfactory添加了这个处理器后就会在每次调用getbean方法装配实例的时候,都会传入根据“图纸”装配出的bean实例(包括装配过程中创建的依赖实例bean)调用这两个方法,这些方法可以对这些bean实例实施修改。
下面是一个这样的例子(mainmodule及其依赖关系和本文之前的例子相同):
class modulec { private string x; public string getx() { return x; } public void setx(string x) { this.x = x; } } class modulepostprocessor implements beanpostprocessor{ @override public object postprocessafterinitialization(object object, string string) throws beansexception { system.out.println(string); if(object instanceof modulec){ system.out.println(string); ((modulec)object).setx("after"); } return object; } @override public object postprocessbeforeinitialization(object object, string string) throws beansexception { if(object instanceof modulec){ ((modulec)object).setx("before"); } return object; } } public class verysimpleiockernal { public static void main(string[] args) throws classnotfoundexception, beansexception, instantiationexception, illegalaccessexception { defaultlistablebeanfactory beanfactory = new defaultlistablebeanfactory(); xmlbeandefinitionreader reader = new xmlbeandefinitionreader(beanfactory); reader.loadbeandefinitions(new classpathresource("beans.xml")); modulepostprocessor postprocessor = new modulepostprocessor(); beanfactory.addbeanpostprocessor(postprocessor); mainmodule module = (mainmodule) beanfactory.getbean("mainmodule"); modulec modulec = (modulec) beanfactory.getbean("modulec"); system.out.println(modulec.getx()); } }
这是bean的依赖关系配置文件:
<?xml version="1.0" encoding="utf-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/xmlschema-instance" xsi:schemalocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="mainmodule" class="com.rocking.demo.mainmodule"> <property name="modulea"> <ref bean="modulea"/> </property> <property name="moduleb"> <ref bean="moduleb"/> </property> </bean> <bean id="modulea" class="com.rocking.demo.depmoduleaimpl"> <property name="infoa"> <value>${modulea.infoa}</value> </property> </bean> <bean id="moduleb" class="com.rocking.demo.depmodulebimpl"> <property name="infob"> <value>info of moduleb</value> </property> </bean> <bean id="modulec" class="com.rocking.demo.modulec"> </bean> </beans>
从最终的结果我们可以看出,每次调用getbean方法得到的bean实例(包括因依赖关系生成的)都将被beanpostprocessor获取进行前置和后置处理。
除了类似上面的beanpostprocessor的办法对装配好的bean再做处理外,spring还可以通过配置init-method和destroy-method来对bean的初始化和销毁过程设置回调函数,这些回调函数也还可以灵活地提供更改bean实例的机会。
整个spring ioc的过程其实总体来说和我们自己写的ioc原型在本质上是一样的,只不过通过复杂的设计使得ioc的过程能够更灵活有效地提供给使用者更多的发挥空间,除此之外,spring的ioc也在安全性、容器的稳定性、metadata到bean转换的高效性上做到了精美的设计,使得ioc这一spring容器的基础得以稳固。