这不是您可以直接使用切入点执行的操作,因为正如您所注意到的,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<Object> measuredObjects 不考虑同一对象可以分配给多个字段的事实。如果我们想考虑这一点,我们需要将实例和字段的唯一组合映射到相应的值,即我们需要某种Map<Field,<Map<Object, Object>>,其中嵌套映射中的键是实例(或静态的类名fields) 并且该值是当前字段值。管理这种数据结构将比我们现在拥有的简单集合更复杂,特别是如果我们还想在取消分配值后删除条目。
-
目前我们使用简单的HashSet。实际上,这是不正确的,因为普通集合检查对象值相等,而不是对象标识。 IE。我们可能有冲突的实例a 和b,其中a.equals(b),但a != b。为了解决这个问题,我们需要一个IdentitySet,或者根据我在第一个要点中建议的改进,一个IdentityMap。
-
考虑到前面两个要点,更好的选择可能是一个专用类MeasuredEntity,其字段为Field field、Object instance、Object value,并正确定义equals和hashCode方法。然后我们可以再次使用一个简单的哈希集来记账。
-
警告:任何类型的基于 AspectJ 的簿记都无法解决一个问题:如果一个对象当前分配给任何 @Measured 字段,例如 A.x,那么如果您调用 @,当然会触发测量建议987654363@。但是无法知道同一对象是否同时分配给非测量字段B.y 或局部变量myLocal。因此,如果调用 B.y.doSomething() 或 myLocal.doSomething(),测量建议也会触发,这可能是正常的,也可能是无意的,具体取决于您的个人情况。我觉得没问题,你只要觉知就好了。
-
如果实例超出范围并且有资格进行垃圾回收,则不会从集合或映射中删除对实例的引用。为了实现这一点,我们需要使用弱引用。
-
一个较小的问题是getField(..) 使用field.setAccessible(true)。我们可能希望在某种try {} finally {} 中使用后重置该字段的可访问状态,首先将原始值存储在boolean 变量中,然后在finally 块中重置它。但是,如果这不打扰您,这确实只是一个小问题,并且在经常调用时也会使代码执行速度变慢。
结论:人工记账不仅复杂,而且在实践中也带来了一些难以完全解决的挑战。不走这条路而是改变应用程序设计,以便能够在不记录内部对象状态的情况下应用方面,有效地打破类封装,是更好的选择 IMO。