Jakarta Commons Chain

Commons Chain

开始使用Commons Chain
作为程序开发人员,我们经常需要对一个实际上程序性的系统应用面向对象的方法。商业分析家和管理人员描述这样的系统时通常不使用类层次和序列图,而是使用流程图和工作流图表。但是不论如何,使用面向对象的方法解决这些问题时会带来更多的灵活性。面向对象的设计模式提供了有用的结构和行为来描述这种顺序的处理,比如模版方法(Template Method)[GoF]和责任链(Chain of Responsibility)[GoF]。

Jakarta Commons的子项目Chain将上述两个模式组合成一个可复用的Java框架用于描述顺序的处理流程。这个在Jakarta Commons project社区中开发的框架,已经被广泛的接受并且使用于许多有趣的应用中,特别的是他被Struts和Shale应用框架作为处理HTTP请求处理的基础机制。你可以在需要定义和执行一组连续的步骤时使用Commons Chain。

至于经典设计模式,开发者和架构师普遍使用模版方法(Template Method)造型顺序处理。模版方法(Template Method)中使用一个抽象的父类定义使用的算法:处理的步骤,具体实现交给子类。当然,父类也可以为算法所使用的方法提供一个缺省实现。

由于模版方法(Template Method)依赖继承——子类必须继承定义了算法的父类——因此使用这个模式的软件表现出紧耦合而且缺少灵活性。又由于实现类添加自己的行为前必须扩展父类,沟每⑷嗽北幌拗朴诶嗖愦沃校佣拗屏顺绦蛏杓频牧榛钚浴ommons Chain使用配置文件定义算法,在程序运行时解析配置文件,从而很好的解决了这个问题。

现在来看一下Commons Chain是怎样工作的,我们从一个人造的例子开始:二手车销售员的商业流程。下面是销售流程的步骤:
1.        得到用户信息
2.        试车
3.        谈判销售
4.        安排财务
5.        结束销售

现在假设使用模版方法(Template Method)造型这个流程。首先建立一个定义了算法的抽象类:

清单1

public abstract class SellVehicleTemplate {
        public void sellVehicle() {
        getCustomerInfo();
        testDriveVehicle();
        negotiateSale();
        arrangeFinancing();
        closeSale();
        }

        public abstract void getCustomerInfo();
        public abstract void testDriveVehicle();
        public abstract void negotiateSale();
        public abstract void arrangeFinancing();
        public abstract void closeSale();       
}



现在来看一下怎样用Commons Chain实现这个流程。首先,下载Commons Chain。你可以直接下载最新的zip或tar文件,也可以从CVS或者SubVersion源码库检出Commons Chain模块得到最新的代码。解压缩打包文件,将commons-chain.jar放入你的classpath中。

使用Commons Chain实现这个商业流程,必须将流程中的每一步写成一个类,这个类需要有一个public的方法execute()。这和传统的命令模式(Command pattern)实现相同。下面简单实现了“得到用户信息”:

清单2

package com.jadecove.chain.sample;

import org.apache.commons.chain.Command;
import org.apache.commons.chain.Context;

public class GetCustomerInfo implements Command {
        public boolean execute(Context ctx) throws Exception {
                System.out.println("Get customer info");
                ctx.put("customerName","George Burdell");
                return false;
        }
}



由于只是演示,这个类并没有做很多工作。这里将用户名放入了Context对象ctx中。这个Context对象连接了各个命令。暂时先将这个对象想象成根据关键字存取值的哈希表。所有后来的命令可以通过它访问刚才放入的用户名。TestDriveVehicle,NegotiateSale和 ArrangeFinancing命令的实现只是简单的打印了将执行什么操作。

清单3

package com.jadecove.chain.sample;

import org.apache.commons.chain.Command;
import org.apache.commons.chain.Context;

public class TestDriveVehicle implements Command {
        public boolean execute(Context ctx) throws Exception {
                System.out.println("Test drive the vehicle");
                return false;
        }
}

public class NegotiateSale implements Command {
        public boolean execute(Context ctx) throws Exception {
                System.out.println("Negotiate sale");
                return false;
        }
}

public class ArrangeFinancing implements Command {
        public boolean execute(Context ctx) throws Exception {
                System.out.println("Arrange financing");
                return false;
        }
}



CloseSale从Context对象中取出GetCustomerInfo放入的用户名,并将其打印。
清单4

package com.jadecove.chain.sample;

import org.apache.commons.chain.Command;
import org.apache.commons.chain.Context;

public class CloseSale implements Command {
        public boolean execute(Context ctx) throws Exception {
                System.out.println("Congratulations "
                  +ctx.get("customerName")
                        +", you bought a new car!");
                return false;
        }
}



现在你可以将这个流程定义成一个序列(或者说“命令链”)。
清单5

package com.jadecove.chain.sample;

import org.apache.commons.chain.impl.ChainBase;
import org.apache.commons.chain.Command;
import org.apache.commons.chain.Context;
import org.apache.commons.chain.impl.ContextBase;

public class SellVehicleChain extends ChainBase {
        public SellVehicleChain() {
                super();
                addCommand(new GetCustomerInfo());
                addCommand(new TestDriveVehicle());
                addCommand(new NegotiateSale());
                addCommand(new ArrangeFinancing());
                addCommand(new CloseSale());
        }
        public static void main(String[] args) throws Exception {
                Command process = new SellVehicleChain();
                Context ctx = new ContextBase();
                process.execute(ctx);
        }
}



运行这个类将会输出以下结果:
Get customer info
Test drive the vehicle
Negotiate sale
Arrange financing
Congratulations George Burdell, you bought a new car!

在进一步深入之前,让我们来看一下我们使用了的Commons Chain的类和接口。


Command 类和Chain类的关系就是组合模式(Composite pattern)[GoF]的例子:Chain不仅由多个Command组成,而且自己也是Command。这使你可以非常简单得将单个命令(Command)替换成由多个命令(Command)组成的链(Chain)。这个由Command对象唯一操作定义的方法代表了一个直接的命令:

public boolean execute(Context context);

参数context仅仅是一个存放了名称-值对的集合。接口Context在这里作为一个标记接口:它扩展了java.util.Map但是没有添加任何特殊的行为。于此相反,类ContextBase不仅提供了对Map的实现而且增加了一个特性:属性-域透明。这个特性可以通过使用Map的put和get 方法操作JavaBean的域,当然这些域必须使用标准的getFoo和setFoo方法定义。那些通过JavaBean的“setter”方法设置的值,可以通过对应的域名称,用Map的get方法得到。同样,那些用Map的put方法设置的值可以通过JavaBean的“getter”方法得到。

例如,我们可以创建一个专门的context提供显式的customerName属性支持。
清单6

package com.jadecove.chain.sample;

import org.apache.commons.chain.impl.ContextBase;

public class SellVehicleContext extends ContextBase {

       
        private String customerName;

        public String getCustomerName() {
                return customerName;
        }
       
        public void setCustomerName(String name) {
                this.customerName = name;
        }
}



现在你既可以进行Map的一般属性存取操作同时也可以使用显式的JavaBean的访问和修改域的方法,这两个将产生同样的效果。但是首先你需要在运行SellVehicleChain时实例化SellVehiceContext而不是ContextBase。
清单7

public static void main(String[] args) throws Exception {
                Command process = new SellVehicleChain();
                Context ctx = new SellVehicleContext();
                process.execute(ctx);
        }



尽管你不改变GetCustomerInfo中存放用户名的方法——仍然使用ctx.put("customerName", "George Burdell")——你可以在CloseSale中使用getCustomerName()方法得到用户名。
清单8

        public boolean execute(Context ctx) throws Exception {
            SellVehicleContext myCtx = (SellVehicleContext) ctx;
            System.out.println("Congratulations "
                                   + myCtx.getCustomerName()
                                  + ", you bought a new car!");
            return false;
        }



那些依赖类型安全和context的显式域的命令(Command)可以利用标准的getter和setter方法。当一些新的命令(Command)被添加时,它们可以不用考虑context的具体实现,直接通过Map的get和put操作属性。不论采用何种机制,ContextBase类都可以保证命令(Command)间可以通过context互操作。

下面这个例子展示了如何使用Commons Chain的API建立并执行一组顺序的命令。当然,和现在大多数Java软件一样,Commons Chain可以使用XML文件作为配置文件。你可以将“汽车销售”流程的步骤在XML文件中定义。这个文件有个规范的命名chain- config.xml。

清单9

<catalog>
  <chain name="sell-vehicle">
    <command   id="GetCustomerInfo"
        className="com.jadecove.chain.sample.GetCustomerInfo"/>
    <command   id="TestDriveVehicle"
        className="com.jadecove.chain.sample.TestDriveVehicle"/>
    <command   id="NegotiateSale"
        className="com.jadecove.chain.sample.NegotiateSale"/>
    <command   id="ArrangeFinancing"
        className="com.jadecove.chain.sample.ArrangeFinancing"/>
    <command   id="CloseSale"
        className="com.jadecove.chain.sample.CloseSale"/>
  </chain>
</catalog>



Chain的配置文件可以包含多个链定义,这些链定义可以集合进不同的编目中。在这个例子中,链定义在一个默认的编目中定义。事实上,你可以在这个文件中定义多个名字的编目,每个编目可拥有自己的链组。

现在你可以使用Commons Chain提供的类载入编目并得到指定的链,而不用像SellVehicleChain中那样自己在程序中定义一组命令:
清单10

package com.jadecove.chain.sample;

import org.apache.commons.chain.Catalog;
import org.apache.commons.chain.Command;
import org.apache.commons.chain.Context;
import org.apache.commons.chain.config.ConfigParser;
import org.apache.commons.chain.impl.CatalogFactoryBase;

public class CatalogLoader {
        private static final String CONFIG_FILE =
                "/com/jadecove/chain/sample/chain-config.xml";
        private ConfigParser parser;
        private Catalog catalog;
       
        public CatalogLoader() {
                parser = new ConfigParser();
        }
        public Catalog getCatalog() throws Exception {
                if (catalog == null) {
               
        parser.parse(this.getClass().getResource(CONFIG_FILE));               
       
                }
                catalog = CatalogFactoryBase.getInstance().getCatalog();
                return catalog;
        }
        public static void main(String[] args) throws Exception {
                CatalogLoader loader = new CatalogLoader();
                Catalog sampleCatalog = loader.getCatalog();
                Command command = sampleCatalog.getCommand("sell-vehicle");
                Context ctx = new SellVehicleContext();
                command.execute(ctx);
        }
}



Chain 使用Commons Digester来读取和解析配置文件。因此你需要将Commons Digester.jar加入classpath中。我使用了1.6版本并且工作得很好。Digester使用了Commons Collectios(我使用的版本是3.1),Commons Logging(版本1.0.4),Commons BeanUtils(1.7.0),因此你也需要将它们的jar文件加入classpath中。在加入这些jar后,CatalogLoader就可以被编译和运行,它的输出和另外两个测试完全相同。

现在你可以在XML文件中定义链,并可以在程序中得到这个链(别忘了链也是命令),这样扩展的可能性和程序的灵活性可以说是无限的。假设过程“安排财务”实际上由一个完全分离的商业部门处理。这个部门希望为这种销售建立自己的工作流程。 Chain提供了嵌套链来实现这个要求。因为链本身就是命令,因此你可以用指向另一个链的引用替换一个单一用途的命令。下面是增加了新流程的链的定义:
清单11

<catalog name="auto-sales">
   <chain name="sell-vehicle">
         <command   id="GetCustomerInfo"
                 className="com.jadecove.chain.sample.GetCustomerInfo"/>
         <command   id="TestDriveVehicle"
                 className="com.jadecove.chain.sample.TestDriveVehicle"/>
         <command   id="NegotiateSale"
                 className="com.jadecove.chain.sample.NegotiateSale"/>
         <command
                 className="org.apache.commons.chain.generic.LookupCommand"
             catalogName="auto-sales"
                      name="arrange-financing"
                  optional="true"/>
         <command   id="CloseSale"
                 className="com.jadecove.chain.sample.CloseSale"/>
   </chain>
   <chain name="arrange-financing">
         <command   id="ArrangeFinancing"
                 className="com.jadecove.chain.sample.ArrangeFinancing"/>
   </chain>
</catalog>



Commons Chain提供了一个常用的命令LookupCommand来查找和执行另一个链。属性optional用于控制当指定的嵌套链没有找到时如何处理。 optional=true时,即使链没找到,处理也会继续。反之,LookupCommand将抛出 IllegalArgumentException,告知指定的命令未找到。

在下面三种情况下,命令链将结束:
1.        命令的execute方法返回true
2.        运行到了链的尽头
3.        命令抛出异常

当链完全处理完一个过程后,命令就返回true。这是责任链模式(Chain of Responsibility)的基本概念。处理从一个命令传递到另一个命令,直到某个命令(Command)处理了这个命令。如果在到达命令序列尽头时仍没有处理返回true,也假设链已经正常结束。

当有命令抛出错误时链就会非正常结束。在Commons Chain中,如果有命令抛出错误,链的执行就会中断。不论是运行时错误(runtime exception)还是应用错误(application exception),都会抛出给链的调用者。但是许多应用都需要对在命令之外定义的错误做明确的处理。Commons Chain提供了Filter接口来满足这个要求。Filter继承了Command,添加了一个名为postprocess的方法。

public boolean postprocess(Context context, Exception exception);
只要Filter的execute方法被调用,不论链的执行过程中是否抛出错误,Commons Chain都将保证Filter的postprocess方法被调用。和servlet的过滤器(filter)相同,Commons Chain的Filter按它们在链中的顺序依次执行。同样,Filter的postprocess方法按倒序执行。你可以使用这个特性实现自己的错误处理。下面是一个用于处理我们例子中的错误的Filter:
清单12

package com.jadecove.chain.sample;

import org.apache.commons.chain.Context;
import org.apache.commons.chain.Filter;

public class SellVehicleExceptionHandler implements Filter {

        public boolean execute(Context context) throws Exception {
                System.out.println("Filter.execute() called.");
                return false;
        }

        public boolean postprocess(Context context,
                                 Exception exception) {
                if (exception == null) return false;
                System.out.println("Exception "
                              + exception.getMessage()
                              + " occurred.");
                return true;
        }
}



Filter在配置文件中的定义就和普通的命令(Command)定义相同:
清单13

<chain name="sell-vehicle">
  <command   id="ExceptionHandler"
     className =
           "com.jadecove.chain.sample.SellVehicleExceptionHandler"/>
  <command   id="GetCustomerInfo"
      className="com.jadecove.chain.sample.GetCustomerInfo"/>



Filter 的execute方法按定义的序列调用。然而,它的postprocess方法将在链执行完毕或抛出错误后执行。当一个错误被抛出时, postprocess方法处理完后会返回true,表示错误处理已经完成。链的执行并不会就此结束,但是本质上来说这个错误被捕捉而且不会再向外抛出。如果postprocess方法返回false,那错误会继续向外抛出,然后链就会非正常结束。

让我们假设ArrangeFinancing因为用户信用卡损坏抛出错误。SellVehicleExceptionHandler就能捕捉到这个错误,程序输出如下:
Filter.execute() called.
Get customer info
Test drive the vehicle
Negotiate sale
Exception Bad credit occurred.

结合了过滤器(filter)和子链技术后,你就可以造型很复杂的工作流程。


Commons Chain是一个很有前途的框架,现在仍在开发,新的功能被频繁地添加到其中。在下一篇关于Commons Chain的文章中,我们将研究Struts 1.3中是如何使用Commons Chain的。

Struts 1.3中用完全使用Commons Chain的类替换了原来的处理HTTP请求的类。如果你以前自己定制过Struts的请求处理(request processor),你将发现处理这个问题时Commons Chain为程序带来了很好的灵活性。
 
 
url:http://sugongqing.javaeye.com/blog/244933
 
 
 
======================================================================== 
 
 

Apache Commons Chain简明手册

基本对象
1. 接口。它是Commons Chain中最重要的接口,表示在Chain中的具体某一步要执行的命令。它只有一个方法:boolean execute(Context context)。如果返回true,那么表示Chain的处理结束,Chain中的其他命令不会被调用;返回false,则Chain会继续调用下一个Command,直到: Command
-          返回true; Command
-          抛出异常; Command
-          的末尾; Chain
2. 接口。它表示命令执行的上下文,在命令间实现共享信息的传递。Context接口的父接口是Map,ContextBase实现了Context。对于web环境,可以使用WebContext类及其子类(FacesWebContext、PortletWebContext和ServletWebContext)。 Context

3. 接口。它表示“命令链”,要在其中执行的命令,需要先添加到Chain中。Chain的父接口是Command,ChainBase实现了它。 Chain

4. 接口。它的父接口是Command,它是一种特殊的Command。除了Command的execute,它还包括一个方法:boolean postprocess(Context context, Exception exception)。Commons Chain会在执行了Filter的execute方法之后,执行postprocess(不论Chain以何种方式结束)。Filter的执行execute的顺序与Filter出现在Chain中出现的位置一致,但是执行postprocess顺序与之相反。如:如果连续定义了filter1和filter2,那么execute的执行顺序是:filter1 -> filter2;而postprocess的执行顺序是:filter2 -> filter1。 Filter
5. 接口。它是逻辑命名的Chain和Command集合。通过使用它,Command的调用者不需要了解具体实现Command的类名,只需要通过名字就可以获取所需要的Command实例。 Catalog
基本使用
1.         执行由顺序的命令组成的流程,假设这条流程包含1、2和3步。
þ        实现要执行的命令步骤:

public class Command1 implements Command {
    public boolean execute(Context arg0) throws Exception {
        System.out.println("Command1 is done!");
        return false;
    }
}
public class Command2 implements Command {
    public boolean execute(Context arg0) throws Exception {
        System.out.println("Command2 is done!");    
        return false;
    }
}
public class Command3 implements Command {
    public boolean execute(Context arg0) throws Exception {
        System.out.println("Command3 is done!");
        return true;
    }
}

 

 

 

þ        注册命令,创建执行的Chain:

public class CommandChain extends ChainBase {
    //增加命令的顺序也决定了执行命令的顺序
    public CommandChain(){
        addCommand( new Command1());
        addCommand( new Command2());
        addCommand( new Command3());
    }
   
    public static void main(String[] args) throws Exception{
        Command process = new CommandChain();
        Context ctx= new ContextBase();
        process.execute( ctx);
    }
}

 

 

 

2.         使用配置文件加载Command。除了在程序中注册命令之外,还可以使用配置文件来完成。
þ        对于例1,配置文件可以写成:

<?xml version="1.0" encoding="gb2312"?>
<catalog>
       <chain name="CommandChain">
        <!-- 定义的顺序决定执行的顺序 -->
              <command id="command1" className= "chain.Command1"/>
              <command id="command2" className= "chain.Command2"/>
              <command id="command3" className= "chain.Command3"/>
       </chain>

    <command name="command4" className="chain.Command1"/>

</catalog>

 

þ        装入配置文件的代码如下:

public class CatalogLoader {
    static final String cfgFile= "/chain/chain-cfg.xml";   
    public static void main(String[] args) throws Exception{
        CatalogLoader loader= new CatalogLoader();
        ConfigParser parser= new ConfigParser();
       
        parser.parse( loader.getClass().getResource( cfgFile));
        Catalog catalog= CatalogFactoryBase.getInstance().getCatalog();
        //加载Chain
        Command cmd= catalog.getCommand("CommandChain");
        Context ctx= new ContextBase();
        cmd.execute( ctx);
//加载Command
cmd= catalog.getCommand( "command4");
        cmd.execute( ctx);
    }
}

 

注意:使用配置文件的话,需要使用Commons Digester。而Digester则依赖:Commons Collections、Commons Logging和Commons BeanUtils。

3.         加载Catalog到web应用。为了在web应用中加载Catalog,需要在对应的web.xml中添加:

<context-param>

 <param-name>org.apache.commons.chain.CONFIG_CLASS_RESOURCE</param-name>

 <param-value>resources/catalog.xml</param-value>

</context-param>

<listener>

 <listener-class>org.apache.commons.chain.web.ChainListener</listener-class>

</listener>

 

缺省情况下,Catalog会被加载到Servlet Context中,对应的属性名字是“catalog”。因此获取Catalog:
Catalog catalog = (Catalog) request.getSession()

                            .getServletContext().getAttribute("catalog");

4.         的使用。Filter是一种特殊的Command,它除了execute方法会被执行之外,同时还会在Chain执行完毕之后(不论是正常结束还是异常结束)执行postprocess。因此,可以将它和Servlet中的Filter做类比:execute相当于处理前操作(相对下一个Command来说),postprocess相当于处理后操作。Filter的使用以及配置和Command完全一样,为了在Command1之前添加一个Filter: Filter
þ        定义Filter

public class Filter1 implements Filter {
    public boolean postprocess(Context arg0, Exception arg1) {
        System.out.println("Filter1 is after done!");
        return false;
    }
    public boolean execute(Context arg0) throws Exception {
        System.out.println("Filter1 is done!");
        return false;
    }
}

 

 

 

þ        修改配置文件,在上述的配置文件中的command1之前添加:
<command id="filter1" className= "chain.Filter1"/>
       Filter的还有一个常用的用法:对于异常的过滤。当Command抛出异常时,最终中会返回到最开始的调用处。有时期望不抛出这些异常,而在内部消化掉,那么就可以利用Filter。因为Commons Chain确保会调用已经执行了execute方法的Filter的postprocess方法,即使在出现异常时也是如此。因此,对应的postprocess方法可以写为:
       public boolean postprocess(Context arg0, Exception arg1) {
        //返回true,表示非空异常已被处理,无需再抛出。
        //否则,异常会被抛出
        if( null!= arg1) return true;
        else return false;
    }
5.         对于复杂的Chain,可能需要使用内嵌的Chain,内嵌Chain可以类比一个子过程。此时,可以使用LookupCommand。以例1为例,假设其中的command2需要扩展成为一个子过程,那么配置文件修改如下:

<?xml version="1.0" encoding="UTF-8"?>
<catalog>
       <chain name="CommandChain">
              <command id="command1" className= "chain.Command1"/>
              <command id="filter1" className= "chain.Filter1"/>

              <command

className="org.apache.commons.chain.generic.LookupCommand"

                     name="chain_command3"

                     optional="true"/>
              <command id="command2" className= "chain.Command2"/>
       </chain>

       <chain name="chain_command3">

              <command id="command3" className= "chain.Command3"/>

       </chain>
</catalog>

 

其中,optional如果设为true,那么如果没有找到对应的类时,程序不会抛出异常。此时,仿佛命令不存在一样。如果为false,那么在找不到对应的类时,会抛出异常。
6.         的使用。配置文件的引入,使得Commons Chain的灵活性大大的提高。在实际的使用过程中,存在着同一个Command被多个Chain使用的情形。如果每次都书写Command的类名,尤其是前面的包名特别长的情况下,是非常枯燥的。而<define>的作用就是为了解决这样的麻烦。通过定义Command和Chain的别名,来简化书写。例5的配置文件,可以书写成: <define>

<?xml version="1.0" encoding="gb2312"?>
<catalog>
    <!-- Command的别名,以后直接使用即可 -->
       <define name="command1" className="chain.Command1"/>
       <define name="command2" className="chain.Command2"/>
       <define name="command3" className="chain.Command3"/>
       <define name="filter1" className="chain.Filter1"/>
       <define name="lookupCommand"
                  className="org.apache.commons.chain.generic.LookupCommand"/>
      
       <chain name="CommandChain">
              <command1 id="1"/>
              <filter1 id="2"/>
              <lookupCommand name="chain_command3" optional="true"/>
              <command2 id="3"/>
       </chain>
      
       <chain name="chain_command3">
              <command3 id="3"/>
       </chain>
      
       <command1 name="command4"/>
</catalog>

 

 

 

总结
       Commons Chain实现了Chain of Responsebility和Command模式,其中的Catalog + 配置文件的方式使得调用方和Command的实现方的耦合度大大的降低,提高了灵活性。对于配置文件,通常可以:
-          作为Command的索引表,需要时按名字索引创建实例。
-          利用Chain以及内嵌Chain,完成一组连续任务和Command的复用,引入Filter可以获得与Servlet Filter一样的好处。

-          使用<define>定义别名,简化书写。

你可能感兴趣的:(Jakarta Commons Chain)