围绕注释字段调用的方面 [英] Aspect around call on annotated field

查看:26
本文介绍了围绕注释字段调用的方面的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我希望 AspectJ 在任何方法的所有调用周围注入测量代码,在用 @Measured 注释的字段上并捕获方法的名称.这就是我所拥有的:

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

用例:

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.

推荐答案

这不是你可以直接用切入点做的事情,因为 get(),正如你所注意到的,与 get() 完全不同code>call() 或 execution() 切入点.get() 连接点在 call() 完成之前已经完全通过.此外,call() 不知道它所调用的目标对象是否恰好被分配给一个或多个(带注释的)类成员.

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.

我认为您想要实现的目标在概念上是有问题的.您应该注释要测量的类或方法,而不是类成员.但是对于它的价值,我将向您展示一个解决方案.警告:解决方案涉及手动簿记和反射.因此,它有点慢,但对于您的目的来说可能仍然足够快.你可以决定是否试一试.请注意,这个解决方案让我感到不安,因为它感觉不像是 AOP 的良好应用.

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:

字段注释:

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

稍后玩的示例课程:

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

使用示例类的驱动程序应用程序:

请注意四个成员中只有两个 - 一个原始类型和一种对象类型 - 由 @Measured 注释,而另外两个没有注释.我这样做是为了有正面和负面的例子,以便查看方面是否正常工作.

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.

另一件重要的事情是,以前分配给带注释的类成员的对象一旦不再分配给该成员,就不应再由方面报告.IE.oldMyClass.waitForAWhile(); 不应该被测量.

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

方面:

该方面负责两件事:簿记和测量.详细说明:

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

  • 每当一个值被分配给 @Measured 字段时,它就会被记录在一组 measuredObjects 中,因为这是以后知道方法何时被调用的唯一方法调用那个对象,它真的应该被测量.
  • 虽然在 before() : set() 通知中很容易获取新值,但不幸的是没有直接的方法来获取旧值.这就是为什么我们需要使用反射来找出丑陋的小助手方法 getField(Signature signature) 的原因.
  • 为什么我们仍然需要旧值?因为为了保持干净的簿记,我们必须从 measuredObjects 集合中删除未分配的对象.
  • 另请注意,measuredObjects 不是我实现它的方式线程安全的,但如果需要,您可以只使用同步集合.
  • call() 通知首先检查它是否可以在 measuredObjects 中找到目标对象,如果找不到则停止执行.否则它测量方法调用的运行时间.这很简单.
  • 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.

哦,顺便说一句,我在这里使用的是更简洁、更具表现力的原生 AspectJ 语法,而不是丑陋的注释样式.如果您对此有任何疑问,请告诉我.

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

控制台日志:

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

如您所见,方面的行为是正确的.它只报告对象 MyClass[measured] 上的方法调用一次,而它被分配给一个 @Measured 字段,但当一个方法被调用后不会已经被取消分配并被 MyClass[another measure] 取代.后者随后被正确报告.您还可以看到方面如何完美地工作,即使对于 String measured" 这样的原语也是如此.

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".

享受吧!

更新 2021-04-22:我又找到了这个旧答案,想提一下示例代码的其他一些问题,除了线程不安全:

Update 2021-04-22: I found this old answer again and wanted to mention a few other problems with the sample code, in addition to thread-unsafety: