《Struts2技术内幕》 新书部分篇章连载(八)—— XWork容器概览
第5章生命之源——XWork中的容器
对象的生命周期管理在基于面向对象的编程语言中是一个永恒的话题。从语法上讲,面向对象的高级编程语言都是以“对象”为中心的。而对象之间的继承关系、嵌套引用关系所形成的对象树结构为我们进行对象级别的逻辑操作提供了足够的语法支持。但这样一来,对象之间所形成的复杂关系也就为对象生命周期的管理带来了问题:
- 在程序的运行期,我们如何创建我们所需要的对象?
- 当我们创建一个新的对象时,如何保证与这个对象所关联的依赖关系(其关联对象)也能够被正确地创建出来呢?
在本章中,我们就来探讨这一额外的编程元素——容器(Container)的方方面面,并深入分析XWork框架的容器(Container)实现机制。
5.2XWork容器概览
在上一节中,我们已经探讨了引入容器的重要意义以及容器在对象生命周期管理中的作用。XWork作为一个优秀的开发框架,在其内部也实现了一个小型的容器。接下来,我们将对XWork中实现的容器做一个简单的介绍,其中包括容器的定义、容器的管辖范围和容器的基本操作。
5.2.1XWork容器的定义
XWork框架中的容器,被定义成为一个Java接口,其相关源码,如代码清单5-1所示:
public interface Container extends Serializable { /** * 定义默认的对象获取标识 */ String DEFAULT_NAME = "default"; /** * 进行对象依赖关系注入的基本操作接口,作为参数的object将被XWork容器进行处理。 * object内部声明有@Inject的字段和方法,都将被注入受到容器托管的对象, * 从而建立起依赖关系。 */ void inject(Object object); /** * 创建一个类的实例并进行对象依赖注入 */ <T> T inject(Class<T> implementation); /** * 根据type和name作为唯一标识,获取容器中的Java类的实例 */ <T> T getInstance(Class<T> type, String name); /** * 根据type和默认的name(default)作为唯一标识,获取容器中的Java类的实例 */ <T> T getInstance(Class<T> type); /** * 根据type获取与这个type所对应的容器中所有注册过的name * @param type * @return */ Set<String> getInstanceNames(Class<?> type); /** * 设置当前线程的作用范围的策略 */ void setScopeStrategy(Scope.Strategy scopeStrategy); /** * 删除当前线程的作用范围的策略 */ void removeScopeStrategy(); }
从容器(Container)的接口定义方法来看,它完全能够符合我们之前所讨论的容器设计的基本原则之一:简单而全面。从接口的内容和表现形式来看,他也能符合我们的对容器的基本要求:容器首先被设计成了一个接口而不是具体的实现类;而整个接口定义中既包含了获取对象实例的方法,也包含了管理对象依赖关系的方法。
在这里,我们可以看到容器设计的基本原则在一定程度上指导着容器的接口设计,因为我们更加关心容器能够对外提供什么样的服务,而并不是容器自身的数据结构。
从源码中,我们可以依照方法的不同作用对这些操作接口进行分类:
- 获取对象实例——getInstance、getInstanceName
- 处理对象依赖关系——inject
- 处理对象的作用范围策略——setScopeStrategy、removeScopeStrategy
这是容器实现中最为基本的一个特性,也是由容器(Container)自身的设计初衷所决定的。如果我们在整个系统中能够获取到多个不同的容器的对象实例,或者容器的对象实例在整个系统中的作用域又存在局域性,那么我们依托容器进行对象生命周期管理就会变得混乱不堪。
这一条结论,是我们对容器(Container)实现的基本要求。从这条结论中,我们可以看到两个不同的方面:
- 容器的初始化需求——我们应该掌握好容器初始化的时机,并考虑如何对容器实例进行系统级别的缓存
- 系统与容器的通讯机制——我们应该提供一种有效的机制与这个全局的容器实例进行沟通
5.2.2XWork容器的管辖范围
既然引入容器(Container)的主要目的在于管理对象的生命周期,那么在明确了XWork的容器定义之后,我们就非常有必要去了解一下XWork容器的管辖范围。换句话说,如果我们拥有了这个全局的容器(Container)实例,当我们调用容器的操作接口时,到底操作的是哪些对象呢?
从容器(Container)操作接口的角度,容器的两类操作接口:获取对象实例(getInstance)和实施依赖注入(inject),它们所操作的对象也有所不同。接下来我们就对这两类不同的操作接口分别进行分析。
5.2.2.1获取对象实例
当我们调用容器的getInstance方法来获取对象实例时,我们只能够获取到那些“被容器接管”的对象的实例。那么,哪些对象属于“被容器接管”的对象呢?
在第三章中,我们已经介绍过Struts2/XWork的配置元素以及这些配置元素的分类。当时,我们把XML配置文件中基本节点的分为两类:其中一类是bean节点和constant节点,我们把这两个节点统称为容器配置元素;另外一类则是package节点,这个节点下的所有配置定义都被称之为事件映射关系。而我们进行配置元素分类的基本思路是按照XML节点所表达的逻辑含义和该节点在程序中所起的作用进行的分类。
现在,当我们回过头来再来看配置元素的分类时,我们就能理解“容器配置元素”的真正含义了。在XML配置元素中,bean节点被广泛用于定义框架级别的内置对象和自定义对象;而constant节点和Properties文件中的配置选项,则被用于定义系统级别的运行参数。我们之所以把这两类节点统称为“容器配置元素”,就是因为他们所定义的对象的生命周期,都是由容器(Container)所管理的,这些对象也就是所谓的“被容器接管”的对象。
根据之前的分析,这些对象主要可以被分为三类:
- 在bean节点中声明的框架内部对象
- 在bean节点中声明的自定义对象
- 在constant节点和Properties文件中声明的系统运行参数
在这里,我们对这三类容器托管对象的归纳,实际上蕴含了我们对自定义对象纳入XWork容器管理的过程:只要在Struts2/XWork的配置文件中进行声明即可。
5.2.2.2对象的依赖注入
当我们调用容器的inject方法来实施依赖注入操作时,所操作的对象却不仅仅限于“容器配置元素”中所定义的对象。因为我们对于inject方法的定义是说:只要传入一个对象的实例,容器将负责建立起传入对象实例与容器托管对象之间的依赖关系。
由此可见,虽然传入inject的操作对象是任意的,然而实施依赖注入操作时的那些依赖对象却是被容器(Container)接管的对象。这就为我们为任意对象与XWork容器中所管理的对象之间建立起一条通道提供了有效的途径。
这一条结论对我们非常关键,因为它不仅反映了容器的基本职责,也是我们日后进行应用级别对象操作的理论基础。有关容器的两大类操作的具体实现机制,我们将在之后的章节中陆续给出分析。
从方法的命名上,inject非常直观,表达了“注入”的含义,与我们之前所提到的“依赖注入”的概念是吻合的。如果我们继续深入思考一下inject方法的逻辑,我们就会发现这个方法的内部实现实际上蕴含了系统与容器对象之间的通讯机制。根据之前我们在XWork的容器(Container)对象的定义,我们可以看到inject方法的调用流程:当某个对象实例作为参数传入方法之后,该方法会扫描传入对象内部声明有@Inject这个Annotation的字段、方法、构造函数、方法参数并将他们注入容器托管对象,从而建立起传入对象与容器托管对象之间的依赖关系。
由此可见,整个流程的调用过程被一个神秘的Annotation有效地驱动。我们接下来就首先来看看@Inject这个Annotation的定义,如代码清单5-2所示:
@Target({METHOD, CONSTRUCTOR, FIELD, PARAMETER}) @Retention(RUNTIME) public @interface Inject { /** * 进行依赖注入的名称。如果不声明,这个名称会被设置为‘default’ */ String value() default DEFAULT_NAME; /** * 是否必须进行依赖注入,仅仅对于方法和参数有效。 */ boolean required() default true; }
从@Inject的定义中,我们看到这个Annotation可以被设置在任何对象的方法、构造函数、内部实例变量或者参数变量之中。在这里,我们可以看到对于@Inject的使用并不受限于对象本身。它既可以被加入到Struts2/XWork的内置对象之上,也可以被加到任意我们自行编写的对象之上。一旦它被加入到我们自定义的对象之中,那么我们就建立起了自定义对象与容器托管对象之间的联系。因为被加入了@Inject这个Annotation的方法、构造函数、内部实例变量或者方法参数变量,实际上是在告诉容器:“请为我注入由容器托管的对象实例”。
细细考虑这个过程,它不正是我们引入容器来解决对象生命周期管理的目标吗?当我们需要寻求容器帮忙时,只要在恰当的地方加入一个标识符Annotation,容器在进行依赖注入操作时,就能够知晓并接管整个过程了。在这里,我们看到两个过程共同构成了XWork容器进行对象依赖注入操作的步骤:
- 为某个对象的方法、构造函数、内部实例变量、方法参数变量加入@Inject的Annotation
- 调用容器(Container)的inject方法,完成被加入Annotation的那些对象的依赖注入
5.2.3XWork容器操作详解
5.2.3.1通过容器(Container)接口进行对象操作
在了解了XWork中的容器的操作定义以及XWork容器的管辖范围之后,我们可以看看如何通过直接操作容器(Container)的实例来进行对象操作。
我们首先来看看如何通过容器(Container)对象来获取对象实例。我们在这里摘取了XWork框架中的一个处理类DefaultUnknownHandlerManager进行说明,其相关源码如代码清单5-3所示:
public class DefaultUnknownHandlerManager implements UnknownHandlerManager { protected ArrayList<UnknownHandler> unknownHandlers; private Configuration configuration; private Container container; @Inject public void setConfiguration(Configuration configuration) { this.configuration = configuration; build(); } @Inject public void setContainer(Container container) { this.container = container; build(); } protected void build() { // 如果configuration对象不为空,则依次从configuration对象 // 以及Container中读取UnknowHandler的实例 if (configuration != null && container != null) { List<UnknownHandlerConfig> unkownHandlerStack = configuration.getUnknownHandlerStack(); unknownHandlers = new ArrayList<UnknownHandler>(); if (unkownHandlerStack != null && !unkownHandlerStack.isEmpty()) { // 根据一定顺序获取UnknownHandlers实例 for (UnknownHandlerConfig unknownHandlerConfig : unkownHandlerStack) { // 调用container对象的getInstance方法获取UnknownHandler UnknownHandler uh = container.getInstance(UnknownHandler.class, unknownHandlerConfig.getName()); unknownHandlers.add(uh); } } else { // 调用container对象的getInstanceNames方法获取 // 所有受到容器管理的UnknownHanlder实例名称 Set<String> unknowHandlerNames = container.getInstanceNames(UnknownHandler.class); if (unknowHandlerNames != null) { // 根据名称调用container对象的getInstance方法获取实例 for (String unknowHandlerName : unknowHandlerNames) { UnknownHandler uh = container.getInstance(UnknownHandler.class, unknowHandlerName); unknownHandlers.add(uh); } } } } } // 这里省略了许多其他的代码 }
在这里,我们看到了通过容器(Container)对象获取对象实例的两种方法:getInstance和getInstanceNames。其中,前者用于获取接受容器托管的具体对象实例。后者则被用于对于一个接口的多个不同实现类之间的实例获取的管理。我们在这里需要注意的是,在代码示例中的build方法调用的前提是setContainer方法对于容器(Container)对象的正确初始化。
有关容器(Container)的另外一种操作:依赖注入,我们则通过XWork框架中的核心类ActionSupport的源代码来进行解释说明,如代码清单5-4所示:
public class ActionSupport implements Action, Validateable, ValidationAware, TextProvider, LocaleProvider, Serializable { // 这里省略了许多其他的代码 private TextProvider getTextProvider() { if (textProvider == null) { TextProviderFactory tpf = new TextProviderFactory(); if (container != null) { container.inject(tpf); } textProvider = tpf.createInstance(getClass(), this); } return textProvider; } @Inject public void setContainer(Container container) { this.container = container; } // 这里省略了许多其他的代码 }
在上面的代码中,我们看到两个主要的方法:getTextProvider和setContainer。从逻辑上讲,很明显getTextProvider将以setContainer的存在为基础。setContainer实际上就是框架帮助我们获取全局的容器实例的具体方法。值得我们注意的是@Inject这个Annotation的使用,使得setContainer方法将在ActionSupport初始化时被注入了全局的Container对象。而getTextProvider则在运行期被调用,此时全局的容器(Container)对象中的接口函数就可以被随意调用,并完成依赖注入操作。具体来说,就是代码中的container.inject(tpf)操作。
综合上述的操作容器(Container)进行的两类对象操作:获取受到容器(Container)托管的对象和对象的依赖注入操作,我们可以从中得出使用容器(Container)进行对象操作的几个要点:
- 通过操作容器进行对象操作的基本前提是当前的操作主体能够获得全局的容器实例。因而,全局的容器实例的获取,在操作主体的初始化过程中完成。
- 通过操作容器进行的对象操作都是运行期(Runtime)操作。
- 通过操作容器所获取的对象实例,都是那些受到容器托管的对象实例。
- 通过操作容器进行的依赖注入操作,可以针对任意对象进行,该操作可以建立起任意对象和容器托管对象之间的联系。
5.2.3.2通过Annotation获取容器对象实例
在展开本节的话题之前,我们首先来回顾一下上一节中我们所得出的一个重要结论:
这个重要结论在之前我们对容器进行操作的示例代码中也能够得到证实,那就是以下这样一段公共代码,如代码清单5-5所示:
@Inject public void setContainer(Container container) { this.container = container; }
我们对这段公共代码实际含义的解读是:在当前的对象操作主体进行初始化时,这个方法会被调用,而全局的容器(Container)对象则会被初始化到当前的对象操作主体之中。然而,这个方法并不是对象构造函数的一部分,那么这个方法又是如何被包含在对象的初始化过程中去的呢?在这里,引发这一系列神秘操作的,就是加在方法之上的这个Annotation:@Inject。
在上一节的分析中,我们得知@Inject是建立起任意对象实例与容器托管对象之间桥梁的唯一途径。因此,当我们需要在一个自定义对象(非容器托管)中获得容器托管对象的实例时,我们就可以借助@Inject这个Annotation来实现。下面的例子就展示了这样一个过程,如代码清单5-6所示:
public class ObjectProviderTest { private ObjectFactory objectFactory; @Inject public void setObjectFactory(ObjectFactory objectFactory) { this.objectFactory = objectFactory; } }
在这个例子中,我们使用的对象是一个自定义的对象ObjectProviderTest,然而我们却需要在这个对象中获得容器托管的对象(在这里,ObjectFactory是受到XWork容器托管的框架内置对象)的实例。整个过程则通过@Inject的注入来完成。
在本节中,读者应始终沿着XWork容器进行对象依赖注入的操作步骤进行过程的解读。读到这里,或许读者已经迫不及待地想要弄清楚容器(Container)内部操作的实现细节了。在接下来的章节中,我们就将揭开这个神秘过程的种种细节。