Prüfbarkeit und Dependency Injection - AWS Flow Framework für Java

Die vorliegende Übersetzung wurde maschinell erstellt. Im Falle eines Konflikts oder eines Widerspruchs zwischen dieser übersetzten Fassung und der englischen Fassung (einschließlich infolge von Verzögerungen bei der Übersetzung) ist die englische Fassung maßgeblich.

Prüfbarkeit und Dependency Injection

Das Framework ist auf die Unterstützung von IoC (Inversion of Control, Umkehr des Kontrollflusses) ausgelegt. Aktivitäts- und Workflow-Implementierungen sowie die vom Framework bereitgestellten Worker und Kontextobjekte können mit Containern wie Spring konfiguriert und instanziiert werden. Das Framework kann standardmäßig in das Spring Framework integriert werden. Darüber hinaus wird für die Implementierungen von Einheitentest-Workflows und Aktivitäten eine Integration in JUnit unterstützt.

Spring-Integration

Das Paket "com.amazonaws.services.simpleworkflow.flow.spring" enthält Klassen, die die Verwendung des Spring-Frameworks in Ihren Anwendungen vereinfacht. Dazu zählen benutzerdefinierte Scope-und-Spring-fähige Aktivitäts- und Workflow-Worker: WorkflowScope, SpringWorkflowWorker und SpringActivityWorker. Diese Klassen ermöglichen Ihnen die vollständige Konfiguration Ihrer Workflow- und Aktivitätsimplementierungen sowie der Worker mit Spring.

WorkflowScope

WorkflowScope – Eine benutzerdefinierte Spring Scope-Implementierung, die vom Framework bereitgestellt wird. Mit diesem Scope können Sie Objekte in Spring-Container erstellen, dessen Lebensdauer an die der Entscheidungsaufgabe angepasst ist. Die Beans in diesem Scope werden immer dann instanziiert, wenn der Worker eine neue Entscheidungsaufgabe empfängt. Sie sollten diesen Scope für Workflow-Implementierungs-Beans und anderen Beans, von denen er abhängt, verwenden. Die von Spring bereitgesellten Singleton- und Prototype-Scopes sollten nicht für Workflow-Implementierungs-Beans eingesetzt werden, da das Framework erfordert, dass für jede Entscheidungsaufgabe eine neue Bean erstellt werden kann. Wenn Sie dies nicht tun, kommt es zu einem unerwünschten Verhalten.

Das folgende Beispiel zeigt einen Ausschnitt einer Spring-Konfigurationen, bei der der WorkflowScope registriert und anschließend für die Konfiguration einer Workflow-Implementierungs-Bean und einer Aktivitäts-Client-Bean eingesetzt wird.

<!-- 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>

Die Konfigurationszeile <aop:scoped-proxy proxy-target-class="false" />, die bei der Konfiguration der workflowImpl-Bean verwendet wird, ist erforderlich, da WorkflowScope ein Proxying mittels CGLIB nicht unterstützt. Sie sollte diese Konfiguration für alle Beans im WorkflowScope verwenden, die mit anderen Beans in einem anderen Scope verbunden sind. In diesem Fall muss die workflowImpl-Bean mit einer Workflow-Worker-Bean in einem Singleton-Scope verknüpft werden (siehe Beispiel unten).

Weitere Informationen zur Verwendung benutzerdefinierter Scopes finden Sie in der Spring Framework-Dokumentation.

Spring-fähige Worker

Bei der Arbeit mit Spring sollten Sie die Spring-fähigen Worker-Klassen nutzen, die vom Framework bereitgestellt werden: SpringWorkflowWorker und SpringActivityWorker. Diese Worker können mittels Spring in Ihre Anwendung eingefügt werden, wie im folgenden Beispiel gezeigt. Die Spring-fähigen Worker implementieren Springs SmartLifecycle-Schnittstelle und starten standardmäßig automatisch das Abrufen von Aufgaben, wenn der Spring-Kontext initialisiert wurde. Sie können diese Funktion deaktivieren, indem Sie die disableAutoStartup-Eigenschaft des Workers auf true setzen.

Das folgende Beispiel zeigt die Konfiguration eines Entscheiders. In diesem Beispiel werden die Schnittstellen MyActivities und MyWorkflow (hier nicht abgebildet) sowie die entsprechenden Implementierungen MyActivitiesImpl und MyWorkflowImpl verwendet. Die generierten Client-Schnittstellen und -Implementierungen sind MyWorkflowClient/MyWorkflowClientImpl und MyActivitiesClient/MyActivitiesClientImpl (ebenfalls nicht abgebildet).

Der Aktivitäts-Client wird über die "auto wire"-Funktion von Spring in die Workflow-Implementierung eingefügt:

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

Die Spring-Konfiguration des Entscheiders sieht wie folgt aus:

<?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>

Da der SpringWorkflowWorker vollständig in Spring konfiguriert wird und automatisch mit dem Abrufen beginnt, wenn der Spring-Kontext initialisiert wird, ist der Hostprozess für den Entscheider recht einfach:

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

Entsprechend kann auch der Aktivitäts-Worker wie folgt konfiguriert werden:

<?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>

Der Hostprozess des Aktivitäts-Workers ähnelt dem des Entscheiders:

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

Einfügen des Entscheidungskontexts

Wie Ihre Workflow-Implementierung von den Kontextobjekten abhängt, können Sie diese ebenfalls ganz einfach mit Spring einfügen. Das Framework registriert kontextbasierte Beans automatisch im Spring-Container. Im folgenden Codeausschnitt wurden beispielsweise verschiedene Kontextobjekte automatisch verknüpft. Eine weitere Spring-Konfiguration der Kontextobjekte ist nicht erforderlich.

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

Wenn Sie die Kontextobjekte in der Workflow-Implementierung über die Spring-XML-Konfiguration konfigurieren möchten, verwenden Sie die Bean-Namen, die in der WorkflowScopeBeanNames-Klasse im Paket "com.amazonaws.services.simpleworkflow.flow.spring" deklariert sind. z. B.:

<!-- 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>

Alternativ können Sie auch einen DecisionContextProvider in die Bean der Workflow-Implementierung einfügen und zum Erstellen des Kontexts verwenden. Dies ist hilfreich, wenn Sie benutzerdefinierte Implementierungen des Providers und Kontexts bereitstellen möchten.

Einfügen von Ressourcen in Aktivitäten

Sie können Aktivitätsimplementierungen mit einem IoC-Container instanziieren und konfigurieren und Ressourcen wie Datenbankverbindungen einfügen, indem Sie diese als Eigenschaften der Klasse der Aktivitätsimplementierung deklarieren. Diese Ressourcen werden in der Regeln als Singletons definiert. Beachten Sie, dass Aktivitätsimplementierungen vom Aktivitäts-Worker auf verschiedenen Threads aufgerufen werden. Deshalb muss der Zugriff auf freigegebene Ressourcen synchronisiert werden.

JUnit-Integration

Das Framework stellt JUnit-Erweiterungen sowie Testimplementierungen von Kontextobjekten, beispielsweise einer Testuhr, zur Verfügung, die Sie zum Schreiben und Ausführen von Einheitentests mit JUnit verwenden können. Mit diesen Erweiterungen ist ein lokaler Inline-Test der Workflow-Implementierung möglich.

Schreiben eines einfachen Einheitentests

Verwenden Sie zum Entwerfen von Tests für Ihren Workflow die WorkflowTest-Klasse aus dem Paket "com.amazonaws.services.simpleworkflow.flow.junit". Diese Klasse ist eine Framework-spezifische JUnitMethodRuleImplementierung und führt Ihren Workflow-Code lokal aus und ruft Aktivitäten inline auf anstatt Amazon SWF zu verwenden. Dadurch haben Sie die Möglichkeit, Ihre Test so oft Sie möchten, auszuführen, ohne dass Gebühren anfallen.

Wenn Sie diese Klasse verwenden möchten, deklarieren Sie einfach ein Feld vom Typ WorkflowTest und versehen es mit der Anmerkung @Rule. Erstellen Sie vor der Ausführung Ihrer Tests ein neues WorkflowTest-Objekt und fügen Sie diesem Ihre Aktivitäts- und Workflow-Implementierungen hinzu. Sie können die generierte Workflow-Client-Factory zum Erstellen eines Clients und zum Starten der Ausführung des Workflows verwenden. Das Framework stellt zudem einen benutzerdefinierten JUnit-Runner, FlowBlockJUnit4ClassRunner, bereit, der für Ihre Workflow-Tests obligatorisch ist. z. B.:

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

Sie können zudem für jede Aktivitätsimplementierung, die Sie zu WorkflowTest hinzufügen, eine separate Aufgabenliste angeben. Wenn Sie beispielsweise eine Workflow-Implementierung haben, die Aktivitäten in hostspezifischen Aufgabenlisten plant, können Sie die Aktivität in der Aufgabenliste der einzelnen Hosts registrieren:

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

Beachten Sie, dass der Code in @Test asynchron ist. Deshalb sollten Sie die Ausführung mit dem asynchronen Workflow-Client starten. Zur Überprüfung der Testergebnisse steht eine AsyncAssert-Hilfsklasse zur Verfügung. Diese Klasse ermöglicht Ihnen das Warten auf sog. Promises, die darüber informieren, dass die Operation vor der Verifizierung der Ergebnisse abgeschlossen ist. In diesem Beispiel wird auf das Ergebnis der Workflow-Ausführung gewartet, um vor dem Verifizieren der Testausgabe fertig zu sein.

Wenn Sie Spring benutzen, dann kann die SpringWorkflowTest-Klasse anstelle der WorkflowTest-Klasse verwendet werden. SpringWorkflowTest stellt Eigenschaften bereit, die Sie verwenden können, um Aktivitäts- und Workflow-Implementierungen einfach über die Spring-Konfiguration zu konfigurieren. Genau wie die Spring-fähigen Worker sollten Sie zum Konfigurieren von Workflow-Implementierungs-Beans den WorkflowScope verwenden. Das sorgt dafür, dass für jede Entscheidungsaufgabe eine neue Workflow-Implementierungs-Bean generiert wird. Achten Sie darauf, dass bei der Konfiguration dieser Beans die Einstellung "scoped-proxy proxy-target-class" auf false gesetzt ist. Weitere Informationen finden Sie im Abschnitt zur Spring-Integration. Das Beispiel der Spring-Konfiguration, das in diesem Abschnitt gezeigt wird, kann geändert werden, um den Workflow mit SpringWorkflowTest zu testen:

<?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>

Nachahmen von Aktivitätsimplementierungen

Sie können während des Testens echte Aktivitätsimplementierungen verwenden. Wenn Sie aber nur einen Einheitentest für die Workflow-Logik durchführen möchten, sollten Sie die Aktivitäten nachahmen. Dazu stellen Sie eine Mock-Implementierung der Aktivitätsschnittstelle für die WorkflowTest-Klasse bereit. z. B.:

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

Alternativ können Sie eine Mock-Implementierung des Aktivitäts-Clients bereitstellen und in Ihre Workflow-Implementierung einfügen.

Testen von Kontextobjekten

Wie Ihre Workflow-Implementierung von den Kontextobjekten des Frameworks abhängt, beispielsweise vonDecisionContextSie müssen nichts Besonderes tun, um solche Workflows testen zu können. Wird ein Test mittels WorkflowTest durchgeführt, werden automatisch Testkontextobjekte eingefügt. Greift Ihre Workflow-Implementierung auf Kontextobjekte zu, beispielsweise mitDecisionContextProviderImpl—es wird die Testimplementierung erhalten. Sie können diese Testkontextobjekte in Ihrem Testcode ändern (@Test-Methode), um relevante Testfälle zu entwerfen. Erstellt Ihr Workflow beispielsweise einen Timer, können Sie dafür sorgen, dass der Timer ausgelöst wird, indem Sie die clockAdvanceSeconds-Methode auf der WorkflowTest-Klasse aufrufen, um die Uhr vorzustellen. Mit der ClockAccelerationCoefficient-Eigenschaft im WorkflowTest können Sie ebenfalls die Uhrzeit vorstellen, damit der Timer früher als üblich ausgelöst wird. Erstellt Ihr Workflow beispielsweise einen Timer für eine Stunde, können Sie ClockAccelerationCoefficient auf 60 setzen, damit der Timer in einer Minute ausgelöst wird. Standardmäßig ist ClockAccelerationCoefficient auf "1" gesetzt.

Weitere Informationen zu den Paketen "com.amazonaws.services.simpleworkflow.flow.test" und "com.amazonaws.services.simpleworkflow.flow.junit" finden Sie in der AWS SDK for Java-Dokumentation.