使用Spring AOP和AspectJ进行工作流程编排

1.简介

您需要实现一个类似于流程的流程,最好是嵌入式流程,并且希望它是可配置的,可扩展的,易于管理和维护的。 您是否需要全尺寸BPM引擎,它带有自己的抽象负载,对于您正在寻找的简单流程编排来说似乎很繁重,或者有没有我们可以使用的轻量级替代方案而无需使用全尺寸BPM引擎? 本文演示了如何使用面向方面的编程(AOP)技术来构建和编排高度可配置和可扩展但轻量级的嵌入式处理流程。 当前的示例基于Spring AOP和Aspect J,但是可以使用其他AOP技术来实现相同的结果。

2.问题

在进一步了解之前,我们需要首先对实际问题有更好的理解,然后尝试将对问题的理解与一组可用的模式,工具和/或技术相匹配,看看是否可以找到合适的解决方案。 我们的问题是流程本身,因此让我们对其有更好的了解。 过程是什么? 流程是导致达成既定目标的协调活动的集合。 活动是指令执行的单元,是流程的基础 。 每个活动都在一块共享数据(上下文)上进行操作,以实现流程整体目标的一部分。 过程目标中已实现的部分表示已完成的事实 ,用于协调剩余活动的执行。 实质上,这将流程重新定义为仅基于事实集的规则模式来协调定义该流程的活动的执行。 为了使流程协调活动的执行,它必须了解以下属性:

  • 活动 -定义此过程的活动
  • 共享数据/上下文 -定义共享活动所完成的数据和事实的机制
  • 过渡规则 -根据已注册的事实,定义上一个活动结束后接下来要进行的活动
  • 执行决策 -定义执行过渡规则的机制
  • 初始数据/上下文(可选) -此过程要操作的共享数据的初始状态

下图显示了该过程的高级结构:

现在,我们可以按照以下要求对流程进行形式化:

  • 定义将流程组装为活动集合的机制
  • 定义个人活动
  • 定义共享数据的占位符
  • 定义在流程范围内协调那些活动的执行的机制
  • 定义过渡规则和执行决策机制,该机制根据活动记录的事实来实施过渡规则

3.建筑与设计

我们将通过解决前四个需求开始定义架构:

  • 定义将流程组装为活动集合的机制
  • 定义个人活动
  • 定义共享数据的占位符
  • 定义在流程范围内协调那些活动的执行的机制

活动是无状态工作人员,应接收包含一些数据(上下文)的令牌。 活动应在执行此活动定义的业务逻辑的同时,通过对其进行读取和写入来对该共享数据令牌进行操作。 共享数据令牌定义了流程的执行上下文。

为了忠实于我们先前设置的轻量级原则,没有理由不将我们的活动定义为实现纯旧Java接口(POJI)的纯旧Java对象(PO​​JO)。

这是Activity接口的定义,具有单个process(Object obj)方法,其中输入参数表示共享数据(上下文)的占位符:

public interface Activity {
   public void process (Object data);
}

共享数据的占位符可以是结构化或非结构化(即Map)对象。 这完全取决于您。 当前,为简单起见,我们的Activity接口将其定义为java.lang.Object ,但在实际环境中,它可能表示为某种类型的结构化对象图,该图的结构对于流程的所有参与者都是已知的。

处理
由于流程是活动的集合,因此我们需要弄清楚组装和执行此类集合的机制。

有很多方法可以做到这一点。 其中之一是将所有活动保留在某种类型的有序集合中,然后通过它进行迭代以预定的顺序调用每个活动。 这种方法的可配置性和可扩展性肯定会受到影响,因为过程控制和执行的所有方面都会被硬编码。

我们还可以以某种非常规的方式查看流程,并说该流程是一种无行为的抽象,没有具体的实现。 但是,通过活动过滤器链过滤此抽象将定义此过程的性质,状态和行为。

假设我们有一个名为GenericProcess的类,它定义了process(..)方法:

public class GenericProcess {
   public void process(Object obj){
      System.out.println("executing process");
   }
}

如果我们直接通过输入对象来调用process(..)方法,则不会发生太多事情,因为process(..)方法不会做很多事情,并且上下文的状态将保持不变。 但是,如果我们想以某种方式找到在调用process(..)方法之前引入活动并让该活动修改输入对象的process(..) ,则process(..)方法仍将保持不变,但是由于输入对象由活动进行预处理,流程上下文的整体结果将发生变化。

侦听过滤器应用于目标资源的技术已在“侦听过滤器”模式中得到了充分证明,并且在当今的企业应用程序中得到了广泛使用。 典型示例是Servlet过滤器。

拦截过滤器模式通过过滤器包装现有的应用程序资源,该过滤器拦截请求的接收和响应的发送。 拦截过滤器可以预处理或重定向应用程序请求,并且可以对应用程序响应的内容进行后处理或替换。 拦截过滤器也可以一个接一个地堆叠,以向现有资源添加一系列独立的,可声明部署的服务,而无需更改源代码-http://java.sun.com/blueprints/patterns/InterceptingFilter.html

从历史上看,该体系结构用于解决非功能性问题,例如安全性,事务处理等。但是您可以清楚地看到,通过从应用程序库中组装类似过程的结构,可以轻松地将相同的方法应用于解决应用程序的功能特征。代表各个活动的拦截过滤器链。

下图显示了如何通过过滤器链拦截对流程的调用,其中每个过滤器都与流程的各个活动相关联,从而使实际的目标流程组件无所事事,从而使其成为空且可重用的目标对象。 切换过滤器链,您将拥有一个不同的过程

剩下要做的唯一一件事就是看看是否有一个框架可以帮助我们以一种优雅的方式来组装这样的东西。

基于代理的Spring AOP似乎是最理想的选择,因为它为我们提供了简单的结构以及最重要的执行机制。 它将允许我们定义代表给定进程的活动的拦截过滤器的有序集合。 通过这些过滤器代理传入的流程请求,以活动过滤器中实现的行为来装饰流程。

这使我们只剩下一个要求:

  • 定义过渡规则和执行决策机制,该机制根据活动记录的事实来实施过渡规则

基于代理的筛选器的优点在于过渡机制本身。 每次调用目标对象(进程)时,代理机制都会依次调用拦截过滤器。 这是免费提供的,并且在必须始终调用每个Activity的情况下效果很好。 但实际上并非总是如此。 我们在问题定义中较早时做出的声明之一是: “已实现的过程目标部分表示已完成的事实 ,用于协调其余活动的执行” -这意味着一项活动的完成并不一定会向另一项活动。 在实际过程中,过渡必须严格基于先前活动完成和/或未完成的事实。 这些事实必须在共享数据占位符处注册,以便可以进行查询。

实现它就像在拦截过滤器中抛出IF语句一样简单:

public Object invoke(MethodInvocation invocation){
   if (fact(s) exists){
      invoke activity
   }
}

但这会带来一些问题。 在我们探讨它们之前,先清除一件事。 在当前结构中,每个拦截过滤器都与相应的POJO Activity紧密耦合,这是正确的。 我们可以轻松地将所有Activity逻辑保留在拦截过滤器本身的内部。 阻止我们执行此操作的唯一一件事就是希望将Activity保持为POJO,这意味着拦截过滤器中的代码将简单地委派给Activity callback

这意味着,如果将过渡评估逻辑放在Activity内,则实际上会将两个关注点耦合在一起(活动转换逻辑和活动业务逻辑),这将违反关注点分离的基本架构原理,并导致关注点/代码耦合 。 另一个问题将涉及在所有拦截过滤器之间重复相同的转换逻辑。 我们称其为关注/代码分散 。 转换逻辑可以切入所有拦截滤波器,正如您可能已经猜到的那样,AOP再次成为首选技术。
我们需要做的是写一个环绕建议 ,使我们可以拦截对实际过滤器类的目标方法的调用,评估输入,并通过允许或禁止执行目标方法来做出转换决定。 唯一的警告是我们的目标类恰好是拦截过滤器本身。 所以从本质上讲,我们试图拦截拦截器 。 不幸的是,Spring AOP不能仅仅因为它是基于代理的而在这里为我们提供帮助,并且由于我们的拦截过滤器已经是代理基础结构的一部分,因此我们根本无法代理该代理。

但是AOP的最佳功能之一是它具有几种不同的风格和实现方式(即,基于代理的,字节码编织等)。 尽管我们不能使用基于代理的AOP来代理另一个代理,但是使用字节码编织AOP并没有什么阻碍,它会通过编织(编译时或加载时)我们的代理来检测代理的各个拦截过滤器将评估逻辑过渡到其中,从而使过渡和业务逻辑保持分离。 使用AspectJ之类的框架可以轻松实现这一点。 通过这样做,我们将第二个AOP层引入我们的体系结构,这具有非常有趣的含义。 我们正在使用Spring AOP来解决功能性问题,例如对活动进行检测的过程,而AspectJ用于解决非功能性问题,例如活动导航和过渡。

下图显示了流程流程的最终结构,显示为两个AOP层,其中功能性AOP层负责通过一组有序的拦截过滤器来组装流程,而功能性AOP层则解决过渡治理的问题。

为了在工作中演示此体系结构,我们将实现一个示例用例- 采购项目 ,该项目定义了一个简单的流程。

4.用例(采购项目)

想象一下您在网上购物。 您已选择商品,将其放入购物车,经过结帐阶段,提供了信用卡信息,最后提交了购买商品的请求 。 系统将启动“ 采购物料”流程

前提条件
流程必须接收包含项目,账单和运输信息的数据

主要流量
1.验证项目可用性
2.获得信贷授权
3.流程运输

当前,该过程定义了3个活动,如下图所示:

此图还显示了未进行管理的活动过渡。 但是实际上,如果没有可用的物品该怎么办? 是否应该执行“获取信用授权”活动? 是否应遵循“流程运输”?

另一个有趣的警告是基于以下条件: 信用授权无法自动获得(授权网络已关闭),您或客户服务代表必须直接致电信用公司以获取授权号。 一旦获得该授权号并将其输入系统,应在什么时候重新开始或继续该过程? 开始还是直接进入运输? 我会说要运送,但是如何? 如何在不维护和管理大量执行控制的情况下从中间重新启动流程?

有趣的是,使用AOP,我们既不需要维护执行控制,也不需要维护过程的方向。 它是由框架本身完成的,同时通过拦截过滤器活动链来代理请求。 我们需要做的就是提出一种机制,该机制将允许或禁止基于所记录的事实执行单个筛选器。

“验证产品的可用性”将注册的事实项目可用 。 该事实应作为“获取信用授权”的前提,该条件还将注册信用授权的事实,这将作为“流程运输”活动的前提。 是否存在事实还可以用来确定何时执行特定活动,这使我们回到“手动信用验证”方案,以及如何从中间甚至更好的问题重新启动流程: 我们如何重新启动该过程是否重复该过程中已经执行的活动?

请记住,共享数据令牌(上下文)还表示流程的状态。 此状态包含在此过程中注册的所有事实 。 对这些事实进行评估以做出过渡决策。 因此,在我们的“手动信用验证”场景中,如果我们从头开始重新提交整个流程,那么我们的过渡管理机制在遇到第一个活动“验证物料可用性”时会很快意识到物料可用事实已经存在注册并不需要重复此活动,因此它将跳至下一个活动-“信用验证”。 由于还记录了信用授权的事实 (通过某种手动输入),因此它将再次跳至下一个活动“流程发送”,仅允许该活动执行和完成流程。

在转到实际示例之前,还有一个更重要的话题需要讨论,那就是定义活动的顺序
尽管起初看起来似乎很像,但这些活动的顺序在这些活动定义的流程的过渡决策中没有任何作用。

流程中活动的顺序仅代表流程本身的平衡 - 基于事实的可能性和概率的策略,事实的存在将为下一个活动执行或阻止其执行创造理想的环境。 活动顺序的更改绝不能影响整个过程

例:
传说:
d-取决于
p-产生
处理:
ProcessContext = A(d-1,2; p-3)-> B(d-1,3; p-4,5)-> C(d-4,5);

根据上面的公式,当流程在给定的ProcessContext中启动时,要查看的第一个活动是A,这取决于事实1和事实2在被调用之前是否存在。 假设事实1和事实2确实存在于ProcessContext中,则活动A将要执行并产生事实3。下一行活动是取决于事实1和事实3的活动B。知道了我们的过程,我们确定了事实1和事实2的可能性。在活动A之前发生的事实3非常低。 但是,在激活活动A之后事实3存在的可能性和概率非常高,因此活动B的顺序(其中B跟随A)。

但是,如果我们改变活动B和A的顺序,将会发生什么变化?
ProcessContext = B(d-1,3; p-4,5)-> A(d-1,2; p-3)-> C(d-4,5);

不多。 调用流程时,维护事实注册表的ProcessContext将Swift确定没有注册足够的事实来允许调用活动B,因此它将跳至下一个活动A,即事实1和事实2确实存在。对事实的评估将确定是否已注册足够的事实,以允许调用A活动,等等。 活动C也将被跳过,因为它缺少活动B产生的先决条件。如果使用同一ProcessContext重新提交流程,则将调用活动B,因为活动A在上一次调用流程时注册了活动所需的事实B满足其前提。 由于ProcesContext知道活动A会完成其工作,因此将跳过活动A。 由于活动B注册了足够的事实以满足活动C的先决条件,因此也将调用活动C。

因此,您可以看到,切换活动顺序不会改变流程行为,但可能会影响流程的自动化特性。

5.例子

该示例包含以下工件:

您可以看到,通用过程的实现不包含任何重要代码,并且实际上不应包含任何重要代码。 该类的唯一目的是充当目标类,以应用表示单个活动的拦截筛选器链。

其对应的Spring定义:

采购项目流程的其余配置包括3个部分:

第1部分 (第14行)-流程组装AOP配置,该配置包含将GenericProcessImpl.execute(..)方法定义为联接点的切入点 。 您还可以看到我们正在使用bean(purchseItem) 切入点表达式来限定我们要拦截的bean。 这将允许我们通过创建另一个GenericProcessImpl实例来定义一个以上的进程,该实例具有不同的bean名称并应用了不同的过滤器链。
它还包含对实现为Aopaliance拦截器的“活动”过滤器的引用。 默认情况下,过滤器是按照从上到下的顺序链接的,但是为了更加明确,我们也使用order属性。

第2部分 (第30行)-通过定义ActivityFilterInterceptor的三个实例来配置活动拦截器。 每个实例都将注入稍后定义的相应POJO Activity bean以及事实属性。 Facts属性定义了一个简单的规则机制,该机制使我们可以指定一个简单的条件,基于该条件,将允许或不允许执行基础活动。 例如: validateItemFilter定义“!VALIDATED_ITEM”事实规则,该规则将解释为: 调用活动,除非VALIDATED_ITEM fact在事实注册表中注册 。 这一事实将与事实注册表尽快validatItemActivity执行,这将使本次活动执行,如果事实尚未注册和保护这一活动的执行如果再处理将被重新提交与在这个事实已经被注册了相同的执行上下文注册。

第3部分 (第47行)-为我们的流程配置三个POJO活动。

ActivityFilterInterceptor-它所做的只是调用底层的POJO Activity并注册该活动返回的事实(第53行),从而使POJO Activity可以不了解事实注册表或该过程中任何其他基础结构组件的位置(请参阅下文) )。 但是,正如我们稍后将看到的,此拦截器本身的调用是由AspectJ建议(第二AOP层)根据每个拦截器的配置中指定的事实规则控制的,从而控制各个活动的执行。

各个POJO活动仅返回他们要注册的所有事实的String数组,然后通过拥有拦截器在Fact Registry中进行注册(请参见上文)。

TransitionGovernorAspect-是一个AspectJ组件,它拦截代表单个Activity的每个Spring AOP拦截器的调用。 它通过在周围建议中评估事实规则与当前事实注册表的位置使用“ 周围建议”来做到这一点,从而决定是继续还是跳过底层活动拦截器的调用。 它通过调用任何这样做进行(..)自己的调用对象的方法(ProceedingJoinPoint thisJoinPoint)或调用进行(..) 拦截过滤器的调用对象(MethodInvocation来proxyMethodInvocation)的方法。

由于它是作为AspectJ方面实现的,因此我们需要在META-INF/aop.xml提供配置(请参见下文)。

由于我们将使用加载时AOP,因此我们需要在Spring配置中注册weaver。 我们将使用context名称空间来做到这一点:

<context:load-time-weaver/>

现在,我们可以进行测试了。 如您所见,该测试没有什么特别的。 步骤如下:

  • 获取ApplicationContext
  • 获取GenericProcess对象
  • 创建事实注册表列表
  • 创建代表输入数据以及执行上下文的对象(在我们的示例中为Map)
  • 调用处理方法

由于我们正在使用AspectJ加载时编织,因此我们需要提供-javaagent选项作为VM参数。
VM参数是:

-javaagent:lib / spring-agent.jar

lib目录中已经有spring-agient.jar
执行后,您应该看到类似于以下的输出:

从测试中可以看到,事实的初始列表为空,但是如果要使用现有事实填充它,则流程将被更改。
通过取消注释将事实添加到注册表行的注释来尝试一下。
在测试中取消注释以下行:

// factRegistry.add("VALIDATED_ITEM");

并且您的输出应更改:

六,结论

该方法演示了如何使用两层AOP来组装,编排和控制流程。 第一层使用Spring AOP实现,并将流程组装为拦截过滤器链,其中每个过滤器都注入了单独的活动。 第二层使用AspectJ实现,并提供流程的编排和流程控制。 通过一系列拦截过滤器代理我们的流程,使我们可以定义和维护流程的方向。 但是代理机制还提供了执行环境,而无需诸如BPM之类的单独引擎。 我们通过使用提供控制和执行机制的现有技术(Spring AOP)来做到这一点。

该方法重量轻且嵌入式。 它使用现有的Spring基础结构,并在流程是精心策划的活动的集合的前提下构建。 每个活动都是一个POJO,并且完全不知道管理该活动的任何基础架构/控制器组件。 这带来了几个好处。 除了松耦合的典型体系结构优势以及OSGi等技术的日益普及和采用外,将活动和活动调用控制保持分离,也为将活动实施为OSGi服务打开了大门,使每个活动都可以独立进行受管理的单元(已部署,已更新,未部署等)。 测试是另一个好处。 由于活动是POJO,因此可以在应用过程之外将它们作为POJO进行测试。 它们具有非常明确的输入/输出合同(需要的数据和期望输出的数据)。 您可以单独测试每个活动。

将控制逻辑(拦截过滤器)与业务逻辑(POJO活动)分开,还允许您插入更复杂的规则外观来处理事实规则 ,以及在测试过渡控制逻辑不会影响基础活动实现的业务逻辑的地方进行测试。

活动是独立的构建基块,可以在某些其他过程中重复使用。 例如,在组装一些其他需要信用验证的过程时,可以很容易地重用“信用验证”活动。

7.参考资料和资源

翻译自: https://www.infoq.com/articles/Orchestration-Oleg-Zhurakousky/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值