Inter-type declarations are challenging to support using an annotation style. For code style aspects compiled with the ajc compiler, the entire type system can be made aware of inter-type declarations (new supertypes, new methods, new fields) and the completeness and correctness of it can be guaranteed. Achieving this with an annotation style is hard because the source code may simply be compiled with javac where the type system cannot be influenced and what is compiled must be 'pure java'.
AspectJ 1.5.0 introduced @DeclareParents, an attempt to offer something like that which is achievable with code style declare parents and the other intertype declarations (fields, methods, constructors). However, it has proved too challenging to get close to the expressiveness and capabilities of code style in this area and effectively @DeclareParents is offering just a mixin strategy. The definition of mixin I am using here is that when some interface I is mixed into some target type T then this means that all the methods from I are created in T and their implementations are simple forwarding methods that call a delegate which that provides an implementation of I.
The next section covers @DeclareParents but AspectJ 1.6.4 introduces @DeclareMixin - an improved approach to defining a mixin and the choice of a different name for the annotation will hopefully alleviate some of the confusion about why @DeclareParents just doesn't offer the same semantics as the code style variant. Offering @DeclareMixin also gives code style developers a new tool for a simple mixin whereas previously they would have avoided @DeclareParents thinking what it could only do was already achievable with code style syntax.
The defaultImpl attribute of @DeclareParents may become deprecated if @DeclareMixin proves popular, leaving @DeclareParents purely as a way to introduce a marker interface.
Consider the following aspect:
public aspect MoodIndicator { public interface Moody {}; private Mood Moody.mood = Mood.HAPPY; public Mood Moody.getMood() { return mood; } declare parents : org.xyz..* implements Moody; before(Moody m) : execution(* *.*(..)) && this(m) { System.out.println("I'm feeling " + m.getMood()); } }
This declares an interface Moody , and then makes two inter-type declarations on the interface - a field that is private to the aspect, and a method that returns the mood. Within the body of the inter-type declared method getMoody , the type of this is Moody (the target type of the inter-type declaration).
Using the annotation style this aspect can be written:
@Aspect public class MoodIndicator { // this interface can be outside of the aspect public interface Moody { Mood getMood(); }; // this implementation can be outside of the aspect public static class MoodyImpl implements Moody { private Mood mood = Mood.HAPPY; public Mood getMood() { return mood; } } // the field type must be the introduced interface. It can't be a class. @DeclareParents(value="org.xzy..*",defaultImpl=MoodyImpl.class) private Moody implementedInterface; @Before("execution(* *.*(..)) && this(m)") void feelingMoody(Moody m) { System.out.println("I'm feeling " + m.getMood()); } }
This is very similar to the mixin mechanism supported by AspectWerkz. The effect of the @DeclareParents annotation is equivalent to a declare parents statement that all types matching the type pattern implement the given interface (in this case Moody). Each method declared in the interface is treated as an inter-type declaration. Note how this scheme operates within the constraints of Java type checking and ensures that this has access to the exact same set of members as in the code style example.
Note that it is illegal to use the @DeclareParents annotation on an aspect' field of a non-interface type. The interface type is the inter-type declaration contract that dictates which methods are declared on the target type.
// this type will be affected by the inter-type declaration as the type pattern matches package org.xyz; public class MoodTest { public void test() { // see here the cast to the introduced interface (required) Mood mood = ((Moody)this).getMood(); ... } }
The @DeclareParents annotation can also be used without specifying a defaultImpl value (for example, @DeclareParents("org.xyz..*")). This is equivalent to a declare parents ... implements clause, and does not make any inter-type declarations for default implementation of the interface methods.
Consider the following aspect:
public aspect SerializableMarker { declare parents : org.xyz..* implements Serializable; }
Using the annotation style this aspect can be written:
@Aspect public class SerializableMarker { @DeclareParents("org.xyz..*") Serializable implementedInterface; }
If the interface defines one or more operations, and these are not implemented by the target type, an error will be issued during weaving.
Consider the following aspect:
public aspect MoodIndicator { public interface Moody {}; private Mood Moody.mood = Mood.HAPPY; public Mood Moody.getMood() { return mood; } declare parents : org.xyz..* implements Moody; before(Moody m) : execution(* *.*(..)) && this(m) { System.out.println("I'm feeling " + m.getMood()); } }
This declares an interface Moody, and then makes two inter-type declarations on the interface - a field that is private to the aspect, and a method that returns the mood. Within the body of the inter-type declared method getMoody, the type of this is Moody (the target type of the inter-type declaration).
Using the annotation style this aspect can be written:
@Aspect public class MoodIndicator { // this interface can be outside of the aspect public interface Moody { Mood getMood(); }; // this implementation can be outside of the aspect public static class MoodyImpl implements Moody { private Mood mood = Mood.HAPPY; public Mood getMood() { return mood; } } // The DeclareMixin annotation is attached to a factory method that can return instances of the delegate // which offers an implementation of the mixin interface. The interface that is mixed in is the // return type of the method. @DeclareMixin("org.xyz..*") public static Moody createMoodyImplementation() { return new MoodyImpl(); } @Before("execution(* *.*(..)) && this(m)") void feelingMoody(Moody m) { System.out.println("I'm feeling " + m.getMood()); } }
Basically, the @DeclareMixin annotation is attached to a factory method. The factory method specifies the interface to mixin as its return type, and calling the method should create an instance of a delegate that implements the interface. This is the interface which will be delegated to from any target matching the specified type pattern.
Exploiting this syntax requires the user to obey the rules of pure Java. So references to any targeted type as if it were affected by the Mixin must be made through a cast, like this:
// this type will be affected by the inter-type declaration as the type pattern matches package org.xyz; public class MoodTest { public void test() { // see here the cast to the introduced interface (required) Mood mood = ((Moody)this).getMood(); ... } }
Sometimes the delegate instance may want to perform differently depending upon the type/instance for which it is behaving as a delegate. To support this it is possible for the factory method to specify a parameter. If it does, then when the factory method is called the parameter will be the object instance for which a delegate should be created:
@Aspect public class Foo { @DeclareMixin("org.xyz..*") public static SomeInterface createDelegate(Object instance) { return new SomeImplementation(instance); } }
It is also possible to make the factory method non-static - and in this case it can then exploit the local state in the surrounding aspect instance, but this is only supported for singleton aspects:
@Aspect public class Foo { public int maxLimit=35; @DeclareMixin("org.xyz..*") public SomeInterface createDelegate(Object instance) { return new SomeImplementation(instance,maxLimit); } }
Although the interface type is usually determined purely from the return type of the factory method, it can be specified in the annotation if necessary. In this example the return type of the method extends multiple other interfaces and only a couple of them (I and J) should be mixed into any matching targets:
// interfaces is an array of interface classes that should be mixed in @DeclareMixin(value="org.xyz..*",interfaces={I.class,J.class}) public static InterfaceExtendingLotsOfInterfaces createMoodyImplementation() { return new MoodyImpl(); }
There are clearly similarities between @DeclareMixin and @DeclareParents but @DeclareMixin is not pretending to offer more than a simple mixin strategy. The flexibility in being able to provide the factory method instead of requiring a no-arg constructor for the implementation also enables delegate instances to make decisions based upon the type for which they are the delegate.
Any annotations defined on the interface methods are also put upon the delegate forwarding methods created in the matched target type.