Aspect around call on annotated field

﹥>﹥吖頭↗ 提交于 2019-12-11 04:34:22

问题


I want AspectJ to inject the measuring code around all invocations of any method, on fields annotated with @Measured and capture the method's name. This is what I have:

@Pointcut("get(@my.annotation.Measured * *) && @annotation(measured)")
public void fieldAnnotatedWithMeasured(Measured measured) {}

@Around(value = "fieldAnnotatedWithMeasured(measured)", argNames = "joinPoint,measured")
public Object measureField(ProceedingJoinPoint joinPoint, Measured measured) throws Throwable {...}

Use case:

public class A { 

  @Measured private Service service;
  ...
  void call(){
    service.call(); // here I want to measure the call() time and capture its name
  }

This seems to surround only the access to the field, not the method invocation. I want to capture the invoked method name instide the advise.


回答1:


This is not something you can do directly with a pointcut because get(), as you have noticed, is entirely different from call() or execution() pointcuts. The get() joinpoint has completely passed before the moment the call() is done. Furthermore, the call() has no idea about whether or not the target object it is called upon happens to be assigned to one or more (annotated) class members.

I think that what you want to achieve is conceptually problematic. You should rather annotate the classes or methods you want to measure, not class members. But for what it is worth, I am going to present you with a solution. Caveat: The solution involves manual bookkeeping and also reflection. Thus, it is kinda slow but maybe still fast enough for your purpose. You can decide if you give it a try. Please note that this solution is something I feel uneasy with because it does not feel like a good application of AOP.

Okay, so here is our test setup:

Field annotation:

package de.scrum_master.app;

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

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Measured {}

Sample class to play around with later:

package de.scrum_master.app;

public class MyClass {
    private String name;

    public MyClass(String name) {
        super();
        this.name = name;
    }

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

    void waitForAWhile() throws InterruptedException {
        Thread.sleep(200);
    }
}

Driver application using the sample class:

Please note how only two of the four members - one primitive and one object type - are annotated by @Measured and two others are not. I did this in order to have positive as well as negative examples so as to see whether the aspect works correctly.

Another important thing is that an object formerly assigned to an annotated class member should no longer be reported by the aspect as soon as it is no longer assigned to that member. I.e. oldMyClass.waitForAWhile(); should not be measured.

package de.scrum_master.app;

public class Application {
    String foo = "unmeasured";
    @Measured String bar = "measured";
    MyClass myClass1 = new MyClass("unmeasured");
    @Measured MyClass myClass2 = new MyClass("measured");

    void doSomething() throws InterruptedException {
        foo.length();
        bar.length();
        myClass1.waitForAWhile();
        myClass2.waitForAWhile();

        MyClass oldMyClass = myClass2;
        myClass2 = new MyClass("another measured");
        // This call should not be reported by the aspect because the object
        // is no longer assigned to a member annotated by @Measured
        oldMyClass.waitForAWhile();
        // This call should be reported for the new member value
        myClass2.waitForAWhile();
    }

    public static void main(String[] args) throws InterruptedException {
        new Application().doSomething();
    }
}

Aspect:

The aspect takes care of two things: bookkeeping and measuring. In detail:

  • Whenever a value gets assigned to a @Measured field, it is recorded in a set of measuredObjects because this is the only way to later know that when a method is called on that object, it really should be measured.
  • While it is easy to get hold of the new value in a before() : set() advice, unfortunately there is no straightforward way to get hold of the old value. This is why we need the ugly little helper method getField(Signature signature) using reflection in order to find out.
  • Why do we need the old value anyway? Because in order to have clean bookkeeping we have to remove unassigned objects from the measuredObjects set.
  • Please also note that measuredObjects is not thread-safe the way I implemented it, but you can just use a synchronized collection if you need that.
  • The call() advice first checks if it can find the target object in measuredObjects and stops executing if it cannot. Otherwise it measures the method call's runtime. This is straightforward.

Oh, and by the way, I am using the cleaner and more expressive native AspectJ syntax here, not the ugly annotation style. If you have any problems with that, please let me know.

package de.scrum_master.app;

import java.lang.reflect.Field;
import java.util.HashSet;
import java.util.Set;

import org.aspectj.lang.Signature;
import org.aspectj.lang.SoftException;

import de.scrum_master.app.Measured;

public aspect MyAspect {
    private Set<Object> measuredObjects = new HashSet<>(); 

    before(Measured measured, Object newValue, Object object) :
        set(* *) &&
        @annotation(measured) &&
        args(newValue) &&
        target(object) 
    {
        try {
            Field field = getField(thisJoinPoint.getSignature()); 
            Object oldValue = field.get(object); 
            System.out.println(thisJoinPoint);
            System.out.println("  old value = " + oldValue);
            System.out.println("  new value = " + newValue);
            measuredObjects.remove(oldValue);
            measuredObjects.add(newValue);
        }
        catch (Exception e) {
            throw new SoftException(e);
        }
    }

    Object around(Object object) :
        call(* *(..)) &&
        target(object) &&
        !within(MyAspect)
    {
        if (!measuredObjects.contains(object))
            return proceed(object);
        long startTime = System.nanoTime();
        Object result = proceed(object);
        System.out.println(thisJoinPoint);
        System.out.println("  object   = " + object);
        System.out.println("  duration = " + (System.nanoTime() - startTime) / 1e6 + " ms");
        return result;
    }

    private Field getField(Signature signature) throws NoSuchFieldException {
        Field field = signature.getDeclaringType().getDeclaredField(signature.getName());
        field.setAccessible(true);
        return field;
    }
}

Console log:

set(String de.scrum_master.app.Application.bar)
  old value = null
  new value = measured
set(MyClass de.scrum_master.app.Application.myClass2)
  old value = null
  new value = MyClass[measured]
call(int java.lang.String.length())
  object   = measured
  duration = 0.080457 ms
call(void de.scrum_master.app.MyClass.waitForAWhile())
  object   = MyClass[measured]
  duration = 200.472326 ms
set(MyClass de.scrum_master.app.Application.myClass2)
  old value = MyClass[measured]
  new value = MyClass[another measured]
call(void de.scrum_master.app.MyClass.waitForAWhile())
  object   = MyClass[another measured]
  duration = 200.461208 ms

As you can see, the aspect behaves correctly. It only reports the method call on object MyClass[measured] once, while it is assigned to a @Measured field, but not when a method is called upon it after it has already been unassigned and replaced by MyClass[another measured]. The latter is correctly reported subsequently. You also see how the aspect works beautifully even for primitives like the String "measured".

Enjoy!



来源:https://stackoverflow.com/questions/38281190/aspect-around-call-on-annotated-field

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