Static crosscutting

Advice declarations change the behavior of classes they crosscut, but do not change their static type structure. For crosscutting concerns that do operate over the static structure of type hierarchies, AspectJ provides inter-type member declarations and other declare forms.

Inter-type member declarations

AspectJ allows the declaration of members by aspects that are associated with other types.

An inter-type method declaration looks like

  • [ Modifiers ] Type OnType . Id(Formals) [ ThrowsClause ] { Body }
  • abstract [ Modifiers ] Type OnType . Id(Formals) [ ThrowsClause ] ;

The effect of such a declaration is to make OnType support the new method. Even if OnType is an interface. Even if the method is neither public nor abstract. So the following is legal AspectJ code:

  interface Iface {}

  aspect A {
      private void Iface.m() {
	  System.err.println("I'm a private method on an interface");
      }
      void worksOnI(Iface iface) {
	  // calling a private method on an interface
	  iface.m();
      }
  }

An inter-type constructor declaration looks like

  • [ Modifiers ] OnType . new ( Formals ) [ ThrowsClause ] { Body }

The effect of such a declaration is to make OnType support the new constructor. It is an error for OnType to be an interface.

Inter-type declared constructors cannot be used to assign a value to a final variable declared in OnType. This limitation significantly increases the ability to both understand and compile the OnType class and the declaring aspect separately.

Note that in the Java language, classes that define no constructors have an implicit no-argument constructor that just calls super(). This means that attempting to declare a no-argument inter-type constructor on such a class may result in a conflict, even though it looks like no constructor is defined.

An inter-type field declaration looks like one of

  • [ Modifiers ] Type OnType . Id = Expression;
  • [ Modifiers ] Type OnType . Id;

The effect of such a declaration is to make OnType support the new field. Even if OnType is an interface. Even if the field is neither public, nor static, nor final.

The initializer, if any, of an inter-type field declaration runs before the class-local initializers defined in its target class.

Any occurrence of the identifier this in the body of an inter-type constructor or method declaration, or in the initializer of an inter-type field declaration, refers to the OnType object rather than to the aspect type; it is an error to access this in such a position from a static inter-type member declaration.

Access modifiers

Inter-type member declarations may be public or private, or have default (package-protected) visibility. AspectJ does not provide protected inter-type members.

The access modifier applies in relation to the aspect, not in relation to the target type. So a private inter-type member is visible only from code that is defined within the declaring aspect. A default-visibility inter-type member is visible only from code that is defined within the declaring aspect's package.

Note that a declaring a private inter-type method (which AspectJ supports) is very different from inserting a private method declaration into another class. The former allows access only from the declaring aspect, while the latter would allow access only from the target type. Java serialization, for example, uses the presense of a private method void writeObject(ObjectOutputStream) for the implementation of java.io.Serializable. A private inter-type declaration of that method would not fulfill this requirement, since it would be private to the aspect, not private to the target type.

The access modifier of abstract inter-type methods has one constraint: It is illegal to declare an abstract non-public inter-type method on a public interface. This is illegal because it would say that a public interface has a constraint that only non-public implementors must fulfill. This would not be compatible with Java's type system.

Conflicts

Inter-type declarations raise the possibility of conflicts among locally declared members and inter-type members. For example, assuming otherPackage is not the package containing the aspect A, the code

  aspect A {
      private Registry otherPackage.onType.r;
      public void otherPackage.onType.register(Registry r) {
	    r.register(this);
	    this.r = r;
      }
  }

declares that onType in otherPackage has a field r. This field, however, is only accessible from the code inside of aspect A. The aspect also declares that onType has a method "register", but makes this method accessible from everywhere.

If onType already defines a private or package-protected field "r", there is no conflict: The aspect cannot see such a field, and no code in otherPackage can see the inter-type "r".

If onType defines a public field "r", there is a conflict: The expression

  this.r = r

is an error, since it is ambiguous whether the private inter-type "r" or the public locally-defined "r" should be used.

If onType defines a method "register(Registry)" there is a conflict, since it would be ambiguous to any code that could see such a defined method which "register(Registry)" method was applicable.

Conflicts are resolved as much as possible as per Java's conflict resolution rules:

  • A subclass can inherit multiple fields from its superclasses, all with the same name and type. However, it is an error to have an ambiguous reference to a field.
  • A subclass can only inherit multiple methods with the same name and argument types from its superclasses if only zero or one of them is concrete (i.e., all but one is abstract, or all are abstract).

Given a potential conflict between inter-type member declarations in different aspects, if one aspect has precedence over the other its declaration will take effect without any conflict notice from compiler. This is true both when the precedence is declared explicitly with declare precedence as well as when when sub-aspects implicitly have precedence over their super-aspect.

Extension and Implementation

An aspect may change the inheritance hierarchy of a system by changing the superclass of a type or adding a superinterface onto a type, with the declare parents form.

  • declare parents: TypePattern extends Type;
  • declare parents: TypePattern implements TypeList;

For example, if an aspect wished to make a particular class runnable, it might define appropriate inter-type void run() method, but it should also declare that the class fulfills the Runnable interface. In order to implement the methods in the Runnable interface, the inter-type run() method must be public:

  aspect A {
      declare parents: SomeClass implements Runnable;
      public void SomeClass.run() { ... }
  }

Interfaces with members

Through the use of inter-type members, interfaces may now carry (non-public-static-final) fields and (non-public-abstract) methods that classes can inherit. Conflicts may occur from ambiguously inheriting members from a superclass and multiple superinterfaces.

Because interfaces may carry non-static initializers, each interface behaves as if it has a zero-argument constructor containing its initializers. The order of super-interface instantiation is observable. We fix this order with the following properties: A supertype is initialized before a subtype, initialized code runs only once, and the initializers for a type's superclass are run before the initializers for its superinterfaces. Consider the following hierarchy where {Object, C, D, E} are classes, {M, N, O, P, Q} are interfaces.

    Object  M   O
	 \ / \ /
	  C   N   Q
	   \ /   /
	    D   P
	     \ /
	      E

when a new E is instantiated, the initializers run in this order:

    Object M C O N D Q P E

Warnings and Errors

An aspect may specify that a particular join point should never be reached.

  • declare error: Pointcut: String;
  • declare warning: Pointcut: String;

If the compiler determines that a join point in Pointcut could possibly be reached, then it will signal either an error or warning, as declared, using the String for its message.

Softened exceptions

An aspect may specify that a particular kind of exception, if thrown at a join point, should bypass Java's usual static exception checking system and instead be thrown as a org.aspectj.lang.SoftException, which is subtype of RuntimeException and thus does not need to be declared.

  • declare soft: Type: Pointcut;

For example, the aspect

  aspect A {
      declare soft: Exception: execution(void main(String[] args));
  }

Would, at the execution join point, catch any Exception and rethrow a org.aspectj.lang.SoftException containing original exception.

This is similar to what the following advice would do

  aspect A {
      void around() execution(void main(String[] args)) {
	  try { proceed(); }
	  catch (Exception e) {
	      throw new org.aspectj.lang.SoftException(e);
	  }
      }
  }

except, in addition to wrapping the exception, it also affects Java's static exception checking mechanism.

Like advice, the declare soft form has no effect in an abstract aspect that is not extended by a concreate aspect. So the following code will not compile unless it is compiled with an extending concrete aspect:

  abstract aspect A {
    abstract pointcut softeningPC();

    before() : softeningPC() {     
      Class.forName("FooClass"); // error:  uncaught ClassNotFoundException
    }    
                                                      
    declare soft : ClassNotFoundException : call(* Class.*(..));
  }

Advice Precedence

An aspect may declare a precedence relationship between concrete aspects with the declare precedence form:

  • declare precedence : TypePatternList ;

This signifies that if any join point has advice from two concrete aspects matched by some pattern in TypePatternList, then the precedence of the advice will be the order of in the list.

In TypePatternList, the wildcard "*" can appear at most once, and it means "any type not matched by any other pattern in the list".

For example, the constraints that (1) aspects that have Security as part of their name should have precedence over all other aspects, and (2) the Logging aspect (and any aspect that extends it) should have precedence over all non-security aspects, can be expressed by:

  declare precedence: *..*Security*, Logging+, *;

For another example, the CountEntry aspect might want to count the entry to methods in the current package accepting a Type object as its first argument. However, it should count all entries, even those that the aspect DisallowNulls causes to throw exceptions. This can be accomplished by stating that CountEntry has precedence over DisallowNulls. This declaration could be in either aspect, or in another, ordering aspect:

  aspect Ordering {
      declare precedence: CountEntry, DisallowNulls;
  }
  aspect DisallowNulls {
      pointcut allTypeMethods(Type obj): call(* *(..)) && args(obj, ..);
      before(Type obj):  allTypeMethods(obj) {
	  if (obj == null) throw new RuntimeException();
      }
  }
  aspect CountEntry {
      pointcut allTypeMethods(Type obj): call(* *(..)) && args(obj, ..);
      static int count = 0;
      before():  allTypeMethods(Type) {
	  count++;
      }
  }

Various cycles

It is an error for any aspect to be matched by more than one TypePattern in a single decare precedence, so:

  declare precedence:  A, B, A ;  // error

However, multiple declare precedence forms may legally have this kind of circularity. For example, each of these declare precedence is perfectly legal:

  declare precedence: B, A;
  declare precedence: A, B;

And a system in which both constraints are active may also be legal, so long as advice from A and B don't share a join point. So this is an idiom that can be used to enforce that A and B are strongly independent.

Applies to concrete aspects

Consider the following library aspects:

  abstract aspect Logging {
      abstract pointcut logged();

      before(): logged() {
          System.err.println("thisJoinPoint: " + thisJoinPoint);
      }
  }

  abstract aspect MyProfiling {
      abstract pointcut profiled();

      Object around(): profiled() {
          long beforeTime = System.currentTimeMillis();
          try {
              return proceed();
          } finally {
              long afterTime = System.currentTimeMillis();
              addToProfile(thisJoinPointStaticPart,
                           afterTime - beforeTime);
          }
      }
      abstract void addToProfile(
          org.aspectj.JoinPoint.StaticPart jp,
          long elapsed);
  }

In order to use either aspect, they must be extended with concrete aspects, say, MyLogging and MyProfiling. Because advice only applies from concrete aspects, the declare precedence form only matters when declaring precedence with concrete aspects. So

  declare precedence: Logging, Profiling;

has no effect, but both

  declare precedence: MyLogging, MyProfiling;
  declare precedence: Logging+, Profiling+;

are meaningful.

Statically determinable pointcuts

Pointcuts that appear inside of declare forms have certain restrictions. Like other pointcuts, these pick out join points, but they do so in a way that is statically determinable.

Consequently, such pointcuts may not include, directly or indirectly (through user-defined pointcut declarations) pointcuts that discriminate based on dynamic (runtime) context. Therefore, such pointcuts may not be defined in terms of

  • cflow
  • cflowbelow
  • this
  • target
  • args
  • if

all of which can discriminate on runtime information.