Custom action bean annotations are a powerful feature that are used in a few places in Strecks. Action bean annotations allow you to extend the contract between the action bean and the action controller without changing the interface which the action bean and the action controller use to communicate with each other. A good example is in allowing different forms of navigation to be used with the same controller.
Adding a custom action bean annotations is a bit more complex than adding other
forms of annotations, such as validation, data binding and dependency injection.
The best way to see how it works is to show an existing working example.
The example we use for this is the @DispatchMethod
annotation, which by the
LookupDispatchActionController
implementations when implementing Streck's version of
LookupDispatchAction
.
If you are not familiar with LookupDispatchAction
, its purpose is to allow individual buttons
in a multi-button form to be mapped to methods within an action class.
The basic mechanisms behind custom action bean annotations are as follows:
All of this may sound somewhat abstract, so our concrete example should help to clarify.
We mentioned that the action bean identifies its collaborating action controller. The code below shows an
example usage of the @DispatchMethod
annotation.
@Controller(name = BasicLookupDispatchController.class) public class ExampleBasicLookupSubmitAction implements BasicSubmitAction { public void preBind() { } public String execute() { //code omitted return "success"; } @DispatchMethod(key = "button.add") public String insert() { //code omitted return "success"; } @DispatchMethod(key = "button.delete") public String delete() { //code omitted return "success"; } public String cancel() { //code omitted return "success"; } }
The @DispatchMethod
is used to denote the resource key which will be mapped to the annotated
method. In Struts's LookupDispatchAction
, this role is performed by a implementing a method
returning a Map
containing this mapping.
Notice how ExampleBasicLookupSubmitAction
identifies
BasicLookupDispatchController
as the action controller.
ActionBeanAnnotationReader
represents
functionality for extracting some information from annotations contained within an action bean, and
defines two methods:
public interface ActionBeanAnnotationReader<T extends Object> { public boolean readAnnotations(Class actionBeanClass); public void populateController(T controller); }
The job of the first method is to read the annotations in of an action bean Class
instance, while
populateController()
is the method used to transfer the extracted information to the controller.
The class which implements the logic for extracting message key to method name mappings from the
@DispatchMethod
annotation is DispatchMethodLookupReader
, which is shown in essence below:
public class DispatchMethodLookupReader implements ActionBeanAnnotationReader<LookupDispatchActionController> { private Map<String, String> keyMethodMap = new HashMap<String, String>(); public boolean readAnnotations(Class actionBeanClass) { Method[] methods = actionBeanClass.getMethods(); boolean found = false; for (Method method : methods) { DispatchMethod annotation = method.getAnnotation(DispatchMethod.class); if (annotation != null) { String key = annotation.key(); String methodName = method.getName(); keyMethodMap.put(key, methodName); found = true; } } return found; } public void populateController(LookupDispatchActionController controller) { controller.setKeyMethodMap(Collections.unmodifiableMap(keyMethodMap)); } }
The implementation is pretty straightforward - readAnnotations()
simply builds the key to method map
which in plain Struts would be returned via a concrete method implementation.
The return value of readAnnotations()
is used to indicate whether the annotation reader found the
annotations it was looking for. populateController()
is then used to pass the keyMethodMap
on
to the controller, which clearly needs to be implement the LookupDispatchActionController
interface.
Our action controller has two requirements. It needs some way of configuring which action bean annotation readers
should be used to inspect the collaborating action bean's annotations,
and also must have a way to receive the information extracted.
To these ends, there are two important things to notice about the implementation of
BasicLookupDispatchController
,
shown below:
@ActionInterface(name = BasicSubmitAction.class) @ReadDispatchLookups public class BasicLookupDispatchController extends BasicDispatchController implements LookupDispatchActionController { private Map<String, String> keyMethodMap; public void setKeyMethodMap(Map<String, String> keyMethodMap) { this.keyMethodMap = keyMethodMap; } @Override protected ViewAdapter executeAction(Object actionBean, ActionContext context) { //implementation omitted } //rest of class omitted }
First, the controller action needs to implement an interface which the action bean annotation reader
can use to configure the controller action. As we have seen,
the interface is LookupDispatchActionController
, which
has the following form:
public interface LookupDispatchActionController extends ControllerAction { void setKeyMethodMap(Map<String, String> keyMethodMap); }
The purpose of this interface is to allow the action controller to be
populated with the message key to method name mapping
read using the @DispatchMethod
annotations.
Second, the action controller needs a way of letting Strecks know which
ActionBeanAnnotationReaders
should scan for annotations.
The mechanism is very similar to the Annotation Factory pattern used for identifying
data binding and conversion annotations.
It is applied through the use of @ReadDispatchLookups
to annotate the action controller class definition.
The implementation of @ReadDispatchLookups
is shown below:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @BeanAnnotationReader(DispatchMethodLookupReader.class) public @interface ReadDispatchLookups { }
The key is in the use of the @BeanAnnotationReader
annotation.
The role of this annotation is simply to define an
ActionBeanAnnotationReader
implementation to use to extract action bean
metadata. In other words, by using the @ReadDispatchLookups
in our
action controller, we are saying "use the class @DispatchMethodLookupReader to find annotations
and extract information from the action bean, passing this on to the action controller instance".
The example we have used so far is quite straightforward in that the ActionBeanAnnotationReader
simply looks for occurrences of a single annotation. More advanced functionality can be
supported by applying the Annotation Factory Pattern to the ActionBeanAnnotationReader
implementation. Here, the ActionBeanAnnotationReader
is not looking for a particular annotation,
but for annotations which themselves are annotated with a factory annotation, that is,
an annotation which can be used to identify a factory class which understands how to process
the annotation. This mechanism is applied to support pluggable navigation, as well as supporting
action bean source configuration (which allows, for example, action beans to be retrieved
as Spring beans).
For action bean source configuration, the ActionBeanAnnotationReader
implementation is class is BeanSourceAnnotationReader
. The essence of this
class's readAnnotations()
method is shown below:
public boolean readAnnotations(Class actionClass) { Annotation[] annotations = actionClass.getAnnotations(); boolean found = false; for (Annotation annotation : annotations) { Class<? extends Annotation> annotationType = annotation.annotationType(); BeanSourceReaderClass beanSourceReaderClass = annotationType.getAnnotation(BeanSourceReaderClass.class); if (beanSourceReaderClass != null) { BeanSourceReader beanSourceReader = ReflectHelper.createInstance(beanSourceReaderClass.value(), BeanSourceReader.class); beanSource = beanSourceReader.readBeanSource(actionClass, annotation); found = true; } } return found; } }
Notice how BeanSourceAnnotationReader
is not reading the annotations itself, but instead
delegates this task to a BeanSourceReader
instance.
Each BeanSourceReader
implementation would look
for a particular annotation or set of annotations. For example, Spring-managed action beans are
configured using the class SpringBeanSourceReader
, shown below:
public class SpringBeanSourceReader implements BeanSourceReader { public ActionBeanSource readBeanSource(Class actionClass, Annotation annotation) { Assert.notNull(actionClass); Assert.notNull(annotation); SpringBean actionBean = (SpringBean) annotation; return new SpringActionBeanSource(actionClass, actionBean.name()); } }
Pluggable navigation works in a similar way.
A navigation annotation uses a NavigationReader
instance
to find particular navigation-related annotations.
For example, @NavigateForward
annotations use a
are read using an instance of NavigateReader
,
which implements NavigationReader
.
We can see this is the case by looking at the implementation of
the @NavigateForward
annotation itself.
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @NavigationInfo(value = NavigateReader.class) public @interface NavigateForward { Class<? extends NavigationMarker> handler() default DefaultNavigationHandlerFactory.class; }
Here, @NavigationInfo
is the factory annotation
used to identify the NavigationReader
implemention class
used to read the annotation.