可試性與相依性插入 - AWS Flow Framework 對於爪哇

本文為英文版的機器翻譯版本,如內容有任何歧義或不一致之處,概以英文版為準。

可試性與相依性插入

框架設計旨在容易控制反轉 (IoC)。活動與工作流程實作以及框架提供的工作者和內容物件,都可以使用如 Spring 的容器予以設定和執行個體化。框架可立即提供與 Spring Framework 的整合。此外,也已提供與 JUnit 的整合,進行工作流程和活動實作的單元測試。

Spring 整合

com.amazonaws.services.simpleworkflow.flow.spring 套裝服務包含的類別,方便您在應用程式中使用 Spring 框架。這些包括自訂的 Scope 和 Spring 感知活動及工作流程工作者:WorkflowScopeSpringWorkflowWorkerSpringActivityWorker。這些類別讓您完全透過 Spring 設定您的工作流程和活動實作以及工作者。

WorkflowScope

WorkflowScope 是框架提供的自訂 Spring Scope 實作。此範圍讓您在生命週期不超過決策任務生命週期的 Spring 容器中建立物件。每次工作者收到決策任務時,在此範圍內的 Bean 都會執行個體化。您應該為工作流程實作 Bean 及其相依的任何其他 Bean 使用此範圍。Spring 提供的單一和原型範圍不應用於工作流程實作 Bean,因為框架需要為每項決策任務建立新的 Bean。無法執行此作業會造成未預期的行為。

下例示範 Spring 組態的程式碼片段,註冊 WorkflowScope 後,用它設定工作流程實作 Bean 和活動用戶端 Bean。

<!-- register AWS Flow Framework for Java WorkflowScope --> <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer"> <property name="scopes"> <map> <entry key="workflow"> <bean class="com.amazonaws.services.simpleworkflow.flow.spring.WorkflowScope" /> </entry> </map> </property> </bean> <!-- activities client --> <bean id="activitiesClient" class="aws.flow.sample.MyActivitiesClientImpl" scope="workflow"> </bean> <!-- workflow implementation --> <bean id="workflowImpl" class="aws.flow.sample.MyWorkflowImpl" scope="workflow"> <property name="client" ref="activitiesClient"/> <aop:scoped-proxy proxy-target-class="false" /> </bean>

組態程式碼:<aop:scoped-proxy proxy-target-class="false" />,用於 workflowImpl Bean 的組態,為必要項目,因為 WorkflowScope 不支援使用 CGLIB 做為代理。您應該為 WorkflowScope 中所有接到不同範圍中其他 Bean 的 Bean 使用此組態。在本例中,workflowImpl Bean 需要接到單一範圍中的工作流程工作者 Bean (請參閱以下的完整範例)。

您可以在 Spring Framework 文件中深入了解使用自訂的範圍。

Spring 感知工作者

使用 Spring 時,您應該使用框架提供的 Spring 感知工作者類別:SpringWorkflowWorkerSpringActivityWorker。這些工作者可使用 Spring 插入您的應用程式,如下一個範例所示。Spring 感知工作者預設會實作 Spring 的 SmartLifecycle 界面,在 Spring 內容初始化時自動開始輪詢任務。您可以將工作者的 disableAutoStartup 屬性設成 true 來關閉此功能。

下列範例示範如何設定決策者。本例使用 MyActivitiesMyWorkflow 界面 (此處不顯示) 以及對應的實作 MyActivitiesImplMyWorkflowImpl。產生的用戶端界面和實作為 MyWorkflowClient/MyWorkflowClientImplMyActivitiesClient/MyActivitiesClientImpl (此處也不顯示)。

活動用戶端會使用 Spring 的自動接線功能插入到工作流程實作中:

public class MyWorkflowImpl implements MyWorkflow { @Autowired public MyActivitiesClient client; @Override public void start() { client.activity1(); } }

決策者的 Spring 組態如下所示:

<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> <!-- register custom workflow scope --> <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer"> <property name="scopes"> <map> <entry key="workflow"> <bean class="com.amazonaws.services.simpleworkflow.flow.spring.WorkflowScope" /> </entry> </map> </property> </bean> <context:annotation-config/> <bean id="accesskeys" class="com.amazonaws.auth.BasicAWSCredentials"> <constructor-arg value="{AWS.Access.ID}"/> <constructor-arg value="{AWS.Secret.Key}"/> </bean> <bean id="clientConfiguration" class="com.amazonaws.ClientConfiguration"> <property name="socketTimeout" value="70000" /> </bean> <!-- Amazon SWF client --> <bean id="swfClient" class="com.amazonaws.services.simpleworkflow.AmazonSimpleWorkflowClient"> <constructor-arg ref="accesskeys" /> <constructor-arg ref="clientConfiguration" /> <property name="endpoint" value="{service.url}" /> </bean> <!-- activities client --> <bean id="activitiesClient" class="aws.flow.sample.MyActivitiesClientImpl" scope="workflow"> </bean> <!-- workflow implementation --> <bean id="workflowImpl" class="aws.flow.sample.MyWorkflowImpl" scope="workflow"> <property name="client" ref="activitiesClient"/> <aop:scoped-proxy proxy-target-class="false" /> </bean> <!-- workflow worker --> <bean id="workflowWorker" class="com.amazonaws.services.simpleworkflow.flow.spring.SpringWorkflowWorker"> <constructor-arg ref="swfClient" /> <constructor-arg value="domain1" /> <constructor-arg value="tasklist1" /> <property name="registerDomain" value="true" /> <property name="domainRetentionPeriodInDays" value="1" /> <property name="workflowImplementations"> <list> <ref bean="workflowImpl" /> </list> </property> </bean> </beans>

因為 SpringWorkflowWorker 在 Spring 中完全設定且會在 Spring 內容初始化時自動開始輪詢,決策者的裝載程序很簡單:

public class WorkflowHost { public static void main(String[] args){ ApplicationContext context = new FileSystemXmlApplicationContext("resources/spring/WorkflowHostBean.xml"); System.out.println("Workflow worker started"); } }

同樣地,活動工作者可設定如下:

<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> <!-- register custom scope --> <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer"> <property name="scopes"> <map> <entry key="workflow"> <bean class="com.amazonaws.services.simpleworkflow.flow.spring.WorkflowScope" /> </entry> </map> </property> </bean> <bean id="accesskeys" class="com.amazonaws.auth.BasicAWSCredentials"> <constructor-arg value="{AWS.Access.ID}"/> <constructor-arg value="{AWS.Secret.Key}"/> </bean> <bean id="clientConfiguration" class="com.amazonaws.ClientConfiguration"> <property name="socketTimeout" value="70000" /> </bean> <!-- Amazon SWF client --> <bean id="swfClient" class="com.amazonaws.services.simpleworkflow.AmazonSimpleWorkflowClient"> <constructor-arg ref="accesskeys" /> <constructor-arg ref="clientConfiguration" /> <property name="endpoint" value="{service.url}" /> </bean> <!-- activities impl --> <bean name="activitiesImpl" class="asadj.spring.test.MyActivitiesImpl"> </bean> <!-- activity worker --> <bean id="activityWorker" class="com.amazonaws.services.simpleworkflow.flow.spring.SpringActivityWorker"> <constructor-arg ref="swfClient" /> <constructor-arg value="domain1" /> <constructor-arg value="tasklist1" /> <property name="registerDomain" value="true" /> <property name="domainRetentionPeriodInDays" value="1" /> <property name="activitiesImplementations"> <list> <ref bean="activitiesImpl" /> </list> </property> </bean> </beans>

活動工作者裝載程序類似決策者:

public class ActivityHost { public static void main(String[] args) { ApplicationContext context = new FileSystemXmlApplicationContext( "resources/spring/ActivityHostBean.xml"); System.out.println("Activity worker started"); } }

插入決策內容

如果您的工作流程實作依賴這些內容物件,您也可以透過 Spring 輕鬆插入它們。框架自動在 Spring 容器中註冊與內容相關的 Bean。例如,在下列程式碼片段中,已自動接線各種內容物件。不需要內容物件的其他 Spring 組態。

public class MyWorkflowImpl implements MyWorkflow { @Autowired public MyActivitiesClient client; @Autowired public WorkflowClock clock; @Autowired public DecisionContext dcContext; @Autowired public GenericActivityClient activityClient; @Autowired public GenericWorkflowClient workflowClient; @Autowired public WorkflowContext wfContext; @Override public void start() { client.activity1(); } }

如果您想要透過 Spring XML 組態在工作流程實作中設定內容物件,請在 com.amazonaws.services.simpleworkflow.flow.spring 套裝服務中使用 WorkflowScopeBeanNames 類別中宣告的 Bean 名稱。例如:

<!-- workflow implementation --> <bean id="workflowImpl" class="asadj.spring.test.MyWorkflowImpl" scope="workflow"> <property name="client" ref="activitiesClient"/> <property name="clock" ref="workflowClock"/> <property name="activityClient" ref="genericActivityClient"/> <property name="dcContext" ref="decisionContext"/> <property name="workflowClient" ref="genericWorkflowClient"/> <property name="wfContext" ref="workflowContext"/> <aop:scoped-proxy proxy-target-class="false" /> </bean>

或者,您可將 DecisionContextProvider 插入到工作流程實作 Bean 中,用它建立內容。如果您想提供自訂的提供者和內容實作,這會很有用。

在活動中插入資源

您可以使用控制反轉 (IoC) 容器執行個體化活動實作並予以設定,然後將資源宣告為活動實作類別的屬性,輕鬆插入資料庫連線等資源。這類資源一般範圍限定為單一個。請注意,活動實作在多執行緒上是由活動工作者呼叫。因此,共享資源的存取必須予以同步。

JUnit 整合

框架提供 JUnit 延伸以及內容物件的測試實作 (例如測試時鐘),您可用來撰寫及執行搭配 JUnit 的單元測試。使用這些延伸,您可以在本機測試您的內嵌工作流程實作。

撰寫簡單的單元測試

為針對您的工作流程撰寫測試,請使用 com.amazonaws.services.simpleworkflow.flow.junit 套裝服務中的 WorkflowTest 類別。這個類是一個特定於框架的 JUnitMethodRule實作並在本機執行您的工作流程程式碼,呼叫內嵌活動而不是通過 Amazon SWF。這可讓您彈性執行測試,不限次數,也不產生任何費用。

為使用此類別,只要宣告 WorkflowTest 類型的欄位並標記以 @Rule 註釋即可。執行您的測試之前,請先建立新的 WorkflowTest 物件,並將您的活動和工作流程實作新增至此物件。然後,您可以使用產生的工作流程用戶端服務團隊來建立用戶端並開始執行工作流程。框架也提供自訂的 JUnit 執行器 FlowBlockJUnit4ClassRunner,您必須將它用於您的工作流程測試中。例如:

@RunWith(FlowBlockJUnit4ClassRunner.class) public class BookingWorkflowTest { @Rule public WorkflowTest workflowTest = new WorkflowTest(); List<String> trace; private BookingWorkflowClientFactory workflowFactory = new BookingWorkflowClientFactoryImpl(); @Before public void setUp() throws Exception { trace = new ArrayList<String>(); // Register activity implementation to be used during test run BookingActivities activities = new BookingActivitiesImpl(trace); workflowTest.addActivitiesImplementation(activities); workflowTest.addWorkflowImplementationType(BookingWorkflowImpl.class); } @After public void tearDown() throws Exception { trace = null; } @Test public void testReserveBoth() { BookingWorkflowClient workflow = workflowFactory.getClient(); Promise<Void> booked = workflow.makeBooking(123, 345, true, true); List<String> expected = new ArrayList<String>(); expected.add("reserveCar-123"); expected.add("reserveAirline-123"); expected.add("sendConfirmation-345"); AsyncAssert.assertEqualsWaitFor("invalid booking", expected, trace, booked); } }

您也可以為每一個新增至 WorkflowTest 的活動實作另行指定任務清單。例如,如果您的工作流程實作會在主機特定的任務清單中排程活動,您就可以在每部主機的任務清單中註冊活動:

for (int i = 0; i < 10; i++) { String hostname = "host" + i; workflowTest.addActivitiesImplementation(hostname, new ImageProcessingActivities(hostname)); }

請注意,@Test 中的程式碼為非同步。因此,您應該使用非同步工作流程用戶端來啟動執行。為驗證您的測試結果,也會提供 AsyncAssert 協助類別。此類別允許您等候 Promise 就緒再驗證結果。在本例中,我們會先等候工作流程執行的結果就緒,再驗證測試輸出。

如果您要使用 Spring,則可以使用 SpringWorkflowTest 類別,而不是 WorkflowTest 類別。SpringWorkflowTest 提供的屬性,可讓您輕鬆透過 Spring 組態用來設定您的活動和工作流程實作。就像 Spring 感知工作者一樣,您應該使用 WorkflowScope 來設定工作流程實作 Bean。這可確保為每一項決策任務建立新的工作流程實作 Bean。確定設定範圍限定代理 proxy-target-class 設定設為 false 的這些 Bean。如需詳細資訊,請參閱「Spring 整合」一節。在「Spring 整合」一節中示範的 Spring 組態範例可變更為使用 SpringWorkflowTest 來測試工作流程:

<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans ht tp://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframe work.org/schema/aop/spring-aop-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> <!-- register custom workflow scope --> <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer"> <property name="scopes"> <map> <entry key="workflow"> <bean class="com.amazonaws.services.simpleworkflow.flow.spring.WorkflowScope" /> </entry> </map> </property> </bean> <context:annotation-config /> <bean id="accesskeys" class="com.amazonaws.auth.BasicAWSCredentials"> <constructor-arg value="{AWS.Access.ID}" /> <constructor-arg value="{AWS.Secret.Key}" /> </bean> <bean id="clientConfiguration" class="com.amazonaws.ClientConfiguration"> <property name="socketTimeout" value="70000" /> </bean> <!-- Amazon SWF client --> <bean id="swfClient" class="com.amazonaws.services.simpleworkflow.AmazonSimpleWorkflowClient"> <constructor-arg ref="accesskeys" /> <constructor-arg ref="clientConfiguration" /> <property name="endpoint" value="{service.url}" /> </bean> <!-- activities client --> <bean id="activitiesClient" class="aws.flow.sample.MyActivitiesClientImpl" scope="workflow"> </bean> <!-- workflow implementation --> <bean id="workflowImpl" class="aws.flow.sample.MyWorkflowImpl" scope="workflow"> <property name="client" ref="activitiesClient" /> <aop:scoped-proxy proxy-target-class="false" /> </bean> <!-- WorkflowTest --> <bean id="workflowTest" class="com.amazonaws.services.simpleworkflow.flow.junit.spring.SpringWorkflowTest"> <property name="workflowImplementations"> <list> <ref bean="workflowImpl" /> </list> </property> <property name="taskListActivitiesImplementationMap"> <map> <entry> <key> <value>list1</value> </key> <ref bean="activitiesImplHost1" /> </entry> </map> </property> </bean> </beans>

模擬活動實作

您可以在測試期間使用真實的活動實作,但若只想要對工作流程邏輯進行單元測試,您應該模擬活動。將活動界面的模擬實作提供給 WorkflowTest 類別,即可執行此作業。例如:

@RunWith(FlowBlockJUnit4ClassRunner.class) public class BookingWorkflowTest { @Rule public WorkflowTest workflowTest = new WorkflowTest(); List<String> trace; private BookingWorkflowClientFactory workflowFactory = new BookingWorkflowClientFactoryImpl(); @Before public void setUp() throws Exception { trace = new ArrayList<String>(); // Create and register mock activity implementation to be used during test run BookingActivities activities = new BookingActivities() { @Override public void sendConfirmationActivity(int customerId) { trace.add("sendConfirmation-" + customerId); } @Override public void reserveCar(int requestId) { trace.add("reserveCar-" + requestId); } @Override public void reserveAirline(int requestId) { trace.add("reserveAirline-" + requestId); } }; workflowTest.addActivitiesImplementation(activities); workflowTest.addWorkflowImplementationType(BookingWorkflowImpl.class); } @After public void tearDown() throws Exception { trace = null; } @Test public void testReserveBoth() { BookingWorkflowClient workflow = workflowFactory.getClient(); Promise<Void> booked = workflow.makeBooking(123, 345, true, true); List<String> expected = new ArrayList<String>(); expected.add("reserveCar-123"); expected.add("reserveAirline-123"); expected.add("sendConfirmation-345"); AsyncAssert.assertEqualsWaitFor("invalid booking", expected, trace, booked); } }

或者,您可以提供活動用戶端的模擬實作,將它插入您的工作流程實作中。

測試內容物件

如果您的工作流程實作依賴框架內容物件(例如,DecisionContext您不需要採取任何動作對此類工作流程進行測試。透過 WorkflowTest 執行測試時,它會自動插入測試內容物件。當您的工作流實現訪問上下文對象時(例如,使用DecisionContextProviderImpl-它將得到測試實現。您可在您的測試程式碼中 (@Test 方法) 操控這些測試內容物件,建立有趣的測試案例。例如,如果您的工作流程建立計時器,您可以在 WorkflowTest 類別上呼叫 clockAdvanceSeconds 方法建立計時器觸發,向前撥動時鐘的時間。您也可以在 WorkflowTest 上使用 ClockAccelerationCoefficient 屬性,加速時鐘,建立比平常早的計時器觸發。例如,如果您的工作流程建立一小時的計時器,您可以將 ClockAccelerationCoefficient 設成 60,建立一分鐘的計時器觸發。ClockAccelerationCoefficient 預設會設定為 1。

如需 com.amazonaws.services.simpleworkflow.flow.test 和 com.amazonaws.services.simpleworkflow.flow.junit 套裝服務的詳細資訊,請參閱AWS SDK for Java文件。