【问题标题】:Aspect around call on annotated field对注释字段的调用方面
【发布时间】:2016-11-11 21:07:57
【问题描述】:

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

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

这似乎只围绕对字段的访问,而不是方法调用。我想在建议中捕获调用的方法名称。

【问题讨论】:

    标签: java aop aspectj aspect


    【解决方案1】:

    这不是您可以直接使用切入点执行的操作,因为正如您所注意到的,get()call()execution() 切入点完全不同。在call() 完成之前,get() 连接点已经完全通过。此外,call() 不知道调用它的目标对象是否恰好分配给一个或多个(带注释的)类成员。

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

    好的,这是我们的测试设置:

    字段注释:

    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 注释,而另外两个则没有。我这样做是为了有正面和负面的例子,以查看方面是否正常工作。

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

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

    方面:

    方面负责两件事:簿记和测量。详细:

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

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

    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 measured] 替换后调用它时不报告。后者随后被正确报告。即使对于像 String "measured" 这样的原语,您也会看到方面如何完美地工作。

    享受吧!


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

    • Set&lt;Object&gt; measuredObjects 不考虑同一对象可以分配给多个字段的事实。如果我们想考虑这一点,我们需要将实例和字段的唯一组合映射到相应的值,即我们需要某种Map&lt;Field,&lt;Map&lt;Object, Object&gt;&gt;,其中嵌套映射中的键是实例(或静态的类名fields) 并且该值是当前字段值。管理这种数据结构将比我们现在拥有的简单集合更复杂,特别是如果我们还想在取消分配值后删除条目。

    • 目前我们使用简单的HashSet。实际上,这是不正确的,因为普通集合检查对象值相等,而不是对象标识。 IE。我们可能有冲突的实例ab,其中a.equals(b),但a != b。为了解决这个问题,我们需要一个IdentitySet,或者根据我在第一个要点中建议的改进,一个IdentityMap

    • 考虑到前面两个要点,更好的选择可能是一个专用类MeasuredEntity,其字段为Field fieldObject instanceObject value,并正确定义equalshashCode方法。然后我们可以再次使用一个简单的哈希集来记账。

    • 警告:任何类型的基于 AspectJ 的簿记都无法解决一个问题:如果一个对象当前分配给任何 @Measured 字段,例如 A.x,那么如果您调用 @,当然会触发测量建议987654363@。但是无法知道同一对象是否同时分配给非测量字段B.y 或局部变量myLocal。因此,如果调用 B.y.doSomething()myLocal.doSomething(),测量建议也会触发,这可能是正常的,也可能是无意的,具体取决于您的个人情况。我觉得没问题,你只要觉知就好了。

    • 如果实例超出范围并且有资格进行垃圾回收,则不会从集合或映射中删除对实例的引用。为了实现这一点,我们需要使用弱引用。

    • 一个较小的问题是getField(..) 使用field.setAccessible(true)。我们可能希望在某种try {} finally {} 中使用后重置该字段的可访问状态,首先将原始值存储在boolean 变量中,然后在finally 块中重置它。但是,如果这不打扰您,这确实只是一个小问题,并且在经常调用时也会使代码执行速度变慢。

    结论:人工记账不仅复杂,而且在实践中也带来了一些难以完全解决的挑战。不走这条路而是改变应用程序设计,以便能够在不记录内部对象状态的情况下应用方面,有效地打破类封装,是更好的选择 IMO。

    【讨论】:

    • 2021-04-22 更新:请注意,我刚刚更新了答案,加入了一些新的考虑。
    猜你喜欢
    • 1970-01-01
    • 2021-03-28
    • 1970-01-01
    • 2023-03-16
    • 1970-01-01
    • 2021-10-17
    • 2016-07-19
    • 2020-01-20
    • 1970-01-01
    相关资源
    最近更新 更多