Monday, March 30, 2009

Tutorial Spring Batch - File Converter, Parte 2

Ya un rato sin poner algo en el blog. Y dado que tengo pendiente la parte 2 del tutorial de Spring Batch, pues empecemos...

La primera parte de este tutorial se enfoco mas a la teoría y los conceptos básicos que maneja Spring Batch. Esta segunda parte sera mas practica y nos enfocaremos a crear el miniproyecto tutorial que demuestre un poco las funciones de este framework. Solo espero no extenderme tanto y no hacer una segunda parte de esta segunda parte :P.

Para este ejercicio vamos a suponer que tenemos que hacer un sistema que toma archivos de texto con información de contactos donde los datos vienen separados por comas y que se tiene que transformar a otro archivo de texto donde la información vendrá en registros de tamaño fijo. Cada linea del archivo separado por comas representa la información de un contacto en el siguiente estructura:
  1. Nombre
  2. Apellido
  3. e-mail personal
  4. e-mail del trabajo
  5. telefono
  6. pagina web
Empezaremos creando la plantilla del proyecto con maven 2 y el plugin archetype ejecutando el siguiente comando:
$> mvn archetype:create -DgroupId=<aqui-va-el-groupId> -DartifactId=<aqui-va-el-artifactId>
En este caso use groupId=com.jabaddon.tutorials.springbatch.fileconverter y artifactId=file-converter. Para mas información de maven 2 y el uso del plugin archetype consulten la referencia que dejo al final.

Una vez ejecutado el comando tendremos una plantilla de proyecto simple en maven 2. Para poder hacer uso de Spring Batch tendremos que indicarle a nuestro proyecto en maven donde encontrar las librerías necesarias de Spring Batch para empezar a codificar. Como usaremos la versión 2.0.0.RC2 hay que agregar a nuestro pom.xml el repositorio de spring-batch:
12:    <repositories>
13: <repository>
14: <id>spring-s3</id>
15: <name>Spring Maven MILESTONE Repository</name>
16: <url>http://s3.amazonaws.com/maven.springframework.org/milestone</url>
17: </repository>
18: </repositories>
Y las librerías que usaremos son:
20:    <dependencies>
21: <!-- Para las pruebas unitarias -->
22: <dependency>
23: <groupId>junit</groupId>
24: <artifactId>junit</artifactId>
25: <version>3.8.1</version>
26: <scope>test</scope>
27: </dependency>
28:
29: <!-- Para las pruebas unitarias con Spring -->
30: <dependency>
31: <groupId>org.springframework</groupId>
32: <artifactId>spring-test</artifactId>
33: <version>2.5.5</version>
34: <scope>test</scope>
35: </dependency>
36:
37: <!-- Para el logging -->
38: <dependency>
39: <groupId>log4j</groupId>
40: <artifactId>log4j</artifactId>
41: <version>1.2.8</version>
42: </dependency>
43:
44: <!-- Spring batch -->
45: <dependency>
46: <groupId>org.springframework.batch</groupId>
47: <artifactId>spring-batch-core</artifactId>
48: <version>2.0.0.RC2</version>
49: </dependency>
50:
51: <!-- Spring batch -->
52: <dependency>
53: <groupId>org.springframework.batch</groupId>
54: <artifactId>spring-batch-execution</artifactId>
55: <version>1.0.0.m4</version>
56: </dependency>
57:
58: <!-- Para la conexion a la base de datos -->
59: <dependency>
60: <groupId>commons-dbcp</groupId>
61: <artifactId>commons-dbcp</artifactId>
62: <version>1.2</version>
63: </dependency>
64:
65: <!-- El driver jdbc para mysql -->
66: <dependency>
67: <groupId>mysql</groupId>
68: <artifactId>mysql-connector-java</artifactId>
69: <version>5.1.6</version>
70: </dependency>
71:
72: </dependencies>
Ahora, yo tuve problemas a la hora de compilar el proyecto ya que intentaba el compilador hacerlo usando la versión 1.3 de java, por lo que tuve que agregarle lo siguiente al pom.xml para obligarlo a compilar con java 1.5:
74:    <build>
75: <plugins>
76: <plugin>
77: <groupId>org.apache.maven.plugins</groupId>
78: <artifactId>maven-compiler-plugin</artifactId>
79: <configuration>
80: <source>1.5</source>
81: <target>1.5</target>
82: <archive>
83: <manifest>
84: <addClasspath>true</addClasspath>
85: </manifest>
86: </archive>
87: </configuration>
88: </plugin>
89: </plugins>
90: </build>
Job

El Job de este proyecto solo consistira de un paso, el cual se encargara de leer la informacion de un archivo para escribirlo en otro. El bean en el xml de spring de este Job lo definiremos como:
66:    <job id="process">
67: <step id="loadNWriteFile">
68: <tasklet reader="reader" writer="writer" commit-interval="1"/>
69: </step>
70: </job>
El Step esta definido por un tasklet que se compone de un reader y un writer. El bean del reader lo definimos como:
56:    <beans:bean id="reader" scope="step" class="org.springframework.batch.item.file.FlatFileItemReader">
57: <beans:property name="resource" value="#{jobParameters[input.file.name]}" />
58: <beans:property name="lineMapper" ref="lineMapper" />
59: </beans:bean>
Lectura (reader)

El 'reader' tiene dos propiedades. La propiedad 'resource' indica la url del archivo que leera y la propiedad 'lineMapper' define un bean que mapea cada linea del archivo a un POJO (que nosotros definiremos como la clase Contacto).

Como se logra notar el valor de la propiedad 'resource' del bean 'reader' tiene el valor de '#{jobParameters[input.file.name]}', esto es asi porque de esta forma le indicamos a Spring Batch que el valor lo tome del parametro que recibe el job con el nombre 'input.file.name'.

El lineMapper que usa el reader esta definido de la siguiente forma:
51:    <beans:bean id="lineMapper" class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
52: <beans:property name="lineTokenizer"><beans:ref bean="tokenizer" /></beans:property>
53: <beans:property name="fieldSetMapper"><beans:ref bean="fieldSetMapper" /></beans:property>
54: </beans:bean>
Un LineMapper en Spring Batch es el encargado de (con ayuda de un Tokenizer) leer las lineas de una archivo para estos datos mapearlos a un POJO via el FieldSetMapper.

Como nuestro archivo esta separado por comas el Tokenizer que definimos es uno al cual se le puede especificar el caracter que divide los datos de una linea del archivo:
41:    <beans:bean id="tokenizer"
42: class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer">
43: <beans:property name="delimiter"><beans:value>,</beans:value></beans:property>
44: <beans:property name="names" value="nombre,apellido,mailPersonal,mailTrabajo,telefono,paginaWeb" />
45: </beans:bean>
Este tokenizer tiene dos propiedades. La propiedad 'delimiter' indica el caracter por el cual vendra separada la informacion que en nuestro caso es una coma ','. La propiedad 'names' indica el nombre que le pondremos a cada uno de los valores en la linea de informacion en el archivo.

El fieldSetMapper es una clase que nosotros escribimos implementando de la clase de Spring Batch org.springframework.batch.item.file.mapping.FieldSetMapper y sobreescribiendo el metodo mapFieldSet, mismo que recibe como argumento un objeto de la clase org.springframework.batch.item.file.transform.FieldSet con la cual tenemos acceso a los campos definos en el tokenizer. Por ejemplo, la implementacion del metodo mapFieldSet en el codigo del tutorial luce asi:
 8:    public Contacto mapFieldSet(FieldSet fieldset) {
9: Contacto newContacto = new Contacto();
10:
11: newContacto.setNombre(fieldset.readString("nombre"));
12: newContacto.setApellido(fieldset.readString("apellido"));
13: newContacto.setMailPersonal(fieldset.readString("mailPersonal"));
14: newContacto.setMailTrabajo(fieldset.readString("mailTrabajo"));
15: newContacto.setTelefono(fieldset.readString("telefono"));
16: newContacto.setPaginaWeb(fieldset.readString("paginaWeb"));
17:
18: return newContacto;
19: }
20:
Como se logra ver la clase FieldSet contiene varios metodos readXXX con la cual se puede leer cada uno de los valores definidos en el tokenizer.

Escritura (writer)

La informacion que lee un reader es pasada a un writer para que este haga lo que tenga que hacer con esa informacion. En nuestro caso lo que vamos a hacer con esa informacion es escribirla en otro formato a otro archivo. El bean del writer lo definimos como:
61:    <beans:bean id="writer" scope="step"
62: class="com.jabaddon.tutorials.springbatch.fileconverter.ContactoItemWriter">
63: <beans:property name="archivoSalida" value="#{jobParameters[output.file.name]}" />
64: </beans:bean>
Este writer es una clase que nosotros escribimos implementando la clase org.springframework.batch.item.ItemWriter e implementado el metodo write(List items). A esta clase se le agrego una propiedad en la que le mandamos como parametro del job la ruta y nombre del archivo en el cual se va a escribir '#{jobParameters[output.file.name]}'. El metodo write de la clase que escribimos luce asi:
26:    public void write(List<? extends Contacto> items) throws Exception {
27: LOGGER.debug("### -> write() ###");
28: BufferedWriter out = null;
29: try {
30: new File(archivoSalida).createNewFile();
31: out = new BufferedWriter(new FileWriter(archivoSalida, true));
32: LOGGER.info("-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*");
33: for (Contacto item : items) {
34: LOGGER.info("Escribiendo item : " + item);
35: out.write(item.toString());
36: out.write("\n");
37: }
38: LOGGER.info("-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*");
39: }
40: finally {
41: if (out != null) {
42: out.close();
43: }
44: }
45: LOGGER.debug("### <- write() ###");
46: }
En esta clase es donde esta la logica de escribir el otro archivo.

JobRepository

Para que Spring Batch trabaje y guarde bitacora de los procesos que corre y persista valores es necesario crear una base de datos en la cual guarda todos estos valores. Actualmente tiene soporte para las bases de datos: db2, derby, hsqldb, mysql, oracle10 y postgresql. En el caso de este tutorial se uso mysql.

El JobRepository es la interfaz con la cual un Job interactua con la base de datos para realizar la persistencia y busqueda de datos. El bean del Job, aunque no lo tiene especificado, apunta por default a otro bean con nombre 'jobRepository' el cual lo definimos asi:
32:    <beans:bean id="jobRepository"
33: class="org.springframework.batch.core.repository.support.JobRepositoryFactoryBean"
34: p:dataSource-ref="dataSource" p:transactionManager-ref="transactionManager" />
35:
De esta forma el bean dataSource y transactionManager estan definidos asi:
13:    <beans:bean id="dataSource"
14: class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
15: <beans:property name="driverClassName" value="com.mysql.jdbc.Driver"/>
16: <beans:property name="url" value="jdbc:mysql://localhost/springbatch_tutorial"/>
17: <beans:property name="username" value="root"/>
18: <beans:property name="password" value="admin"/>
19: <beans:property name="maxActive" value="5"/>
20: <beans:property name="initialSize" value="1"/>
21: </beans:bean>
22:
23: <beans:bean id="transactionManager"
24: class="org.springframework.jdbc.datasource.DataSourceTransactionManager" lazy-init="true">
25: <beans:property name="dataSource" ref="dataSource" />
26: </beans:bean>
Los diversos scripts para crear el esquema de la base de datos lo trae el zip de la distribucion de Spring Batch.

JobLauncher

Ya creado todo para poder usar Spring Batch necesitamos definir un ultimo bean el cual nos ayudara a levantar y correr los Jobs. Este bean es el jobLauncher y lo definimos asi:
32:    <beans:bean id="jobLauncher"
33: class="org.springframework.batch.core.launch.support.SimpleJobLauncher">
34: <beans:property name="jobRepository" ref="jobRepository" />
35: </beans:bean>
Este bean como lo mencione antes con el cual vamos a poder iniciar Jobs mandandole los parametros adecuados para que corran. En el caso de este tutorial se definio una clase de prueba unitaria la cual tiene el metodo siguiente:
51:    public void testJob() throws Exception {
52: LOGGER.debug("### -> testJob() ###");
53: String userdir = System.getProperty("user.dir");
54: String inputFileName = "file:///" + userdir + "/src/test/files/datos-contactos.csv";
55: String outputFileName = userdir + "/target/datos-contactos.txt";
56: JobLauncher jobLauncher = (JobLauncher) this.getApplicationContext().getBean("jobLauncher");
57: JobParametersBuilder paramsBuilder = new JobParametersBuilder();
58: paramsBuilder.addLong("date.miliseconds", System.currentTimeMillis());
59: paramsBuilder.addString("input.file.name", inputFileName);
60: paramsBuilder.addString("output.file.name", outputFileName);
61: JobExecution jobExecution =
62: jobLauncher.run((Job) this.getApplicationContext().getBean("process"),
63: paramsBuilder.toJobParameters());
64:
65: LOGGER.debug("### <- testJob() ###");
66: }
En este metodo se definen las rutas de los archivos a leer y a escribir y se crean los parametros que se le enviaran al Job y con ayuda del bean JobLauncher se ejecuta el Job.

Referencias

No comments: