---- | |
Tapestry IoC Decorators | |
---- | |
Tapestry IoC Decorators | |
<Decoration> is the name of a popular design pattern. Using decoration, | |
an existing object's behavior can be extended without changing the implementation | |
of the object. | |
Instead, a new object is placed <around> the existing object. The rest of the world | |
sees this new object, termed an <<interceptor>>. The interceptor implements the | |
same interface as the underlying object being decorated. | |
A common example for this is the Java I/O library. The abstract InputStream | |
base class has a very simple API for reading bytes from a stream (and a few | |
other things). Subclasses of InputStream provide a wide array of other options | |
such as buffering, encryption or decryption, as well as control over the source | |
of data read by the stream. All of these <concerns> are encapsulated in different | |
implementations of InputStream, and all can be connected together in a kind of | |
pipline, using the common InputStream API. | |
Tapestry IoC uses a similar approach, where a one or more of interceptor objects, | |
all implementing the service interface, are strung together. The service's | |
proxy (responsible for just-in-time instantiation of the service implementation) | |
is at one end of this pipeline, the core service implementation is at the other. | |
For each method in the service interface, the interceptor object can perform | |
some operations before and after re-invoking the same method on the | |
core service implementation. This is another design pattern: <delegation>. An interceptor | |
can even catch exceptions thrown by the underlying implementation and react to them. | |
A sufficiently clever interceptor could retry a method if an exception is thrown, or | |
could "soften" a checked exception by wrapping it in a RuntimeException. | |
Decorators often are used in the context of <cross-cutting concerns>, such as logging or | |
transaction management. This approach is a kind of <aspect oriented design>. | |
One such cross cutting concern is lazy initialization of services. In HiveMind, services | |
are created only as needed, when a method of a service interface is first invoked. | |
This concern is supplied by the Tapestry IoC framework itself, but similar | |
concerns are easily implemented as decorations. | |
Whereas the popular AspectJ framework changes the compiled bytecode of your | |
classes (it calls the process "weaving"), | |
with Tapestry IoC, the approach is to wrap your existing classes in new objects. These | |
wrapper objects are often dynamically created at runtime. | |
It is also common to have <multiple> decorations on a single service. In this case, | |
a whole stack of interceptor objects will be created, each delegating to the next. | |
Tapestry IoC provides control over the order in which such decorations occur. | |
Decorations are driven by service decoration methods. Often, a reusable service | |
exists to do the grunt work of creating and instantiating a new class. | |
Service Decoration Methods | |
+---------------------+ | |
package org.example.myapp.services; | |
import org.apache.tapestry5.ioc.services.LoggingDecorator; | |
import org.slf4j.Logger; | |
public class MyAppModule | |
{ | |
public static Indexer build() | |
{ | |
return new IndexerImpl(); | |
} | |
public static <T> T decorateIndexer(Class<T> serviceInterface, T delegate, | |
String serviceId, Logger logger, | |
LoggingDecorator decorator) | |
{ | |
return decorator.build(serviceInterface, delegate, serviceId, logger); | |
} | |
} | |
+---------------------+ | |
The method decorateIndexer() is a service decorator method because it starts with the | |
word "decorate". In this simple case, only the myapp.Indexer service will be decorated, | |
even if there are other services in this module or others ... this is because | |
of the name match ("decorateIndexer" and "buildIndexer"), but we'll shortly see how | |
annotations can be used to target many services for decoration. | |
We are using the parameterized types here (the \<T\>), to reinforce the fact that the delegate object | |
passed in (which will be the core service implementation, or some other interceptor) | |
must implement the service interface, and that the decorator method must return an | |
instance of the service interface. | |
The values that may be provided to a decorator method are exactly the same as for a builder | |
method, with one addition: The underlying service will be passed in | |
as a parameter of type java.lang.Object (after type erasure, the <<<T delegate>>> parameter | |
becomes <<<Object delegate>>>). A decorator method must have one | |
parameter for this purpose. | |
In the above example, the decorator method recieves the core service implementation, | |
the service interface for the Indexer service, the Log for the Indexer service, | |
and an interceptor factory that generates logging interceptors. | |
The "heavy lifting" is provided by the factory, which will create a new interceptor | |
that logs method entry before delegating to the core service implementation. The interceptor | |
will also log method parameters, return values, and even log exceptions. | |
The return value of the method is the new interceptor. You may return null if your | |
decorator method decides not to decorate the supplied service. | |
Of course, nothing stops you from combining building with decorating inside | |
the service builder method: | |
+---------------------+ | |
package org.example.myapp.services; | |
import org.apache.tapestry5.ioc.services.LoggingDecorator; | |
import org.slf4j.Logger; | |
public class MyAppModule | |
{ | |
public static Indexer build(Class serviceInterface, Logger logger, | |
LoggingDecorator decorator) | |
{ | |
return decorator.build(serviceInterface, logger, new IndexerImpl()); | |
} | |
} | |
+---------------------+ | |
But as we'll see, its possible to have a single decorator method work on many different | |
services by using annotations. | |
Targetting Multiple Services | |
By using the | |
{{{../apidocs/org/apache/tapestry5/ioc/annotations/Match.html}@Match annnotation}}, | |
you may identify which services are to be decorated. | |
The value specified in the Match annotation is one or more patterns. These patterns | |
are used to match services. In a pattern, a "*" at the start or end of a string | |
match zero or more characters. | |
For example, to target all the services in your module: | |
+---------------------+ | |
@Match("*") | |
public static <T> T decorateLogging(Class<T> serviceInterface, T delegate, | |
String serviceId, Logger logger, | |
LoggingDecorator decorator) | |
{ | |
return decorator.build(serviceInterface, delegate, serviceId, logger); | |
} | |
+---------------------+ | |
You can use multiple patterns with @Match, in which case, the decorator will be applied | |
to a service that matches <any> of the patterns. For instance, if you only wanted | |
logging for your data access and business logic services, you might end up with | |
<<<@Match("Data*", "*Logic")>>> (based, of course, on how you name your services). | |
As the precending example showed, a simple "glob" matching is supported, where a asterisk ('*') | |
may be used at the start or end of the match string to match any number of characters. | |
As elsewhere, matching is case insensitive. | |
Thus, <<<@Match("*")>>> is dangerous, because it will match every service in every | |
module. | |
<Note: It is not possible to decorate the services of the TapestryIOCModule.> | |
<Note: Another idea will be other ways of matching services: base on inheritance of the | |
service interface and/or based on the presence of particular class annotations on the | |
service interface. None of this has been implemented yet, and can readily be accompllished | |
inside the decorator method (which will return null if it decides the service doesn't | |
need decoration).> | |
Ordering of Decorators | |
In cases where multiple decorators will apply to a single service, you can control | |
the order in which decorators are applied using an additional annotation: | |
{{{../apidocs/org/apache/tapestry5/ioc/annotations/Order.html}@Order}}. | |
This annotation allows any number of {{{order.html}ordering constraints}} | |
to be specified for the decorator, to order it relative to | |
any other decorators. | |
For example, you almost always want logging decorators to come first, so: | |
+---------------------+ | |
@Match("*") | |
@Order("before:*") | |
public static <T> T decorateLogging(Class<T> serviceInterface, T delegate, | |
String serviceId, Logger logger, | |
LoggingDecorator decorator) | |
{ | |
return decorator.build(serviceInterface, delegate, serviceId, logger); | |
} | |
+---------------------+ | |
"before:*" indicates that this decorator should come before any decorator in <any> module. | |
<<Note:>> the ordering of decorators is in terms of the <effect> desired. | |
Internally, the decorators are invoked | |
last to first (since each once receives the "next" interceptor as its delegate). | |
So the core service implementation is created (via a service builder method) | |
and that is passed to the last decorator method. The interceptor created there | |
is passed to the the next-to-last decorator method, and so forth. | |
Creating your own Decorators | |
Decorators are a limited form of Aspect Oriented Programming, so we have | |
borrowed some of that terminology here. | |
A decorator exists to create an <interceptor>. The interceptor wraps around | |
the service (because these interceptors can get chained, we talk about the "delegate" and not the "service"). | |
Each method of the interceptor will take <advice>. Advice | |
is provided by a {{{../apidocs/org/apache/tapestry5/ioc/MethodAdvice.html}MethodAdvice}} instance. | |
The sole method, <<<advise()>>>, recieves an | |
{{{../apidocs/org/apache/tapestry5/ioc/Invocation.html}Invocation}}. | |
MethodAdvice gives you a chance to see what the method invocation <is>; you can query | |
the name of the method, and the types and values of the parameters. | |
The MethodAdvice can override the parameters if necessary, then invoke <<<proceed()>>>. This | |
call invokes the corresponding method on the original object, the delegate. | |
If the method call throws a runtime exception, that exception is not caught. Your method advice can | |
put a try ... catch block around the call to proceed() if interested in catching runtime exception. | |
Checked exceptions are not thrown (since they are not part of the proceed() method's signature). Instead | |
the invocation's <<<isFail()>>> method will return true. You can then retrieve the exception or override it. | |
In the normal success case, you can ask for the return value and even override it before | |
returning from the advise() method. | |
In other words, you have total control. Your MethodAdvise can query or change parameters, decide whether | |
it proceed into the original code, it can intercept exceptions that are thrown and replace them, and can | |
query or even replace the return value. | |
The | |
{{{../apidocs/org/apache/tapestry5/ioc/services/AspectDecorator.html}AspectDecorator}} service | |
is how you put your MethodAdvise into action. | |
By way of an example, we'll show an implementation of the LoggingDecorator service: | |
+----+ | |
public class LoggingDecoratorImpl implements LoggingDecorator | |
{ | |
private final AspectDecorator aspectDecorator; | |
private final ExceptionTracker exceptionTracker; | |
public LoggingDecoratorImpl(AspectDecorator aspectDecorator, ExceptionTracker exceptionTracker) | |
{ | |
this.aspectDecorator = aspectDecorator; | |
this.exceptionTracker = exceptionTracker; | |
} | |
public <T> T build(Class<T> serviceInterface, T delegate, String serviceId, final Logger logger) | |
{ | |
final ServiceLogger serviceLogger = new ServiceLogger(logger, exceptionTracker); | |
MethodAdvice advice = new MethodAdvice() | |
{ | |
public void advise(Invocation invocation) | |
{ | |
boolean debug = logger.isDebugEnabled(); | |
if (debug) serviceLogger.entry(invocation); | |
try | |
{ | |
invocation.proceed(); | |
} | |
catch (RuntimeException ex) | |
{ | |
if (debug) serviceLogger.fail(invocation, ex); | |
throw ex; | |
} | |
if (!debug) return; | |
if (invocation.isFail()) | |
{ | |
Exception thrown = invocation.getThrown(Exception.class); | |
serviceLogger.fail(invocation, thrown); | |
return; | |
} | |
serviceLogger.exit(invocation); | |
} | |
}; | |
return aspectDecorator.build(serviceInterface, delegate, advice, | |
String.format("<Logging interceptor for %s(%s)>", serviceId, | |
serviceInterface.getName())); | |
} | |
} | |
+---+ | |
<The actual code has been refactored slightly since this documentation was written.> | |
Most of the logging logic occurs inside the ServiceLogger object, the MethodAdvice exists to call the right methods at | |
the right time. A Logger doesn't <change> parameter values (or thrown exceptions, or the result), it just | |
captures and logs the data. | |
Notice that for runtime exceptions, we catch the exception, log it, and rethrow it. | |
For checked exceptions, we use isFail() and getThrown(). | |
The AspectDecorator service can also be used in more complicated ways: it is possible to | |
only advise some of the methods and not others, or use different advice for different methods. Check the | |
JavaDoc for more details. | |