14.12. Using Annotations
These are the annotations that you can add to your action classes:
- @Process
- The simplest implementation involves creating an action with a a basic plain old Java object (POJO) with a single method, annotated with @Process:
public class MyLogAction { @Process public void log(Message message) { // log the message... } }
The @Process annotation serves to identify the class as a valid ESBaction
. In cases in which there are multiple methods in the class, it also identifies the method which is to be used for processing the message instance (or some part of the message. This is explained in more depth when the @BodyParam, @PropertyParam and @AttachmentParam annotations are discussed.)To configure an instance of thisaction
on apipeline
, use the same process as that for low/base levelaction
implementations (these being those that extendAbstractActionPipelineProcessor
or implementActionLifecycle
or one of its other sub-types or abstract implementations):<service .....> <actions> <action name="logger" class="com.acme.actions.MyLogAction" /> </actions> </service>
In cases in which multiple methods annotated with @Process are associated with theaction
implementation, use the process attribute to specify which of them is to be used for processing the message instance:<service .....> <actions> <action name="logger" class="com.acme.actions.MyLogAction" process="log" /> </actions> </service>
@Process methods can be implemented to return:- void. This means there will be no return value, as with the logger action implementation above.
- message: This is a message instance. This becomes the active/current instance on the action pipeline.
- another type. If the method does not return a message instance, the object instance that is returned will be set on the current message instance on the action pipeline.. Where you should set it on the message depends on the set-payload-location<action> configuration property, which default according to the normal
MessagePayloadProxy
rules.
Use @Process methods to specify parameters in a range of different ways. You can:- specify the message instance as a method parameter.
- specify one or more arbitrary parameter types. The Enterprise Service Bus framework will search for data of that type in the active/current pipeline message instance. Firstly, it will search the message body, then properties, then attachments and pass this data as the values for those parameters (or
null
if not found).
An example of the first option was depicted above in the logger action. Here is an example of the second option:public class OrderPersister { @Process public OrderAck storeOrder(OrderHeader orderHeader, OrderItems orderItems) { // process the order parameters and return an ack... } }
In this example, the @Process method is relying on a previous action in thepipeline
to create theOrderHeader
andOrderItem
object instances and attach them to the current message. (Perhaps a more realistic implementation would have a generic action implementation that decodes an XML or EDI payload to an order instance, which it would then returns. TheOrderPersister
would then take an order instance as its sole parameter.) Here is an example: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... } }
Chain the two actions together in the service configuration:<actions> <action name="decode" class="com.acme.orders.OrderDecoder" /> <action name="persist" class="com.acme.orders.OrderPersister" /> </actions>
The code is easier to read in Option #2 because there are less annotations, but it carries a risk because the process of run-time "hunting" through the message for the appropriate parameter values is not completely deterministic. Due to this, Red Hat supports the @BodyParam, @PropertyParam and @AttachmentParam annotations.Use these @Process method parameter annotations to explicitly define from where in the message an individual parameter value for the @Process method is to be retrieved. As their names suggest, each of these annotations allow you to specify a named location (in the message body, properties or attachments) for a specific 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 message location specified does not contain a value, then null will be passed for this parameter (the @Process method instance can decide how to handle this). If, on the other hand, the specified location contains a value of the wrong type, aMessageDeliverException
will be thrown. - @ConfigProperty
- Most actions require some degree of custom configuration. In the ESB action configuration, the 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>
To utilise these properties, use the low/base level action implementations (do so by extendingAbstractActionPipelineProcessor
or by implementingActionLifecycle
). This involves working with theConfigTree
class, (which is supplied to the action via its constructor). In order to implement an action, follow these steps:- Define a constructor on the action class that supplies the
ConfigTree
instance. - Obtain all of the relevant action configuration properties from the
ConfigTree
instance. - Check for mandatory action properties and raise exceptions in those places where they are not specified on the <action> configuration.
- Decode all property values from strings (as supplied on the
ConfigTree
) to their appropriate types as used by the action implementation. For example, decidejava.lang.String
tojava.io.File
,java.lang.String
to Boolean,java.lang.String
to long and so forth. - Raise exceptions at those places where the configured value cannot be decoded to the target property type.
- Implement unit tests on all the different configuration possibilities to ensure that the tasks listed above were completed properly.
While the tasks above are generally straightforward, they can be laborious, error-prone and lead to inconsistencies across actions with regard to how configuration mistakes are handled. You may also be required to add a lot of code resulting in additional confusion.The annotated action addresses these problems via @ConfigProperty. Expand the MyLogActions implementation, which has two mandatory configuration properties: logFile and logLevel: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
You can also define the @ConfigProperty annotation on "setter" methods (instead of on the field).That is all that needs to be done. When the Enterprise Service Bus deploys the action, it examines both the implementation and the maps found within the decoded value (including any support for enums, as with the LogLevel enum above). It finds the action fields possessing the @ConfigProperty annotation. The developer is not required to deal with theConfigTree
class at all or develop any extra code.By default, every class field possessing the @ConfigProperty annotation is mandatory. Non-mandatory fields are handled in one of these two ways:- by specifying
use = Use.OPTIONAL
on the field's @ConfigProperty annotation. - by specifying a defaultVal on the field's @ConfigProperty annotation. (This is optional.)
To make the log action's properties optional only, implement the action like this: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 supports two additional fields:- name: use this to explicitly specify the name of the action configuration property to be used to populate the field of that name on the action instance.
- choice: use this field to constrain the configuration values allowed for itself. This can also be achieved using an enumeration type (as with the LogLevel).
The name field might be used in various situations such as when migrating an old action (that uses the low/base level implementation type) to the newer annotation-based implementation, only to find that the old configuration name for a property (which cannot be changed for backward-compatibility reasons) does not map to a valid Java field name. Taking the log action as an example, imagine that this was the old configuration for the log action:<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 specify the name on the @ConfigProperty annotation: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... } }
The bean configuration's property values are decoded from their string values. To then match them against the appropriate POJO bean property type, these simple rules are used:- If the property type has a single-argument string constructor, use that.
- If it is a "primitive," use its object type's single-argument string constructor. For example, if it is an int, use the Integer object.
- If it is an enum, use
Enum.valueOf
to convert the configuration string to its enumeration value.
- @Initialize and @Destroy
- Sometimes action implementations need to perform initialization tasks at deployment time. They may also need to perform a clean-up whilst being undeployed. For these reasons, there are @Initialize and @Destroy method annotations.Here are some examples. At the time of deployment, the logging action may need to perform some checks (that, for example, files and directories exist). It may also perform some initialization tasks (such as opening the log file for writing). When it is undeployed, the action may need to perform some clean-up tasks (such as closing the file). Here is the code to perform these tasks:
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 of the @ConfigProperty annotations will have been processed by the time the ESB deployer invokes the @Initialize methods. Therefore, the @Initialize methods can rely on these fields being ready before they execute the customised initialization.Note
There is no need to always use both of these annotations to specify methods. Only specify them if there is a need; in other words, if a method only needs initialization, only use the @Initialize annotation (You do not have to supply a "matching" method annotated with the @Destroy annotation).Note
It is possible to specify a single method and annotate it with both @Initialize and @Destroy.Note
You can optionally specify aConfigTree
parameter on @Initialize methods. Do this to have access to the actions which underlie theConfigTree
instance. - @OnSuccess and @OnException
- Use these annotations to specify those methods to be executed on a successful or failed execution, respectively, of that
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... } }
In the cases of both of these annotations, the parameters passed to the methods are optional. You can supply none, some or all of the parameters shown above. The enterprise service bus' framework resolves the relevant parameters in each case.