欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

《Struts2技术内幕》 新书部分篇章连载(八)—— XWork容器概览

程序员文章站 2022-03-02 21:05:25
...
第5章 生命之源 —— XWork中的容器

对象的生命周期管理在基于面向对象的编程语言中是一个永恒的话题。从语法上讲,面向对象的高级编程语言都是以“对象”为中心的。而对象之间的继承关系、嵌套引用关系所形成的对象树结构为我们进行对象级别的逻辑操作提供了足够的语法支持。但这样一来,对象之间所形成的复杂关系也就为对象生命周期的管理带来了问题:

  • 在程序的运行期,我们如何创建我们所需要的对象?
  • 当我们创建一个新的对象时,如何保证与这个对象所关联的依赖关系(其关联对象)也能够被正确地创建出来呢?
这两大问题不仅是面向对象的编程语言中的核心问题,也是每个框架在进行设计时必须跨越的坎。因而,业界对于这样类的问题也早有公论:

downpour 写道
结论 为了更好地管理好对象的生命周期,我们有必要在程序逻辑中引入一个额外的编程元素,这个元素就是容器(Container)。


在本章中,我们就来探讨这一额外的编程元素 —— 容器(Container)的方方面面,并深入分析XWork框架的容器(Container)实现机制。

5.2 XWork容器概览

在上一节中,我们已经探讨了引入容器的重要意义以及容器在对象生命周期管理中的作用。XWork作为一个优秀的开发框架,在其内部也实现了一个小型的容器。接下来,我们将对XWork中实现的容器做一个简单的介绍,其中包括容器的定义、容器的管辖范围和容器的基本操作。

5.2.1 XWork容器的定义

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)被定义为一个Java接口,那么我们同时也来关注一下容器的实现类的一些基本特性。

downpour 写道
结论 容器(Container)是一个辅助的编程元素,它在整个系统中应该被实例化为一个全局的、单例的对象。


这是容器实现中最为基本的一个特性,也是由容器(Container)自身的设计初衷所决定的。如果我们在整个系统中能够获取到多个不同的容器的对象实例,或者容器的对象实例在整个系统中的作用域又存在局域性,那么我们依托容器进行对象生命周期管理就会变得混乱不堪。

downpour 写道
结论 容器(Container)在系统初始化时进行自身的初始化。系统应该提供一个可靠的、在任何编程层次都能够对这个全局的容器或者容器中管理对象进行访问的机制。


这一条结论,是我们对容器(Container)实现的基本要求。从这条结论中,我们可以看到两个不同的方面:

  • 容器的初始化需求 —— 我们应该掌握好容器初始化的时机,并考虑如何对容器实例进行系统级别的缓存
  • 系统与容器的通讯机制 —— 我们应该提供一种有效的机制与这个全局的容器实例进行沟通
有关这两个不同方面的实现机理,我们将在接下来的章节中陆续给出源码级别的解析。其中有关容器的初始化过程,蕴含在整个框架的初始化主线中,我们将在第九章中详细解读。而系统与容器的通讯机制,则涉及到了XWork容器自身的数据结构和实现机理,因而也成为了本章的重点之一。读者在这里应体会XWork框架在容器的设计上与之前我们所提到的容器设计的基本原则之间的吻合度,这对我们整个面向对象的设计理念将有极大的提升。

5.2.2 XWork容器的管辖范围

既然引入容器(Container)的主要目的在于管理对象的生命周期,那么在明确了XWork的容器定义之后,我们就非常有必要去了解一下XWork容器的管辖范围。换句话说,如果我们拥有了这个全局的容器(Container)实例,当我们调用容器的操作接口时,到底操作的是哪些对象呢?

从容器(Container)操作接口的角度,容器的两类操作接口:获取对象实例(getInstance)和实施依赖注入(inject),它们所操作的对象也有所不同。接下来我们就对这两类不同的操作接口分别进行分析。

5.2.2.1 获取对象实例

当我们调用容器的getInstance方法来获取对象实例时,我们只能够获取到那些“被容器接管”的对象的实例。那么,哪些对象属于“被容器接管”的对象呢?
在第三章中,我们已经介绍过Struts2 / XWork的配置元素以及这些配置元素的分类。当时,我们把XML配置文件中基本节点的分为两类:其中一类是bean节点和constant节点,我们把这两个节点统称为容器配置元素;另外一类则是package节点,这个节点下的所有配置定义都被称之为事件映射关系。而我们进行配置元素分类的基本思路是按照XML节点所表达的逻辑含义和该节点在程序中所起的作用进行的分类。

现在,当我们回过头来再来看配置元素的分类时,我们就能理解“容器配置元素”的真正含义了。在XML配置元素中,bean节点被广泛用于定义框架级别的内置对象和自定义对象;而constant节点和Properties文件中的配置选项,则被用于定义系统级别的运行参数。我们之所以把这两类节点统称为“容器配置元素”,就是因为他们所定义的对象的生命周期,都是由容器(Container)所管理的,这些对象也就是所谓的“被容器接管”的对象。

downpour 写道
结论XWork容器所管理的对象,包括了所有框架配置定义中的“容器配置元素”。


根据之前的分析,这些对象主要可以被分为三类:
  • 在bean节点中声明的框架内部对象
  • 在bean节点中声明的自定义对象
  • 在constant节点和Properties文件中声明的系统运行参数
在这里需要注意的是,我们通过容器获取到的这些对象的实例,不仅自身被初始化,对象内部的所有依赖对象也已经被正确地实施依赖注入。很显然,这就是我们使用容器(Container)进行对象生命周期管理的好处。

在这里,我们对这三类容器托管对象的归纳,实际上蕴含了我们对自定义对象纳入XWork容器管理的过程:只要在Struts2 / XWork的配置文件中进行声明即可

5.2.2.2 对象的依赖注入

当我们调用容器的inject方法来实施依赖注入操作时,所操作的对象却不仅仅限于“容器配置元素”中所定义的对象。因为我们对于inject方法的定义是说:只要传入一个对象的实例,容器将负责建立起传入对象实例与容器托管对象之间的依赖关系。

由此可见,虽然传入inject的操作对象是任意的,然而实施依赖注入操作时的那些依赖对象却是被容器(Container)接管的对象。这就为我们为任意对象与XWork容器中所管理的对象之间建立起一条通道提供了有效的途径。

downpour 写道
结论 调用XWork容器的inject方法,能够帮助我们将容器所管理的对象(包括框架的内置对象以及系统的运行参数)注入到任意的对象实例中去,从而建立起任意对象与框架元素沟通的桥梁。


这一条结论对我们非常关键,因为它不仅反映了容器的基本职责,也是我们日后进行应用级别对象操作的理论基础。有关容器的两大类操作的具体实现机制,我们将在之后的章节中陆续给出分析。

从方法的命名上,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的那些对象的依赖注入
因此,我们在这里顺利解决了我们在容器定义中所提到的一个核心问题:如何建立起系统到容器或者容器托管对象的沟通桥梁 —— 通过@Inject声明来完成。

5.2.3 XWork容器操作详解

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)操作。
  • 通过操作容器所获取的对象实例,都是那些受到容器托管的对象实例。
  • 通过操作容器进行的依赖注入操作,可以针对任意对象进行,该操作可以建立起任意对象和容器托管对象之间的联系。
读者在这里或许对这些结论还一知半解,在之后的章节中,我们将一一为读者解开这些容器操作中的疑惑。在这里,读者应首先谨记这四个要点,理解它们的基本要义含义,并且将它们作为XWork容器操作的基本结论。因为我们将在之后的源码分析中经常遇到需要直接操作全局容器实例的范例,牢记这些基本结论之后读者对于Struts2 / XWork中内置对象的操作就不会产生障碍。

5.2.3.2 通过Annotation获取容器对象实例

在展开本节的话题之前,我们首先来回顾一下上一节中我们所得出的一个重要结论:

downpour 写道
结论通过操作容器(Container)进行对象操作的基本前提是当前的操作主体能够获得全局的容器实例。因而,全局的容器实例的获取,在操作主体的初始化过程中完成。


这个重要结论在之前我们对容器进行操作的示例代码中也能够得到证实,那就是以下这样一段公共代码,如代码清单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)内部操作的实现细节了。在接下来的章节中,我们就将揭开这个神秘过程的种种细节。