AOP pointcut for argument which has a field with annotation?

我的梦境 提交于 2020-03-04 15:35:31

问题


Problem: I want to call a method by hand using AOP when a certain method get called with a specific type argument which has a field with a specific annotation.

Right now I am able to do this in two different way : 1. ' call a method by hand using AOP when a certain method get called with a specific type argument' . Then fetch the annotated field via reflection from the joinpoint.

2.or annotate the type itself with the fieldnames as annotation value

But besides these how should I put these in the pointcut expression at once tocheck whether the annotated field is present ?

Example:

class  A {
}

class B extends A{
  someField;
}

class C extends A{
  @CustomAnnotation
  someField;
}

there are overloaded methods where I want to take a 'before' action: like this:

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

With the following pointcut I am able to catch the action when the parameter type is 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
    }

With this solution both B and C class get captured. What I try to accomplish is to put this line of code into the pointcut expression:

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

So only class C will be captured.
Something like this: @Pointcut("execution(* somePackage.doSomething((A.fieldhas(@CustomAnnotation))))")

edit2: for the requirements part: I have to overwrite the value (it is a private field but has a public setter).


回答1:


Okay, even after asking several times I got no clear answer from you when and where you want to manipulate your field values. So I am showing you three different ways. All involve using full-fledged AspectJ and I will also use native syntax because the first way I am going to show you does not work in annotation-style syntax. You need to compile the aspects with the AspectJ compiler. Whether you weave it into your application code during compile time or via load time weaving is up to you. My solution works completely without Spring, but if you are a Spring user you can combine it with Spring and even mix it with Spring AOP. Please read the Spring manual for further instructions.

The ways I show you in my sample code are:

  1. Inter-type declaration (ITD): This is the most complex way and it uses the hasfield() pointcut designator. In order to use it, the AspectJ compiler needs to be called with the special flag -XhasMember. In Eclipse with installed AJDT the setting is named "Has Member" in the project settings under "AspectJ Compiler", "Other". What we do here is:

    • make all classes with annotated fields implement a marker interface HasMyAnnotationField
    • whenever a method with a parameter type implementing the interface is called, something is printed on the console and optionally the field value is manipulated via reflection, probably similar to your own solution.
  2. Manipulate the field value during write access via set() advice. This persistently changes the field value and it does not need any ITD with marker interface, special compiler flags and reflection like solution 1.

  3. Transparently manipulate the value returned from field read access via get() advice. The field itself remains unchanged.

Probably you want #2 or #3, I am showing solution #1 for completeness's sake.

Enough words, here is the complete MCVE:

Field annotation:

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 {}

Sample class using field annotation:

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 + "]";
  }
}

Driver application:

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);
    }
  }
}

Console log without aspects:

  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]

No surprises here. We created two MyClass objects and called some Application methods, only two of which actually have MyClass parameters (i.e. parameter types with at least one field annotated by MyAnnotation). We expect something to happen when the aspects kicks in. But before we write the aspects we need something else first:

Marker interface for classes with @MyAnnotation fields:

package de.scrum_master.app;

public interface HasMyAnnotationField {}

And here are our aspects:

Aspects showing 3 ways of manipulating field values:

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() + "~";
  }
}

Console log with all 3 aspects activated:

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

As you can see,

  1. reflective access surrounds the field value by # every time one of the methods doSomethingSpecial(..) or doSomethingVerySpecial(..) is called - in total 3x because of the for loop, resulting in ### pre- and suffixes in the end.

  2. field write access happens only once during object creation and persistently changes the string value to upper case.

  3. field read access transparently wraps the stored value in ~ characters which are not stored, otherwise they would grow more like the # characters from method 1 because read access happens multiple times.

Please also note that you can determine whether you want to access all annotated fields like in hasfield(@MyAnnotation * *) or maybe limit just to a certain type like in set(@MyAnnotation String *) or get(@MyAnnotation String *).

For more information, e.g about ITD via declare parents and the more exotic pointcut types used in my sample code, please refer to the AspectJ documentation.

Update: After I have split my monolithic aspect into 3 separate aspects I can say that if you do not need the first solution using hasfield() but one of the other two, probably you can use @AspectJ annotation style in order to write the aspects, compile them with a normal Java compiler and let the load time weaver take care of finishing the aspect and weaving it into the application code. The native syntax limitation only applies to the first aspect.



来源:https://stackoverflow.com/questions/56724407/aop-pointcut-for-argument-which-has-a-field-with-annotation

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!