首页 > 解决方案 > 具有带注释字段的参数的 AOP 切入点?

问题描述

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

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

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

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

例子:

class  A {
}

class B extends A{
  someField;
}

class C extends A{
  @CustomAnnotation
  someField;
}

有一些重载的方法我想采取“之前”的行动:像这样:

  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 类都被捕获。我试图完成的是将这行代码放入切入点表达式中:

“用反射检查字段是否被注释”

所以只会捕获 C 类。
像这样的东西: @Pointcut("execution(* somePackage.doSomething((A.fieldhas(@CustomAnnotation))))")

edit2:对于需求部分:我必须覆盖值(它是一个私有字段,但有一个公共设置器)。

标签: springspring-aop

解决方案


好的,即使在问了几次之后,我也没有得到你的明确回答,你想在何时何地操作你的字段值。所以我向你展示了三种不同的方式。所有这些都涉及使用成熟的 AspectJ,我还将使用本机语法,因为我要向您展示的第一种方法不适用于注释样式的语法。您需要使用 AspectJ 编译器编译方面。无论是在编译时还是通过加载时编织将其编织到应用程序代码中,都取决于您。我的解决方案在没有 Spring 的情况下完全可以工作,但如果您是 Spring 用户,您可以将其与 Spring 结合使用,甚至可以将其与 Spring AOP 混合使用。请阅读 Spring 手册以获取更多说明。

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

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

    • 使所有带注释字段的类都实现标记接口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 {}

使用字段注释的示例类:

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带有字段的类的标记接口:

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###~]

如你看到的,

  1. #每次方法之一doSomethingSpecial(..)或被调用时,反射访问都会围绕字段值doSomethingVerySpecial(..)- 由于for循环,总共 3 倍,最终导致###前缀和后缀。

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

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

另请注意,您可以确定是否要访问所有带注释的字段,如 inhasfield(@MyAnnotation * *)或可能仅限于特定类型,如 inset(@MyAnnotation String *)get(@MyAnnotation String *)

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

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


推荐阅读