JBoss ESB Action Annotations

JBossESB 4.9 introduces a new set of Action Annotations that will hopefully help make it easier to create clean ESB Action implementations.  We want to hide the complexities of implementing interfaces and abstract classes, as well as dealing with the ConfigTree type.

 

We'll just list the Annotations first and then discuss how they're used:

 

  1. @Process
  2. @ConfigProperty
  3. @Initialize
  4. @Destroy
  5. @BodyParam
  6. @PropertyParam
  7. @AttachmentParam
  8. @OnSucess
  9. @OnException

 

 

@Process

The most basic form of Action implementation involves creating a simple POJO with a single method, annotated with the @Process annotation as follows:

 

public class MyLogAction {

    @Process
    public void log(Message message) {
        // log the message...
    }
}

 

The @Process annotation is what Identifies the class as being a valid ESB Action.  In the case where there are multiple methods in the class, it also identifies the method to be used for processing the message instance (or some part of the message - more on this later with the @BodyParam, @PropertyParam and @AttachmentParam).

 

Configuring an instance of this Action into a Service pipeline is the same as with the low/base level Actions implementations i.e. those that extend AbstractActionPipelineProcessor or implement ActionLifecycle (or one of its other sub-types or abstract implementations):

 

<service .....>
    <actions>
        <action name="logger" class="com.acme.actions.MyLogAction" />   
    </actions>
</service>

 

In cases where the Action implementation has multiple methods annotated with the @Process annotation, you simply use the process attribute to specify which of the annotated methods is to be used for processing the Message instance (as before):

 

<service .....>
    <actions>
        <action name="logger" class="com.acme.actions.MyLogAction" process="log" />   
    </actions>
</service>

 

@Process Method Return Values

@Process methods can be implemented to return:

 

  • void: No return value - as with our logger action implementation.
  • Message: An ESB Message instance.  This message instance then becomes the active/current instance on action pipeline.
  • Some other type.  If the method does not return an ESB Message instance, the returned object instance is set into the active/current ESB Message instance on the action pipeline.  Where it is set on the Message depends on the "set-payload-location" <action> configuration property, which default according to the normal MessagePayloadProxy rules.

 

@Process Method Parameters

@Process methods can specify parameters in a range of different ways. You can:

 

  1. Specify the ESB Message instance as a parameter to the method.
  2. Specify one or more arbitrary parameter types.  The ESB framework will "hunt" (searching the Message Body, then Properties, then Attachments) for data of that type in the active/current pipeline Message instance and pass this data as the values for those parameters (or null if not found).

 

An example of option #1 was shown above in the logger action.  An example of option #2 might be something like:

 

public class OrderPersister {

    @Process
    public OrderAck storeOrder(OrderHeader orderHeader, OrderItems orderItems) {
        // process the order parameters and return an ack...
    }
}

 

So in the above example, the @Process method is obviously relying on a previous action (in the action pipeline) creating OrderHeader and OrderItems object instances and attaching them to the active/current ESB Message instance.  Perhaps a more realistic implementation of such a use case would be to have a generic action implementation that decodes an XML/EDI/whatever payload to an Order instance, which it returns.  Then to have the OrderPersister take an Order instance as it's sole parameter e.g.

 

public class OrderDecoder {

    @Process
    public Order decodeOrder(String orderXML) {
        // decode the order XML to an ORder instance... 
    }
}

public class OrderPersister {

    @Process
    public OrderAck storeOrder(Order order) {
        // persist the order and return an ack...
    }
}

 

And then chain the 2 actions together in the ESB serice configuration:

 

<actions>
    <action name="decode" class="com.acme.orders.OrderDecoder" />
    <action name="persist" class="com.acme.orders.OrderPersister" />
</actions>

 

Option #2 (above) can make your code read a little more clearly (less annotation noise), but it carries the obvious risks in that the process of runtime "hunting" through the ESB Message for the appropriate parameter values is not 100% deterministic.  For that reason, we support the @BodyParam, @PropertyParam and @AttachmentParam method property annotations.

 

@BodyParam, @PropertyParam and @AttachmentParam

These @Process method parameter annotations allow you to be explicit in terms of where in the ESB Message you wish to get an individual parameter value for the @Process method.

 

As the names suggest, each of these annotations allow you to specify a named location (in the ESB Message Body, Properties and Attachments) for a specific @Process method parameter:

 

public class OrderPersister {

 

    @Process

    public OrderAck storeOrder(@BodyParam("order-header") OrderHeader orderHeader, @BodyParam("order-items") OrderItems orderItems) {
        // process the order parameters and return an ack...
    }
}

 

If the specified ESB Message location does not contain a value, then a null value will be passed for this parameter value and the @Process method instance can decide how to handle this.  If the specified ESB Message location contains a value of the wrong type, a MessageDeliverException exception is thrown.

 

Should probably also support injecting parameter values from the Context?

 

@ConfigProperty

Most actions will require some user configuration.  In the ESB action configuration, the action configuration properties are supplied as <property> sub-elements of the <action> element:

 

<action name="logger" class="com.acme.actions.MyLogAction">
    <property name="logFile" value="logs/my-log.log" />
    <property name="logLevel" value="DEBUG" />
</action>

 

Getting these configuration properties into your action using the low/base level Action implementations (extend AbstractActionPipelineProcessor or implement ActionLifecycle etc) involves working with the ConfigTree class, which is supplied to the Action via it's constructor.  The action implementor is forced to:

 

  1. Define a constructor on the Action class that supplies the ConfigTree instance.
  2. Get all the relevant action configuration properties from the ConfigTree instance.
  3. Check for mandatory action properties, raising exceptions where they are not specified on the <action> configuration.
  4. Decode all property values from Strings (as supplied on the ConfigTree) to their appropriate types as used by the action implementation e.g. java.lang.String -> java.io.File, java.lang.String -> boolean, java.lang.String -> long etc.
  5. Raise exceptions where the configured value is not decodable to the target property type.
  6. Implement unit tests on all the different configuration possibilities to ensure the above tasks were completed properly.

 

So, while the above tasks are not too difficult for the action implementor, they are a bit of a pain to implement.  It can be quite laborious, error prone and lead to inconsistencies across actions wrt how configuration errors are handled.  Then of course... there can be quite a bit of code involved with just handling action configuration, which can be quite noisy and sitracting wrt what the action implementation is really about.

 

With the annotated action, we take care of most of the above through the @ConfigProperty annotation.  Expanding the MyLogActions action implementation, which has 2 mandatory configuration properties of logFile and logLevel (as outlined in the above config example):

 

public class MyLogAction {

    @ConfigProperty
    private File logFile;

    @ConfigProperty
    private LogLevel logLevel;
    
    public static enum LogLevel {
        DEBUG, 
        INFO, 
        WARN
    }

    @Process
    public void log(Message message) {
        // log the message at the configured log level...
    }
}

 

Note: The @ConfigProperty annotation can also be defined on setter methods (Vs on the Field).

 

And that's it!  When the ESB deploys the action, it looks into the action implementation and maps in the decoded value (including support for enums, as with the LogLevel enum above) of the action properties/fields that are annotated with the @ConfigProperty annotation.  No need to deal with the ConfigTree class at all.  No need for any noisy code.

 

So by default, all class Fields annotated with the @ConfigProperty annotation are mandatory.  Non-mandatory Fields are handled in one of two ways:

 

  1. By specifying use = Use.OPTIONAL on the @ConfigProperty annotation on the Field.
  2. By specifying a defaultVal on the @ConfigProperty annotation on the Field.  Specifying a defaultVal implies it is OPTIONAL.

 

So, if we wanted to make the properties on our log action non-mandatory, we could implement the action as:

 

public class MyLogAction {

    @ConfigProperty(defaultVal = "logs/my-log.log")
    private File logFile;

    @ConfigProperty(use = Use.OPTIONAL)
    private LogLevel logLevel;
    
    public static enum LogLevel {
        DEBUG, 
        INFO, 
        WARN
    }

    @Process
    public void log(Message message) {
        // log the message...
    }
}

 

The @ConfigProperty annotation also supports two additional fields that may not be used too often:

 

  • name: The name field on the @ConfigProperty annotation allows you to explicitly specify the name of the action configuration property to be used to populate that Field of the action instance.
  • choice: The choice field on the @ConfigProperty annotation allows you  to constrain the possible allowed configuration values for that Field.  For most cases, the same can be achieved using an enumeration type (as with the LogLevel enumeration type).  We possibly could drop this and let users just use enums, but I think I like having it... gut feeling is that enums might not always work... but not sure why I think that

 

An example of when the name field might be used is when migrating an old action implementation (that uses the low/base level  implementation type) to use the newer annotation based implementation, and the old config name for a property (which you cannot change for backward compatibility reasons) does not map to a valid Java Field name.  Taking our log action as an example, supposing the old configuration for the log action was as follows:

 

<action ...>
    <property name="log-file" value="logs/my-log.log" />
    <property name="log-level" value="DEBUG" />
</action>

 

The property names here do not map to valid Java Field names, so in this case, we need to specify the name on the @ConfigProperty annotation, as follows:

 

public class MyLogAction {

    @ConfigProperty(name = "log-file")
    private File logFile;

    @ConfigProperty(name = "log-level")
    private LogLevel logLevel;
    
    public static enum LogLevel {
        DEBUG, 
        INFO, 
        WARN
    }

    @Process
    public void log(Message message) {
        // log the message...
    }
}

 

Decoding Property Values

At the moment, the decoding of the property values is handled using the Smooks DataDecoder mechanisms.  This is well hidden from the user, but we should remove this dependency and replicate the same behavior inside the ESB.

 

@Initialize and @Destroy

Sometimes action implementations need to perform some initialization at deploy time, as well as some cleanup/uninitialization when undeploying.  For this, we have the @Initialize and @Destroy method annotations.

 

At deploy time, our logging action would probably want to perform some checks (e.g. file exists, directory exists etc), as well as some initialization (e.g. open the log file for writing etc).  Then at undeploy, it may need to perform some uninitialization tasks (e.g. close the file):

 

public class MyLogAction {

    @ConfigProperty
    private File logFile;

    @ConfigProperty
    private LogLevel logLevel;
    
    public static enum LogLevel {
        DEBUG, 
        INFO, 
        WARN
    }
    
    @Initialize
    public void initializeLogger() {
        // Check if file already exists… check if parent folder exists etc...
        // Open the file for writing...
    }
    
    @Destroy
    public void cleanupLogger() {
        // Close the file...
    }

    @Process
    public void log(Message message) {
        // log the message...
    }
}

 

Note:  All @ConfigProperty annotations have been processed by the time the @Initialize methods are invoked by the ESB deployer.  Therefore, the @Initialize methods can rely on these fields being initialized before execution of the custom initialization specified in the @Initialize method.

 

Note: You do not need specify methods using both these annotations.  You only need to specify what is needed i.e. if your method only needs initialization, you only need a method annotated with the @Initialize annotation i.e. you don't need to specify a "matching" method annotated with the @Destroy annotation.

 

Note:  If you want, you can specify a single method and annotate it with both @Initialize and @Destroy.

 

Note: @Initialize methods can optionally specify a ConfigTree parameter, if they want to have access to the actions underlying ConfigTree instance.

 

@OnSuccess and @OnException

These method annotations allow you to specify methods to be executed on a successful/failed execution of the action pipeline in which the action is configured.

 

public class OrderPersister {

    @Process
    public OrderAck storeOrder(Order order) {
        // persist the order and return an ack...
    }
    
    @OnSuccess
    public void logOrderPersisted(Message message) {
        // log it...
    }
    
    @OnException
    public void manualRollback(Message message, Throwable theError) {
        // manually rollback...
    }
}

 

Note:  In the case of both of these annotations, the parameters passed to the annotated methods are optional.  You can supply none, some or all of the parameters shown above.  The ESB framework resolves the relevant parameters in both cases.

 

Backward Compatibility

This functionality does not have a Backward Compatibility impact in any way.  All actions, implemented using the existing low/base level action implementation mechanisms, will work in exactly the same way.  In fact, this Annotated Actions functionality is built on top of these existing low/base level mechanism.

 

Tooling

The JBoss ESB Tools in JBossTools/JBDS will now need to offer 2 options when the user selects to create a new action implementation:

 

  1. As AbstractActionPipelineProcessor implementation.
  2. As annotated POJO.

 

This could be offered as a radio button on the wizard (ala the "JUnit 3" or "JUnit 4" radio buttons on the existing JUnit test case wizard).  Default selection should be the "As annotated POJO" option.