Staccatissimo-Instrument is the annotation-oriented instrumentation tool of the Staccatissimo project, built on top of Javassist. It allows compile-time annotation processing and class instrumentation in an event-handling fashion.
Staccatissimo-Instrument is a tool for processing annotations with instrumentation capabilities. It consist of an instrumenter that runs on compiled classes directory with a extremly simple lifecycle:
The instrumenter must be run programatically, so you will need a main-like class to do experiment with it. Doing that is quite simple, just need to invoke net.sf.staccatocommons.instrument.InstrumentationRunner.runInstrumentation(InstrumenterConfigurer, Directory, String). For example:
InstrumentationRunner.runInstrumentation(new InstrumenterConfigurer() { public void configureInstrumenter(InstrumenterConfiguration instrumenter) { instrumenter .addAnnotationHanlder(handler1) .addAnnotationHanlder(handler2) .setInstrumentationMark(mark); } }, new Directory("target/classes"), "");
As you can see, the instrumenter itself is completely hidden, but you can register annotation handlers and an "instrumentation mark" using an InstrumenterConfigurer. In this example you can already see most of the elements of the Staccatissimo-instrument API: handlers, instrumentation marks and configurers.
As seen before, the configurer is just a callback for registering custom components into the instrumenter, exposed as an InstrumenterConfiguration argument.
Handlers contain the concrete logic of annotation processing. There are four specific handler interfaces, one for each supported annotation element: ArgumentAnnotationHandler, ClassAnnotationHandler, ConstructorAnnotationHandler and MethodAnnotationHandler. In addition to specific methods defined by each interface - see each interface javadoc for more details - all handlers need to override the method Class<? extends Annotation> AnnotationHandler.getSupportedAnnotationType(), which allows the instrumenter to relate the proper handler o handlers with each annotation - there is no problem in registering more than one handler for the same.
AnnotationHandler's methods for performing concrete processing take two arguments: the annotation to process and a context, which represents the surrounding the annotation discovered by the instrumenter, which gives access to Javassits API. There are four specific AnnotationContexts which correspond to each AnnotationHandler: MethodAnnotationContext, ConstructorAnnotationContext, ArgumentAnnotationContext and ClassAnnotationContext.
The complete code can be found here
Lets suppose that you have an Account, whose balance can be augmented by depositing money, or reseted to zero. Both operations must be logged. Also, when depositing, some preconditions must be checked: the amount to deposit must be not null and >= 0. A näive implementation will look like:
public class Account { private static final Logger logger = ...; ... public void deposit(BigDecimal amount) { if (amount == null) { String message = "amount must not be null"; logger.severe(message); throw new IllegalArgumentException(message); } if (amount.compareTo(BigDecimal.ZERO) < 0) { String message = "amount must be positive"; logger.severe(message); throw new IllegalArgumentException(message); } balance = balance.add(amount); logger.warning("deposit"); } public void resetBalance() { balance = BigDecimal.ZERO; logger.warning("resetBalance"); } ... }
The previous code has several flaws. That which is of particular interest here is that the logic of "whenever methods like resetBalance and deposit are evaluated, log the invocation" and "when a non negative constraint is violated, throw an exception" are harcoded and not encapsulated properly.
Using Staccatissimo-instrument we can create some annotations that encapsulate that behavior, and document it, by free. So lets define two annotations: Loggeable, and NonNegativeDecimal:
@Documented @Retention(RetentionPolicy.CLASS) @Target(ElementType.METHOD) public @interface Loggeable {} @Documented @Retention(RetentionPolicy.CLASS) @Target(ElementType.PARAMETER) public @interface NonNegativeDecimal { String value(); }
The second step is to define the annotation processors. A very simple implementation would look like the following:
public class LogHandler implements MethodAnnotationHandler<Loggeable> { public Class<Loggeable> getSupportedAnnotationType() { return Loggeable.class; } public void preProcessAnnotatedMethod(Loggeable annotation, MethodAnnotationContext context) throws CannotCompileException { if (!hasLogger(context)) { CtClass declaringClass = context.getMethod().getDeclaringClass(); CtField field = CtField.make( "private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(\"" + declaringClass.getName() + "\");", declaringClass); declaringClass.addField(field); } } public void postProcessAnnotatedMethod(Loggeable annotation, MethodAnnotationContext context) throws CannotCompileException { context.getMethod().insertAfter("logger.warning(\"" + context.getMethod().getName() + "\");"); context.getMethod().addCatch("logger.severe($e.getMessage());throw $e;", getThrowable(context)); } .... }
Finally, we need to annotate the methods and arguments with the new annotations:
public class Account2 { .... @Loggeable public void deposit(@NonNull @NonNegativeDecimal("amount") BigDecimal amount) { balance = balance.add(amount); } @Loggeable public void resetBalance() { balance = BigDecimal.ZERO; } ... }
Now it is easy to see that the resulting code is much more clean and simple than the original.
In order to run the instrumenter and test the Account2 code, use the following junit4 snippet:
@org.junit.BeforeClass public static void before() throws Exception { InstrumentationRunner.runInstrumentation(new InstrumenterConfigurer() { public void configureInstrumenter(InstrumenterConfiguration instrumenter) { instrumenter // .addAnnotationHanlder(new LogHandler()) .addAnnotationHanlder(new NonNegativeDecimalHandler()) .setInstrumentationMark(new SimpleInstrumentationMark("log-example", "instrumented-1.0")); } }, new Directory("target/classes"), ""); } @org.junit.Test(expected = IllegalArgumentException.class) public void testNegativeAmmount() throws Exception { Account2 account2 = new Account2(BigDecimal.valueOf(10)); account2.resetBalance(); account2.deposit(BigDecimal.valueOf(-10)); }
The test should pass, as in the third line a negative decimal is passed. Also, you should gete a log output similar to the following:
02/01/2011 23:09:10 net.sf.staccatocommons.instrument.examples.Account2 resetBalance WARNING: resetBalance 02/01/2011 23:09:10 net.sf.staccatocommons.instrument.examples.Account2 deposit SEVERE: amount must be positive
Perhaps the reader has already notice that the idea behind this example is the separation of crosscutting concerns, and is probably saying: "That is not new! It is nothing I couldn't have achieved using AOP!". And that is true, the final result is quite similar. However, Staccatissimo-instrument is by no means a replacement of your favorite Aspects framework or language extension, because: