使用 AspectJ 模拟接口和方法的注释继承

2022-01-13 00:00:00 inheritance annotations java aspectj

经常有人问像这样的 AspectJ 问题,所以我想在以后可以轻松链接到的地方回答.

我有这个标记注释:

package de.scrum_master.app;导入 java.lang.annotation.Inherited;导入 java.lang.annotation.Retention;导入 java.lang.annotation.RetentionPolicy;@遗传@Retention(RetentionPolicy.RUNTIME)公共@interface 标记{}

现在我像这样注释一个接口和/或方法:

package de.scrum_master.app;@Marker公共接口 MyInterface {无效一个();@Marker 无效二();}

这是一个小驱动程序,它也实现了接口:

package de.scrum_master.app;公共类应用程序实现 MyInterface {@覆盖公共无效一个(){}@覆盖公共无效二(){}公共静态无效主要(字符串[]参数){应用程序应用程序=新应用程序();应用程序.one();application.two();}}

现在当我定义这个方面时,我希望它会被触发

  • 对于带注释的类的每个构造函数执行和
  • 每次执行带注释的方法.

package de.scrum_master.aspect;导入 de.scrum_master.app.Marker;公共方面 MarkerAnnotationInterceptor {after() : 执行((@Marker *).new(..)) &&!within(MarkerAnnotationInterceptor) {System.out.println(thisJoinPoint);}after() : 执行(@Marker * *(..)) &&!within(MarkerAnnotationInterceptor) {System.out.println(thisJoinPoint);}}

不幸的是,方面没有打印任何内容,就像类 Application 和方法 two() 没有任何 @Marker 注释一样.为什么 AspectJ 不拦截它们?

解决方案

这里的问题不是AspectJ,而是JVM.在 Java 中,注释在

  • 接口,
  • 方法或
  • 其他注释

从不被

继承
  • 实现类,
  • 覆盖方法或
  • 使用带注解的类.

注解继承仅适用于类到子类,但前提是超类中使用的注解类型带有元注解@Inherited,参见JDK JavaDoc.

AspectJ 是一种 JVM 语言,因此可以在 JVM 的限制范围内工作.这个问题没有通用的解决方案,但是对于您希望模拟注释继承的特定接口或方法,您可以使用如下解决方法:

package de.scrum_master.aspect;导入 de.scrum_master.app.Marker;导入 de.scrum_master.app.MyInterface;/*** 一个已知的 JVM 限制是注解永远不会从接口继承* 实现类或从方法到覆盖方法,请参见解释* <a href=>https://docs.oracle.com/javase/8/docs/api/java/lang/annotation/Inherited.html>>JDK API</a>.* <p>* 这是一个手动的 AspectJ 小技巧.**/公共方面 MarkerAnnotationInheritor {//实现类应该继承标记注解声明 @type: MyInterface+ : @Marker;//覆盖方法'two'应该继承标记注解声明@method : void MyInterface+.two() : @Marker;}

请注意:有了这个方面,您可以从接口和带注释的方法中删除(文字)注释,因为 AspectJ 的 ITD(类型间定义)机制会将它们添加回接口加上所有实现/覆盖类/方法.

现在运行 Application 时的控制台日志显示:

执行(de.scrum_master.app.Application())执行(无效 de.scrum_master.app.Application.two())

顺便说一句,您还可以将方面直接嵌入到界面中,以便将所有内容集中在一个地方.请注意将 MyInterface.java 重命名为 MyInterface.aj 以帮助 AspectJ 编译器识别它必须在这里做一些工作.

package de.scrum_master.app;公共接口 MyInterface {无效一个();无效二();//由于 https://bugs.eclipse.org/bugs/show_bug.cgi?id=571104,此处不能省略静态"公共静态方面 MarkerAnnotationInheritor {//实现类应该继承标记注解声明 @type: MyInterface+ : @Marker;//覆盖方法'two'应该继承标记注解声明@method : void MyInterface+.two() : @Marker;}}

2021-02-11 更新: 有人建议对后一种解决方案进行编辑,称嵌套在接口 MyInterface 内的方面 MarkerAnnotationInheritor 是隐式 public static,因此方面声明中的修饰符可以省略.原则上这是正确的,因为默认情况下接口的成员(方法、嵌套类)始终是公共的,并且非静态内部类定义在接口内部也没有意义(没有实例可以绑定它).不过,我喜欢在示例代码中明确说明,因为并非所有 Java 开发人员都知道这些细节.

此外,如果我们省略 static,当前版本 1.9.6 中的 AspectJ 编译器会引发错误.我刚刚为这个问题创建了 AspectJ issue #571104.p>

Often people ask AspectJ questions like this one, so I want to answer it in a place I can easily link to later.

I have this marker annotation:

package de.scrum_master.app;

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

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

Now I annotate an interface and/or methods like this:

package de.scrum_master.app;

@Marker
public interface MyInterface {
  void one();
  @Marker void two();
}

Here is a little driver application which also implements the interface:

package de.scrum_master.app;

public class Application implements MyInterface {
  @Override
  public void one() {}

  @Override
  public void two() {}

  public static void main(String[] args) {
    Application application = new Application();
    application.one();
    application.two();
  }
}

Now when I define this aspect, I expect that it gets triggered

  • for each constructor execution of an annotated class and
  • for each execution of an annotated method.

package de.scrum_master.aspect;

import de.scrum_master.app.Marker;

public aspect MarkerAnnotationInterceptor {
  after() : execution((@Marker *).new(..)) && !within(MarkerAnnotationInterceptor) {
    System.out.println(thisJoinPoint);
  }

  after() : execution(@Marker * *(..)) && !within(MarkerAnnotationInterceptor) {
    System.out.println(thisJoinPoint);
  }
}

Unfortunately the aspect prints nothing, just as if class Application and method two() did not have any @Marker annotation. Why does AspectJ not intercept them?

解决方案

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.

相关文章