领先一步
VMware 提供培训和认证,助您加速进步。
了解更多在 The Spring Experience 大会上,我主持了一个关于各方面内容的会议。其中一个是我上周描述的 Hibernate 同步切面。另一个是能够捕获首次故障和系统状态的切面,有时称为首次故障数据捕获(First-Failure Data Capture - FFDC)。我主持这个会议是为了展示一些非常有用的切面,但这些切面在实践中可能还不常见。我经常听到人们询问除了日志、跟踪、事务管理和安全之外的其他切面。我认为 Hibernate 同步切面和 FFDC 切面都是很好的例子。
让我们设定以下两个目标
为了做到这一点,首先让我们设计一个能够为我们跟踪我们想要记录的数据的类。我们将这个类称为 CallContext。我省略了实际的实现。我会在下一篇文章中发布代码,这里的实现并不重要,而且除此之外,它只是一个非常直接的数据持有者。
public class CallContext {
/**
* Registers the root call of this call context.
* We want to distinguish between the root call
* and all subsequent calls issued in the context
* of the root call.
*/
public void setRootCall(JoinPoint rootCall) { ... }
/**
* Registers a call at a certain depth.
* @param the call to register
* @param the depth of the call
*/
public void registerCall(JoinPoint call, int depth) { ... }
/**
* Registers the first failure of this call context.
* A first failure might already have occurred in which
* case subsequent registrations of the same or different
* failures will be ignored.
*/
public void setFirstFailure(Throwable t) { ... }
/**
* Log the entire call context (i.e. output it to
* System.out).
*/
public void log(Throwable t) { ... }
}
如您所见,我们使用 AspectJ 的 JoinPoint 类型来识别程序中发生的事件。
如您所见,我只是稍微对事物进行了切分,以便出现“在*某事*发生之前/之后,做*某事*”形式的句子。剩下的唯一事情就是确定这两个*某事*,然后我们就完成了。让我们分别处理这三个不同的逻辑片段。
pointcut businessService() : call(* (@BusinessService *..*).*(..)) || call(@BusinessService * *(..));
现在我们已经识别出业务服务,可以完成第一个场景了。
public aspect FirstFailureDataCapturer {
public CallContext callContext = new CallContext();
pointcut businessService() : call(@BusinessService *..*).*(..)) ||
call(@BusinessService * *(..));
before() : businessService() {
// 'thisJoinPoint' is an implicit variable (just like 'this')
// that represents the current join point
this.callContext.setRootCall(thisJoinPoint);
}
}
pointcut methodCall() : call(* *(..));
如果我们使用这个切入点,我们会将该场景应用于*所有方法*,但我们只想将其应用于业务服务内部的方法。因此,我们需要限制此切入点的范围。我们可以通过使用*cflow* 切入点指示符来做到这一点。*cflow 切入点指示符* 接受另一个切入点,并将其限制为该切入点上下文中的事件。让我们看看如何使用它来解决当前问题。请这样理解:'业务服务中的方法调用是 方法调用(参见上面定义的切入点) 同时(并且)在业务服务的 控制流 中(参见前面定义的另一个切入点)。'
pointcut methodCallInBusinessService() : methodCall() && cflow(businessService());
让我们进一步说,我们不想注册*所有*方法调用,而只是一部分有限的调用。以下定义了一个可追踪的方法,只标识了我认为相关的那些方法。它还排除了在切面自身中定义的方法(或在切面的控制流中)。后者防止了无限循环的发生。让我们也把它读出来:可追踪的方法是 业务服务中的方法调用(参见上面定义的切入点) 同时不(并且不)在 FirstFailureDataCapturer 中定义的正在执行的通知 的 控制流 中,并且它也不应该是对 equals()、hashCode() 或 getClass() 的调用。
pointcut traceableMethod() :
methodCallInBusinessService() &&
!cflow(within(FirstFailureDataCapturer) && adviceexecution()) &&
!call(* equals(Object)) && !call(* hashCode()) && !call(* getClass());
让我们使用这个切入点来实现我们确定的第二个场景。在上面对场景的描述中,我们没有指定还需要跟踪当前深度。我们使用一个 before 通知来记录当前调用。让我们也使用同一个通知来跟踪深度,并使用一个 after 通知来将深度重置回其先前的状态。
public aspect FirstFailureDataCapturer {
public CallContext callContext = new CallContext();
public int currentDepth = 0;
// other pointcuts and advices omitted
pointcut methodCallInBusinessService() : methodCall() && cflow(businessService());
pointcut traceableMethod() :
methodCallInBusinessService() &&
!cflow(within(FirstFailureDataCapturer) && adviceexecution())) &&
!call(* equals(Object)) && !call(* hashCode()) && !call(* getClass());
before() : traceableMethod() {
currentDepth++;
callContext.registerCall(thisJoinPoint, currentDepth);
}
after() : traceableMethod() {
currentDepth--;
}
}
潜在的故障点是 a) 从方法中逃逸的异常 或 b) 方法内部的异常处理器(然后被包装、吞没、可能重新抛出等等)。让我们使用这个定义来实现我们的第三个场景。第一个切入点只是使用可追踪方法切入点来识别潜在的故障点。我们稍后会使用 after throwing 通知来完成我们场景的一部分。第二个更有趣一些。它定义了一个切入点,用于识别业务服务控制流中的异常处理器(catch 块)。使用这个切入点,我们可以识别例如被捕获、包装和重新抛出的异常(或被捕获和吞没的异常)。
pointcut potentialFailurePoint() : traceableMethod();
pointcut exceptionHandler(Throwable t) : handler(*) && args(t) && cflow(businessService());
我们将使用 before 和 after 通知来完成第三个场景。首先:在异常处理器之前,记录异常
public aspect FirstFailureDataCapturer {
private CallContext context = new CallContext();
// other members omitted
before(Throwable t) : exceptionHandler(t) {
this.callContext.setFirstFailure(t);
}
}
现在,让我们定义另一个通知
public aspect FirstFailureDataCapturer {
private CallContext context = new CallContext();
// other members omitted
after() throwing(Throwable t) : potentialFailurePoint() {
this.callContext.setFirstFailure(t);
}
}
public aspect FirstFailureDataCapturer {
private CallContext context = new CallContext();
// other members omitted
after() throwing(Throwable t) : businessService() {
this.callContext.log(t);
}
}
@BusinessService public Car manufactureCar(CarModel model) {
Set <Part> parts = inventory.getPartsForModel(model);
return assemblyLine.assembleCarFromParts(model, parts);
}
在这个示例中,CarPartsInventory 是一个桩(stub),并没有真正做任何有用的事情。
public Set<Part> getPartsForModel(CarModel model) {
return new HashSet<Part>();
}
有趣的部分是 CarAssemblyLine。正如您在下面的代码中看到的,CarAssemblyLine 中有一些奇怪的代码。它首先抛出一个异常,自己捕获它,然后将其重新抛出为一个相当 MeaninglessException(无意义异常)。
public Car assembleCarFromParts(CarModel model, Set<Part> parts) {
try {
throw new OnStrikeException("The workers are on strike!");
} catch (OnStrikeException e) {
throw new MeaninglessException();
}
}
显然,在正常情况下,这个问题的真正原因(根本原因)在这种情况下永远不会被识别出来(它被捕获了,没有记录...抛出了另一个异常,并且根本原因没有传递),而且我们永远无法*精确地*在真实且首次故障(OnStrikeException)发生的那个时间点注册系统状态。幸运的是,现在我们有了 FirstFailureDataCapturer,我们可以注册根本原因并记录它。下面您可以找到一个序列图和我运行测试的一些输出。如您所见,我们不仅获得了调用堆栈,还获得了**在此业务服务执行上下文中发生的所有调用**,换句话说:完整的调用树。
现在我们已经识别出故障发生的*真实*点,我们也可以开始捕获系统状态了。可以想象,CarPlant 第 18 行的系统状态可能与 CarPlant 第 20 行的系统状态不同,而我们的 FirstFailureDataCapturer 允许我们在*正确的时间点*注册系统状态。
那么,系统状态到底是什么呢?这完全取决于运行时、您的特定应用程序以及您感兴趣的内容。几个例子:
捕获系统状态现在非常容易,我们可以在 CallContext.setFirstFailure() 方法内部进行,例如。
public aspect FirstFailureDataCapturer {
public CallContext callContext = new CallContext();
pointcut businessService() : call(@BusinessService * *(..)) || call(* (@BusinessService *..*).*(..)) || call(@BusinessService * *(..));
before() : businessService() {
// 'thisJoinPoint' is an implicit variable (just like 'this')
// that represents the current join point
this.callContext.setRootCall(thisJoinPoint);
}
}
如您所见,Call Context 在 FirstFailureDataCapturer 实例化时被实例化。当然,现在的问题是:*FirstFailureDataCapturer 何时以及实例化多少次*?而且,当您回答了这个问题后,可能会想到另一个问题:*如果在多线程环境中使用此切面会发生什么*?在下一部分中,我将讨论所有这些问题,并对切面进行一些其他更改以使其更完善。与此同时,您当然可以在评论中尝试回答这些问题 :)!我也将在下一部分提供该切面的源代码。