具有带注释字段的参数的 AOP 切入点?

AOP pointcut for argument which has a field with annotation?

问题:当使用特定类型参数调用某个方法时,我想使用 AOP 手动调用方法,该类型参数具有带特定注释的字段。

现在我可以用两种不同的方式做到这一点: 1. '当使用特定类型参数调用某个方法时,使用 AOP 手动调用方法'。然后通过连接点的反射获取带注释的字段。

2.or 用字段名作为注释值来注释类型本身

但是除了这些我应该如何立即将它们放在切入点表达式中以检查是否存在注释字段?

示例:

class  A {
}

class B extends A{
  someField;
}

class C extends A{
  @CustomAnnotation
  someField;
}

有重载方法,我想在其中执行 'before' 操作: 像这样:

  public void doSomething(A a);
  public void doSomething(X x);

使用以下切入点,我可以在参数类型为 A 时捕获操作:

    @Pointcut("execution(* somePackage.doSomething((A)))")
    public void customPointCut() {
    }

    @Before("customPointCut()")
    public void customAction(JoinPoint joinPoint) throws Throwable{   
              //examining fields with reflection whether they are annotated or not
              //action
    }

通过这个解决方案,B 和 C class 都被捕获了。 我试图完成的是将这行代码放入切入点表达式中:

"examining fields with reflection whether they are annotated or not"

所以只有class C会被捕获。
像这样:@Pointcut("execution(* somePackage.doSomething((A.fieldhas(@CustomAnnotation))))")

edit2:对于需求部分:我必须覆盖该值(它是一个私有字段,但有一个 public setter)。

好吧,即使问了好几次,我也没有从你那里得到明确的答案,你想在何时何地操作你的字段值。因此,我将向您展示 三种不同的方式。 所有方式都涉及使用 full-fledged AspectJ 我还将使用本机语法,因为第一种方式我我将向您展示在 annotation-style 语法中不起作用。您需要使用 AspectJ 编译器编译方面。是在编译时将其编织到您的应用程序代码中还是通过加载时编织由您决定。我的解决方案在没有 Spring 的情况下完全有效,但如果您是 Spring 用户,您可以将其与 Spring 结合使用,甚至可以将其与 Spring AOP 混合使用。请阅读 Spring 手册以获取更多说明。

我在示例代码中向您展示的方式是:

  1. Inter-type 声明 (ITD):这是最复杂的方式,它使用 hasfield() 切入点指示符。为了使用它,需要使用特殊标志 -XhasMember 调用 AspectJ 编译器。在安装了 AJDT 的 Eclipse 中,该设置在 "AspectJ Compiler"、"Other" 下的项目设置中被命名为 "Has Member"。我们在这里做的是:

    • 使所有带有注释字段的 class 实现标记接口 HasMyAnnotationField
    • 每当调用实现接口的参数类型的方法时,都会在控制台上打印一些内容,并且可以选择通过反射操作字段值,这可能类似于您自己的解决方案。
  2. 通过set() 建议在写入访问期间操纵字段值。这会持续更改字段值,并且不需要任何带有标记接口的 ITD、特殊编译器标志和反射,如解决方案 1。

  3. 通过 get() 建议透明地操纵从字段读取访问返回的值。该字段本身保持不变。

可能您想要#2 或#3,为了完整起见,我展示了解决方案#1。

够多了,这里是完整的MCVE:

字段注释:

package de.scrum_master.app;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Retention(RUNTIME)
@Target(FIELD)
public @interface MyAnnotation {}

示例 class 使用字段注释:

package de.scrum_master.app;

public class MyClass {
  private int id;
  @MyAnnotation
  private String name;

  public MyClass(int id, String name) {
    this.id = id;
    this.name = name;
  }

  @Override
  public String toString() {
    return "MyClass [id=" + id + ", name=" + name + "]";
  }
}

驱动申请:

package de.scrum_master.app;

public class Application {
  public void doSomething() {}

  public void doSomethingElse(int i, String string) {}

  public void doSomethingSpecial(int i, MyClass myClass) {
    System.out.println("  " + myClass);
  }

  public int doSomethingVerySpecial(MyClass myClass) {
    System.out.println("  " + myClass);
    return 0;
  }

  public static void main(String[] args) {
    Application application = new Application();
    MyClass myClass1 = new MyClass(11, "John Doe");
    MyClass myClass2 = new MyClass(11, "Jane Doe");
    for (int i = 0; i < 3; i++) {
      application.doSomething();
      application.doSomethingElse(7, "foo");
      application.doSomethingSpecial(3, myClass1);
      application.doSomethingVerySpecial(myClass2);
    }
  }
}

没有方面的控制台日志:

  MyClass [id=11, name=John Doe]
  MyClass [id=11, name=Jane Doe]
  MyClass [id=11, name=John Doe]
  MyClass [id=11, name=Jane Doe]
  MyClass [id=11, name=John Doe]
  MyClass [id=11, name=Jane Doe]

这里没有惊喜。我们创建了两个 MyClass 对象并调用了一些 Application 方法,其中只有两个实际上具有 MyClass 参数(即参数类型至少有一个由 MyAnnotation 注释的字段)。我们期望方面开始时会发生一些事情。但是在我们编写方面之前,我们首先需要做一些其他事情:

具有 @MyAnnotation 个字段的 classes 的标记接口:

package de.scrum_master.app;

public interface HasMyAnnotationField {}

这是我们的方面:

显示 3 种操作字段值的方式的方面:

package de.scrum_master.aspect;

import java.lang.reflect.Field;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.SoftException;
import org.aspectj.lang.reflect.MethodSignature;

import de.scrum_master.app.HasMyAnnotationField;
import de.scrum_master.app.MyAnnotation;

public aspect ITDAndReflectionAspect {

  // Make classes with @MyAnnotation annotated fields implement marker interface
  declare parents : hasfield(@MyAnnotation * *) implements HasMyAnnotationField;

  // Intercept methods with parameters implementing marker interface
  before() : execution(* *(.., HasMyAnnotationField+, ..)) {
    System.out.println(thisJoinPoint);
    manipulateAnnotatedFields(thisJoinPoint);
  }

  // Reflectively manipulate @MyAnnotation fields of type String
  private void manipulateAnnotatedFields(JoinPoint thisJoinPoint) {
    Object[] methodArgs = thisJoinPoint.getArgs();
    MethodSignature signature = (MethodSignature) thisJoinPoint.getSignature();
    Class<?>[] parameterTypes = signature.getParameterTypes();
    int argIndex = 0;
    for (Class<?> parameterType : parameterTypes) {
      Object methodArg = methodArgs[argIndex++];
      for (Field field : parameterType.getDeclaredFields()) {
        field.setAccessible(true);
        if (field.getAnnotation(MyAnnotation.class) == null)
          continue;
        // If using 'hasfield(@MyAnnotation String *)' we can skip this type check 
        if (field.getType().equals(String.class)) {
          try {
            field.set(methodArg, "#" + ((String) field.get(methodArg)) + "#");
          } catch (IllegalArgumentException | IllegalAccessException e) {
            throw new SoftException(e);
          }
        }
      }
    }
  }

}
package de.scrum_master.aspect;

import de.scrum_master.app.MyAnnotation;

public aspect SetterInterceptor {
  // Persistently change field value during write access
  Object around(String string) : set(@MyAnnotation String *) && args(string) {
    System.out.println(thisJoinPoint);
    return proceed(string.toUpperCase());
  }
}
package de.scrum_master.aspect;

import de.scrum_master.app.MyAnnotation;

public aspect GetterInterceptor {
  // Transparently return changed value during read access
  Object around() : get(@MyAnnotation String *) {
    System.out.println(thisJoinPoint);
    return "~" + proceed() + "~";
  }
}

已激活所有 3 个方面的控制台日志:

set(String de.scrum_master.app.MyClass.name)
set(String de.scrum_master.app.MyClass.name)
execution(void de.scrum_master.app.Application.doSomethingSpecial(int, MyClass))
get(String de.scrum_master.app.MyClass.name)
  MyClass [id=11, name=~#JOHN DOE#~]
execution(int de.scrum_master.app.Application.doSomethingVerySpecial(MyClass))
get(String de.scrum_master.app.MyClass.name)
  MyClass [id=11, name=~#JANE DOE#~]
execution(void de.scrum_master.app.Application.doSomethingSpecial(int, MyClass))
get(String de.scrum_master.app.MyClass.name)
  MyClass [id=11, name=~##JOHN DOE##~]
execution(int de.scrum_master.app.Application.doSomethingVerySpecial(MyClass))
get(String de.scrum_master.app.MyClass.name)
  MyClass [id=11, name=~##JANE DOE##~]
execution(void de.scrum_master.app.Application.doSomethingSpecial(int, MyClass))
get(String de.scrum_master.app.MyClass.name)
  MyClass [id=11, name=~###JOHN DOE###~]
execution(int de.scrum_master.app.Application.doSomethingVerySpecial(MyClass))
get(String de.scrum_master.app.MyClass.name)
  MyClass [id=11, name=~###JANE DOE###~]

如您所见,

    每次调用 doSomethingSpecial(..)doSomethingVerySpecial(..) 方法之一时,
  1. 反射访问将字段值包围 # - 总共 3 倍,因为 for 循环,最后导致 ### 前缀和后缀。

  2. 字段写入访问仅在对象创建期间发生一次,并持续将字符串值更改为大写。

  3. 字段读取访问透明地将存储值包装在未存储的 ~ 个字符中,否则它们会变得更像方法 1 中的 # 个字符,因为发生读取访问多次。

另请注意,您可以决定是要访问所有带注释的字段,如 hasfield(@MyAnnotation * *) 中那样,还是仅限于某些类型,如 set(@MyAnnotation String *)get(@MyAnnotation String *) 中。

有关更多信息,例如关于通过 declare parents 的 ITD 和我的示例代码中使用的更奇特的切入​​点类型,请参阅 AspectJ 文档。

更新: 在我将整体方面拆分为 3 个独立的方面之后,我可以说,如果您不需要使用 hasfield() 的第一个解决方案,但需要其中一个另外两个,您可能可以使用@AspectJ 注释样式来编写方面,使用普通的Java 编译器编译它们,让加载时间编织器负责完成方面并将其编织到应用程序代码中。本机语法限制仅适用于第一个方面。