Emulate Annotation Inheritance for Interfaces and Methods with Aspectj

Emulate annotation inheritance for interfaces and methods with AspectJ

The problem here is not AspectJ but the JVM. In Java, annotations on

  • interfaces,
  • methods or
  • other annotations

are never inherited by

  • implementing classes,
  • overriding methods or
  • classes using annotated annotations.

Annotation inheritance only works from classes to subclasses, but only if the annotation type used in the superclass bears the meta annotation @Inherited, see JDK JavaDoc.

AspectJ is a JVM language and thus works within the JVM's limitations. There is no general solution for this problem, but for specific interfaces or methods you wish to emulate annotation inheritance for, you can use a workaround like this:

package de.scrum_master.aspect;

import de.scrum_master.app.Marker;
import de.scrum_master.app.MyInterface;

/**
* It is a known JVM limitation that annotations are never inherited from interface
* to implementing class or from method to overriding method, see explanation in
* <a href="https://docs.oracle.com/javase/8/docs/api/java/lang/annotation/Inherited.html">JDK API</a>.
* <p>
* Here is a little AspectJ trick which does it manually.
*
*/
public aspect MarkerAnnotationInheritor {
// Implementing classes should inherit marker annotation
declare @type: MyInterface+ : @Marker;
// Overriding methods 'two' should inherit marker annotation
declare @method : void MyInterface+.two() : @Marker;
}

Please note: With this aspect in place, you can remove the (literal) annotations from the interface and from the annotated method because AspectJ's ITD (inter-type definition) mechanics adds them back to the interface plus to all implementing/overriding classes/methods.

Now the console log when running the Application says:

execution(de.scrum_master.app.Application())
execution(void de.scrum_master.app.Application.two())

By the way, you could also embed the aspect right into the interface so as to have everything in one place. Just be careful to rename MyInterface.java to MyInterface.aj in order to help the AspectJ compiler to recognise that it has to do some work here.

package de.scrum_master.app;

public interface MyInterface {
void one();
void two();

// Cannot omit 'static' here due to https://bugs.eclipse.org/bugs/show_bug.cgi?id=571104
public static aspect MarkerAnnotationInheritor {
// Implementing classes should inherit marker annotation
declare @type: MyInterface+ : @Marker;
// Overriding methods 'two' should inherit marker annotation
declare @method : void MyInterface+.two() : @Marker;
}
}

Update 2021-02-11: Someone suggested an edit to the latter solution, saying that the aspect MarkerAnnotationInheritor nested inside interface MyInterface is implicitly public static, so the modifiers in the aspect declaration could be omitted. In principle this is true, because members (methods, nested classes) of interfaces are always public by default and a non-static inner class definition would not make sense inside an interface either (there is no instance to bind it to). I like to be explicit in my sample code, though, because not all Java developers might know these details.

Furthermore, currently the AspectJ compiler in version 1.9.6 throws an error if we omit static. I have just created AspectJ issue #571104 for this problem.

Aspect to use on methods in classes that has annotation that is inside another annotation

According to my other answer I would have expected an aspect like this to work:

package de.scrum_master.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class TestAspect {
@Before(
"execution(* *(..)) && (" +
"within(@de.scrum_master.app.Annotation2 *) || " +
"within(@(@de.scrum_master.app.Annotation2 *) *) || " +
"within(@(@(@de.scrum_master.app.Annotation2 *) *) *)" +
")"
)
public void myAdvice1(JoinPoint joinPoint) {
System.out.println(joinPoint);
}
}

Unfortunately it does not, which seems to be an AspectJ limitation. I created Bugzilla ticket #549437 for it, which you might want to follow.

Workaround: Annotate TestService directly.

aspectj pointcut for inherited interface methods

I replicated your situation in an MCVE as follows:

Base class implementing DataSource methods, but not the interface:

package de.scrum_master.app;

import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;

public class BaseClass {
public PrintWriter getLogWriter() throws SQLException { return null; }
public void setLogWriter(PrintWriter out) throws SQLException {}
public void setLoginTimeout(int seconds) throws SQLException {}
public int getLoginTimeout() throws SQLException { return 0; }
public Logger getParentLogger() throws SQLFeatureNotSupportedException { return null; }
public <T> T unwrap(Class<T> iface) throws SQLException { return null; }
public boolean isWrapperFor(Class<?> iface) throws SQLException { return false; }
public Connection getConnection() throws SQLException { return null; }
public Connection getConnection(String username, String password) throws SQLException { return null; }
}

Subclass implementing interface DataSource, inheriting methods from base class:

package de.scrum_master.app;

import javax.sql.DataSource;

public class SubClass extends BaseClass implements DataSource {}

Driver application:

package de.scrum_master.app;

import java.sql.SQLException;

public class Application {
public static void main(String[] args) throws SQLException {
System.out.println("Aspect should not kick in");
new BaseClass().getConnection();
new BaseClass().getConnection("user", "pw");

System.out.println("Aspect should kick in");
new SubClass().getConnection();
new SubClass().getConnection("user", "pw");
}
}

Aspect:

This aspect uses the pointcut you are currently using.

package de.scrum_master.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class DataSourceConnectionAspect {
@Before("execution(public java.sql.Connection *.getConnection(..)) && target(javax.sql.DataSource)")
public void myAdvice(JoinPoint thisJoinPoint) {
System.out.println(thisJoinPoint);
}
}

Console log:

Aspect should not kick in
Aspect should kick in
execution(Connection de.scrum_master.app.BaseClass.getConnection())
execution(Connection de.scrum_master.app.BaseClass.getConnection(String, String))

No surprises here, everything works as expected. In my opinion this is an efficient way to do it. Of course the aspect code will be woven into each method matching public java.sql.Connection *.getConnection(..)) and there will be a runtime check if target(javax.sql.DataSource) really applies, see also the javap output:

Compiled from "BaseClass.java"
public class de.scrum_master.app.BaseClass {
(...)

public java.sql.Connection getConnection() throws java.sql.SQLException;
Code:
0: aload_0
1: instanceof #76 // class javax/sql/DataSource
4: ifeq 21
7: invokestatic #70 // Method de/scrum_master/aspect/DataSourceConnectionAspect.aspectOf:()Lde/scrum_master/aspect/DataSourceConnectionAspect;
10: getstatic #58 // Field ajc$tjp_0:Lorg/aspectj/lang/JoinPoint$StaticPart;
13: aload_0
14: aload_0
15: invokestatic #64 // Method org/aspectj/runtime/reflect/Factory.makeJP:(Lorg/aspectj/lang/JoinPoint$StaticPart;Ljava/lang/Object;Ljava/lang/Object;)Lorg/aspectj/lang/JoinPoint;
18: invokevirtual #74 // Method de/scrum_master/aspect/DataSourceConnectionAspect.myAdvice:(Lorg/aspectj/lang/JoinPoint;)V
21: aconst_null
22: areturn

public java.sql.Connection getConnection(java.lang.String, java.lang.String) throws java.sql.SQLException;
Code:
0: aload_1
1: astore 4
3: aload_2
4: astore 5
6: aload_0
7: instanceof #76 // class javax/sql/DataSource
10: ifeq 31
13: invokestatic #70 // Method de/scrum_master/aspect/DataSourceConnectionAspect.aspectOf:()Lde/scrum_master/aspect/DataSourceConnectionAspect;
16: getstatic #79 // Field ajc$tjp_1:Lorg/aspectj/lang/JoinPoint$StaticPart;
19: aload_0
20: aload_0
21: aload 4
23: aload 5
25: invokestatic #82 // Method org/aspectj/runtime/reflect/Factory.makeJP:(Lorg/aspectj/lang/JoinPoint$StaticPart;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Lorg/aspectj/lang/JoinPoint;
28: invokevirtual #74 // Method de/scrum_master/aspect/DataSourceConnectionAspect.myAdvice:(Lorg/aspectj/lang/JoinPoint;)V
31: aconst_null
32: areturn

(...)
}

I.e. the runtime check also occurs for classes happening to implement these very special method pattern if the current instance is not a DataSource. But that should be rare.

There is one alternative involving ITD (inter-type declaration): You can make the base class directly implement the interface and then return to using your more efficient original pointcut. In annotation-based syntax this would like this:

package de.scrum_master.aspect;

import javax.sql.DataSource;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.DeclareParents;

@Aspect
public class DataSourceConnectionAspect {
@DeclareParents("de.scrum_master.app.BaseClass")
private DataSource dataSource;

@Before("execution(public java.sql.Connection javax.sql.DataSource+.getConnection(..))")
public void myAdvice(JoinPoint thisJoinPoint) {
System.out.println(thisJoinPoint);
}
}

Unfortunately, with the AspectJ version I used to test this, the AspectJ compiler throws an exception. That might be a bug, I will look into it later and report to the maintainer. Update: I created AspectJ bug ticket #550494 for this problem. Update 2: The bug was fixed in AspectJ 1.9.5.

But if you just use native AspectJ syntax it works. The only bad news is that if you use javac + LTW and rely on the AspectJ weaver to finish the aspect during class-loading, this will no longer work. You have to compile an aspect in native syntax with the AspectJ compiler ajc.

package de.scrum_master.aspect;

import javax.sql.DataSource;

import de.scrum_master.app.BaseClass;

public aspect DataSourceConnectionAspect {
declare parents: BaseClass implements DataSource;

before() : execution(public java.sql.Connection javax.sql.DataSource+.getConnection(..)) {
System.out.println(thisJoinPoint);
}
}

Now the console log changes to:

Aspect should not kick in
execution(Connection de.scrum_master.app.BaseClass.getConnection())
execution(Connection de.scrum_master.app.BaseClass.getConnection(String, String))
Aspect should kick in
execution(Connection de.scrum_master.app.BaseClass.getConnection())
execution(Connection de.scrum_master.app.BaseClass.getConnection(String, String))

Of course "Aspect should not kick in" no longer applies here because now we do expect it to kick in, of course, as BaseClass now directly implements the DataSource interface.

A little disclaimer: This approach only works if all the interface methods are really present in the base class, which fortunately is the case for org.apache.tomcat.jdbc.pool.DataSourceProxy, i.e. you can adapt my aspect accordingly. If the base class would only implement part of the expected interface methods, you could also add them via ITD in native syntax, but I am not going to elaborate on that here, my answer is quite long already.

Last, but not least, this is what the byte code looks like with the new approach:

Compiled from "BaseClass.java"
public class de.scrum_master.app.BaseClass implements javax.sql.DataSource {
(...)

public java.sql.Connection getConnection() throws java.sql.SQLException;
Code:
0: getstatic #58 // Field ajc$tjp_0:Lorg/aspectj/lang/JoinPoint$StaticPart;
3: aload_0
4: aload_0
5: invokestatic #64 // Method org/aspectj/runtime/reflect/Factory.makeJP:(Lorg/aspectj/lang/JoinPoint$StaticPart;Ljava/lang/Object;Ljava/lang/Object;)Lorg/aspectj/lang/JoinPoint;
8: astore_1
9: invokestatic #70 // Method de/scrum_master/aspect/DataSourceConnectionAspect.aspectOf:()Lde/scrum_master/aspect/DataSourceConnectionAspect;
12: aload_1
13: invokevirtual #74 // Method de/scrum_master/aspect/DataSourceConnectionAspect.ajc$before$de_scrum_master_aspect_DataSourceConnectionAspect$1$19879111:(Lorg/aspectj/lang/JoinPoint;)V
16: aconst_null
17: areturn

public java.sql.Connection getConnection(java.lang.String, java.lang.String) throws java.sql.SQLException;
Code:
0: aload_1
1: astore 4
3: aload_2
4: astore 5
6: getstatic #77 // Field ajc$tjp_1:Lorg/aspectj/lang/JoinPoint$StaticPart;
9: aload_0
10: aload_0
11: aload 4
13: aload 5
15: invokestatic #80 // Method org/aspectj/runtime/reflect/Factory.makeJP:(Lorg/aspectj/lang/JoinPoint$StaticPart;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Lorg/aspectj/lang/JoinPoint;
18: astore_3
19: invokestatic #70 // Method de/scrum_master/aspect/DataSourceConnectionAspect.aspectOf:()Lde/scrum_master/aspect/DataSourceConnectionAspect;
22: aload_3
23: invokevirtual #74 // Method de/scrum_master/aspect/DataSourceConnectionAspect.ajc$before$de_scrum_master_aspect_DataSourceConnectionAspect$1$19879111:(Lorg/aspectj/lang/JoinPoint;)V
26: aconst_null
27: areturn

(...)
}

If you compare the two javap logs you will not only notice that now it says implements javax.sql.DataSource but also that in the old version there were 22/32 bytecode instructions for the two methods, whereas the in new version there are just 17/27. For instance, in the old version you see instanceof #76 // class javax/sql/DataSource. In the new version the instanceof check is no longer necessary.

You can decide by yourself if this makes it worth for you to use ITD and native syntax. I personally use native syntax and ajc anyway, so I would do it. If you never used the AspectJ compiler before and exclusively use LTW, the decision might be different. If there even would be a measurable performance gain is yet another question. I assume that in scenarios involving SQL database calls, probably not AspectJ is what eats up your performance. ;-) I was just curious to find out and answer your question.


Update: Alternative solution without ITD

According to your comment you want to avoid ITD, even though I think it is a clean and elegant solution. But there is also a way to optimise pointcut matching and performance like this:

package de.scrum_master.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class AlternativeSolutionAspect {
@Pointcut("execution(public java.sql.Connection getConnection(..))")
private static void getConnection() {}

@Pointcut("within(javax.sql.DataSource+)")
private static void withinDataSource() {}

@Pointcut("target(javax.sql.DataSource)")
private static void targetDataSource() {}

@Before("withinDataSource() && getConnection()")
public void interceptStatically(JoinPoint thisJoinPoint) {
System.out.println("[static] " + thisJoinPoint);
}

@Before("!withinDataSource() && getConnection() && targetDataSource()")
public void interceptDynamically(JoinPoint thisJoinPoint) {
System.out.println("[dynamic] " + thisJoinPoint);
}
}

Explanation:

  • Advice interceptStatically takes care of finding all method executions for "normal" cases, i.e. (base) classes both implementing the interface and the corresponding methods.
  • Advice interceptDynamically takes care of the (exotic) rest, i.e. method executions where the actual instance implements the interface, but the method was defined in a (base) class not implementing the interface. The difference to your own purely dynamic solution is that here I am explicitly excluding the cases which can be determined statically.

Now what does that mean if we compare my DataSourceConnectionAspect to this AlternativeSolutionAspect? First let me add another sample class in order to make it clearer:

package de.scrum_master.app;

import java.sql.Connection;
import java.sql.SQLException;

import javax.sql.DataSource;

public class SubClassOverridingMethods extends BaseClass implements DataSource {
@Override
public Connection getConnection() throws SQLException {
return super.getConnection();
// return null;
}

@Override
public Connection getConnection(String username, String password) throws SQLException {
return super.getConnection(username, password);
// return null;
}
}

Now we extend the driver application by additional method calls:

package de.scrum_master.app;

import java.sql.SQLException;

public class Application {
public static void main(String[] args) throws SQLException {
System.out.println("Aspect should not kick in without ITD, but should with ITD");
new BaseClass().getConnection();
new BaseClass().getConnection("user", "pw");

System.out.println("Aspect should kick in");
new SubClass().getConnection();
new SubClass().getConnection("user", "pw");

System.out.println("Aspect should kick in");
new SubClassOverridingMethods().getConnection();
new SubClassOverridingMethods().getConnection("user", "pw");
}
}

The rest remains like in my example above.

Console log for DataSourceConnectionAspect:

Aspect should not kick in without ITD, but should with ITD
execution(Connection de.scrum_master.app.BaseClass.getConnection())
execution(Connection de.scrum_master.app.BaseClass.getConnection(String, String))
Aspect should kick in
execution(Connection de.scrum_master.app.BaseClass.getConnection())
execution(Connection de.scrum_master.app.BaseClass.getConnection(String, String))
Aspect should kick in
execution(Connection de.scrum_master.app.SubClassOverridingMethods.getConnection())
execution(Connection de.scrum_master.app.BaseClass.getConnection())
execution(Connection de.scrum_master.app.SubClassOverridingMethods.getConnection(String, String))
execution(Connection de.scrum_master.app.BaseClass.getConnection(String, String))

In case 3 you see 4 lines of log output for 2 method calls because the overriding methods call super.getConnection(..). If they would just do something without using super calls, there would only be one log line per method call, of course.

Console log for AlternativeSolutionAspect:

Aspect should not kick in without ITD, but should with ITD
Aspect should kick in
[dynamic] execution(Connection de.scrum_master.app.BaseClass.getConnection())
[dynamic] execution(Connection de.scrum_master.app.BaseClass.getConnection(String, String))
Aspect should kick in
[static] execution(Connection de.scrum_master.app.SubClassOverridingMethods.getConnection())
[dynamic] execution(Connection de.scrum_master.app.BaseClass.getConnection())
[static] execution(Connection de.scrum_master.app.SubClassOverridingMethods.getConnection(String, String))
[dynamic] execution(Connection de.scrum_master.app.BaseClass.getConnection(String, String))

As we do not use ITD here, nothing gets intercepted for case 1. Case 2 is intercepted dynamically while in case 3 the overriding methods can be determined statically and the super method dynamically. Again, if there were no super calls, we would only have one line of log output per method call for case 3.

P.S.: Your own solution would also match twice in case of super calls, just in case you wondered. But it would match dynamically both times, making it slower.

Pointcut matching methods which have been annotated directly or in an inherited interface?

I don't know how AspectJ deals with annotations in such a scenario, but if he only checks the implementing class for a certain annotation, and that annotation is only found on the interface that the class implements, Java will report that infact that annoation is not present on the class method. You should annotate your annotation with @Inherited:

http://download.oracle.com/javase/6/docs/api/java/lang/annotation/Inherited.html

maybe this will do the trick (though in this case you should make sure that your Advice is not called multiple times).

Can annotations be added to methods or members using AOP frameworks?

Yes, you can use AspectJ for that purpose (quoting the AspectJ cheat sheet, see also AspectJ Development Kit Developer's Notebook):

  • declare @type: C : @SomeAnnotation;
    declares the annotation @SomeAnnotation on the type C.
  • declare @method: * C.foo*(..) : @SomeAnnotation;
    declares the annotation @SomeAnnotation on all methods declared in C starting with foo.
  • declare @constructor: C.new(..) : @SomeAnnotation;
    declares the annotation @SomeAnnotation on all constructors declared in C.
  • declare @field: * C.* : @SomeAnnotation;
    declares the annotation @SomeAnnotation on all fields declared in C.

Just in case you wanted to ask: This feature is only supported in native AspectJ syntax, not in annotation-style @AspectJ syntax.


Update: Here is some sample code showing

  • how to add annotations to classes, interfaces and methods,
  • that even annotations marked as @Inherited are only inherited from class to subclass, never from interface to class and never to subclass methods (a typical Java caveat many developers are unaware of, see Emulate annotation inheritance for interfaces and methods with AspectJ for an explanation and a possible workaround),
  • how via AspectJ you can still apply an annotation to classes implementing an interface via + subclass specifier, e.g. in MyInterface+,
  • how another aspect can immediately see and utilise annotations added via declare @type, declare @method etc.

Class hierarchy including abstract base classes and an interface:

package de.scrum_master.app;

public interface MyInterface {
void doSomething();
int doSomethingElse(int a, int b);
String sayHelloTo(String name);
}
package de.scrum_master.app;

public abstract class NormalBase implements MyInterface {
@Override
public abstract void doSomething();

@Override
public int doSomethingElse(int a, int b) {
return a + b;
}

@Override
public abstract String sayHelloTo(String name);
}
package de.scrum_master.app;

public class Normal extends NormalBase {
@Override
public void doSomething() {
//System.out.println("Doing something normal");
}

@Override
public String sayHelloTo(String name) {
return "A normal hello to " + name;
}

public void doNothing() {
//System.out.println("Being lazy in a normal way");
}
}
package de.scrum_master.app;

public abstract class SpecialBase {
public abstract void doFoo();
public abstract void makeBar();
}
package de.scrum_master.app;

public class Special extends SpecialBase implements MyInterface {
@Override
public void doSomething() {
//System.out.println("Doing something special");
}

@Override
public int doSomethingElse(int a, int b) {
return a * b;
}

@Override
public String sayHelloTo(String name) {
return "A special hello to " + name;
}

@Override
public void doFoo() {
//System.out.println("Doing foo");
}

@Override
public void makeBar() {
//System.out.println("Making bar");
}

public int doZot() {
return 11;
}

public String makeBlah() {
return "Blah";
}
}
package de.scrum_master.app;

public class SpecialTwo extends SpecialBase {
@Override
public void doFoo() {
//System.out.println("Doing foo");
}

@Override
public void makeBar() {
//System.out.println("Making bar");
}

public String doXxx() {
return "Xxx";
}

public int makeBlah() {
return 22;
}
}

Driver application creating all kinds of object, calling all kinds of methods:

package de.scrum_master.app;

public class Application {
public static void main(String[] args) {
System.out.println("Normal instance");
Normal normal = new Normal();
normal.doSomething();
normal.doSomethingElse(3, 5);
normal.sayHelloTo("John");
normal.doNothing();

System.out.println("\nNormal instance as NormalBase");
NormalBase normalBase = normal;
normalBase.doSomething();
normalBase.doSomethingElse(3, 5);
normalBase.sayHelloTo("John");

System.out.println("\nNormal instance as MyInterface");
MyInterface myInterface = normal;
myInterface.doSomething();
myInterface.doSomethingElse(3, 5);
myInterface.sayHelloTo("John");

System.out.println("\nSpecial instance");
Special special = new Special();
special.doSomething();
special.doSomethingElse(7, 8);
special.doFoo();
special.doZot();
special.makeBar();
special.makeBlah();
special.sayHelloTo("Jane");

System.out.println("\nSpecial instance as SpecialBase");
SpecialBase specialBase = special;
specialBase.doFoo();
specialBase.makeBar();

System.out.println("\nSpecial instance as MyInterface");
myInterface = special;
myInterface.doSomething();
myInterface.doSomethingElse(7, 8);
myInterface.sayHelloTo("Jane");

System.out.println("\nSpecialTwo instance");
SpecialTwo specialTwo = new SpecialTwo();
specialTwo.doFoo();
specialTwo.makeBar();
specialTwo.makeBlah();
specialTwo.doXxx();

System.out.println("\nSpecialTwo instance as SpecialBase");
specialBase = specialTwo;
specialBase.doFoo();
specialBase.makeBar();
}
}

Some marker annotations later to be added to interfaces, classes, methods by an aspect:

package de.scrum_master.app;

import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface InterfaceMarker {}
package de.scrum_master.app;

import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface ClassMarker {}
package de.scrum_master.app;

import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface MethodMarker {}

Aspect adding annotations to interfaces, classes, methods:

package de.scrum_master.aspect;

import de.scrum_master.app.ClassMarker;
import de.scrum_master.app.InterfaceMarker;
import de.scrum_master.app.MethodMarker;
import de.scrum_master.app.MyInterface;
import de.scrum_master.app.SpecialBase;
public aspect AnnotationGenerator.


Related Topics



Leave a reply



Submit