blog.smart-java.nl
Ordina J-Technologies – Java Blog



Flat file parsing met Spring Batch

By: Richard Kettelerij, 7 November 2008

Legacy systemen gebruiken vaak flat files voor gegevens uitwisseling. Bij integratie met dergelijk systemen moeten deze files worden geparst. In zo’n geval kan je natuurlijk zelf met de Java Scanner of StingTokenizer aan de slag gaan maar het is waarschijnlijk verstandiger om een bestaand framework te gebruiken. Spring Batch biedt hiervoor een elegante oplossing.

Mocht je overigens meer willen weten over Spring Batch en batch processing in het algemeen? Schrijf je dan in voor Special Meeting op 18 november a.s.

Bestandsdefinitie
Als voorbeeld gebruiken we een fixed-width file afkomstig uit een legacy systeem van een fictieve online videotheek. Zoals gebruikelijk bij deze vorm van gegevensuitwisseling bestaat het bestand uit verschillende soorten records: Een header record met metadata als een bedrijfsnaam en batchnummer, een footer record met een controlegetal en uiteraard een aantal records met de daadwerkelijke informatie; in dit geval film titels.

H OrdinaVideoStore.nl 12
M Shrek II
M Lord of War
M Godfather, The
M Kungfu Panda
F 0000000000000000006

Tokenizing
Om bovenstaand bestand te kunnen parsen dient allereerst onderscheid te worden gemaakt tussen de verschillende soorten records. Aangezien dit uit het eerste karakter van elk record kan worden afgeleid gebruiken we de PrefixMatchingCompositeLineTokenizer.

<bean id="movieFileLayout"
class="org.springframework.batch.item.file.transform.PrefixMatchingCompositeLineTokenizer">
	<property name="tokenizers">
		<map>
			<entry key="H" value-ref="movieHeaderRecordLayout" />
			<entry key="M" value-ref="movieRecordLayout" />
			<entry key="F" value-ref="movieFooterRecordLayout" />
		</map>
	</property>
</bean>
 
<bean id="movieHeaderRecordLayout"
	class="org.springframework.batch.item.file.transform.FixedLengthTokenizer">
	<property name="names" value="recordtype, videostore, batchid" />
	<property name="columns" value="1,5-25,28-30" />
</bean>

Deze tokenizer geeft records op basis van het type door aan een nieuwe LineTokenizer. Zoals je in bovenstande configuratie kunt zien mapped deze tokenizer elke fixed-width kolom naar een bijbehorende kolomnaam. Hier zie je duidelijk de kracht van de configuratie mogelijkheden in Spring. Je geeft gewoon in de application context de ranges van gerelateerde kolommen op en Spring Batch doet de rest. Om deze magic mogelijk te maken heeft Spring wel wat hulp nodig in de vorm van een CustomEditorConfigurer. Deze vertaald de range definities (5-3, etc) naar betekenisvolle Range objecten.

<bean id="customEditorConfigurer"
	class="org.springframework.beans.factory.config.CustomEditorConfigurer">
	<property name="customEditors">
	  <map>
	    <entry key="org.springframework.batch.item.file.transform.Range[]">
	      <bean class="org.springframework.batch.item.file.transform.RangeArrayPropertyEditor" />
	    </entry>
	  </map>
	</property>
</bean>

Mapping naar domein objecten
Vervolgens is het de bedoeling om de kolom/veld definities naar domein objecten te vertalen. Hiervoor heeft Spring Batch de FieldSetMapper interface geïntroduceerd. Hoewel we hiermee zelf de mapping van velden naar domein objecten kunnen verzorgen is het mogelijk om dit aan Spring Batch over te laten middels een FieldSetMapper gebaseerd op Spring’s BeanWrapper. Er moet echter ook onderscheid worden gemaakt tussen de verschillende record soorten. Hiervoor is – in tegenstelling tot de prefix-enabled tokenizer – geen standaard fieldset mapper aanwezig. Daarom schijven we zelf een eenvoudige mapper:

public class PrefixAwareMovieFieldSetMapper implements FieldSetMapper {
	private FieldSetMapper headerFieldSetMapper;
	private FieldSetMapper movieFieldSetMapper;
	private FieldSetMapper footerFieldSetMapper;
 
	public Object mapLine(FieldSet fieldSet) {
		final String recordType = fieldSet.readString("recordtype");
		if (recordType.equals("H")) {
			return headerFieldSetMapper.mapLine(fieldSet);
		}
		else if (recordType.equals("M")) {
			return movieFieldSetMapper.mapLine(fieldSet);
		}
		else if (recordType.equals("F")) {
			return footerFieldSetMapper.mapLine(fieldSet);
		}
		throw new IllegalStateException("onbekend record type");
	}
	// setters
}

Wanneer we deze FieldSetMapper configureren is de mapping van tokens naar domein objecten compleet.

<bean id="movieFileFieldSetMapper"
	class="nl.ordina.batch.PrefixAwareMovieFieldSetMapper">
	<property name="headerFieldSetMapper" ref="movieHeaderFieldSetMapper" />
	<property name="movieFieldSetMapper" ref="movieFieldSetMapper" />
	<property name="footerFieldSetMapper" ref="movieFooterFieldSetMapper" />
</bean>
 
<bean id="movieHeaderFieldSetMapper" 
   class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper">
	<property name="prototypeBeanName" value="headerRecord" />
</bean>
 
<bean id="headerRecord" class="nl.ordina.batch.model.MovieHeaderRecord"
scope="prototype" />

Parsen
Nu we hebben gedefinieerd welke karakters naar welke velden mappen, en welke velden naar welk domein object mapped wordt het tijd om bestanden te parsen. Hiervoor gebruiken we een FlatFileItemReader die we injecteren met de eerder gecreërde tokenizer en fieldset mapper. Geconfigureerd binnen een batch job ziet dit er als volgt uit. Wanneer deze job wordt uitgevoerd ontvangt de “DummyWriter” voor elke record in het bronbestand een overeenkomstig domein object.

<bean id="videotheekJob" parent="simpleJob">
  <property name="steps">
    <list>
      <bean id="printRecordStep" parent="simpleStep">
	<property name="itemReader">
	   <bean class="org.springframework.batch.item.file.FlatFileItemReader">
	     <property name="resource" ref="file:///videotheek/films.txt" />
	     <property name="lineTokenizer" ref="movieFileLayout" />
	     <property name="fieldSetMapper" ref="movieFileFieldSetMapper" />
	   </bean>
	</property>
	<property name="itemWriter" ref="dummyWriter" />
      </bean>
    </list>
  </property>
</bean>

Zoals je wellicht opvalt wijst de FlatFileItemReader naar een hardcoded filepath. Momenteel biedt Spring Batch nog geen mogelijkheid om dit voor meerdere bestanden configurabel te maken. Dit heb ik inmiddels gemeld via een JIRA ticket en een oplossing bijgevoegd in de vorm van een DynamicMultiResourceItemReader.

Tot slot, Spring Batch biedt m.i. behoorlijke ondersteuning om diverse soorten bestanden te parsen. Dit is met name handig in applicaties die reeds gebruik maken van het Spring Framework. Maar Spring Batch bevat méér en is zeker het overwegen waard buiten pure Spring applicaties.

4 reacties op “Flat file parsing met Spring Batch”

  1. Jan-Kees van Andel zegt:

    Hmm, Spring Batch is dus misschien meer dan een hype? Dit ziet er wel charmant uit. Sowieso beter dan met het handje file I/O code schrijven…

  2. Richard Kettelerij zegt:

    Spring Batch is meer dan een hype. Het is m.i. bittere noodzaak aangezien JEE voornamelijk op online processing is gericht, terwijl dit lang niet altijd mogelijk is. Spring Batch begint ook langzamerhand een volwassener product te worden. Men is wel een tijd bezig geweest om het juiste abstractie niveau te vinden. Dit is vooral te zien aan het grote aantal API wijzigingen (gebruik het zelf sinds 1.0M5).

  3. Ron Thijssen zegt:

    Ziet er erg leuk uit, ben benieuwd naar wat je dinsdag nog meer te vertellen hebt :)
    Hier bij de klant hebben wij een soort gelijke constructie met hetzelfde principe van een jaar of twee oud. Daar staat alle configuratie echter in de database. Deze moet namelijk klant specifiek kunnen zijn. Echter is deze erg foutgevoelig en niet makkelijk te onderhouden. Dit ziet er een stuk praktischer uit.

  4. rwijs zegt:

    In ons project hebben we CLIEOP03 bestanden met een dergelijke structuur. Ik ga snel eens testen of ik dit gemakkelijk kan toepassen. De bestaande code doet inderdaad een handmatige mapping naar klassen. Lijkt me niet lastig om te bouwen. Op dit gedeelte komen namelijk uitbreidingen, dus kan de moeite lonen.

Laat een reactie achter