Spring Batch – Massenverarbeitung in Java

Keine Kommentare

In den Zeiten von Business Process Engines, ESBs und SOAs könnte man meinen, dass die gute alte Batchverarbeitung in Vergessenheit geraten ist. Doch gerade in Versicherungsunternehmen und Banken müssen große Datenmengen bewegt werden und da ist die Stapelverarbeitung weiterhin das Maß aller Dinge. Für einige Kunden haben wir bereits Batchprozesse auf Java-Basis implementiert und damit gute Erfahrungen gemacht. In vielen Projekten findet man heute aufwändige eigene Implementierungen. Dabei lohnt wie immer ein Blick auf eine Standardtechnologie.

Unser Partner springsource bietet neben den bereits sehr verbreiteten Komponenten, wie z.B. das springframework, Spring MVC oder Spring WebFlow auch ein auf Spring basierendes Open Source Batch Framework an. Leider finden sich im Netz viele veraltete Artikel aus den Anfangszeiten des Frameworks.

Technisch gesehen ist das Framework aber gut dokumentiert. Ich möchte an dieser Stelle nicht auf die einzelnen Details eingehen, sondern ein praxisorientiertes Beispiel vorstellen. Mit Hilfe von Spring Batch lässt sich mit wenig Aufwand und in kurzer Zeit eine funktionierende Lösung implementieren.

Beispiel aus der KFZ-Versicherung

Ein Kunde wechselt zu einem anderen Versicherungsunternehmen und nennt dabei dem neuen Versicherer seine aktuellen Schadenfreiheitsklassen. Um dies zu überprüfen wendet sich der neue Versicherer an den GDV (Gesamtverband der deutschden Versicherer). Der GDV bietet mit dem so genannte VWB-Verfahren die Möglichkeit SF-Klassen beim Vorversicherer zu verifizieren. Als Basis zur Kommunikation dienen hier Textdateien mit einem festen Satzaufbau.

Basiskonfiguration

Die Basiskonfiguration für die Verarbeitung von eingehenden VWB-Nachrichten sieht mit der Spring Batch Version 1.x so aus:

<bean id="vwbIncomingJob" parent="simpleJob">
    <property name="name" value="vwbIncoming" />
    <property name="restartable" value="true" />
    <property name="steps">
        <list>
            <bean parent="skipLimitStep">
                <property name="streams">
                    <list>
                        <ref bean="fileItemReader" />
                    </list>
                </property>            
                <property name="itemReader" ref="itemReader" />
                <property name="itemWriter" ref="itemWriter" />
                <property name="skippableExceptionClasses" value="java.lang.Exception" />
                <property name="fatalExceptionClasses">
                    <value>
                        org.springframework.beans.factory.BeanCreationNotAllowedException,
                        java.lang.IllegalStateException,
                        org.springframework.jndi.JndiLookupFailureException
                    </value>
                </property>
                <property name="skipLimit" value="${job.vwbIncoming.skipLimit}" />
                <property name="commitInterval" value="${job.vwbIncoming.commitInterval}" />
                <property name="listeners">
                    <list>
                        <ref bean="inputFile"/>                             
                        <ref bean="logFileFail" />
                        <ref bean="logFileComplete" />
                        <ref bean="itemLoggerListener"/>                             
                    </list>                    
                </property>                    
            </bean>                           
            <bean parent="taskletStep">
                <property name="tasklet" ref="mailTasklet" />
            </bean>
        </list>
    </property>    
</bean>

Ein Spring Batch Job besteht in den meisten Fällen aus 1-n Steps. Hier wird ein spezieller SkipLimitStep genutzt, wo sich genau konfigurieren lässt, welche Typen von Exceptions ggf. akzeptiert werden können oder welche den Job direkt abbrechen lassen. Dies ist in der Regel sehr hilfreich, da nicht immer alle Sätze korrekt interpretiert werden können und nicht jedesmal der Job neu gestartet werden soll.

Der Konfiguration ist auch zu entnehmen, dass die einzelnen Resources (Eingangsdatei, Logfiles) als Listener in den Step reingehangen werden. Sinn und Zweck ist auch hier die Nutzung einer Spring-Batch-Komponente, die für die saubere Erzeugung und Verarbeitung der Dateien zuständig ist. Zusätzlich lässt sich hier mit Hilfe von Platzhaltern auch der Name der Dateien variabel gestalten.

Beispiel:

<bean id="inputFile" class="org.springframework.batch.core.resource.StepExecutionResourceProxy">
     <property name="filePattern" value="file:${jboss.server.data.dir}${job.vwbIncoming.incoming.path}//%file.name%"/>
</bean>

Der Job gliedert sich in folgende Teilaufgaben:

1. ItemReader: Einlesen der GDV-Datei und Transformation von 1-n-Satzzeilen in ein XML-Dokument

Konfiguration:

<bean id="itemReader" class="org.springframework.batch.item.file.FlatFileItemReader" >    
    <property name="comments">
        <list>
            <value>#</value>
            <value>**</value>
            <value>KONTROLLE</value>
         </list>        
    </property>    
    <property name="lineTokenizer" ref="flatFileTokenizer"/>    
    <property name="resource" ref="inputFile"/>        
    <property name="fieldSetMapper" ref="vwbDokumentFieldSetMapper"/>                         
</bean>   
 
<bean id="flatFileTokenizer"   class="org.springframework.batch.item.file.transform.PrefixMatchingCompositeLineTokenizer">
    <property name="tokenizers">
        <map>
            <entry key="10" value-ref="recordType10" />
            <entry key="20" value-ref="recordType20" />
            <entry key="21" value-ref="recordType21" />
            [...]
        </map>
    </property>
</bean>
 
<bean id="recordType10" class="org.springframework.batch.item.file.transform.FixedLengthTokenizer">
    <property name="names" value="satzart, verbandsVorgangsnummer, datum, vuGstNrNachvers, vsnrBeimNachvers,                                                              anfragegrund, fahrzeugidentifizierungsNrBeimNachvers, [...]" />
    <property name="columns" value="1-2, 3-20, 7-14, 21-28, 29-48, 49-50, 51-67, [...]"/>               
</bean>

Dieser Vorgang kann fast ausschließlich über Konfiguration erzeugt werden. Der FlatFileItemReader erhält eine Referenz auf die Eingangsdatei und liefert die einzelnen Zeilen an einen LineTokenizer. Die Standard-Implementierung des PrefixMatchingCompositeLineTokenizer sorgt für die Transformation in FieldSets, vergleichbar mit einem Array bzw. einem Datenbank-ResultSet, wo sich die einzelnen Felder über die Feldposition ansprechen lassen. Der GDV liefert jeden Satz mit einer Satzart als Präfix, wodurch der LineTokenizer immer genau weiß, welche Felder er mappen muss. Für z.B. dynamische Satzlängen sind natürlich auch bereits fertige Klassen verfügbar.
Der FieldSetMapper ist hier die einzige Stelle, wo man selber Hand anlegen muss. Die Implementierung der Methode public Object mapLine(FieldSet fieldSet) erzeugt aus einem FieldSet das Zielobjekt. In diesem Beispiel wird mit Hilfe einer generischen Implementierung ein Java-Objekt erzeugt, welches später mit Hilfe von XStream in ein XML-Dokument transformiert wird.

2. ItemWriter: Verarbeitung und Persistierung der Items im Zielsystem

Aus der Sicht von Spring Batch passiert hier nicht viel und sollte auch nicht! Das Ziel sollte hier immer die Delegation an einen Business-Service sein, der die Verarbeitung vornimmt. Daraus ergibt sich nicht nur der Vorteil der besseren Testbarkeit, sondern ein Batch ist neben den verschiedenen Online-Komponenten nur ein Auslöser dieser Funktion. In einer ersten Ausbaustufe wird das Dokument lediglich an den Zielvertrag angehangen, welches im Nachgang natürlich eine manuelle Bearbeitung erfordert.

3. Tasklet: E-Mail-Versand der Logfiles

Wie bei jeder sauber implementierten Software-Komponente darf natürlich auch hier das Monitoring nicht fehlen. Denkbar sind hier verschiedene Herangehensweisen. Spring Batch bietet hier für fast jede Stelle im Job-Ablauf eine Listener-Schnittstelle an. Im VWB-Beispiel werden pro Item Logeinträge geschrieben, die Auskunft über den Erfolg/Misserfolg der Verarbeitung geben. Im letzten Arbeitsschritt versendet das MailTasklet die entsprechenden Logfiles an die verantwortlichen Personen.

<bean id="vwbIncomingTasklet" class="com.codecentric.example.batch.tasklet.MailTasklet">
    <property name="mailTo">
        <list>
            <value>${job.vwbIncoming.receiver1}</value>
            <value>${job.vwbIncoming.receiver2}</value>
            <value>${job.vwbIncoming.receiver3}</value>
        </list>
    </property>
    <property name="mailSubject" value="${job.vwbIncoming.betreff}" />
    <property name="mailText" value="${job.vwbIncoming.body}" />
    <property name="mailFrom" value="${jobs.mailtemplate.sender}" />    
    <property name="attachments">
        <map>
            <entry key="vwbIncomingErfolgreich" value-ref="logFileComplete" />
            <entry key="vwbIncomingFehler" value-ref="logFileFail" />
         </map>
    </property>           
</bean>

Testen

Und wie man es natürlich von Spring gewöhnt ist, wird auch bei Spring Batch sehr viel Wert auf einfache Testbarkeit der Komponenten gelegt. Die Job-Konfiguration lässt sich unter Berücksichtigung der notwendigen Abhängigkeiten mit den bekannten Bordmitteln von Spring testen. Hier ein Beispiel, welches die Grundlage für einen Test bilden kann:

@ContextConfiguration(locations={"classpath:/jobs/vwbIncoming.xml"})
public class VwbIncomingJobITest extends AbstractJUnit4SpringContextTests {
 
    /** Der Job-Ausführer */
    @Autowired
    private JobLauncher jobLauncher;
 
    /** Der zu testende Job */
    @Autowired
    @Qualifier("vwbIncomingJob")
    private Job job;
 
    /** Der Service für die Verarbeitung der eingehenden Dokumente */
    private BusinessService businessServiceMock;
 
    /** Die zu testende Eingabedatei */
    private static final String INPUT_FILE = "src/test/resources/vwbIncoming/vwbTest.txt";
 
    private JobParametersBuilder builder;
 
    @Before
    public void setUp() {
        businessServiceMock= (BusinessService ) applicationContext.getBean("businessServiceMock");
        builder = new JobParametersBuilder();
        Resource inputFile = new FileSystemResource(INPUT_FILE);
        builder.addString("file.name", inputFile.getFilename());
    }
 
    @Test
    public void testLaunchVwbIncomingJob() throws Exception {
        expect(businessServiceMock.verarbeiteVwbDokument(isA(VwbDokument.class))).andReturn(Boolean.TRUE);
        replay(businessServiceMock);
        JobExecution jobExecution = jobLauncher.run(job, builder.toJobParameters());
        verify(businessServiceMock);
        assertTrue(jobExecution.getStatus().equals(BatchStatus.COMPLETED));
    }
 
    [...]
 
}

Ausblick

Das gezeigte Beispiel orientiert sich noch an der alten Version des Frameworks. Aktuell ist 2.1 als Release verfügbar und bietet neben nützlichen Neuerungen auch eine Vereinfachung der Konfiguration. Hierauf werd ich dann aber in einem meiner nächsten Blogeinträge eingehen und die Unterschiede anhand des Beispiels deutlich machen. Ein weiteres interessantes Thema in diesem Zusammenhang ist auch die Nutzung der Komponente Spring Integration, wo wir dann wieder in der ESB-Welt angekommen wären 😉 Für Feedback und weitere Themenvorschläge in Bezug auf Spring Batch bin ich natürlich immer dankbar 🙂

Dennis Schulte

Dennis Schulte ist seit 2009 als Senior IT Consultant bei der codecentric AG tätig. Er unterstützt seine Kunden insbesondere im Bereich Enterprise-Architekturen, Microservices, Continuous Delivery, DevOps und der Optimierung von IT-Prozessen. Aufgrund seiner langjährigen Erfahrung als Architekt und Entwickler verfügt er über ein umfassendes Wissen im Bereich Java und Open-Source-Technologien. Seine Projektschwerpunkte liegen in der Architekturberatung und der Durchführung von Projekten in Enterprise-Unternehmen.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.