A Look at Commons Chain, Part 1

发布时间:2014-10-25 2:19:37

As developers, we are often required to apply object-oriented approaches to systems that are procedural in nature. Business analysts and managers illustrate such systems using flowcharts and workflow diagrams instead of class hierarchies and sequence diagrams. Object orientation, however, brings flexibility into the mix when applied to solving these problems. Object-oriented design patterns, such as the Template Method [GoF] and Chain of Responsibility [GoF], provide useful structures and behaviors for representing sequential processing.   The Jakarta Commons subproject Chain codifies and combines these patterns into a reusable Java framework for representing sequential process flows. This framework, developed under the community umbrella of the Jakarta Commons project, has been quietly gaining acceptance and usage in a number of interesting applications, most notably as the underlying mechanism for handling HTTP request processing in the Struts and Shale web application frameworks. You can use Commons Chain in situations where you need to define and execute a sequential set of steps. With respect to the classic design patterns, developers and architects commonly apply the Template Method pattern for modeling sequential processing. With the Template Method, an abstract parent class defines the algorithm used: the steps of the process. It's up to the subclasses to provide the implementation. Alternatively, the parent class can be a concrete class that provides a default implementation of the algorithm's methods. Because the Template Method relies on inheritance--subclasses must inherit from the algorithm-defining parent class--software that uses this pattern tends to exhibit tight coupling and less flexibility. Because concrete classes must extend the parent class to add custom behavior, you limit the flexibility of the design--you are locked into the class hierarchy. Commons Chain solves this problem by allowing for the algorithm to be defined through a configuration file interpreted at runtime. To see how Commons Chain works, let's start with a somewhat contrived example: the business process employed by purveyors of pre-owned vehicles (a.k.a., used car salespeople). Here are the steps that compose the sales process: Get customer information Test-drive vehicle Negotiate sale Arrange financing Close sale Now suppose that you wanted to model this flow using the Template Method pattern. You could create an abstract class--defining the algorithm--that looks something like this: 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(); } Now let's see how you could implement this process using Commons Chain. First, download Commons Chain. You can grab the latest nightly download as a .zip or .tar file, or you can acquire the most up-to- date code by checking out the Commons Chain module from the CVS or SubVersion source repositories. Extract the archive, placing the commons-chain.jar file on your classpath. To implement the business process using Commons Chain, implement each step in the process as a class that has a single public "do it all" method named execute(). This is a traditional usage of the Command pattern. Here's a simple implementation of the "Get customer information" step. 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; } } For illustration purposes, this class doesn't do much. However, it does store the customer's name in the Context. The Context object provides the glue between commands. For the time being, think of the Context as nothing more than a hash table that you can stuff values into, and pull values out of, by key. All subsequent commands can now access this data. The TestDriveVehicle, NegotiateSale, and ArrangeFinancing command classes are simple implementations that simply print out what the command would do. 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; } } The CloseSale implementation uses the context to extract the customer's name, set in the GetCustomerInfo command. 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; } } Now you can define the process as a sequence or "chain of commands." 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); } } Running the main method results in the following output: Get customer info Test drive the vehicle Negotiate sale Arrange financing Congratulations George Burdell, you bought a new car! Before going further, let's take a look at the classes and interfaces of Commons Chain that we used.Figure 1.The relationship between the Command and Chain classes exemplifies the Composite pattern [GoF]; a chain is not only composed of commands, but is itself a command. This allows you to easily replace single commands with entire sub-chains. The method defined by the Command object's single operation represents a straightforward command: public boolean execute(Context context); The context is nothing more than a collection of name-value pairs. The Context interface serves as a marker interface: it extends java.util.Map but does not add any additional behavior. The ContextBase class, on the other hand, not only provides the Map implementation, but it also adds a characteristic known as attribute-property transparency. This characteristic allows you to access JavaBean properties, defined with traditional getFoo and setFoo methods, using the put and get methods defined by the Map interface. Values stored using a JavaBean "setter" method can be retrieved, by property name, using the Map's get method. Conversely, values stored using the Map's put method can be retrieved using the JavaBean "getter" method. For our example, we can create a specialized context providing explicit support for the customerName property. 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; } } Now you can use, with equal interoperability, the generic attributes of the Map along with the explicit JavaBean accessor and mutator methods. But first, you need instantiate the SellVehiceContext instead of ContextBase when you run the SellVehicleChain: public static void main(String[] args) throws Exception { Command process = new SellVehicleChain(); Context ctx = new SellVehicleContext(); process.execute(ctx); } Though you didn't change how GetCustomerInfo stores the customer name--it still uses ctx.put("customerName", "George Burdell")--you can get the customer's name in the CloseSale class using the getCustomerName() method. public boolean execute(Context ctx) throws Exception { SellVehicleContext myCtx = (SellVehicleContext) ctx; System.out.println("Congratulations " + myCtx.getCustomerName() + ", you bought a new car!"); return false; } Those commands that rely on type safety and explicit properties of the context can utilize the traditional property getter and setter methods. As new commands are added, they can be written without regard to the specific context implementation, provided that the properties are accessed through the standard Map get and put methods. Regardless of which mechanism is used, the ContextBase class ensures that the commands can interoperate through the shared context. This example shows how you can use the Commons Chain API to create and execute a sequence of commands. Of course, like almost every new piece of software written in Java these days, Commons Chain can be configured via an XML file. Applying this capability to the "sell vehicle" process, you can now define the sequence of commands in an XML file. The canonical name for this file is chain-config.xml. <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> The Chain configuration file can contain multiple chain definitions grouped together into catalogs. For this example, the chain definition is defined within the default catalog. You can, in fact, have multiple named catalogs within this file, each with its own set of chains. Now, instead of defining the sequence of commands as was done in the SellVehicleChain, you load the catalog and retrieve the named chain using classes provided by Commons Chain. 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 uses the Commons Digester to read and parse the configuration file. To use this capability, you will need to add the Commons Digester .jar file to your classpath. I used version 1.6 and had no problems. Digester depends on Commons Collections (I used version 3.1), Commons Logging (version 1.0.4), and Commons BeanUtils 1.7.0. You will need to add these .jars to your classpath, as well. After adding these .jar files to my classpath, the CatalogLoader successfully compiled and ran. The output is exactly like that generated by the other two tests.         Now that you can define the chain in an XML file, and retrieve the chain, which is itself a command, the possibilities for extension and flexibility are limitless. Suppose that the "Arrange financing" process was actually handled by a completely separate department of the business. This department wants to set up its own workflow for that part of the sale. Chain supports this concept through the use of nested chains. Since a chain is a command, you can replace a single use of a command with a reference to another chain. Here's the chain configuration with this new flow added to the mix: <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 provides the general-use command LookupCommand for discovering and executing another chain. The optional attribute controls how LookupCommand handles the case when the nested chain is not found in the specified catalog. If optional=true, then the process continues, even if the chain to execute cannot be found. Otherwise, LookupCommand throws an IllegalArgumentException, indicating that the command could not be found. There are three ways of ending a command chain: A command returns true from its execute method. The end of the chain is reached. A command throws an exception. A command should return true if the chain has completely handled the process. This notion is the basis of the Chain of Responsibility. Processing is handed off from command to command, until a command handles the command. If no process returns true, before reaching the end of the command sequence, the chain is assumed to have completed normally. A chain ends abnormally when any exception is thrown by a command. With Commons Chain, if a command throws an exception, the chain is broken. The exception, be it a runtime exception or application exception, will bubble up to the original caller of the chain. But many applications need explicit exception handling that is defined external to any commands. Commons Chain provides a facility for this using the Filter interface. Filter extends Command, adding a postprocess method. public boolean postprocess(Context context, Exception exception); Commons Chain guarantees that the postprocess method will be called if the Filter's execute method is called, regardless of any thrown exceptions. Like servlet filters, Chain Filters are executed in the order that they appear in the command sequence. Likewise, each Filter's postprocess method is called in reverse order. You can use this feature of Chain to implement exception handlers. Here's a Filter that traps exceptions that may occur in the sample chain. 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; } } You define the Filter in the chain configuration file just as you would a normal Command. <chain name="sell-vehicle"> <command id="ExceptionHandler" className = "com.jadecove.chain.sample.SellVehicleExceptionHandler"/> <command id="GetCustomerInfo" className="com.jadecove.chain.sample.GetCustomerInfo"/> The Filter's execute method is called in sequence. However, the postprocess method is not called until the chain reaches its end or a command throws an exception. If an exception was raised, then the postprocess method handles the exception and returns true, indicating that the exception was handled. The chain does terminate at this point, but the exception is essentially caught and does not propagate further. If the postprocess method returns false, the exception bubbles up, causing abnormal termination of the chain. Let's suppose that the ArrangeFinancing command threw an exception because a customer had bad credit. The SellVehicleExceptionHandler would catch that exception, resulting in output like the following: Filter.execute() called. Get customer info Test drive the vehicle Negotiate sale Exception Bad credit occurred. By combining techniques like filtering and sub-chains, you can model some fairly complicated workflows. Commons Chain is a promising framework that is currently undergoing active development. It's still very new and features are being added quite frequently. In the next article on Commons Chain, we'll take a look at how it's being used in Struts 1.3. Struts 1.3 completely replaces its existing HTTP-request-processor class with one driven by Commons Chain. If you have ever had to write a custom Struts request processor, you can appreciate the flexibility that Commons Chain brings to the table.