首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 开发语言 > 编程 >

struts2源码分析-IOC器皿的实现机制(上篇)

2012-08-02 
struts2源码分析-IOC容器的实现机制(上篇)说起 IOC 容器,依赖注入等名词,大家的第一印象往往是spring,因为

struts2源码分析-IOC容器的实现机制(上篇)
   说起 IOC 容器,依赖注入等名词,大家的第一印象往往是spring,因为spring刚出道的时候招牌就是 IOC和AOP等核心功能,而且我们在应用程序中使用spring最多的功能之一也是其 IOC 容器提供的。而 struts2做为一个 web层的MVC实现框架,其核心功能主要是帮助我们处理 http请求,但是 struts2本身也包含了一个 IOC 容器,用来支撑struts2的运行环境,并具备对象的获取和依赖注入功能,在搭建 ssh架构的时候,如果我们不配置 struts2和spring整合的插件,不把 Action转交给 spring-ioc来托管,那么struts2自身的ioc容器也足以满足我们的需求。而且个人以为 相比于 spring的ioc实现, struts2-ioc 的源代码更加精致、小巧,便于研究和学习。

   一、struts2-IOC容器简介.

    对于 一个ioc 容器来说,其核心功能是对象获取和依赖注入,在struts2 中容器的接口表示如下:


   从Containter接口的表示方法中,我们可以非常直观的看出 ioc容器的对象获取和依赖注入这两个功能被表示为重载方法 getInstance 和 inject,这里我先对 getInstance(Class<T> type, String name) 和 inject(Object ) 这个方法进行简单的分析。

   getInstance(Class<T> type, String name) 方法是从struts2容器中获取对象,那么strus2-ioc管理的是哪些对象呢?或者说其管理的对象范围。我们都知道struts2程序启动初始化时会去加载至少3个xml配置文件: struts-default.xml , struts-plugin.xml 和 struts.xml,其中前两者是struts2自带的框架级别的配置,后者是我们自己项目中的应用级别的配置,那么ioc容器所管理的对象就是在这其中配置的对象,我以我们开发中常见的 struts.xml为例,现在我想将两个 Service 对象放入 struts2-ioc中,配置如下:

  如果我想从 struts2-ioc 容器中获取 UserServiceImp1 对象,代码为:ActionContext.getContainter().getInstance(UserService.class,"service1"); 大家应该看明白 getInstance(Class<T> type, String name) 方法的使用方式了吧,其中的 type 即为 配置文件中声明的接口类型(当然也可以是具体类), class 为其中一种具体实现方式,而 name 为 标识,所以结论就是 以 type和name作为联合主键从ioc容器管理的对象中获取相应的对象!

  对于 inject(Object o),顾名思义,从字面上来看就知道是进行依赖注入操作,这里要强调的是,该方法是为参数Object o 这个对象注入在容器中管理的对象,建立起参数对象Object o 和容器管理对象之间的依赖关系, 这个参数对象 Object o 可以是容器管理的对象,也可以是我们自定义的对象,即它可以是任意对象,不必和getInstance(Class<T> type, String name)方法那样只能是容器管理的对象。 那么容器怎么判断应该为该任意对象的哪些字段或者方法参数实施依赖注入呢?在 Spring中我们是通过 @Autowired 注解或者 在 xml 文件中配置依赖关系的。在 struts2的ioc实现中,则也是通过注解的方式实现,即 @Inject 来实现的,一旦我们在任意对象的方法参数、构造参数、字段上加入@Inject注解声明,那么就是在告诉ioc容器:请为我注入由容器管理的对象实例吧。

  


  以上 Inject 注解中的 value字段对应的就是之前我们在 xml 配置文件中的 name 属性,如之前的 "service1" 或 "service 2" ,其默认值为 "default".Struts2通过此Inject注解,架起了任意对象与ioc容器进行通信的桥梁,使得受ioc容器管理的对象能够注入到任意对象对应的带Inject注解的方法参数和字段上。

 

下面我就以struts2中创建一个Action对象并进行依赖注入为例子,看看实际开发中是如何使用该功能的。

  

    我们看到了,在实例字段中我们加入了 @Inject 声明进行依赖注入,那么结合之前所述可以推测到,struts2的源码必定在创建Action的时候,调用的 inject 方法对Action对象进行依赖注入,果不其然,源码如下:

   


    在 injectInternalBeans 方法内调用了 Containter容 器的 inject(Object o)方法对创建的Action对象进行依赖注入操作. 到这里,我们算是对 struts2的ioc容器有了个初步的了解,知道了如何使用struts2-ioc容器获取对象和进行依赖注入。那么接下来,我们就要深入到struts2的ioc容器实现部分,看看其到底是如何实现获取对象和依赖注入功能的。

 

  二、Struts2-IOC容器初始化详解。

   提到IOC容器的初始化就不得不提到struts2 中另一种元素:package 的初始化,因为在struts2的初始化过程中不仅对容器初始化,也包含了对package这种事件映射元素的初始化,这里先简单说下这两者的区别,我们来看看常见的struts.xml中的配置:

    



   其中的 bean和constant节点,再加上 properties文件中的配置元素,这三者称为容器配置元素,即是由容器管理的对象,在容器初始化过程中要注册到容器中去。而对于 Package节点,里面包含了 action,interceptor,result等运行时的事件映射节点,这些节点元素并不需要纳入容器中管理。所以这里一个结论就是 struts2初始化的核心就是对容器配置元素和事件映射元素这两种不同元素的初始化过程,再进一步的讲就是将以各种形式配置的这两种元素转换为JAVA对象并统一管理的过程。由于这两种配置元素都有各种各样的表现形式,如之前看到的 xml配置的形式,属性文件properties的形式,还有我们进行扩展时自定义的其他形式,struts2插件中的 annotation形式等等。所以struts2在加载这些缤纷繁杂的各种配置形式时做了一定的考虑,先看如下的类图:

    struts2源码分析-IOC器皿的实现机制(上篇)

    从上图中我们可以清楚的看到,针对两种不同的元素类型,struts2设计了两个接口: ContainterProvider和PackageProvider,它们对外提供的功能主要就是从配置文件中加载对应的元素并转换为JAVA对象。这里的 ConfigurationProvider 则是应用了java中的接口多重继承机制,目的在于对两种Provider加载器进一步抽象,使得我们不必关心具体的实现方式是针对哪种类型的加载器,提供一个统一的接口和操作方法;而且对于诸如struts.xml这样的配置文件来说,其中即包括了容器配置元素也包括了Package事件映射元素,那么对应的加载器必须同时具备ContainterProvider和PackageProvider的接口方法,这样就只需要实现 ConfigurationProvider 接口即可。而对于只需要处理一种元素类型的加载器来说,个人认为只需要实现ContainterProvider或PackageProvider的其中一个接口即可。比方说在 struts2-plugin中的注解插件包中,我们将 Package中的元素以注解的方式进行配置,那么struts2在初始化的时候除了从struts.xml中读取package元素外,还需要扫描所有的Action类,读取其中我们配置的package元素注解,并转化为java对象,对于这样的注解加载器来说,由于只需要处理Pakacge元素的加载,所以provider实现类只需要实现PackageProvider接口,翻开源代码查看,也确实验证了我的想法,这里又不花太多篇幅了,这部分的源码在 struts2-convention-plugin 下,有兴趣的话可以看看。在struts2的核心包中,具体的Provider 实现类主要有以下几种:

    struts2源码分析-IOC器皿的实现机制(上篇)

    可以清楚的看出,DefaultPropertiesProvider主要是处理属性文件形式的配置文件,而 StrtusXmlConfigurationProvider则主要是处理以 XML形式存在的配置文件,比如 strtus-default.xml , struts.xml 和 struts-plugin.xml。之前提到过,加载器的作用就是在struts初始化的时候将各种不同形式的配置文件中的元素转换为java对象,进一步的理解就是将容器配置元素和package事件处理元素读取到系统中来并建立对应的Java对象,struts初始化的最终结果就是读取了所有容器配置元素并创建出Container(IOC容器)对象和PackageConfig(pckage事件处理对象),我们知道,无论是Container对象还是PackageConfig对象,在创建它们的时候都不可能是一个简单的 new 操作就可以的,因为它们二者的内部结构是复杂的,前者必须是包含了所有的容器配置对象而后者必须包含了所有的package时间配置对象,为了创建这两个内部结构复杂的对象,struts2在这里使用了构造者模式,即存在一个构造者对象分别为 containter和PackageConfig收集参数字段,前者是收集容器配置对象,后者是收集package事件映射对象,而各种Provider实现类就是往构造者对象中进行注册读取的配置对象,也就是构造者对象的参数收集过程,以一个时序图来表示 Containter的创建初始化过程:

    struts2源码分析-IOC器皿的实现机制(上篇)

                                                      (图:struts2-IOC容器的初始化时序图)

   这里我们首先关注的是和Provider有关的部分,我们看到 在Configuration中循环调用了每一个Provider实现类的  register()方法,这个register()方法是在 ContainterProvider接口中声明的,所以该方法的作用就是往IOC容器(Container)中注册将要被容器托管的对象,可以想象下ContainerProvider 的register()方法肯定就是解析各种形式的容器配置元素,转化为Java对象,然后注册到容器的构造者对象 ContainerBuilder中去,其中的 factory()就是这个注册的作用,待所有的ContainterProvider实现类将各自对应的容器配置元素都注册到ContainerBuilder中之后,Configuration调用ContainerBuilder的create()方法就能返回一个被正确初始化了的IOC容器Container了。接下来我以一个Provider的实现类

StrutsXmlConfigurationProvider为例子,来具体看一看它的register()方法。StrutsXmlConfigurationProvider主要是负责将 struts-default.xml,struts-plugin.xml和struts.xml中的容器配置元素和package配置元素加载到系统中来,它实现了 ConfigurationProvider接口,所以其具体的 register()方法就是解析xml文件中配置的容器配置元素并注册到ContainerBuilder中去,而具体的loadPackage()方法就是解析xml配置文件中的package元素并注册到package构造者对象PackageConfig.Builder中去(这是一个内部类).

   


 代码开头的两个for循环就是对加载进来的Dom4j的 Document对象进行遍历,处理其中的每一个xml节点。再次拿起之前我们在 struts.xml 中配置的 Bean节点为例:

     


 我们结合以上的代码来看看struts是如何将这个容器配置元素注册到ContainerBuilder中去的。首先在 if("bean".equals(nodename)) 中对这个 bean节点进行出来,刚开始的几行代码很容易看明白,无非就是读取这个bean节点中的各个属性,这里重点是:type,name,class,scope这四个属性,对于前三者在之前已经介绍过了,对于scope属性,就是说当我们在程序中向容器要这个对象的时候,容器是返回一个new的新对象呢,还是单例对象或者其他形式的对象,即这个对象的作用域范围,就像我们在spring中注册一个bean 元素的时候,不是也可以指定它的作用范围吗?这里和spring中的类似,通过容器获取对象的时候容器默认返回的是一个单实例对象,我们可以通过bean节点的scope属性改变:

  


  这里的 Scope是一个枚举类型,稍后我会介绍到,这里我们只需要知道它定义了对象的不同作用域,每一个Scope枚举实例代表一个作用域。我们看到bean的默认作用域范围是 Scope.SINGLETON,即单实例。default 代表的是每次返回一个新对象,request代表request请求作用域,session代表会话作用域,thread代表一个线程范围内的作用域,即ThreadLocal作用域内。接下来我们重点看看这行代码:

  


  这行代码的作用就是往容器构造对象ContainerBuilder中注册将要被容器接纳托管的对象,我们之前说过,在调用 Container.getInstance 方法的时候,IOC容器是以参数 type和name作为联合主键从容器中获取对象的,那么这里在初始化时往容器中注册对象的时候  ctype和name就是这个联合主键了,其对应的值一般认为就是对应的对象了,但是这里貌似不对,应该我们看到的值是 LocatableFactory,这好像是一个对象工厂,而不是对象本身吧。我们进入LocatableFactory内部看看究竟:

 


  咦,这里面看到了 create 方法,它根据我们传入的 Class 对象返回了一个Object实例,再来看看 Factory接口:

   


   看到这里,我们仿佛明白了,容器中接纳和托管的原来不是对象本身,而是对象工厂,当我们需要容器提供一个对象的时候,容器是调用的对象工厂中的 create 方法来创建并返回对象的。而对于具体的对象创建方式,我们可以通过实现Factory接口,实现其create方法即可,这里的Factory实现类为LocatableFactory,其create方法为调用Container的重载方法 inject(Class cl) 创建并返回一个新对象,该对象已经接受过容器的依赖注入了。具体的 inject(Class cl)实现细节,我们留到后面介绍容器操作方法 inject 和 getInstance 的时候再详细说明。当然了,如果我们自定义了别的Factory实现类,我们在 create 方法中完全可以根据我们的需要实现任意的对象创建方法,比如: class.newInstance() 这种最基本的方式或者从 JSON串转换为一个JAVA对象,或者反序列化创建对象等等,这就是工厂方法模式的优点,创建对象的方式可以很灵活。

  我们注意到 Factory 的代码注释上写到: A custom factory (客户端的 Factory), 啥意思? 难道还有容器自己内部的 Factory ?我们跟随这行源代码:

  


  进入到容器构造对象内部探个究竟,

  

 
   果然,我们在 ContainerBuilder中发现 我们传入的 Factory 被一个匿名内部类 internalFactory 给包装了,看起来这个 InternalFactory似乎和我们之前的 Factory具有相同的接口,我们来看看它的源码:

  

 

  果然和我们的想法一样,从名称上来看,貌似这个 InternalFactory是在容器内部使用的,而Factory则是我们客户端程序员使用的,它将在 ContainerBuilder的factory重载方法中被InternalFactory包装,不知道大家发现了没有,这里其实就是一个装饰着模式的运用,目标对象为我们传入的 LocatableFactory ,它的接口类型为Factory,在这里它被具有相同接口方法的 InternalFactory 包装,当调用internalFactory的create方法时,加入了额外的代码  Context externalContext = context.getExternalContext(); 主要是获取context上下文,然后才调用目标对象上的create方法:return factory.create(externalContext); 我们继续看到该 factory方法的最后一行代码为:

  这里调用了factory的另一个重载方法,我们进入看看它又做了哪些事情:

   


   我们重点关注的是这两行代码:

   


  先看第一行代码,结合我们之前的例子,这里的几个参数 key.getType()为 UserService.class ,key.getName()为 service1 , factory为包装了locatableFactory的InternalFactory实例, 参数 scope为 Scope.SINGLETON,所以我们调用的就是 Scope.SINGLETON 这个枚举实例上的 scopeFactory方法,我们来看看 Scope枚举的部分源码:

     

 

     可以很清楚的看到,在Scope枚举中声明了一个抽象方法 scopeFactory ,所以每一个枚举实例都实现了这个方法,它们各自实现了创建了不同生命周期的对象,还是以我们之前配置的例子来说明:     

   这里并没有配置 bean节点的 scope属性,但是结合我们之前的源码分析可以知道,其默认值为 singleton,所以我刚才写道 “ 参数 scope为 Scope.SINGLETON,我们调用的就是 Scope.SINGLETON 这个枚举实例上的 scopeFactory方法。”  在其内部又创建了InternalFactpry的一个匿名内部类,在create方法中再次包装了factory以实现其单实例的功能,返回的就是又一次经过包装的InternalFactory。Scope.Thread也是类似,只不过它创建的对象声明周期为线程范围内,所以把他/她缓存在ThreadLocal 中。 而Scope.DEFAULT 则是不做处理 直接返回 factory,这样当调用create方法时候,每次都是创建一个新对象。

    让我们的视线重新回到 ContainerBuilder中的这两行代码:

   


  第一行我们刚才已经介绍过了,那么第二行的作用就是把经过枚举Scope处理过的factory放入到一个容器内部的Map缓存中,这样容器才能根据 type和name的联合主键key从容器内部查找对应的对象工厂,然后返回对象。 factories 作为ContainerBuilder内部属性的定义如下:

    


  看到了吧,容器内部缓存的确实是对象工厂。至于 singletonFactories 列表,则是包含了所有配置为 scope="singleton" 的对象工厂,如果 boolean created值为true 的话,那么在 ContainerBuilder创建容器对象 Container的时候,会先调用scope="singleton" 的对象工厂的create方法,即初始化容器的时候把对象生命周期为单实例的对象先创建出来。

   至此,我们已经以一个容器配置元素 


 为例子,讲述他它如何注册到容器创建者对象ContainerBuilder中的流程走了一遍,那么整个容器的初始化过程也就是将各种配置形式的所有容器配置元素注册到容器创建者对象 ContainerBuilder中去,结合 之前给出的struts2-IOC容器的初始化时序图,我们可以清楚的看到,最后由 Configuration 调用 ContainerBuilder 的 create方法返回一个已经被初始化了的 IOC容器对象Container:

   


    其中  final ContainerImpl container = new ContainerImpl( new HashMap<Key<?>, InternalFactory<?>>(factories))  为具体创建了一个容器对象,这里是Container接口的具体实现类 ContainerImpl.

  


     这个构造函数主要做两件事,其一是为 Key(type,name) --- InternalFactory的 Map实例字段 赋值,其来源就是 ContainerBuilder中的factories. 其二为将 type 和 name 的一对多关系保存在 Map实例字段 factoryNamesByType 中,以如下为例:


   那么 factoryNamesByType 的值就是 [ UserService.class ,["service1","service2]" ].

   到此为止,关于Struts2-IOC容器的初始化过程的重点部分已经讲述完毕了,我们发现其重点就是对象工厂的层层包装,即装饰模式的运用,这样当我们要容器要一个对象实例的时候,就会触发一系列的 InternalFactory.create 方法调用。核心结论是容器初始化完毕后其内部的Map字段factories中缓存的是 对象工厂,而不是对象实例本身。  

    让我们回到 struts2-IOC容器的初始化时序图 中的 Configuration 接口上来,因为它把控着容器的初始化逻辑:

    

 

     以上代码执行完毕后,Struts2-IOC容器就被正确的初始化创建出来了!之前说过,struts2在初始化的过程中,主要是对容器对象和package事件映射对象初始化,那么在这个方法的代码中接下来的事情就是顺便把PackageConfig这个对象也一起初始化掉:

    


    至此,struts2的两类配置元素Container和PackageConfig 已经初始化完毕了。在下一篇文章中,我将重点分析  Container中的重载方法 getInstance 和 inject  的具体实现,这两个方法也是任何IOC容器最被常用的方法,我们将看到在Struts2-IOC容器中是如何实现的,敬请期待!



 


 

   

  


 

    

   

 

 

  


 

 

 

 

 

1楼chanedi41分钟前
来支持一个。nps:csdn的页面渲染好慢啊

热点排行