Jonas Bonér bio photo

Jonas Bonér

Present.
Entrepreneur.
Hacker.
Public Speaker.
Powder Skier.
Perpetual Learner.
Jazz Fanatic.
Wannabee Musician.

Twitter LinkedIn Github

Introduction

This article is introducing HyperBeans, a proposal for a framework for Symmetric Aspect-Oriented Software Development in plain Java (i.e. no extensions to Java is being used). These ideas are based on the work I have done in AspectWerkz and in the JVM support for AOP in JRockit and are loosely based on the work on Classpects by Kevin Sullivan and Hridesh Rajan.

If you don't care about the research and background behind the ideas presented in this article (and want to see some code right away), then you should skip the rest of this section.

Aspect-Oriented Software Development (AOSD) in general, tries, among other things, to address the problem defined as the tyranny of dominant decomposition , in which the prominent concerns, usually the business logic, dominate the class design, especially the inheritance hierarchy (as observed in this article).

Historically there has been (and still is) two different main philosophies in AOSD, the asymmetric philosophy and the symmetric philosophy. A detailed discussion about their semantic and philosophical differences is out of the scope for this article (read this article for a detailed discussion).

Juri Memmert has written a good introductory article on the subject in which he defines the different philosophies like this:

The asymmetric philosophy

An asymmetric approach to AOSD relies on the notion that there is a base body of code that is then augmented with aspects. There is a concept ional difference (and an implementation difference) between the base and the aspects so that an aspect can not serve as the base of another composition. Most asymmetric approaches use language extensions to declare aspects as first class entities.

The symmetric philosophy

A symmetric approach to AOSD is based on the notion that all concerns in a system are created equal and, in the asymmetric terminology, can serve as both aspect and base in different compositions. Most symmetric approaches use programming languages as-is, although there are approaches that rely on language extensions as well.

The leading implementation for asymmetric AOSD is AspectJ, while the most well-known symmetric implementation is Hyper/J (now part of the Concern Manipulation Environment (CME) project).

Asymmetric AOSD has so far been the dominant way of implementing AOSD in Java, and in the enterprise space at large. There has been, and still is, a lot of debate in the research community around whether the symmetric or the asymmetric philosophy is the best way of implementing AOSD and many papers have been written on the subject (see the reference section at the end).

When implementing the AspectWerkz AOP framework (which now has been merged with AspectJ), I have been involved in efforts trying to stretch the boundaries of asymmetric AOSD and even though I believe that the asymmetric approach is the preferable in many cases, it definitely has some shortcomings. For example, asymmetric AOSD addresses the problem of 'the tyranny of dominant decomposition' by allowing us to modularize all concerns, but the dominant concern, using aspects. Since the dominant concern needs to serve as "base", the decision deciding which concern should be seen as the dominant one still needs to be taken, and this is a decision that should not be taken lightly since it is something that is usually very hard to change.

When taking a look at how a symmetric approach handles the above given problem, I find the symmetric solution very natural and appealing (at least conceptually). In which, if using Hyper/J's terminology, a system is seen as a multi-dimensional hyperspace, which is built up using different hyperslices (dimensions). A hyperslice is the abstraction of a concern, which can be built up by many components. This gives you the possibility of viewing the hyperspace (your system) differently in regards to different hyperslices (concerns); you have the possibility of looking at the system from the point of view (concern) that you currently want.

I do not want to take any position in regards to if symmetric AOSD is a (or will be a) serious contender to asymmetric AOSD. I simply have not used the symmetric approach to solve real world problems enough yet, but I do believe that it is something worth exploring more. So, let's take a look at the proposal.

HyperBeans – multi-dimensional POJOs

There is no the distinction between classes and aspects, e.g. no need for one concern to serve as "base", all there is is HyperBeans. HyperBeans are regular Plain Old Java Objects (POJOs), they are instantiated with new and you work with them just as with regular classes, no configuration file or similar is needed.

Similarly, there is no the distinction between methods and advice, there are just regular methods, which can be invoked explicitly (like regular methods) or implicitly (when a join point is being executed - like a regular advice).

Deployment and undeployment

HyperBeans are instantiated using the new keyword, which will call the constructor and instantiate the HyperBean just like any other POJO. However, the constructors can also control deployment of the HyperBean.

Deployment of a HyperBean means that its "blessed" methods (advice) are weaved in, while undeployment means that the woven code is removed.

You control deployment of a HyperBean by annotating (one or many of) its constructors with the @Deploy annotation. This example shows how to deploy a HyperBean as a singleton (more on other instantiation models later):

public class POJO {

    @Deploy
    public POJO() {
    }

    ... // remaining methods omitted
}

Since there is no such thing as 'destructors' in Java, we are handling undeployment of HyperBeans by annotating a regular Java method (static or member) with the @Undeploy annotation. This means that when such a method is invoked, the HyperBean will be undeployed. It also give us the possibility of doing additional clean up (closing resources etc.) if necessary:

public class POJO {

    @Undeploy
    public void close() {
        // clean up - close resources etc.
    }

    ... // remaining methods omitted
}

Instantiation models

You can control instantiation model (also called the deployment scope) of the HyperBean by adding a special argument to the constructor. This special argument defines the deployment scope for the HyperBean and needs to be annotated with the @Scope annotation. What this means is that the HyperBean will be deployed with the scope defined by the type of the argument.

For example, adding the argument @Scope Thread scope to a constructor, (which is annotated with the @Deploy annotation), will deploy the HyperBean with the scope of this particular thread.

Here are all the currently supported instantiation models:

Type annotated with @Scope Instantiation model
No argument with a @Scope annotation Singleton
Thread Per Thread
Class Per Type
Instance (but not of type Class or Thread) Per Instance

Here are some examples:
public class POJO {

    /** Singleton deployment */
    @Deploy
    public POJO() {
    }

    /** Per Type deployment */
    @Deploy
    public POJO(@Scope Class scope) {
    }

    /** Per Instance deployment */
    @Deploy
    public POJO(@Scope SomeType scope) {
    }

    /** Per Thread deployment */
    @Deploy
    public POJO(@Scope Thread scope) {
    }

    ... // remaining methods omitted
}

The current semantics for the @Scope annotation is that it:

  • Narrows the scope for the matching. For example using the Per Type instantiation model will add an implicit && within(scope) to the pointcuts defined in the HyperBean (same as in AspectJ).
  • Controls the life cycle. The HyperBean will have the same life cycle as the instance annotated with the @Scope annotation.
  • Controls state management. Tied to the life cycle.

Control flow management - no more cflow

There is no more any need for the cflow pointcut (as defined by AspectJ). Instead we suggest a simple programmatic model for control flow management of HyperBean deployment. I believe that the same functionality can be reached by a combination of the Per Thread instantiation model and the deployment/undeployment API. This approach has similarities with the work done in CaesarJ and its concept of a deploy {..} block. Here is an example:

You have a HyperBean that can be deployed on a per-thread basis:

class POJO {
    @Deploy
    public POJO(@Scope Thread scope) {
    }

    @Undeploy
    public void close() {
    }

    ... // remaining methods omitted
}

Then you can use the following idiom to achieve fine grained control flow (cflow) management:

POJO hyperBean;
try {
    hyperBean = new POJO(Thread.currentThread()); // deploys the HyperBean to the current control flow ONLY

    ... // do stuff - the HyperBean is woven into this control flow ONLY

} finally {
    hyperBean.close(); // undeploy the HyperBean from the control flow
}

If you don't want to have this be tangled with your application logic, you can of course put the block in an advice, (and have its HyperBean be a singleton and instantiated somewhere else):

@Around(..)
public Object cflowAdvice(InvocationContext ctx) {
    POJO hyperBean;
    Object result;
    try {
        hyperBean = new POJO(Thread.currentThread());
        result = ctx.proceed();
    } finally {
         hyperBean.close();
    }
    return result;
}

Cross-cutting methods - advice

So how do we define these "blessed" methods? They are defined using Java 5 annotations, in a similar fashion to AspectWerkz's and AspectJ 5's annotation style of development, but mixed with the API defined by the JVM support for AOP in JRockit (as described in this article ).

Let's take a look at an example:

public class POJO {

    @Before("call(Foo.new(..))")
    public void doSomething() {
        ... // do something before an instance of Foo is created
    }

    ... // remaining methods omitted
}

This is a regular POJO with one "blessed" method that performs some action before an instance of Foo is created. There's really nothing special with this class apart from that one of its method is annotated with a special annotation borrowed from the annotation style syntax in AspectJ. Now let's take a look at a (slightly) more interesting example, one that makes use of contextual information.

public class POJO {

    @Before("call(@test.TraceMe * *.*(..))")
    public static void traceBefore(@CalleeMethod Method callee) {
        System.out.println("--> " + callee.toString());
    }

    @After("call(@test.TraceMe * *.*(..))")
    public static void traceAfter(@CalleeMethod Method callee) {
        System.out.println("< -- " + callee.toString());
    }

    ... // remaining methods omitted
}

This is a regular POJO with two different "blessed" methods (that are tracing invocations to all methods that are annotated with the @Trace annotation). Contextual information (like caller and callee method, caller and callee instance, parameters, return value etc.) is retrieved using annotated parameters borrowed from the JRockit API for AOP .

Here is a list of the current set of defined annotations and their meaning:

Annotation Exposes
@CalleeMethod The callee method (method, constructor, static initializer)
@CallerMethod The caller method (method, constructor, static initializer)
@Callee The callee instance
@Caller The caller instance
@Arguments The invocation arguments (wrapped in an object array)
@Returning The return value (used in 'after returning advice')
@Throwing The exception thrown from a method (used in 'after throwing advice')

These annotated arguments (except caller and callee method) also works as 'filters', the same is true for all unannotated arguments which are treated as the regular arguments to the method (or the field value about to be set etc.). The methods can be either member or static (dependent on if you want to keep state in the instance or not).

Since some people might be curious how "around advice" (i.e. 'interception') is handled, below is an example of how to implement the same functionality as in the class above using an "around advice". Here we introduce the InvocationContext abstraction (similar to the ProceedingJoinPoint abstraction in AspectJ), which serves as the context on which we can invoke the proceed() method in order to continue with the execution flow.

public class POJO {

    @Around("call(@test.TraceMe * *.*(..))")
    public Object trace(InvocationContext ctx,
                        @CalleeMethod Method callee) {
        System.out.println("--> " + callee.toString());
        Object result = ctx.proceed();
        System.out.println("< -- " + callee.toString());
        return result;
    }

    ... // remaining methods omitted
}

Implementation

I have prototyped HyperBeans using the JVM support for AOP in JRockit. The implementation was actually very simple and shows the power of the JRockit API (the JRockit prototype is available now for those who wants to try it out).