Spring unterstützt die Softwareentwicklung von Enterprise-tauglichen JVM-basierenden Anwendungen durch Vereinfachungen, Effektivität, Flexibilität, Portabilität und Förderung guter Programmierpraktiken. Spring besteht aus mehreren Spring-Projekten. Die Basis bildet das Spring Framework, welches Dependency Injection (DI), Aspect-Oriented Programming (AOP), Declarative Transaction Management, MVC Web Application, RESTful Web Services und vieles mehr bietet. Spring Boot erleichtert die einfache Entwicklung eigenständig lauffähiger Anwendungen per Convention over Configuration, die ohne XML-Konfiguration auskommen und alle nötigen Klassenbibliotheken mitbringen. Mit Hilfe einfacher Annotationen werden benötigte Eigenschaften hinzugefügt. Spring Batch erleichtert die einfache Entwicklung von Batch-Anwendungen. Es unterstützt verschiedene Formate für die Konfiguration von Jobs, beispielsweise konventionell per XML, im JSR 352 Style (siehe JSR-352 Support) oder per Java-Konfiguration. Letzteres wird in den folgenden Beispielen gezeigt. Spring Batch konkurriert mit JBatch. |
![]() ![]() |
Der neuere JSR 352 hat glücklicherweise die meisten Begrifflichkeiten von dem schon länger existierenden Spring Batch übernommen, so dass die meisten Beschreibungen für beide Varianten gelten.
> JobExplorer > Steuer- > Datenbank Konsole o.ä. (Meta-Data) > JobOperator > JobLauncher > JobRepository >
Das folgende einfache Beispiel demonstriert:
Das einfache Beispiel kann natürlich nicht die vielen Möglichkeiten und Optionen zeigen, sondern soll lediglich den Einstieg vereinfachen.
Sie können das Beispiel wahlweise entweder downloaden oder wie beschrieben Schritt für Schritt aufbauen und ausführen.
Führen Sie folgende Schritte aus:
Wir könnten uns ein erstes Programmgerüst automatisch vom "Spring Initializr" unter http://start.spring.io erstellen lassen. Im Folgenden werden jedoch alle Schritte manuell ausgeführt, um die Transparenz und Nachvollziehbarkeit zu erhöhen.
Wechseln Sie in Ihr Workspace-Verzeichnis (z.B. \MeinWorkspace) und führen Sie folgende Kommandos aus:
cd \MeinWorkspace
md SpringBatchItemChunkDemo
cd SpringBatchItemChunkDemo
md src\main\java\itemchunkdemo
md src\test\java\itemchunkdemo
md src\main\resources
md src\test\resources
tree /F
Erstellen Sie im SpringBatchItemChunkDemo-Projektverzeichnis die Maven-Projektkonfigurationsdatei: pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>itemchunkdemo</groupId> <artifactId>SpringBatchItemChunkDemo</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.1.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-batch</artifactId> </dependency> <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Sehen Sie sich die Dokus an zu:
Erzeugen Sie im src/main/resources-Verzeichnis zwei Beispieldaten-CSV-Dateien:
BeispielDaten1.csv
Anton,Alfa Berta,Bravo Caesar,Charlie Dora,Delta Emil,Echo Felix,Foxtrot
BeispielDaten2.csv
Otto,Olive
Erzeugen Sie im src/main/resources-Verzeichnis das Datenbank-Initialisierungsskript: schema-all.sql
DROP TABLE Meinedaten IF EXISTS; CREATE TABLE Meinedaten ( id BIGINT IDENTITY NOT NULL PRIMARY KEY, Vorname VARCHAR(20), Nachname VARCHAR(20), Buchstabenanzahl BIGINT );
Erzeugen Sie im src/main/java/itemchunkdemo-Verzeichnis eine DB-Entity-Klasse: Meinedaten.java
package itemchunkdemo; public class Meinedaten { private String vorname; private String nachname; private Long buchstabenanzahl; public Meinedaten() { /* ok */ } public Meinedaten( String vorname, String nachname, Long buchstabenanzahl ) { this.vorname = vorname; this.nachname = nachname; this.buchstabenanzahl = buchstabenanzahl; } public String getVorname() { return vorname; } public String getNachname() { return nachname; } public Long getBuchstabenanzahl() { return buchstabenanzahl; } public void setVorname( String vorname ) { this.vorname = vorname; } public void setNachname( String nachname ) { this.nachname = nachname; } public void setBuchstabenanzahl( Long buchstabenanzahl ) { this.buchstabenanzahl = buchstabenanzahl; } @Override public String toString() { return "MeineDaten: {" + vorname + ", " + nachname + (( buchstabenanzahl != null ) ? (", " + buchstabenanzahl + " Buchstaben}") : "}"); } }
Erzeugen Sie im src/main/java/itemchunkdemo-Verzeichnis einen Batch-Item-Processor: MeinedatenItemProcessor.java
package itemchunkdemo; import org.springframework.batch.item.ItemProcessor; /** ItemProcessor: fuehrt Geschaeftslogik aus */ public class MeinedatenItemProcessor implements ItemProcessor<Meinedaten,Meinedaten> { @Override public Meinedaten process( Meinedaten md ) throws Exception { long n = 0; if( md.getVorname() != null ) { n += md.getVorname().length(); } if( md.getNachname() != null ) { n += md.getNachname().length(); } return new Meinedaten( md.getVorname(), md.getNachname(), Long.valueOf( n ) ); } }
Diese Klasse enthält die eigentliche "Geschäftslogik" und transformiert die Input-Daten zu Output-Daten.
Sehen Sie sich die Javadoc zum
ItemProcessor
und die Erläuterungen unter
ItemProcessor
an.
Im Beispiel werden sowohl die Input- als auch die Output-Daten in Meinedaten-Klassen übergeben.
Aber dies können natürlich auch verschiedene Klassen sein.
Erzeugen Sie im src/main/java/itemchunkdemo-Verzeichnis die Job-Konfigurationsklasse: ItemChunkJobConfiguration.java
package itemchunkdemo; import javax.sql.DataSource; import org.springframework.batch.core.*; import org.springframework.batch.core.configuration.annotation.*; import org.springframework.batch.core.launch.support.RunIdIncrementer; import org.springframework.batch.item.database.BeanPropertyItemSqlParameterSourceProvider; import org.springframework.batch.item.database.JdbcBatchItemWriter; import org.springframework.batch.item.file.FlatFileItemReader; import org.springframework.batch.item.file.mapping.*; import org.springframework.batch.item.file.transform.DelimitedLineTokenizer; import org.springframework.beans.factory.annotation.*; import org.springframework.context.annotation.*; import org.springframework.core.io.*; import org.springframework.jdbc.core.JdbcTemplate; /** Definition des Batch-Prozesses */ @Configuration @EnableBatchProcessing public class ItemChunkJobConfiguration { @Autowired public JobBuilderFactory jobBuilderFactory; @Autowired public StepBuilderFactory stepBuilderFactory; @Autowired public DataSource dataSource; /** ItemReader: liest Input-Daten, im Beispiel aus CSV-Datei */ @Bean @StepScope public FlatFileItemReader<Meinedaten> meinItemReader( @Value("#{jobParameters['InputDateiPfad']}") String inputDateiPfad ) { FlatFileItemReader<Meinedaten> reader = new FlatFileItemReader<Meinedaten>(); reader.setResource( ( inputDateiPfad != null ) ? new FileSystemResource( inputDateiPfad ) : new ClassPathResource( "BeispielDaten1.csv" ) ); reader.setLineMapper( new DefaultLineMapper<Meinedaten>() {{ setLineTokenizer( new DelimitedLineTokenizer() {{ setNames( new String[] { "Vorname", "Nachname" } ); }} ); setFieldSetMapper( new BeanWrapperFieldSetMapper<Meinedaten>() {{ setTargetType( Meinedaten.class ); }} ); }} ); return reader; } /** ItemProcessor: fuehrt Geschaeftslogik aus */ @Bean public MeinedatenItemProcessor meinItemProcessor() { return new MeinedatenItemProcessor(); } /** ItemWriter: schreibt Output-Daten, im Beispiel in Datenbank */ @Bean public JdbcBatchItemWriter<Meinedaten> meinItemWriter() { JdbcBatchItemWriter<Meinedaten> writer = new JdbcBatchItemWriter<Meinedaten>(); writer.setItemSqlParameterSourceProvider( new BeanPropertyItemSqlParameterSourceProvider<Meinedaten>() ); writer.setSql( "Insert into Meinedaten ( Vorname, Nachname, Buchstabenanzahl ) Values ( :vorname, :nachname, :buchstabenanzahl )" ); writer.setDataSource( dataSource ); return writer; } /** Step: bearbeitet Chunks von Items, und ruft hierzu ItemReader, ItemProcessor und ItemWriter auf */ @Bean public Step meinStep() { return stepBuilderFactory.get( "meinStep" ) .<Meinedaten,Meinedaten> chunk( 10 ) .reader( meinItemReader( null ) ) .processor( meinItemProcessor() ) .writer( meinItemWriter() ) .build(); } /** Job: fuehrt Steps aus */ @Bean public Job meinItemChunkJob() { return jobBuilderFactory.get( "meinItemChunkJob" ) .incrementer( new RunIdIncrementer() ) .listener( meinListener() ) .flow( meinStep() ) .end() .build(); } /** Completion-Notification-Listener: ueberprueft das Ergebnis */ @Bean public JobExecutionListener meinListener() { return new JobCompletionNotificationListener( new JdbcTemplate( dataSource ) ); } }
Diese Job-Configuration-Klasse definiert den Batchprozess für das Chunk-Oriented Processing. Sehen Sie sich die Javadoc an zu: @Configuration, @EnableBatchProcessing, JobBuilderFactory, StepBuilderFactory, DataSource, FlatFileItemReader, ItemProcessor, JdbcBatchItemWriter, Step, StepBuilder.chunk(), SimpleStepBuilder.reader(), SimpleStepBuilder.processor(), SimpleStepBuilder.writer(), SimpleStepBuilder.build(), Job, JobBuilderHelper.incrementer(), RunIdIncrementer, JobBuilderHelper.listener(), JobBuilder.flow(), FlowBuilder.end(), FlowJobBuilder.build(), JobExecutionListener, sowie die Erläuterungen unter The Domain Language of Batch, Configuring and Running a Job, Java Config, Configuring a Step, Late Binding of Job and Step Attributes, ItemReaders and ItemWriters.
Sehen Sie sich auch die Liste der alternativ vorhandenen ItemReader- und ItemWriter-Implementierungen an (jeweils unter "All Known Implementing Classes").
Erzeugen Sie im src/main/java/itemchunkdemo-Verzeichnis einen Listener: JobCompletionNotificationListener.java
package itemchunkdemo; import java.sql.*; import java.util.List; import org.springframework.batch.core.*; import org.springframework.batch.core.listener.JobExecutionListenerSupport; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.*; /** Completion-Notification-Listener: ueberprueft das Ergebnis */ public class JobCompletionNotificationListener extends JobExecutionListenerSupport { private final JdbcTemplate jdbcTemplate; @Autowired public JobCompletionNotificationListener( JdbcTemplate jdbcTemplate ) { this.jdbcTemplate = jdbcTemplate; } @Override public void afterJob( JobExecution jobExecution ) { System.out.println( "---- JobInstance.JobName: " + jobExecution.getJobInstance().getJobName() ); System.out.println( " JobExecution.Status: " + jobExecution.getStatus() ); System.out.println( " JobParameters: " + jobExecution.getJobParameters() ); if( jobExecution.getStatus() == BatchStatus.COMPLETED ) { List<Meinedaten> results = jdbcTemplate.query( "Select Vorname, Nachname, Buchstabenanzahl from Meinedaten", new RowMapper<Meinedaten>() { @Override public Meinedaten mapRow( ResultSet rs, int row ) throws SQLException { return new Meinedaten( rs.getString( 1 ), rs.getString( 2 ), Long.valueOf( rs.getLong( 3 ) ) ); } } ); System.out.println( "---- Datenbankinhalt: " ); for( Meinedaten md : results ) { System.out.println( " " + md ); } } } }
Diese Klasse definiert einen Job-Execution-Listener. Sehen Sie sich die Javadoc an zu: JobExecutionListenerSupport, JobExecutionListener, JobExecution, JdbcTemplate.
Erzeugen Sie im src/main/java/itemchunkdemo-Verzeichnis die Main-Startklasse: MeineApplication.java
package itemchunkdemo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class MeineApplication { public static void main( String[] args ) throws Exception { SpringApplication.run( MeineApplication.class, args ); } }
Diese Klasse implementiert die main()-Methode. Sehen Sie sich die Javadoc an zu: @SpringBootApplication und SpringApplication, sowie die Erläuterungen zu: Spring Boot und @SpringBootApplication.
Erzeugen Sie im src/test/java/itemchunkdemo-Verzeichnis den JUnit-Modultest: ItemChunkJobTest.java
package itemchunkdemo; import javax.sql.DataSource; import org.junit.*; import org.junit.runner.RunWith; import org.springframework.batch.core.*; import org.springframework.batch.core.launch.JobLauncher; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @RunWith( SpringJUnit4ClassRunner.class ) @ContextConfiguration( classes = MeineApplication.class ) public class ItemChunkJobTest { @Autowired private JobLauncher jobLauncher; @Autowired private Job job; private JdbcTemplate jdbcTemplate; @Autowired public void setDataSource( DataSource dataSource ) { this.jdbcTemplate = new JdbcTemplate( dataSource ); } @Test public void testJob() throws Exception { // Joblauf ohne Job-Parameter: JobExecution je1 = jobLauncher.run( job, new JobParameters() ); Assert.assertEquals( BatchStatus.COMPLETED, je1.getStatus() ); Assert.assertEquals( 0, je1.getAllFailureExceptions().size() ); Assert.assertEquals( 1, je1.getStepExecutions().size() ); for( StepExecution se : je1.getStepExecutions() ) { Assert.assertEquals( BatchStatus.COMPLETED, se.getStatus() ); // 6 Datensaetze, 1 Chunk-Commit: Assert.assertEquals( 6, se.getReadCount() ); Assert.assertEquals( 6, se.getWriteCount() ); Assert.assertEquals( 1, se.getCommitCount() ); } // Joblauf mit Job-Parameter zum Input-Datei-Pfad: JobExecution je2 = jobLauncher.run( job, new JobParametersBuilder().addString( "InputDateiPfad", "src/main/resources/BeispielDaten2.csv" ).toJobParameters() ); Assert.assertEquals( BatchStatus.COMPLETED, je2.getStatus() ); Assert.assertEquals( 0, je2.getAllFailureExceptions().size() ); Assert.assertEquals( 1, je2.getStepExecutions().size() ); for( StepExecution se : je2.getStepExecutions() ) { Assert.assertEquals( BatchStatus.COMPLETED, se.getStatus() ); // 1 Datensatz, 1 Chunk-Commit: Assert.assertEquals( 1, se.getReadCount() ); Assert.assertEquals( 1, se.getWriteCount() ); Assert.assertEquals( 1, se.getCommitCount() ); } // Ueberpruefe Ergebnis in DB (6 + 1 = 7 Datensaetze): Assert.assertEquals( Integer.valueOf( 7 ), jdbcTemplate.queryForObject( "Select count(*) from Meinedaten", Integer.class ) ); Assert.assertEquals( Integer.valueOf( 70 ), jdbcTemplate.queryForObject( "Select sum(buchstabenanzahl) from Meinedaten", Integer.class ) ); } }
Erzeugen Sie im src/test/resources-Verzeichnis die Logging-Konfiguration: logback-test.xml
<?xml version="1.0" encoding="UTF-8"?> <configuration> <logger name="org.springframework" level="error" /> </configuration>
Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):
[\MeinWorkspace\SpringBatchItemChunkDemo] |- [src] | |- [main] | | |- [java] | | | '- [itemchunkdemo] | | | |- ItemChunkJobConfiguration.java | | | |- JobCompletionNotificationListener.java | | | |- MeineApplication.java | | | |- Meinedaten.java | | | '- MeinedatenItemProcessor.java | | '- [resources] | | |- BeispielDaten1.csv | | |- BeispielDaten2.csv | | '- schema-all.sql | '- [test] | |- [java] | | '- [itemchunkdemo] | | '- ItemChunkJobTest.java | '- [resources] | '- logback-test.xml '- pom.xml
Durch das spring-boot-maven-plugin wird eine ausführbare jar-Datei erzeugt, welche alle benötigten Abhängigkeiten beinhaltet.
Führen Sie im Kommandozeilenfenster aus:
cd \MeinWorkspace\SpringBatchItemChunkDemo
mvn clean test
mvn clean package
java -jar target/SpringBatchItemChunkDemo-1.0-SNAPSHOT.jar
Sowohl beim JUnit-Modultest als auch bei der Ausführung der Jar-Datei erhalten sie (gekürzt):
---- JobInstance.JobName: meinItemChunkJob JobExecution.Status: COMPLETED JobParameters: {...} ---- Datenbankinhalt: MeineDaten: {Anton, Alfa, 9 Buchstaben} MeineDaten: {Berta, Bravo, 10 Buchstaben} MeineDaten: {Caesar, Charlie, 13 Buchstaben} MeineDaten: {Dora, Delta, 9 Buchstaben} MeineDaten: {Emil, Echo, 8 Buchstaben} MeineDaten: {Felix, Foxtrot, 12 Buchstaben} ...
Sehen Sie sich die verwendeten Libs an:
mvn dependency:tree
Falls Sie sich während des Joblaufs den Inhalt der In-memory-HSQLDB ansehen wollen, fügen Sie folgende Zeile an geeigneter Stelle in Ihr Programm ein:
org.hsqldb.util.DatabaseManagerSwing.main( new String[] { "--url", "jdbc:hsqldb:mem:testdb", "--noexit" } );
Beachten Sie, dass die HSQLDB bislang nicht weiter konfiguriert wurde und deshalb als In-Memory-Datenbank verwendet wird, also bei jedem Lauf neu aufgesetzt wird, und am Ende alles vergisst. Für weitere Versuche sollten Sie entweder die HSQLDB als dateibasierte Datenbank konfigurieren, oder alternativ eine andere Datenbank verwenden.
Das folgende einfache Beispiel demonstriert:
Folgender Ablauf wird programmiert:
Start │ V meinLeerzeilenStep │ │ ┌───────┐ V V │ meinArbeitsStep ─┘CONTINUABLE │ │FINISHED │ V meinLeerzeilenStep │ V Ende
Das einfache Beispiel kann natürlich nicht die vielen Möglichkeiten und Optionen zeigen, sondern soll lediglich den Einstieg vereinfachen.
Sie können das Beispiel wahlweise entweder downloaden oder wie beschrieben Schritt für Schritt aufbauen und ausführen.
Führen Sie folgende Schritte aus:
Wechseln Sie in Ihr Workspace-Verzeichnis (z.B. \MeinWorkspace) und führen Sie folgende Kommandos aus:
cd \MeinWorkspace
md SpringBatchTaskletDemo
cd SpringBatchTaskletDemo
md src\main\resources
md src\main\java\springbatchdemo
md src\test\java\springbatchdemo
tree /F
Erstellen Sie im SpringBatchTaskletDemo-Projektverzeichnis die Maven-Projektkonfigurationsdatei: pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>springbatchdemo</groupId> <artifactId>SpringBatchTaskletDemo</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.1.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-batch</artifactId> </dependency> <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Erläuterungen hierzu finden Sie weiter oben bei der SpringBatchItemChunkDemo-pom.xml.
Erzeugen Sie im src/main/resources-Verzeichnis die Logging-Konfiguration: logback.xml
<?xml version="1.0" encoding="UTF-8"?> <configuration> <logger name="org.springframework" level="error" /> </configuration>
Erzeugen Sie im src/main/java/springbatchdemo-Verzeichnis die Main-Startklasse: MeineApplication.java
package springbatchdemo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class MeineApplication { public static void main( String[] args ) throws Exception { SpringApplication.run( MeineApplication.class, args ); } }
Erläuterungen hierzu finden Sie wieder weiter oben.
Erzeugen Sie im src/main/java/springbatchdemo-Verzeichnis die Job-Konfigurationsklasse: TaskletJobConfiguration.java
package springbatchdemo; import org.springframework.batch.core.*; import org.springframework.batch.core.configuration.annotation.*; import org.springframework.batch.core.launch.support.RunIdIncrementer; import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.core.step.tasklet.Tasklet; import org.springframework.batch.repeat.RepeatStatus; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.*; @Configuration @EnableBatchProcessing public class TaskletJobConfiguration { public static final String ANZAHLSTEPS_KEY = "AnzahlSteps"; @Autowired private JobBuilderFactory jobBuilderFactory; @Autowired private StepBuilderFactory stepBuilderFactory; @Bean public Step meinLeerzeilenStep() { return stepBuilderFactory.get( "meinLeerzeilenStep" ).tasklet( new Tasklet() { @Override public RepeatStatus execute( StepContribution sc, ChunkContext cc ) { System.out.println( "" ); return RepeatStatus.FINISHED; } } ).build(); } @Bean public Step meinArbeitsStep() { return stepBuilderFactory.get( "meinArbeitsStep" ).tasklet( new Tasklet() { @Override public RepeatStatus execute( StepContribution sc, ChunkContext cc ) { String anz = cc.getStepContext().getStepExecution().getJobParameters().getString( ANZAHLSTEPS_KEY ); int n = ( anz != null ) ? Integer.parseInt( anz ) : 4; int i = cc.getStepContext().getStepExecution().getCommitCount(); System.out.println( "Hallo Spring Batch mit Tasklet, Tasklet-Execution " + i ); return ( i < n - 1 ) ? RepeatStatus.CONTINUABLE : RepeatStatus.FINISHED; } } ).build(); } @Bean public Job meinTaskletJob() throws Exception { return jobBuilderFactory.get( "meinTaskletJob" ).incrementer( new RunIdIncrementer() ) .start( meinLeerzeilenStep() ) .next( meinArbeitsStep() ) .next( meinLeerzeilenStep() ) .build(); } }
Diese Job-Configuration-Klasse definiert den Batchprozess für das Task-Oriented Processing. Sehen Sie sich die Javadoc an zu: Tasklet, Step, Job, JobBuilder.start(), SimpleJobBuilder.next(), SimpleJobBuilder.build(), StepBuilderFactory, JobBuilderFactory. Weitere Erläuterungen hierzu finden Sie wieder weiter oben.
Erzeugen Sie im src/test/java/springbatchdemo-Verzeichnis den JUnit-Modultest: TaskletJobTest.java
package springbatchdemo; import org.junit.*; import org.junit.runner.RunWith; import org.springframework.batch.core.*; import org.springframework.batch.core.launch.JobLauncher; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @RunWith( SpringJUnit4ClassRunner.class ) @ContextConfiguration( classes = MeineApplication.class ) public class TaskletJobTest { @Autowired private JobLauncher jobLauncher; @Autowired private Job job; @Test public void testJobOk() throws Exception { final int anzahlSteps = 7; boolean b = false; System.out.println( "\nJoblauf mit Job-Parameter anzahlSteps=" + anzahlSteps + ":" ); JobExecution je = jobLauncher.run( job, new JobParametersBuilder().addString( TaskletJobConfiguration.ANZAHLSTEPS_KEY, "" + anzahlSteps ).toJobParameters() ); // Ueberpruefe Job-Ergebnis: Assert.assertEquals( BatchStatus.COMPLETED, je.getStatus() ); Assert.assertEquals( 0, je.getAllFailureExceptions().size() ); Assert.assertEquals( 3, je.getStepExecutions().size() ); for( StepExecution se : je.getStepExecutions() ) { System.out.println( "StepExecution " + se.getId() + ": StepName = " + se.getStepName() + ", CommitCount = " + se.getCommitCount() ); Assert.assertEquals( ( b = !b ) ? 1 : anzahlSteps, se.getCommitCount() ); Assert.assertEquals( BatchStatus.COMPLETED, se.getStatus() ); } } @Test public void testJobMitFehler() throws Exception { System.out.println( "\nJoblauf mit fehlerhaftem Job-Parameter (Text statt Zahl):" ); JobExecution je = jobLauncher.run( job, new JobParametersBuilder().addString( TaskletJobConfiguration.ANZAHLSTEPS_KEY, "xx" ).toJobParameters() ); // Ueberpruefe Job-Ergebnis: Assert.assertEquals( BatchStatus.FAILED, je.getStatus() ); Assert.assertEquals( 2, je.getStepExecutions().size() ); for( StepExecution se : je.getStepExecutions() ) { System.out.println( "StepExecution " + se.getId() + ": StepName = " + se.getStepName() + ", CommitCount = " + se.getCommitCount() + ", BatchStatus = " + se.getStatus() + ", ExitStatus = " + se.getExitStatus().getExitCode() ); } Assert.assertEquals( 1, je.getAllFailureExceptions().size() ); for( Throwable ex : je.getAllFailureExceptions() ) { System.out.println( ex ); } } }
Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):
[\MeinWorkspace\SpringBatchTaskletDemo] |- [src] | |- [main] | | |- [java] | | | '- [springbatchdemo] | | | |- MeineApplication.java | | | '- TaskletJobConfiguration.java | | '- [resources] | | '- logback.xml | '- [test] | '- [java] | '- [springbatchdemo] | '- TaskletJobTest.java '- pom.xml
Durch das spring-boot-maven-plugin wird eine ausführbare jar-Datei erzeugt, welche alle benötigten Abhängigkeiten beinhaltet.
Führen Sie im Kommandozeilenfenster aus:
cd \MeinWorkspace\SpringBatchTaskletDemo
mvn clean package
java -jar target/SpringBatchTaskletDemo-1.0-SNAPSHOT.jar
Bei der Ausführung der Jar-Datei erhalten sie:
Hallo Spring Batch mit Tasklet, Tasklet-Execution 0 Hallo Spring Batch mit Tasklet, Tasklet-Execution 1 Hallo Spring Batch mit Tasklet, Tasklet-Execution 2 Hallo Spring Batch mit Tasklet, Tasklet-Execution 3
Beim JUnit-Modultest wird der ArbeitsStep nicht nur 4 mal ausgeführt, sondern 7 mal, weil der entsprechende JobParameter auf 7 gesetzt wurde. Außerdem erhalten Sie zusätzlich die Ausgabe:
Joblauf mit Job-Parameter anzahlSteps=7: StepExecution 0: StepName = meinLeerzeilenStep, CommitCount = 1 StepExecution 1: StepName = meinArbeitsStep, CommitCount = 7 StepExecution 2: StepName = meinLeerzeilenStep, CommitCount = 1 Joblauf mit fehlerhaftem Job-Parameter (Text statt Zahl): StepExecution 3: StepName = meinLeerzeilenStep, CommitCount = 1, BatchStatus = COMPLETED, ExitStatus = COMPLETED StepExecution 4: StepName = meinArbeitsStep, CommitCount = 0, BatchStatus = FAILED, ExitStatus = FAILED java.lang.NumberFormatException: For input string: "xx"
Das folgende einfache Beispiel demonstriert:
Folgender Ablauf wird programmiert:
Start │ V ArbeitsStep ────────┐ FAILED │ │ ok │ V V OkStep FehlerbehandlungsStep │ │ V V AbschliessenderStep │ V Ende
Sie können das Beispiel wahlweise entweder downloaden oder wie beschrieben Schritt für Schritt aufbauen und ausführen:
Voraussetzung für dieses Beispiel ist das vorherige Beispiel Spring-Batch-Tasklet-Demo.
Wechseln Sie in Ihr Workspace-Verzeichnis (z.B. \MeinWorkspace) und führen Sie folgende Kommandos aus:
cd \MeinWorkspace
xcopy SpringBatchTaskletDemo SpringBatchConditionalFlowDemo\ /S
cd SpringBatchConditionalFlowDemo
ren src\main\java\springbatchdemo\TaskletJobConfiguration.java ConditionalFlowJobConfiguration.java
ren src\test\java\springbatchdemo\TaskletJobTest.java ConditionalFlowJobTest.java
rmdir /S /Q target
tree /F
Ersetzen Sie im neuen SpringBatchConditionalFlowDemo-Projektverzeichnis in der pom.xml die artifactId SpringBatchTaskletDemo durch SpringBatchConditionalFlowDemo.
Ersetzen Sie im src/main/java/springbatchdemo-Verzeichnis den Inhalt der Job-Konfigurationsklasse: ConditionalFlowJobConfiguration.java
package springbatchdemo; import org.springframework.batch.core.*; import org.springframework.batch.core.configuration.annotation.*; import org.springframework.batch.core.launch.support.RunIdIncrementer; import org.springframework.batch.core.scope.context.*; import org.springframework.batch.core.step.tasklet.Tasklet; import org.springframework.batch.repeat.RepeatStatus; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.*; @Configuration @EnableBatchProcessing public class ConditionalFlowJobConfiguration { public static final String OK_ODER_FEHLER = "OK_ODER_FEHLER"; @Autowired private JobBuilderFactory jobBuilderFactory; @Autowired private StepBuilderFactory stepBuilderFactory; /** Tasklet: Zeigt Text im Kommandozeilenfenster */ public class PrintTextTasklet implements Tasklet { final String text; public PrintTextTasklet( String text ) { this.text = text; } @Override public RepeatStatus execute( StepContribution sc, ChunkContext cc ) throws Exception { System.out.println( text ); return RepeatStatus.FINISHED; } } /** Tasklet: "Geschaeftsprozess", entweder erfolgreich oder mit Fehler */ public class ArbeitsTasklet extends PrintTextTasklet { public ArbeitsTasklet( String text ) { super( text ); } @Override public RepeatStatus execute( StepContribution sc, ChunkContext cc ) throws Exception { StepContext stpCtx = cc.getStepContext(); System.out.println( "\n---- Job: " + stpCtx.getJobName() + ", mit JobParametern: " + stpCtx.getJobParameters() ); String okOderFehler = stpCtx.getStepExecution().getJobParameters().getString( OK_ODER_FEHLER ); if( ( okOderFehler != null ) ? !"ok".equalsIgnoreCase( okOderFehler ) : (Math.random() < 0.5) ) { System.out.println( super.text + ": mit Fehler" ); throw new Exception( "-- Dieser Fehler ist korrekt! --" ); } System.out.println( super.text + ": ok" ); return RepeatStatus.FINISHED; } } @Bean public Step arbeitsStep() { return stepBuilderFactory.get( "arbeitsStep" ).tasklet( new ArbeitsTasklet( "ArbeitsStep" ) ).build(); } @Bean public Step fehlerbehandlungsStep() { return stepBuilderFactory.get( "fehlerbehandlungsStep" ).tasklet( new PrintTextTasklet( "FehlerbehandlungsStep" ) ).build(); } @Bean public Step okStep() { return stepBuilderFactory.get( "okStep" ).tasklet( new PrintTextTasklet( "OkStep" ) ).build(); } @Bean public Step abschliessenderStep() { return stepBuilderFactory.get( "abschliessenderStep" ).tasklet( new PrintTextTasklet( "AbschliessenderStep" ) ).build(); } /** Job Abc: fuehrt Steps im "Conditional Flow" aus */ @Bean public Job meinConditionalFlowJobAbc() { return jobBuilderFactory.get( "jobAbc" ).incrementer( new RunIdIncrementer() ) .flow( arbeitsStep() ).on( ExitStatus.FAILED.getExitCode() ).to( fehlerbehandlungsStep() ).next( abschliessenderStep() ) .from( arbeitsStep() ).on( "*" ).to( okStep() ).next( abschliessenderStep() ) .end().build(); } }
Diese Job-Configuration-Klasse definiert den Batchprozess. Beachten Sie, dass der Step arbeitsStep() zweimal im Job-Flow auftaucht, aber natürlich nur einmal aufgerufen wird, nämlich beim ".flow( arbeitsStep() )". Beim ".from( arbeitsStep() )" wird das vorher registrierte Ergebnis erneut ausgewertet.
Sehen Sie sich die Javadoc an zu: JobBuilder.flow(), FlowBuilder.on(), FlowBuilder.TransitionBuilder.to(), FlowBuilder.next(), FlowBuilder.from(), FlowBuilder.end(), FlowJobBuilder.build(), Tasklet, Step, Job, StepBuilderFactory, JobBuilderFactory, sowie die Erläuterungen zum: Conditional Flow.
Falls Sie komplexere "Entscheider" implementieren wollen, sehen Sie sich den JobExecutionDecider für Programmatic Flow Decisions an.
Ersetzen Sie im src/test/java/springbatchdemo-Verzeichnis den Inhalt des JUnit-Modultests: ConditionalFlowJobTest.java
package springbatchdemo; import org.junit.*; import org.junit.runner.RunWith; import org.springframework.batch.core.*; import org.springframework.batch.core.launch.JobLauncher; import org.springframework.beans.factory.annotation.*; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @RunWith( SpringJUnit4ClassRunner.class ) @ContextConfiguration( classes = MeineApplication.class ) public class ConditionalFlowJobTest { @Autowired private JobLauncher jobLauncher; @Autowired private Job jobAbc; @Test public void testJobOk() throws Exception { System.out.println( "\nJoblauf mit Job-Parameter 'ok':" ); JobExecution je = jobLauncher.run( jobAbc, new JobParametersBuilder().addString( ConditionalFlowJobConfiguration.OK_ODER_FEHLER, "ok" ).toJobParameters() ); // Ueberpruefe Job-Ergebnis: String[] steps = new String[] { "arbeitsStep", "okStep", "abschliessenderStep" }; Assert.assertEquals( "jobAbc", je.getJobInstance().getJobName() ); Assert.assertEquals( BatchStatus.COMPLETED, je.getStatus() ); Assert.assertEquals( 0, je.getAllFailureExceptions().size() ); Assert.assertEquals( 3, je.getStepExecutions().size() ); int i = 0; for( StepExecution se : je.getStepExecutions() ) { System.out.println( "StepExecution " + se.getId() + ": CommitCount = " + se.getCommitCount() + ", Status = " + se.getStatus() + ", ExitStatus = " + se.getExitStatus().getExitCode() + ", StepName = " + se.getStepName() ); Assert.assertEquals( BatchStatus.COMPLETED, se.getStatus() ); Assert.assertEquals( 1, se.getCommitCount() ); Assert.assertEquals( steps[i++], se.getStepName() ); } } @Test public void testJobMitFehler() throws Exception { System.out.println( "\nJoblauf mit Job-Parameter 'Fehler':" ); JobExecution je = jobLauncher.run( jobAbc, new JobParametersBuilder().addString( ConditionalFlowJobConfiguration.OK_ODER_FEHLER, "Fehler" ).toJobParameters() ); // Ueberpruefe Job-Ergebnis: String[] steps = new String[] { "arbeitsStep", "fehlerbehandlungsStep", "abschliessenderStep" }; BatchStatus[] baStati = new BatchStatus[] { BatchStatus.ABANDONED, BatchStatus.COMPLETED, BatchStatus.COMPLETED }; ExitStatus[] exStati = new ExitStatus[] { ExitStatus.FAILED, ExitStatus.COMPLETED, ExitStatus.COMPLETED }; int[] commits = new int[] { 0, 1, 1 }; Assert.assertEquals( "jobAbc", je.getJobInstance().getJobName() ); Assert.assertEquals( BatchStatus.COMPLETED, je.getStatus() ); Assert.assertEquals( 1, je.getAllFailureExceptions().size() ); Assert.assertEquals( 3, je.getStepExecutions().size() ); int i = 0; for( StepExecution se : je.getStepExecutions() ) { System.out.println( "StepExecution " + se.getId() + ": CommitCount = " + se.getCommitCount() + ", Status = " + se.getStatus() + ", ExitStatus = " + se.getExitStatus().getExitCode() + ", StepName = " + se.getStepName() ); Assert.assertEquals( commits[i], se.getCommitCount() ); Assert.assertEquals( baStati[i], se.getStatus() ); Assert.assertEquals( exStati[i].getExitCode(), se.getExitStatus().getExitCode() ); Assert.assertEquals( steps[i++], se.getStepName() ); } } }
Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):
[\MeinWorkspace\SpringBatchConditionalFlowDemo] |- [src] | |- [main] | | |- [java] | | | '- [springbatchdemo] | | | |- ConditionalFlowJobConfiguration.java | | | '- MeineApplication.java | | '- [resources] | | '- logback.xml | '- [test] | '- [java] | '- [springbatchdemo] | '- ConditionalFlowJobTest.java '- pom.xml
Durch das spring-boot-maven-plugin wird eine ausführbare jar-Datei erzeugt, welche alle benötigten Abhängigkeiten beinhaltet.
Führen Sie im Kommandozeilenfenster aus:
cd \MeinWorkspace\SpringBatchConditionalFlowDemo
mvn clean package
Führen Sie das folgende Kommando mehrfach aus bis Sie beide Ausgabevarianten erhalten:
java -jar target/SpringBatchConditionalFlowDemo-1.0-SNAPSHOT.jar
Sie erhalten zufallsgesteuert mal den Ok-Fall:
ArbeitsStep: ok OkStep AbschliessenderStep
und mal den Fehlerfall:
ArbeitsStep: mit Fehler FehlerbehandlungsStep AbschliessenderStep
Beim JUnit-Modultest wird per JobParameter gesteuert, ob es sich um den Ok- oder Fehlerfall handeln soll. Der JUnit-Modultest zeigt zusätzlich an:
Joblauf mit Job-Parameter 'ok': StepExecution 0: CommitCount = 1, Status = COMPLETED, ExitStatus = COMPLETED, StepName = arbeitsStep StepExecution 1: CommitCount = 1, Status = COMPLETED, ExitStatus = COMPLETED, StepName = okStep StepExecution 2: CommitCount = 1, Status = COMPLETED, ExitStatus = COMPLETED, StepName = abschliessenderStep Joblauf mit Job-Parameter 'Fehler': StepExecution 3: CommitCount = 0, Status = ABANDONED, ExitStatus = FAILED, StepName = arbeitsStep StepExecution 4: CommitCount = 1, Status = COMPLETED, ExitStatus = COMPLETED, StepName = fehlerbehandlungsStep StepExecution 5: CommitCount = 1, Status = COMPLETED, ExitStatus = COMPLETED, StepName = abschliessenderStep
Das folgende Beispiel demonstriert:
Sie können das Beispiel wahlweise entweder downloaden oder wie beschrieben Schritt für Schritt aufbauen und ausführen:
Voraussetzung für dieses Beispiel ist das vorherige Beispiel Spring-Batch-Conditional-Flow-Demo.
Wechseln Sie in Ihr Workspace-Verzeichnis (z.B. \MeinWorkspace) und führen Sie folgende Kommandos aus:
cd \MeinWorkspace
xcopy SpringBatchConditionalFlowDemo SpringBatchMehrereJobsDemo\ /S
cd SpringBatchMehrereJobsDemo
rmdir /S /Q target
tree /F
Fügen Sie im src/main/java/springbatchdemo-Verzeichnis in der Job-Konfigurationsklasse ConditionalFlowJobConfiguration.java hinter der meinConditionalFlowJobAbc()-Job-Definition folgende weitere Job-Definition hinzu:
/** Job Xyz */ @Bean public Job meinConditionalFlowJobXyz() { return jobBuilderFactory.get( "jobXyz" ).incrementer( new RunIdIncrementer() ) .flow( arbeitsStep() ).on( ExitStatus.FAILED.getExitCode() ).to( fehlerbehandlungsStep() ).next( abschliessenderStep() ) .from( arbeitsStep() ).on( "*" ).to( okStep() ).next( abschliessenderStep() ) .end().build(); }
Um das Beispiel einfach zu halten, verwenden die Job-Definitionen zu den beiden Jobs "jobAbc" und "jobXyz" vorerst dieselben Steps. In einer echten Anwendung werden entweder die beiden Job-Definitionen größere Unterschiede aufweisen, oder beispielsweise abhängig vom Job-Namen lädt das ArbeitsTasklet unterschiedliche Properties oder führt verschiedene Tätigkeiten aus. Weiter unten folgt ein Beispiel, bei dem der Job "jobXyz" um mehrfache Conditional-Flow-Verzweigung erweitert wird.
Wenn Sie jetzt ausführen:
cd \MeinWorkspace\SpringBatchMehrereJobsDemo
mvn clean test
Dann erhalten Sie erwartungsgemäß folgende Fehlermeldung:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'springbatchdemo.ConditionalFlowJobTest': Unsatisfied dependency expressed through field 'job'; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [org.springframework.batch.core.Job] is defined: expected single matching bean but found 2: meinConditionalFlowJobAbc,meinConditionalFlowJobXyz
Ändern Sie Folgendes im src/test/java/springbatchdemo-Verzeichnis im JUnit-Modultest ConditionalFlowJobTest.java.
Ersetzen Sie
@Autowired private Job jobAbc;
durch:
@Autowired @Qualifier( "meinConditionalFlowJobAbc" ) private Job jobAbc; @Autowired @Qualifier( "meinConditionalFlowJobXyz" ) private Job jobXyz;
Fügen Sie eine weitere Test-Methode hinzu:
@Test public void testJobXyz() throws Exception { System.out.println( "\nJoblauf mit jobXyz:" ); JobExecution je = jobLauncher.run( jobXyz, new JobParametersBuilder().addString( ConditionalFlowJobConfiguration.OK_ODER_FEHLER, "ok" ).toJobParameters() ); // Ueberpruefe Job-Ergebnis: Assert.assertEquals( "jobXyz", je.getJobInstance().getJobName() ); Assert.assertEquals( BatchStatus.COMPLETED, je.getStatus() ); }
Führen Sie aus:
cd \MeinWorkspace\SpringBatchMehrereJobsDemo
mvn clean package
--> Sowohl der jobAbc als auch der jobXyz werden ausgeführt.
Führen Sie das folgende Kommando mehrfach aus damit Sie verschiedene Ausgabevarianten erhalten:
java -jar target/SpringBatchConditionalFlowDemo-1.0-SNAPSHOT.jar
--> Auch hierbei werden beide Jobs ausgeführt.
Das Wesentliche bei diesem Beispiel ist die Verwendung der
@Qualifier-Annotation,
siehe hierzu auch:
Fine-tuning annotation-based autowiring with qualifiers.
Falls Ihnen nicht gefällt, dass der Methodenname "meinConditionalFlowJobAbc" als @Qualifier-String verwendet wird:
Sie können alternativ eine "Custom Qualifier Annotation" erstellen und verwenden, siehe:
custom qualifier annotation und
A quality @Qualifier.
Das folgende Beispiel demonstriert:
und wie folgender Ablauf programmiert wird:
Start │ V ArbeitsStep ────────┐ FAILED │ │ ok │ V │ │ ArbeitsStep2 ────────┤ FAILED │ │ ok │ V V OkStep FehlerbehandlungsStep │ │ V V AbschliessenderStep │ V Ende
Sie können das Beispiel wahlweise entweder downloaden oder wie beschrieben Schritt für Schritt aufbauen und ausführen:
Voraussetzung für dieses Beispiel ist das vorherige Beispiel SpringBatchMehrereJobsDemo.
Fügen Sie im src/main/java/springbatchdemo-Verzeichnis in der Job-Konfigurationsklasse ConditionalFlowJobConfiguration.java hinter der arbeitsStep()-Definition eine weitere Step-Definition ein:
@Bean public Step arbeitsStep2() { return stepBuilderFactory.get( "arbeitsStep2" ).tasklet( new ArbeitsTasklet( "ArbeitsStep2" ) ).build(); }
Ersetzen Sie im src/main/java/springbatchdemo-Verzeichnis in der Job-Konfigurationsklasse ConditionalFlowJobConfiguration.java die meinConditionalFlowJobXyz()-Job-Definition:
/** Job Xyz */ @Bean public Job meinConditionalFlowJobXyz() { return jobBuilderFactory.get( "jobXyz" ).incrementer( new RunIdIncrementer() ) .flow( arbeitsStep() ).on( ExitStatus.FAILED.getExitCode() ).to( fehlerbehandlungsStep() ).next( abschliessenderStep() ) .from( arbeitsStep() ).on( "*" ).to( okStep() ).next( abschliessenderStep() ) .end().build(); }
durch:
/** Job Xyz mit mehrfacher Conditional-Flow-Verzweigung */ @Bean public Job meinConditionalFlowJobXyz() { return jobBuilderFactory.get( "jobXyz" ).incrementer( new RunIdIncrementer() ) .start( arbeitsStep() ) .on( ExitStatus.FAILED.getExitCode() ) .to( fehlerbehandlungsStep() ).next( abschliessenderStep() ) .from( arbeitsStep() ) .on( "*" ) .to( arbeitsStep2() ).on( ExitStatus.FAILED.getExitCode() ).to( fehlerbehandlungsStep() ).next( abschliessenderStep() ) .from( arbeitsStep2() ).on( "*" ).to( okStep() ).next( abschliessenderStep() ) .end().build(); }
Fügen Sie im src/test/java/springbatchdemo-Verzeichnis im JUnit-Modultest ConditionalFlowJobTest.java zwei import-Anweisungen hinzu:
import static org.hamcrest.Matchers.is; import static org.junit.Assert.*;
Ersetzen Sie im src/test/java/springbatchdemo-Verzeichnis im JUnit-Modultest ConditionalFlowJobTest.java die testJobXyz()-Testmethode:
@Test public void testJobXyz() throws Exception { System.out.println( "\nJoblauf mit jobXyz:" ); JobExecution je = jobLauncher.run( jobXyz, new JobParametersBuilder().addString( ConditionalFlowJobConfiguration.OK_ODER_FEHLER, "ok" ).toJobParameters() ); // Ueberpruefe Job-Ergebnis: Assert.assertEquals( "jobXyz", je.getJobInstance().getJobName() ); Assert.assertEquals( BatchStatus.COMPLETED, je.getStatus() ); }
durch:
@Test public void testJobXyz() throws JobExecutionException { // Job Xyz im OK-Fall: JobExecution je1 = jobLauncher.run( jobXyz, new JobParametersBuilder().addString( ConditionalFlowJobConfiguration.OK_ODER_FEHLER, "ok" ).toJobParameters() ); assertThat( "Job-Name falsch", je1.getJobInstance().getJobName(), is( "jobXyz" ) ); assertThat( "Job-Step-Anzahl falsch", je1.getStepExecutions().size(), is( 4 ) ); assertThat( "FailureExceptions", je1.getAllFailureExceptions().size(), is( 0 ) ); assertThat( "Job ist nicht COMPLETED", je1.getStatus(), is( BatchStatus.COMPLETED ) ); assertThat( "ExitStatus ist nicht COMPLETED", je1.getExitStatus(), is( ExitStatus.COMPLETED ) ); for( StepExecution se : je1.getStepExecutions() ) { System.out.println( "StepExecution " + se.getId() + ": CommitCount = " + se.getCommitCount() + ", Status = " + se.getStatus() + ", ExitStatus = " + se.getExitStatus().getExitCode() + ", StepName = " + se.getStepName() ); } // Job Xyz im Fehler-Fall: JobExecution je2 = jobLauncher.run( jobXyz, new JobParametersBuilder().addString( ConditionalFlowJobConfiguration.OK_ODER_FEHLER, "Fehler" ).toJobParameters() ); assertThat( "Job-Name falsch", je2.getJobInstance().getJobName(), is( "jobXyz" ) ); assertThat( "Job-Step-Anzahl falsch", je2.getStepExecutions().size(), is( 3 ) ); assertThat( "FailureExceptions", je2.getAllFailureExceptions().size(), is( 1 ) ); assertThat( "Job ist nicht COMPLETED", je2.getStatus(), is( BatchStatus.COMPLETED ) ); assertThat( "ExitStatus ist nicht COMPLETED", je2.getExitStatus(), is( ExitStatus.COMPLETED ) ); for( StepExecution se : je2.getStepExecutions() ) { System.out.println( "StepExecution " + se.getId() + ": CommitCount = " + se.getCommitCount() + ", Status = " + se.getStatus() + ", ExitStatus = " + se.getExitStatus().getExitCode() + ", StepName = " + se.getStepName() ); } }
Führen Sie aus:
cd \MeinWorkspace\SpringBatchMehrereJobsDemo
mvn clean package
--> Sowohl der jobAbc als auch der jobXyz werden sowohl für den Ok- als auch für den Fehler-Fall ausgeführt.
Führen Sie das folgende Kommando mehrfach aus damit Sie verschiedene Ausgabevarianten erhalten:
java -jar target/SpringBatchConditionalFlowDemo-1.0-SNAPSHOT.jar
--> Auch hierbei werden beide Jobs ausgeführt.
Das folgende Beispiel demonstriert:
Folgender Ablauf wird programmiert:
Start │ V Step1 │ V Step2 │ V splitFlow345: Start │ │ │ V V V Step3 Step4 Step5 │ │ │ V V V splitFlow345: Ende │ V Step6 │ V Step7 │ V Ende
Sie können das Beispiel wahlweise entweder downloaden oder wie beschrieben Schritt für Schritt aufbauen und ausführen.
Führen Sie folgende Schritte aus:
Wechseln Sie in Ihr Workspace-Verzeichnis (z.B. \MeinWorkspace) und führen Sie folgende Kommandos aus:
cd \MeinWorkspace
md SpringBatchSplitFlowDemo
cd SpringBatchSplitFlowDemo
md src\main\resources
md src\main\java\springbatchdemo
md src\test\java\springbatchdemo
tree /F
Erstellen Sie im SpringBatchSplitFlowDemo-Projektverzeichnis die schon bekannte Maven-Projektkonfigurationsdatei: pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>springbatchdemo</groupId> <artifactId>SpringBatchSplitFlowDemo</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.1.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-batch</artifactId> </dependency> <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Erläuterungen hierzu finden Sie weiter oben bei der SpringBatchItemChunkDemo-pom.xml.
Erzeugen Sie im src/main/resources-Verzeichnis die ebenfalls bekannte Logging-Konfiguration: logback.xml
<?xml version="1.0" encoding="UTF-8"?> <configuration> <logger name="org.springframework" level="error" /> </configuration>
Erzeugen Sie im src/main/java/springbatchdemo-Verzeichnis die ebenfalls bekannte Main-Startklasse: MeineApplication.java
package springbatchdemo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class MeineApplication { public static void main( String[] args ) throws Exception { SpringApplication.run( MeineApplication.class, args ); } }
Erläuterungen hierzu finden Sie wieder weiter oben.
Erzeugen Sie im src/main/java/springbatchdemo-Verzeichnis die Job-Konfigurationsklasse: SplitFlowJobConfiguration.java
package springbatchdemo; import org.springframework.batch.core.*; import org.springframework.batch.core.configuration.annotation.*; import org.springframework.batch.core.job.builder.FlowBuilder; import org.springframework.batch.core.job.flow.Flow; import org.springframework.batch.core.launch.support.RunIdIncrementer; import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.core.step.tasklet.Tasklet; import org.springframework.batch.repeat.RepeatStatus; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.*; import org.springframework.core.task.SimpleAsyncTaskExecutor; @Configuration @EnableBatchProcessing public class SplitFlowJobConfiguration { @Autowired private JobBuilderFactory jobBuilderFactory; @Autowired private StepBuilderFactory stepBuilderFactory; /** Tasklet: Zeigt Text mehrmals im Kommandozeilenfenster */ public class PrintTextMehrmalsTasklet implements Tasklet { final String text; final int anzahl; public PrintTextMehrmalsTasklet( String text, int anzahl ) { this.text = text; this.anzahl = anzahl; } @Override public RepeatStatus execute( StepContribution sc, ChunkContext cc ) throws Exception { for( int i = 0; i < anzahl; i++ ) { System.out.print( text ); Thread.sleep( 200 ); } System.out.print( " " ); return RepeatStatus.FINISHED; } } @Bean public Step step1() { return stepBuilderFactory.get( "step1" ).tasklet( new PrintTextMehrmalsTasklet( "1 ", 5 ) ).build(); } @Bean public Step step2() { return stepBuilderFactory.get( "step2" ).tasklet( new PrintTextMehrmalsTasklet( "2 ", 5 ) ).build(); } @Bean public Step step3() { return stepBuilderFactory.get( "step3" ).tasklet( new PrintTextMehrmalsTasklet( "3 ", 4 ) ).build(); } @Bean public Step step4() { return stepBuilderFactory.get( "step4" ).tasklet( new PrintTextMehrmalsTasklet( "4 ", 6 ) ).build(); } @Bean public Step step5() { return stepBuilderFactory.get( "step5" ).tasklet( new PrintTextMehrmalsTasklet( "5 ", 8 ) ).build(); } @Bean public Step step6() { return stepBuilderFactory.get( "step6" ).tasklet( new PrintTextMehrmalsTasklet( "6 ", 5 ) ).build(); } @Bean public Step step7() { return stepBuilderFactory.get( "step7" ).tasklet( new PrintTextMehrmalsTasklet( "7 ", 5 ) ).build(); } /** Job: fuehrt die drei Steps step3, step4 und step5 im "Split Flow" parallel aus */ @Bean public Job meinSplitFlowJob() { Flow flow3 = new FlowBuilder<Flow>( "flow3" ).from( step3() ).end(); Flow flow4 = new FlowBuilder<Flow>( "flow4" ).from( step4() ).end(); Flow flow5 = new FlowBuilder<Flow>( "flow5" ).from( step5() ).end(); Flow splitFlow345 = new FlowBuilder<Flow>( "splitFlow345" ) .start( flow3 ) .split( new SimpleAsyncTaskExecutor() ) .add( flow4, flow5 ) .build(); return jobBuilderFactory.get( "meinSplitFlowJob" ).incrementer( new RunIdIncrementer() ) .flow( step1() ) .next( step2() ) .next( splitFlow345 ) .next( step6() ) .next( step7() ) .end().build(); } }
Sehen Sie sich die Javadoc an zu: Flow, FlowBuilder, FlowBuilder.from(), FlowBuilder.end(), FlowBuilder.start(), FlowBuilder.split(), FlowBuilder.SplitBuilder, FlowBuilder.SplitBuilder.add(), FlowBuilder.build(), JobBuilderFactory, JobBuilder, JobBuilder.flow(), FlowBuilder.next(Step), FlowBuilder.next(Flow), FlowBuilder.end(), FlowJobBuilder.build(), SimpleAsyncTaskExecutor.
Erzeugen Sie im src/test/java/springbatchdemo-Verzeichnis den JUnit-Modultest: SplitFlowJobTest.java
package springbatchdemo; import java.text.SimpleDateFormat; import org.junit.*; import org.junit.runner.RunWith; import org.springframework.batch.core.*; import org.springframework.batch.core.launch.JobLauncher; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @RunWith( SpringJUnit4ClassRunner.class ) @ContextConfiguration( classes = MeineApplication.class ) public class SplitFlowJobTest { @Autowired private JobLauncher jobLauncher; @Autowired private Job job; private SimpleDateFormat df = new SimpleDateFormat( "HH:mm:ss.SSS" ); @Test public void testSplitFlowJob() throws Exception { System.out.println( "\nDie drei Steps step3, step4 und step5 werden im 'Split Flow' parallel ausgefuehrt:" ); JobExecution je = jobLauncher.run( job, new JobParameters() ); System.out.println( "" ); // Ueberpruefe Job-Ergebnis: Assert.assertEquals( BatchStatus.COMPLETED, je.getStatus() ); Assert.assertEquals( ExitStatus.COMPLETED, je.getExitStatus() ); Assert.assertEquals( 0, je.getAllFailureExceptions().size() ); Assert.assertEquals( 7, je.getStepExecutions().size() ); double dauerSum = 0.; for( StepExecution se : je.getStepExecutions() ) { Assert.assertEquals( BatchStatus.COMPLETED, se.getStatus() ); Assert.assertEquals( ExitStatus.COMPLETED, se.getExitStatus() ); Assert.assertEquals( 1, se.getCommitCount() ); double dauer = ((double) ((se.getEndTime().getTime() - se.getStartTime().getTime()) / 100)) / 10; dauerSum += dauer; System.out.println( se.getStepName() + ": von " + df.format( se.getStartTime() ) + " bis " + df.format( se.getEndTime() ) + " h, Dauer: " + dauer + " s" ); } System.out.println( "Summe der Steps: " + dauerSum + " s" ); double dauerJob = ((double) ((je.getEndTime().getTime() - je.getStartTime().getTime()) / 100)) / 10; System.out.println( "Gesamtdauer Job: " + dauerJob + " s" ); Assert.assertTrue( dauerSum > dauerJob ); } }
Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):
[\MeinWorkspace\SpringBatchSplitFlowDemo] |- [src] | |- [main] | | |- [java] | | | '- [springbatchdemo] | | | |- MeineApplication.java | | | '- SplitFlowJobConfiguration.java | | '- [resources] | | '- logback.xml | '- [test] | '- [java] | '- [springbatchdemo] | '- SplitFlowJobTest.java '- pom.xml
Durch das spring-boot-maven-plugin wird eine ausführbare jar-Datei erzeugt, welche alle benötigten Abhängigkeiten beinhaltet.
Führen Sie im Kommandozeilenfenster aus:
cd \MeinWorkspace\SpringBatchSplitFlowDemo
mvn clean package
java -jar target/SpringBatchSplitFlowDemo-1.0-SNAPSHOT.jar
Sie könnten beispielsweise folgende Ausgabe erhalten (die Reihenfolge der parallelen Steps 3 bis 5 ist zufällig):
Die drei Steps step3, step4 und step5 werden im 'Split Flow' parallel ausgefuehrt: 1 1 1 1 1 2 2 2 2 2 5 3 4 5 3 4 5 3 4 5 3 4 5 4 5 4 5 5 6 6 6 6 6 7 7 7 7 7 ... Summe der Steps: 7.6 s Gesamtdauer Job: 5.6 s
Sie können deutlich erkennen, dass die vier Steps 1 und 2 sowie 6 und 7 hintereinander, und die drei Steps 3, 4 und 5 parallel ausgeführt werden. Außerdem sehen Sie, dass die Summe der Step-Laufzeiten um 2 Sekunden größer als die Gesamtdauer des Jobs ist.
Das folgende Beispiel demonstriert:
Das folgende bereits weiter oben gezeigte Diagramm zeigt eine Übersicht zu den Zusammenhängen zwischen den vier Batchframework-Modulen JobExplorer, JobOperator, JobLauncher und JobRepository. Genauere Erläuterungen hierzu finden Sie weiter oben:
> JobExplorer > Steuer- > Datenbank Konsole o.ä. (Meta-Data) > JobOperator > JobLauncher > JobRepository >
Sie können das Beispiel wahlweise entweder downloaden oder wie beschrieben Schritt für Schritt aufbauen und ausführen:
Voraussetzung für dieses Beispiel ist irgendeins der vorherigen Beispiele. Im Folgenden wird von einer der obigen Demos im Verzeichnis SpringBatchMehrereJobsDemo ausgegangen, also von Spring-Batch-Demo mit mehreren Jobdefinitionen in einer Job-Konfiguration oder Spring-Batch-Demo mit mehrfacher Conditional-Flow-Verzweigung. Falls Sie ein anderes Projekt als Ausgangsbasis verwenden, muss zumindest der Job-Name angepasst werden.
Fügen Sie im src/test/java/springbatchdemo-Verzeichnis einen weiteren JUnit-Modultest hinzu: JobOperatorTest.java
package springbatchdemo; import static org.hamcrest.Matchers.is; import static org.junit.Assert.*; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.batch.core.*; import org.springframework.batch.core.explore.JobExplorer; import org.springframework.batch.core.launch.*; import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException; import org.springframework.beans.factory.annotation.*; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @RunWith( SpringJUnit4ClassRunner.class ) @ContextConfiguration( classes = MeineApplication.class ) public class JobOperatorTest { @Autowired private JobExplorer jobExplorer; @Autowired private JobOperator jobOperator; @Autowired private JobLauncher jobLauncher; @Autowired @Qualifier( "meinConditionalFlowJobAbc" ) private Job jobAbc; /** Mehrfache Job-Ausfuehrung mit JobLauncher --> Fehler */ @Test public void testMehrfacheJobAusfuehrungMitJobLauncherMitFehler() throws JobExecutionException { JobParameters jp = new JobParametersBuilder() .addString( "JobLauncher-Test", "nur-einmal-ausfuehrbar" ) .addString( "OK_ODER_FEHLER", "ok" ).toJobParameters(); // Erste Job-Ausfuehrung: JobExecution je = jobLauncher.run( jobAbc, jp ); assertThat( "Job-Name falsch", je.getJobInstance().getJobName(), is( "jobAbc" ) ); assertThat( "Job-Step-Anzahl falsch", je.getStepExecutions().size(), is( 3 ) ); assertThat( "Job ist nicht COMPLETED", je.getStatus(), is( BatchStatus.COMPLETED ) ); // Zweite Job-Ausfuehrung mit denselben JobParametern fuehrt zu Fehler, // weil der im Job definierte JobParametersIncrementer ignoriert wird: try { jobLauncher.run( jobAbc, jp ); fail( "'JobInstanceAlreadyCompleteException' muss geworfen werden" ); } catch( JobInstanceAlreadyCompleteException ex ) { /* ok */ } } /** Mehrfache Job-Ausfuehrung mit JobOperator --> funktioniert */ @Test public void testMehrfacheJobAusfuehrungMitJobOperatorOhneFehler() throws JobExecutionException { System.out.println( "\n---- Vorhandene Jobs: " + jobOperator.getJobNames() ); // Erste Job-Ausfuehrung: Long executionId1 = jobOperator.start( "jobAbc", "JobOperator-Test=mehrfach-ausfuehrbar,OK_ODER_FEHLER=ok" ); // Zweite Job-Ausfuehrung funktioniert, weil der im Job definierte JobParametersIncrementer verwendet wird: Long executionId2 = jobOperator.startNextInstance( "jobAbc" ); JobExecution je = jobExplorer.getJobExecution( executionId2 ); assertThat( "Job-Name falsch", je.getJobInstance().getJobName(), is( "jobAbc" ) ); assertThat( "Job-Step-Anzahl falsch", je.getStepExecutions().size(), is( 3 ) ); assertThat( "Job ist nicht COMPLETED", je.getStatus(), is( BatchStatus.COMPLETED ) ); } }
Führen Sie aus:
mvn clean test
Sie erhalten bei der Ausführung dieses JUnit-Modultests folgende Meldung und Exception:
---- Vorhandene Jobs: []
org.springframework.batch.core.launch.NoSuchJobException: No job configuration with the name [jobAbc] was registered
Fügen Sie im src/test/java/springbatchdemo-Verzeichnis folgende Konfigurationsklasse hinzu: JobRegistryBeanPostProcessorConfiguration.java
package springbatchdemo; import org.springframework.batch.core.configuration.JobRegistry; import org.springframework.batch.core.configuration.support.JobRegistryBeanPostProcessor; import org.springframework.context.annotation.*; @Configuration public class JobRegistryBeanPostProcessorConfiguration { @Bean public JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor( JobRegistry jobRegistry ) { JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor = new JobRegistryBeanPostProcessor(); jobRegistryBeanPostProcessor.setJobRegistry( jobRegistry ); return jobRegistryBeanPostProcessor; } }
Jetzt erhalten Sie bei Ausführung der Tests die Meldung:
---- Vorhandene Jobs: [jobAbc, jobXyz]
und die JUnit-Modultests laufen fehlerfrei.
Beim Aufruf von
jobOperator.startNextInstance( "jobAbc" )
erhalten Sie:
---- Job: jobAbc, mit JobParametern: {JobOperator-Test=mehrfach-ausfuehrbar, OK_ODER_FEHLER=ok, run.id=1},
weil der bei der Job-Konfiguration per
jobBuilderFactory.get( "..." ).incrementer( new
RunIdIncrementer()
)...
angegebene
JobParametersIncrementer
den JobParameter run.id=1 hinzugefügt hat.
Bei weiteren Job-Läufen wird diese run.id hochgezählt.
Falls Ihnen der RunIdIncrementer nicht gefällt, können Sie beliebige eigene JobParametersIncrementer definieren, beispielsweise einen UuidIncrementer mit der Methode uuid = UUID.randomUUID().
Das folgende Beispiel demonstriert:
Folgender Ablauf wird programmiert:
Start │ V AvoidDuplicateRun ─────────> Abbruch (falls Job bereits läuft) FAILED │ ok │ │ ┌────────────────────┐Schleife V V │ │ ErsterStep ─────────┐ │ FAILED │ │ │ ok │ │ │ V │ │ │ │ WarteStep ─┘ok │ │ │ V │FAILED (falls max. Anzahl Schleifen überschritten) │ ZweiterStep ─────────┤ FAILED │ │ ok │ V V OkStep FehlerbehandlungsStep │ │ V V AbschliessenderStep │ V Ende
Sie können das Beispiel wahlweise entweder downloaden oder wie beschrieben Schritt für Schritt aufbauen und ausführen.
Führen Sie folgende Schritte aus:
Wechseln Sie in Ihr Workspace-Verzeichnis (z.B. \MeinWorkspace) und führen Sie folgende Kommandos aus:
cd \MeinWorkspace
md SpringBatchStoppableDelayedRetryDemo
cd SpringBatchStoppableDelayedRetryDemo
md src\main\resources
md src\main\java\springbatchdemo
md src\test\java\springbatchdemo
tree /F
Erstellen Sie im SpringBatchStoppableDelayedRetryDemo-Projektverzeichnis die schon bekannte Maven-Projektkonfigurationsdatei: pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>springbatchdemo</groupId> <artifactId>SpringBatchStoppableDelayedRetryDemo</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.1.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-batch</artifactId> </dependency> <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Erläuterungen hierzu finden Sie weiter oben bei der SpringBatchItemChunkDemo-pom.xml.
Erzeugen Sie im src/main/resources-Verzeichnis die ebenfalls bekannte Logging-Konfiguration: logback.xml
<?xml version="1.0" encoding="UTF-8"?> <configuration> <logger name="org.springframework" level="error" /> </configuration>
Erzeugen Sie im src/main/java/springbatchdemo-Verzeichnis die ebenfalls bekannte Main-Startklasse: MeineApplication.java
package springbatchdemo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class MeineApplication { public static void main( String[] args ) throws Exception { SpringApplication.run( MeineApplication.class, args ); } }
Erläuterungen hierzu finden Sie wieder weiter oben.
Erzeugen Sie im src/main/java/springbatchdemo-Verzeichnis die Job-Konfigurationsklasse: StoppableDelayedRetryJobConfiguration.java
package springbatchdemo; import java.util.Set; import org.springframework.batch.core.*; import org.springframework.batch.core.configuration.annotation.*; import org.springframework.batch.core.explore.JobExplorer; import org.springframework.batch.core.launch.support.RunIdIncrementer; import org.springframework.batch.core.scope.context.*; import org.springframework.batch.core.step.tasklet.*; import org.springframework.batch.item.ExecutionContext; import org.springframework.batch.repeat.RepeatStatus; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.*; /** * JobConfiguration fuer: * a) "Delayed Retry" nach Fehlschlag, * b) "Stoppable" Warte-Tasklet. * * Mit dem JobParameter OK_ODER_FEHLER kann fuer Testzwecke vorgegeben werden, ob der ArbeitsStep mit OK oder mit Fehler enden soll. * Nach einem Fehlschlag im ersten ArbeitsStep ("ersterStep") wird nach einer Wartezeit im WarteStep der ArbeitsStep erneut versucht, * maximal ANZ_WIEDERHOL Male. * Fuer Testzwecke kann vorgegeben werden, dass nach ANZ_STEP_FEHL_OK-vielen Steps das Ergebnis auf OK gesetzt wird. * Die Step-Abfolge sowie der WarteStep koennen durch ein Stopp-Kommando abgebrochen werden. * Im OK-Fall werden nach dem "ersterStep" der "zweiterStep", der "okStep" und der "abschliessenderStep" ausgefuehrt. * Im Fehlerfall werden der "fehlerbehandlungsStep" und der "abschliessenderStep" ausgefuehrt. */ @Configuration @EnableBatchProcessing public class StoppableDelayedRetryJobConfiguration { public static final String JOB_NAME = "jobStoppableDelayedRetry"; public static final String OK_ODER_FEHLER = "OK_ODER_FEHLER"; public static final String ANZ_WIEDERHOL = "ANZ_WIEDERHOL"; public static final String ANZ_STEP_FEHL_OK = "ANZ_STEP_FEHL_OK"; public static final String ERGEBNIS_OK = "Ergebnis: OK"; public static final String ERGEBNIS_FEHLER = "Ergebnis: Fehler"; public static final String ABBRUCH_WEIL_JOB_BEREITS_LAEUFT = "Abbruch weil Job bereits laeuft"; public static final String STOPP_SIGNAL_DURCH_STOPPABLETASKLET = "Stopp-Signal durch StoppableTasklet"; @Autowired private StepBuilderFactory stepBuilderFactory; @Autowired private JobBuilderFactory jobBuilderFactory; @Autowired protected JobExplorer jobExplorer; /** Step zur Vermeidung des wiederholten Starts des gleichen Jobs */ @Bean public Step avoidDuplicateRun() { return stepBuilderFactory.get( "avoidDuplicateRun" ).tasklet( new Tasklet() { @Override public RepeatStatus execute( StepContribution sc, ChunkContext cc ) throws Exception { String jobName = cc.getStepContext().getJobName(); Set<JobExecution> jes = jobExplorer.findRunningJobExecutions( jobName ); if( jes.size() > 1 ) { String exitDescription = ABBRUCH_WEIL_JOB_BEREITS_LAEUFT; System.out.println( "\n !!! " + exitDescription + " !!!" ); sc.setExitStatus( new ExitStatus( ExitStatus.FAILED.getExitCode(), exitDescription ) ); } return RepeatStatus.FINISHED; } } ).build(); } /** Tasklet zur Anzeige von Text im Kommandozeilenfenster (und Speicherung im Job-ExecutionContext) */ public class PrintTextTasklet implements Tasklet { final String text; public PrintTextTasklet( String text ) { this.text = text; } @Override public RepeatStatus execute( StepContribution sc, ChunkContext cc ) throws Exception { System.out.println( text ); ExecutionContext ec = cc.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); Object msg = ec.get( "Msg" ); msg = (( msg == null ) ? "" : (msg + ", ")) + text; ec.put( "Msg", msg ); return RepeatStatus.FINISHED; } } /** Tasklet mit zwei Funktionen: * a) Anzeige von Text im Kommandozeilenfenster, * b) speichert Text in der ExitDescription des Steps. */ public class StoreTextAndPrintTextTasklet extends PrintTextTasklet { final String storeText; public StoreTextAndPrintTextTasklet( String printText, String storeText ) { super( printText ); this.storeText = storeText; } @Override public RepeatStatus execute( StepContribution sc, ChunkContext cc ) throws Exception { super.execute( sc, cc ); sc.setExitStatus( sc.getExitStatus().addExitDescription( storeText ) ); return RepeatStatus.FINISHED; } } /** Tasklet mit mehreren Funktionen: * a) zur Anzeige von Text im Kommandozeilenfenster, * b) nimmt Abbruchssignal per StoppableTasklet.stop() entgegen (funktioniert nicht in jedem Fall korrekt), * c) erfragt Abbruchssignal vom JobExplorer (funktioniert immer, aber benoetigt Datenbankzugriff). */ public class StoppablePrintTextTasklet extends PrintTextTasklet implements StoppableTasklet { volatile boolean stopped = false; public StoppablePrintTextTasklet( String text ) { super( text ); } @Override public void stop() { stopped = true; } public void resetStopped() { stopped = false; } public boolean isStopped( StepContribution sc, StepExecution se ) { // Achtung: Die StoppableTasklet-Variante wird nicht von jedem JobOperator unterstuetzt, // und so wie hier implementiert ist sie nicht thread-save, // weshalb doppelte Job-Laeufe ausgeschlossen werden muessen (s.o. avoidDuplicateRun): if( stopped ) { stopped = false; String exitDescription = STOPP_SIGNAL_DURCH_STOPPABLETASKLET; System.out.println( " !!! " + exitDescription + " !!!" ); sc.setExitStatus( sc.getExitStatus().addExitDescription( exitDescription ) ); return true; } // Explizite Stopp-Abfrage, funktioniert immer, aber benoetigt DB-Abfrage: se = jobExplorer.getStepExecution( se.getJobExecutionId(), se.getId() ); if( se.getJobExecution().isStopping() || se.isTerminateOnly() ) { String exitDescription = "Stopp-Signal durch JobExecution"; System.out.println( " !!! " + exitDescription + " !!!" ); sc.setExitStatus( sc.getExitStatus().addExitDescription( exitDescription ) ); return true; } return false; } } /** Stoppable Warte-Tasklet: Falls nicht bereits zu viele Schleifen: Nach Wartezeit erneute Schleife */ public class WarteTasklet extends StoppablePrintTextTasklet { public WarteTasklet( String text ) { super( text ); } @Override public RepeatStatus execute( StepContribution sc, ChunkContext cc ) throws Exception { super.execute( sc, cc ); resetStopped(); StepExecution se = cc.getStepContext().getStepExecution(); int anzahlStepExecutions = se.getJobExecution().getStepExecutions().size(); String anzWiederholStrng = se.getJobParameters().getString( ANZ_WIEDERHOL ); long anzWiederhol = ( anzWiederholStrng != null ) ? Long.parseLong( anzWiederholStrng ) : 5; System.out.println( "anzahlStepExecutions: " + anzahlStepExecutions ); if( anzahlStepExecutions > anzWiederhol ) { System.out.println( super.text + ": keine Wiederholung" ); sc.setExitStatus( ExitStatus.FAILED ); } else { System.out.print( super.text + ": Wiederholung nach Wartezeit " ); for( int i = 0; i < 10 && !isStopped( sc, se ); i++ ) { System.out.print( ". " ); Thread.sleep( 100 ); } System.out.println(); } return RepeatStatus.FINISHED; } } /** Erstes Arbeits-Tasklet */ public class ErstesTasklet extends PrintTextTasklet { public ErstesTasklet( String text ) { super( text ); } @Override public RepeatStatus execute( StepContribution sc, ChunkContext cc ) throws Exception { super.execute( sc, cc ); StepContext stpCtx = cc.getStepContext(); System.out.println( "\n---- Job: " + stpCtx.getJobName() + ", mit JobParametern: " + stpCtx.getJobParameters() ); int anzahlStepExecutions = stpCtx.getStepExecution().getJobExecution().getStepExecutions().size(); String okOderFehler = stpCtx.getStepExecution().getJobParameters().getString( OK_ODER_FEHLER ); String anzStepFehlOkStrng = stpCtx.getStepExecution().getJobParameters().getString( ANZ_STEP_FEHL_OK ); long anzStepFehlOk = ( anzStepFehlOkStrng != null ) ? Long.parseLong( anzStepFehlOkStrng ) : 100; boolean b1 = ( okOderFehler != null ) ? !"ok".equalsIgnoreCase( okOderFehler ) : (Math.random() < 0.5); boolean b2 = anzahlStepExecutions > anzStepFehlOk && anzStepFehlOk > 0; if( !b1 ) { System.out.println( super.text + ": ok" ); } else if( b2 ) { System.out.println( super.text + ": auf ok gesetzt, weil anzahlStepExecutions(=" + anzahlStepExecutions + ") > anzStepFehlOk(=" + anzStepFehlOk + ")" ); } else { System.out.println( super.text + ": mit Fehler" ); sc.setExitStatus( ExitStatus.FAILED ); } return RepeatStatus.FINISHED; } } /** Zweites Arbeits-Tasklet */ public class ZweitesTasklet extends ErstesTasklet { public ZweitesTasklet( String text ) { super( text ); } } @Bean public Step ersterStep() { return stepBuilderFactory.get( "ersterStep" ).tasklet( new ErstesTasklet( "ErstesTasklet" ) ).build(); } @Bean public Step zweiterStep() { return stepBuilderFactory.get( "zweiterStep" ).tasklet( new ZweitesTasklet( "ZweitesTasklet" ) ).build(); } @Bean public Step warteStep() { return stepBuilderFactory.get( "warteStep" ).tasklet( new WarteTasklet( "WarteTasklet" ) ).build(); } @Bean public Step fehlerbehandlungsStep() { return stepBuilderFactory.get( "fehlerbehandlungsStep" ).tasklet( new StoreTextAndPrintTextTasklet( "FehlerbehandlungsStep", ERGEBNIS_FEHLER ) ).build(); } @Bean public Step okStep() { return stepBuilderFactory.get( "okStep" ).tasklet( new StoreTextAndPrintTextTasklet( "OkStep", ERGEBNIS_OK ) ).build(); } @Bean public Step abschliessenderStep() { return stepBuilderFactory.get( "abschliessenderStep" ).tasklet( new PrintTextTasklet( "AbschliessenderStep" ) ).build(); } /** Job: * a) Stoppable: Job kann unterbrochen werden, auch innerhalb des Warte-Tasklets; * b) Delayed Retry: Falls der erste Step fehlschlaegt, wird er nach einer Wartezeit einige Male wiederholt ausgefuehrt */ @Bean public Job meinStoppableDelayedRetryJob() { return jobBuilderFactory.get( JOB_NAME ).incrementer( new RunIdIncrementer() ).listener( meinJobExecutionListener() ) .start( avoidDuplicateRun() ) .next( ersterStep() ) .on( ExitStatus.FAILED.getExitCode() ) .to( warteStep() ).on( ExitStatus.FAILED.getExitCode() ).to( fehlerbehandlungsStep() ).next( abschliessenderStep() ) .from( warteStep() ).on( "*" ).to( ersterStep() ) .from( ersterStep() ) .on( "*" ) .to( zweiterStep() ).on( ExitStatus.FAILED.getExitCode() ).to( fehlerbehandlungsStep() ).next( abschliessenderStep() ) .from( zweiterStep() ).on( "*" ).to( okStep() ).next( abschliessenderStep() ) .end().build(); } /** JobExecutionListener: * Sammelt alle ExitDescription aus den Steps und speichert sie in der ExitDescription der JobExecution */ @Bean public JobExecutionListener meinJobExecutionListener() { return new JobExecutionListener() { @Override public void beforeJob( JobExecution je ) {} @Override public void afterJob( JobExecution je ) { for( StepExecution se : je.getStepExecutions() ) { String sesd = se.getExitStatus().getExitDescription(); if( sesd != null && sesd.length() > 0 ) { String jesd = je.getExitStatus().getExitDescription(); String[] ss = sesd.split( ";" ); for( String s : ss ) { if( jesd == null || !jesd.contains( s.trim() ) ) { je.setExitStatus( je.getExitStatus().addExitDescription( s.trim() ) ); jesd = je.getExitStatus().getExitDescription(); } } } } } }; } }
Sehen Sie sich die Javadoc an zu: SimpleAsyncTaskExecutor, JobLauncher, JobOperator, StoppableTasklet, JobBuilder.start(), FlowBuilder.on(), FlowBuilder.TransitionBuilder.to(), FlowBuilder.next(), FlowBuilder.from(), FlowBuilder.end(), FlowJobBuilder.build(), sowie die Erläuterungen unter: Configuring a JobLauncher asynchronously, Stopping a Job und Conditional Flow.
Erzeugen Sie im src/test/java/springbatchdemo-Verzeichnis den JUnit-Modultest: StoppableDelayedRetryJobTest.java
package springbatchdemo; import static org.hamcrest.Matchers.is; import static org.junit.Assert.*; import java.util.Map; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.batch.core.*; import org.springframework.batch.core.explore.JobExplorer; import org.springframework.batch.core.launch.JobOperator; import org.springframework.batch.item.ExecutionContext; import org.springframework.beans.factory.annotation.*; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @RunWith( SpringJUnit4ClassRunner.class ) @ContextConfiguration( classes = MeineApplication.class ) public class StoppableDelayedRetryJobTest { @Autowired private JobExplorer jobExplorer; @Autowired @Qualifier( "asyncJobOperator" ) private JobOperator asyncJobOperator; @Test public void testWiederholungMitWartezeit() throws JobExecutionException { // OK-Fall: // 1.: AvoidDuplicateRun, // 2.: ErstesTasklet: ok, // 3.: ZweitesTasklet: ok, // 4.: OkStep, // 5.: AbschliessenderStep Long executionId1 = asyncJobOperator.start( StoppableDelayedRetryJobConfiguration.JOB_NAME, "OK_ODER_FEHLER=ok" ); JobExecution je1 = warteBisStatus( executionId1, BatchStatus.COMPLETED, 10 ); printStepExecutions( je1 ); assertThat( "Job-Name falsch", je1.getJobInstance().getJobName(), is( StoppableDelayedRetryJobConfiguration.JOB_NAME ) ); assertThat( "Job-Step-Anzahl falsch", je1.getStepExecutions().size(), is( 5 ) ); assertThat( "FailureExceptions", je1.getAllFailureExceptions().size(), is( 0 ) ); assertThat( "Job ist nicht COMPLETED", je1.getStatus(), is( BatchStatus.COMPLETED ) ); assertThat( "ExitStatus ist nicht COMPLETED", je1.getExitStatus().getExitCode(), is( ExitStatus.COMPLETED.getExitCode() ) ); assertThat( je1.getExitStatus().getExitDescription(), is( StoppableDelayedRetryJobConfiguration.ERGEBNIS_OK ) ); assertThat( "" + je1.getExecutionContext(), is( "{Msg=ErstesTasklet, ZweitesTasklet, OkStep, AbschliessenderStep}" ) ); // Fehler-Fall, nach 4 erfolglosen Versuchen (= 9 Steps) wird mit Fehler abgebrochen: // 1.: AvoidDuplicateRun, // 2.: ErstesTasklet: mit Fehler // 3.: WarteTasklet: Wiederholung nach Wartezeit . . . . . . . . . . // 4.: ErstesTasklet: mit Fehler // 5.: WarteTasklet: Wiederholung nach Wartezeit . . . . . . . . . . // 6.: ErstesTasklet: mit Fehler // 7.: WarteTasklet: Wiederholung nach Wartezeit . . . . . . . . . . // 8.: ErstesTasklet: mit Fehler // 9.: WarteTasklet: keine Wiederholung // 10.: FehlerbehandlungsStep // 11.: AbschliessenderStep Long executionId2 = asyncJobOperator.start( StoppableDelayedRetryJobConfiguration.JOB_NAME, "OK_ODER_FEHLER=Fehler,ANZ_WIEDERHOL=7" ); JobExecution je2 = warteBisStatus( executionId2, BatchStatus.COMPLETED, 10 ); printStepExecutions( je2 ); assertThat( "Job-Name falsch", je2.getJobInstance().getJobName(), is( StoppableDelayedRetryJobConfiguration.JOB_NAME ) ); assertThat( "Job-Step-Anzahl falsch", je2.getStepExecutions().size(), is( 11 ) ); assertThat( "FailureExceptions", je2.getAllFailureExceptions().size(), is( 0 ) ); assertThat( "Job ist nicht COMPLETED", je2.getStatus(), is( BatchStatus.COMPLETED ) ); assertThat( "ExitStatus ist nicht COMPLETED", je2.getExitStatus().getExitCode(), is( ExitStatus.COMPLETED.getExitCode() ) ); assertThat( je2.getExitStatus().getExitDescription(), is( StoppableDelayedRetryJobConfiguration.ERGEBNIS_FEHLER ) ); // 2 Versuche (5 Steps) mit Fehler, dann erfolgreich weiter mit OK: // 1.: AvoidDuplicateRun, // 2.: ErstesTasklet: mit Fehler // 3.: WarteTasklet: Wiederholung nach Wartezeit . . . . . . . . . . // 4.: ErstesTasklet: mit Fehler // 5.: WarteTasklet: Wiederholung nach Wartezeit . . . . . . . . . . // 6.: ErstesTasklet: auf ok gesetzt, weil anzahlStepExecutions(=6) > anzStepFehlOk(=5) // 7.: ZweitesTasklet // 8.: OkStep // 9.: AbschliessenderStep Long executionId3 = asyncJobOperator.start( StoppableDelayedRetryJobConfiguration.JOB_NAME, "OK_ODER_FEHLER=Fehler,ANZ_WIEDERHOL=7,ANZ_STEP_FEHL_OK=5" ); JobExecution je3 = warteBisStatus( executionId3, BatchStatus.COMPLETED, 10 ); printStepExecutions( je3 ); assertThat( "Job-Name falsch", je3.getJobInstance().getJobName(), is( StoppableDelayedRetryJobConfiguration.JOB_NAME ) ); assertThat( "Job-Step-Anzahl falsch", je3.getStepExecutions().size(), is( 9 ) ); assertThat( "FailureExceptions", je3.getAllFailureExceptions().size(), is( 0 ) ); assertThat( "Job ist nicht COMPLETED", je3.getStatus(), is( BatchStatus.COMPLETED ) ); assertThat( "ExitStatus ist nicht COMPLETED", je3.getExitStatus().getExitCode(), is( ExitStatus.COMPLETED.getExitCode() ) ); assertThat( je3.getExitStatus().getExitDescription(), is( StoppableDelayedRetryJobConfiguration.ERGEBNIS_OK ) ); } // Stoppable-Test: Stopp-Signal nach 0,5 Sekunden, waehrend der Wartezeit im WarteTasklet: // --> // 1.: AvoidDuplicateRun, // 2.: ErstesTasklet: mit Fehler // 3.: WarteTasklet: Wiederholung nach Wartezeit . . . . . !!! Stopp-Signal durch StoppableTasklet !!! // --> // StepExecution 0: CommitCount = 1, Status = COMPLETED, ExitStatus = COMPLETED, StepName = avoidDuplicateRun // StepExecution 1: CommitCount = 1, Status = COMPLETED, ExitStatus = FAILED, StepName = ersterStep // StepExecution 2: CommitCount = 1, Status = STOPPED, ExitStatus = STOPPED, StepName = warteStep, ... Stopp-Signal ... @Test public void testStoppable() throws JobExecutionException { final long wartezeitBisAbbruch = 500; System.out.println( "\n---- Vorhandene Jobs: " + asyncJobOperator.getJobNames() ); Long executionId = asyncJobOperator.start( StoppableDelayedRetryJobConfiguration.JOB_NAME, "Test=Stop,OK_ODER_FEHLER=Fehler" ); try { Thread.sleep( wartezeitBisAbbruch ); } catch( InterruptedException e ) {} JobExecution je = jobExplorer.getJobExecution( executionId ); assertThat( je.getStatus(), is( BatchStatus.STARTED ) ); asyncJobOperator.stop( executionId.longValue() ); je = warteBisStatus( executionId, BatchStatus.STOPPED, 2 ); printStepExecutions( je ); assertThat( "Job-Name falsch", je.getJobInstance().getJobName(), is( StoppableDelayedRetryJobConfiguration.JOB_NAME ) ); assertThat( "Job-Step-Anzahl falsch", je.getStepExecutions().size(), is( 3 ) ); assertThat( "FailureExceptions", je.getAllFailureExceptions().size(), is( 0 ) ); assertThat( "Job ist nicht STOPPED", je.getStatus(), is( BatchStatus.STOPPED ) ); assertThat( "ExitStatus ist nicht STOPPED", je.getExitStatus().getExitCode(), is( ExitStatus.STOPPED.getExitCode() ) ); assertThat( je.getExitStatus().getExitDescription(), is( "org.springframework.batch.core.JobInterruptedException; " + StoppableDelayedRetryJobConfiguration.STOPP_SIGNAL_DURCH_STOPPABLETASKLET ) ); assertThat( "" + je.getExecutionContext(), is( "{Msg=ErstesTasklet, WarteTasklet}" ) ); long dauerJob = je.getEndTime().getTime() - je.getStartTime().getTime(); System.out.println( "Jobdauer: " + dauerJob + " ms" ); assertTrue( dauerJob < wartezeitBisAbbruch * 15 / 10 ); } // Doppelter Job-Start: // --> // !!! Abbruch weil Job bereits laeuft !!! // StepExecution: Status = COMPLETED, ExitStatus = FAILED, StepName = avoidDuplicateRun, ExitDescription = Abbruch weil Job bereits laeuft // JobExecution: Status = FAILED, ExitStatus = FAILED, ExitDescription = Abbruch weil Job bereits laeuft @Test public void testDuplicateRun() throws JobExecutionException { Long executionId1 = asyncJobOperator.start( StoppableDelayedRetryJobConfiguration.JOB_NAME, "Test=Dupl1,OK_ODER_FEHLER=Fehler,ANZ_STEP_FEHL_OK=4" ); try { Thread.sleep( 500 ); } catch( InterruptedException e ) {} Long executionId2 = asyncJobOperator.start( StoppableDelayedRetryJobConfiguration.JOB_NAME, "Test=Dupl2,OK_ODER_FEHLER=Fehler,ANZ_STEP_FEHL_OK=4" ); JobExecution je1 = warteBisStatus( executionId1, BatchStatus.COMPLETED, 10 ); JobExecution je2 = warteBisStatus( executionId2, BatchStatus.FAILED, 10 ); System.out.println( "\n1. Job:" ); printStepExecutions( je1 ); assertThat( "Job-Step-Anzahl falsch", je1.getStepExecutions().size(), is( 9 ) ); assertThat( "FailureExceptions", je1.getAllFailureExceptions().size(), is( 0 ) ); assertThat( "Job ist nicht COMPLETED", je1.getStatus(), is( BatchStatus.COMPLETED ) ); assertThat( "ExitStatus ist nicht COMPLETED", je1.getExitStatus().getExitCode(), is( ExitStatus.COMPLETED.getExitCode() ) ); assertThat( je1.getExitStatus().getExitDescription(), is( StoppableDelayedRetryJobConfiguration.ERGEBNIS_OK ) ); System.out.println( "\n2. Job:" ); printStepExecutions( je2 ); assertThat( "Job-Step-Anzahl falsch", je2.getStepExecutions().size(), is( 1 ) ); assertThat( "FailureExceptions", je2.getAllFailureExceptions().size(), is( 0 ) ); assertThat( "Job ist nicht FAILED", je2.getStatus(), is( BatchStatus.FAILED ) ); assertThat( "ExitStatus ist nicht FAILED", je2.getExitStatus().getExitCode(), is( ExitStatus.FAILED.getExitCode() ) ); assertThat( je2.getExitStatus().getExitDescription(), is( StoppableDelayedRetryJobConfiguration.ABBRUCH_WEIL_JOB_BEREITS_LAEUFT ) ); assertThat( "" + je2.getExecutionContext(), is( "{}" ) ); } /** Warte bis die JobExecution den gewuenschten BatchStatus erreicht (oder der Timeout ueberschritten ist) */ private JobExecution warteBisStatus( Long executionId, BatchStatus stat, int maxWartezeitSek ) { for( int i = 0; i < 5 * maxWartezeitSek; i++ ) { JobExecution je = jobExplorer.getJobExecution( executionId ); if( stat == null || je == null || stat.equals( je.getStatus() ) ) { return je; } try { Thread.sleep( 200 ); } catch( InterruptedException e ) {} } return jobExplorer.getJobExecution( executionId ); } /** Zeige die StepExecutions, die JobExecution und den Job-ExecutionContext */ private static void printStepExecutions( JobExecution je ) { for( StepExecution se : je.getStepExecutions() ) { String s = se.getExitStatus().getExitDescription(); if( s != null && s.length() > 0 ) { s = ", ExitDescription = " + s; } System.out.println( "StepExecution " + se.getId() + ": CommitCount = " + se.getCommitCount() + ", Status = " + se.getStatus() + ", ExitStatus = " + se.getExitStatus().getExitCode() + ", StepName = " + se.getStepName() + s ); } String s = je.getExitStatus().getExitDescription(); if( s != null && s.length() > 0 ) { s = ", ExitDescription = " + s; } System.out.println( "JobExecution " + je.getId() + ": JobId = " + je.getJobId() + ", Status = " + je.getStatus() + ", ExitStatus = " + je.getExitStatus().getExitCode() + s ); ExecutionContext ec = je.getExecutionContext(); if( ec != null && !ec.isEmpty() ) { for( Map.Entry<String, Object> e : ec.entrySet() ) { System.out.println( "Job-ExecutionContext: " + e.getKey() + ": " + e.getValue() ); } } } }
Erzeugen Sie im src/test/java/springbatchdemo-Verzeichnis die Konfiguration für den asynchronen JobOperator: AsyncJobOperatorConfiguration.java
package springbatchdemo; import org.springframework.batch.core.configuration.JobRegistry; import org.springframework.batch.core.configuration.support.JobRegistryBeanPostProcessor; import org.springframework.batch.core.explore.JobExplorer; import org.springframework.batch.core.launch.*; import org.springframework.batch.core.launch.support.*; import org.springframework.batch.core.repository.JobRepository; import org.springframework.beans.factory.annotation.*; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.task.SimpleAsyncTaskExecutor; @Configuration public class AsyncJobOperatorConfiguration { @Autowired private JobExplorer jobExplorer; @Autowired private JobRegistry jobRegistry; @Autowired private JobRepository jobRepository; @Autowired @Qualifier("asyncJobLauncher") private JobLauncher asyncJobLauncher; /** JobRegistry: * Falls in Ihrer Laufzeitumgebung noch keine JobRegistry registriert wurde, wird diese Bean benoetigt. * Falls eine JobRegistry bereits registriert ist, muss diese Bean entfallen. */ @Bean public JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor( JobRegistry jobReg ) { JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor = new JobRegistryBeanPostProcessor(); jobRegistryBeanPostProcessor.setJobRegistry( jobReg ); return jobRegistryBeanPostProcessor; } /** Asynchroner JobLauncher */ @Bean public JobLauncher asyncJobLauncher() { SimpleJobLauncher jobLauncher = new SimpleJobLauncher(); jobLauncher.setJobRepository( jobRepository ); jobLauncher.setTaskExecutor( new SimpleAsyncTaskExecutor() ); return jobLauncher; } /** Asynchroner JobOperator */ @Bean public JobOperator asyncJobOperator() { SimpleJobOperator jobOperator = new SimpleJobOperator(); jobOperator.setJobExplorer( jobExplorer ); jobOperator.setJobRegistry( jobRegistry ); jobOperator.setJobRepository( jobRepository ); jobOperator.setJobLauncher( asyncJobLauncher ); return jobOperator; } }
Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):
[\MeinWorkspace\SpringBatchStoppableDelayedRetryDemo] |- [src] | |- [main] | | |- [java] | | | '- [springbatchdemo] | | | |- MeineApplication.java | | | '- StoppableDelayedRetryJobConfiguration.java | | '- [resources] | | '- logback.xml | '- [test] | '- [java] | '- [springbatchdemo] | |- AsyncJobOperatorConfiguration.java | '- StoppableDelayedRetryJobTest.java '- pom.xml
Durch das spring-boot-maven-plugin wird eine ausführbare jar-Datei erzeugt, welche alle benötigten Abhängigkeiten beinhaltet.
Führen Sie im Kommandozeilenfenster aus:
cd \MeinWorkspace\SpringBatchStoppableDelayedRetryDemo
mvn clean package
java -jar target/SpringBatchStoppableDelayedRetryDemo-1.0-SNAPSHOT.jar
Die Ausführungswege und Ausgaben der vier JUnit-Modultests sind oben in der JUnit-Testklasse StoppableDelayedRetryJobTest beschrieben.
Wie Sie den StoppableDelayedRetry-Job in den Spring-Batch-Admin-Batch-Server integrieren können, und dort sowohl die Abbrechbarkeit als auch die Vermeidung doppelter Job-Läufe testen können, sehen Sie weiter unten unter: Besonderheit beim Hinzufügen des Jobs "Stoppable Delayed Retry". Insbesondere wird dort gezeigt, wie Sie auch auf der REST-Client-Seite den Grund des Abbruchs erkennen können, da er in der ExitDescription der JobExecution gespeichert wurde.
Falls Sie Probleme mit der Abbrechbarkeit oder mit dem StoppableTasklet haben, sehen Sie sich an: SimpleJobOperator.stop()-Sourcecode, SystemCommandTasklet-Sourcecode und Graceful Shutdown: Stopping Spring Batch Job Executions (Albert Strasser).
Das folgende Beispiel demonstriert:
Folgender Ablauf wird im JUnit-Modultest ausgeführt:
Start │ V Asynchroner JobOperator startet zwei parallele Jobs │ │ V V Job 0 Job 1 │ │ V V meinConcurrentStep meinConcurrentStep │ │ V V meinConcurrentStep meinConcurrentStep │ │ V V meinConcurrentStep meinConcurrentStep │ │ V V Ende Ende
Sie können das Beispiel wahlweise entweder downloaden oder wie beschrieben Schritt für Schritt aufbauen und ausführen.
Führen Sie folgende Schritte aus:
Wechseln Sie in Ihr Workspace-Verzeichnis (z.B. \MeinWorkspace) und führen Sie folgende Kommandos aus:
cd \MeinWorkspace
md SpringBatchConcurrentDemo
cd SpringBatchConcurrentDemo
md src\main\java\springbatchdemo
md src\test\java\springbatchdemo
md src\main\resources
md src\test\resources
tree /F
Erstellen Sie im SpringBatchConcurrentDemo-Projektverzeichnis die schon bekannte Maven-Projektkonfigurationsdatei: pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>springbatchdemo</groupId> <artifactId>SpringBatchConcurrentDemo</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.1.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-batch</artifactId> </dependency> <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Erläuterungen hierzu finden Sie weiter oben bei der SpringBatchItemChunkDemo-pom.xml.
Erzeugen Sie im src/main/resources-Verzeichnis die ebenfalls bekannte Logging-Konfiguration: logback.xml
<?xml version="1.0" encoding="UTF-8"?> <configuration> <logger name="org.springframework" level="error" /> </configuration>
Erzeugen Sie im src/test/resources-Verzeichnis ein SQL-Skript, um für die HSQL-DB den "MVCC Transaction Mode" zu konfigurieren: HSQLDB-MVCC.sql
SET DATABASE TRANSACTION CONTROL MVCC;
Erzeugen Sie im src/main/java/springbatchdemo-Verzeichnis die schon bekannte Main-Startklasse: MeineApplication.java
package springbatchdemo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class MeineApplication { public static void main( String[] args ) throws Exception { SpringApplication.run( MeineApplication.class, args ); } }
Erläuterungen hierzu finden Sie wieder weiter oben.
Erzeugen Sie im src/main/java/springbatchdemo-Verzeichnis die Job-Konfigurationsklasse: ConcurrentJobConfiguration.java
package springbatchdemo; import org.springframework.batch.core.*; import org.springframework.batch.core.configuration.annotation.*; import org.springframework.batch.core.launch.support.RunIdIncrementer; import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.core.step.tasklet.Tasklet; import org.springframework.batch.item.ExecutionContext; import org.springframework.batch.repeat.RepeatStatus; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.annotation.*; /** * JobConfiguration fuer den JUnit-Modultest "ConcurrentJobTest", der zwei Jobs parallel startet, * und testet, dass die parallel ausgefuehrten Steps sich nicht gegenseitig beeinflussen. */ @Configuration @EnableBatchProcessing public class ConcurrentJobConfiguration { public static final String JOB_NAME = "meinConcurrentJob"; public static final String MEIN_PARM_KEY = "MeinParm"; public static final String MEIN_MSG_KEY = "Msg"; @Autowired private JobBuilderFactory jobBuilderFactory; @Autowired private StepBuilderFactory stepBuilderFactory; /** Wenn der Job mehrfach gestartet wird, wird dieser Step multi-threaded parallel durchlaufen */ @Bean public Step meinConcurrentStep() { return stepBuilderFactory.get( "meinConcurrentStep" ).tasklet( new Tasklet() { @Override public RepeatStatus execute( StepContribution sc, ChunkContext cc ) { // Bei jeder Step-Execution wird der Text aus dem JobParameter zu der Message im ExecutionContext hinzugefuegt // (dies ist in diesem Beispiel der zu speichernde "Zustand"): String meinParm = cc.getStepContext().getStepExecution().getJobParameters().getString( MEIN_PARM_KEY ); ExecutionContext ec = cc.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); Object msg = ec.get( MEIN_MSG_KEY ); if( msg == null ) { msg = ""; } ec.put( MEIN_MSG_KEY, msg + meinParm ); return RepeatStatus.FINISHED; } } ).build(); } @Bean public Job meinConcurrentJob() throws Exception { return jobBuilderFactory.get( JOB_NAME ).incrementer( new RunIdIncrementer() ) .start( meinConcurrentStep() ) .next( meinConcurrentStep() ) .next( meinConcurrentStep() ) .build(); } }
Erzeugen Sie im src/test/java/springbatchdemo-Verzeichnis den JUnit-Modultest: ConcurrentJobTest.java
package springbatchdemo; import java.util.Map; import org.junit.*; import org.junit.runner.RunWith; import org.springframework.batch.core.*; import org.springframework.batch.core.explore.JobExplorer; import org.springframework.batch.core.launch.JobOperator; import org.springframework.batch.item.ExecutionContext; import org.springframework.beans.factory.annotation.*; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @RunWith( SpringJUnit4ClassRunner.class ) @ContextConfiguration( classes = MeineApplication.class ) public class ConcurrentJobTest { @Autowired private JobExplorer jobExplorer; @Autowired @Qualifier( "asyncJobOperator" ) private JobOperator asyncJobOperator; /** Test der Multi-threading-Faehigkeit */ @Test public void testConcurrentJob() throws Exception { // Per asynchronem JobOperator werden zwei parallel laufende JobExecutions gestartet (mit verschiedenen JobParametern): Long executionId1 = asyncJobOperator.start( ConcurrentJobConfiguration.JOB_NAME, "MeinParm=a" ); Long executionId2 = asyncJobOperator.start( ConcurrentJobConfiguration.JOB_NAME, "MeinParm=b" ); JobExecution je1 = warteBisStatus( executionId1, BatchStatus.COMPLETED, 2 ); JobExecution je2 = warteBisStatus( executionId2, BatchStatus.COMPLETED, 2 ); printStepExecutions( je1 ); System.out.println( "" ); printStepExecutions( je2 ); // Da pro JobExecution der "Arbeits-Step" jeweils dreimal durchlaufen wird, wird der JobParameter dreimal gespeichert: Assert.assertEquals( "aaa", je1.getExecutionContext().get( ConcurrentJobConfiguration.MEIN_MSG_KEY ) ); Assert.assertEquals( "bbb", je2.getExecutionContext().get( ConcurrentJobConfiguration.MEIN_MSG_KEY ) ); } /** Warte bis die JobExecution den gewuenschten BatchStatus erreicht (oder der Timeout ueberschritten ist) */ private JobExecution warteBisStatus( Long executionId, BatchStatus stat, int maxWartezeitSek ) { for( int i = 0; i < 5 * maxWartezeitSek; i++ ) { JobExecution je = jobExplorer.getJobExecution( executionId ); if( stat == null || je == null || stat.equals( je.getStatus() ) ) { return je; } try { Thread.sleep( 200 ); } catch( InterruptedException e ) {} } return jobExplorer.getJobExecution( executionId ); } /** Zeige die StepExecutions, die JobExecution und den Job-ExecutionContext */ private static void printStepExecutions( JobExecution je ) { for( StepExecution se : je.getStepExecutions() ) { String s = se.getExitStatus().getExitDescription(); if( s != null && s.length() > 0 ) { s = ", ExitDescription = " + s; } System.out.println( "StepExecution " + se.getId() + ": CommitCount = " + se.getCommitCount() + ", Status = " + se.getStatus() + ", ExitStatus = " + se.getExitStatus().getExitCode() + ", StepName = " + se.getStepName() + s ); } String s = je.getExitStatus().getExitDescription(); if( s != null && s.length() > 0 ) { s = ", ExitDescription = " + s; } System.out.println( "JobExecution " + je.getId() + ": JobId = " + je.getJobId() + ", Status = " + je.getStatus() + ", ExitStatus = " + je.getExitStatus().getExitCode() + s ); ExecutionContext ec = je.getExecutionContext(); if( ec != null && !ec.isEmpty() ) { for( Map.Entry<String, Object> e : ec.entrySet() ) { System.out.println( "Job-ExecutionContext: " + e.getKey() + ": " + e.getValue() ); } } } }
Erzeugen Sie im src/test/java/springbatchdemo-Verzeichnis die auch schon bekannte Konfiguration für den asynchronen JobOperator: AsyncJobOperatorConfiguration.java
package springbatchdemo; import org.springframework.batch.core.configuration.JobRegistry; import org.springframework.batch.core.configuration.support.JobRegistryBeanPostProcessor; import org.springframework.batch.core.explore.JobExplorer; import org.springframework.batch.core.launch.*; import org.springframework.batch.core.launch.support.*; import org.springframework.batch.core.repository.JobRepository; import org.springframework.beans.factory.annotation.*; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.task.SimpleAsyncTaskExecutor; @Configuration public class AsyncJobOperatorConfiguration { @Autowired private JobExplorer jobExplorer; @Autowired private JobRegistry jobRegistry; @Autowired private JobRepository jobRepository; @Autowired @Qualifier("asyncJobLauncher") private JobLauncher asyncJobLauncher; /** JobRegistry: * Falls in Ihrer Laufzeitumgebung noch keine JobRegistry registriert wurde, wird diese Bean benoetigt. * Falls eine JobRegistry bereits registriert ist, muss diese Bean entfallen. */ @Bean public JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor( JobRegistry jobReg ) { JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor = new JobRegistryBeanPostProcessor(); jobRegistryBeanPostProcessor.setJobRegistry( jobReg ); return jobRegistryBeanPostProcessor; } /** Asynchroner JobLauncher */ @Bean public JobLauncher asyncJobLauncher() { SimpleJobLauncher jobLauncher = new SimpleJobLauncher(); jobLauncher.setJobRepository( jobRepository ); jobLauncher.setTaskExecutor( new SimpleAsyncTaskExecutor() ); return jobLauncher; } /** Asynchroner JobOperator */ @Bean public JobOperator asyncJobOperator() { SimpleJobOperator jobOperator = new SimpleJobOperator(); jobOperator.setJobExplorer( jobExplorer ); jobOperator.setJobRegistry( jobRegistry ); jobOperator.setJobRepository( jobRepository ); jobOperator.setJobLauncher( asyncJobLauncher ); return jobOperator; } }
Erzeugen Sie im src/test/java/springbatchdemo-Verzeichnis die DataSource-Konfigurationsklasse, um für die HSQL-DB den "MVCC Transaction Mode" zu konfigurieren: HsqldbMvccConfiguration.java
package springbatchdemo; import javax.sql.DataSource; import org.springframework.context.annotation.*; import org.springframework.jdbc.datasource.embedded.*; @Configuration public class HsqldbMvccConfiguration { /** Vermeidung von: * org.springframework.batch.core.step.FatalStepExecutionException: JobRepository failure forcing rollback, * org.springframework.dao.ConcurrencyFailureException: PreparedStatementCallback; SQL [UPDATE BATCH_STEP_EXECUTION ...]; serialization failure */ @Bean public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType( EmbeddedDatabaseType.HSQL ) .addScript( "classpath:HSQLDB-MVCC.sql" ) .build(); } }
Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):
[\MeinWorkspace\SpringBatchConcurrentDemo] |- [src] | |- [main] | | |- [java] | | | '- [springbatchdemo] | | | |- ConcurrentJobConfiguration.java | | | '- MeineApplication.java | | '- [resources] | | '- logback.xml | '- [test] | |- [java] | | '- [springbatchdemo] | | |- AsyncJobOperatorConfiguration.java | | |- ConcurrentJobTest.java | | '- HsqldbMvccConfiguration.java | '- [resources] | '- HSQLDB-MVCC.sql '- pom.xml
Der "Zustand" besteht bei diesem einfachen Beispiel nur darin, dass bei jeder Ausführung der execute()-Methode des Step-Tasklets jeweils ein Buchstabe gespeichert wird, welcher als JobParameter übergeben wird. Die beiden parallelen Job-Instanzen verwenden denselben Job, aber unterschiedliche JobParameter: einmal "a" und einmal "b". Da der "ConcurrentStep" pro Jobausführung dreimal durchlaufen wird, muss das Ergebnis jeweils genau dreimal den Buchstaben enthalten, also beim ersten Job "aaa" und beim zweiten Job "bbb". Dies überprüft der JUnit-Modultest.
Bei der bisher implementierten Variante speichert das Step-Tasklet den Zustand der vorerigen Steps jeweils im ExecutionContext der JobExecution. Dies ist multi-threading-sicher und die zu bevorzugende Methode, um Zustand (= "State") während der Jobausführung zwischenzuspeichern. Zu beachten ist dabei, dass der ExecutionContext nicht beliebig groß werden darf, da er in der Datenbank zwischengespeichert wird, normalerweise als CLOB in der Tabelle BATCH_JOB_EXECUTION_CONTEXT.
Führen Sie im Kommandozeilenfenster aus:
cd \MeinWorkspace\SpringBatchConcurrentDemo
mvn clean test
Sie erhalten (gekürzt):
... JobExecution 0: Job-ExecutionContext: Msg: aaa ... JobExecution 1: Job-ExecutionContext: Msg: bbb
Wenn Sie Zustand nicht im ExecutionContext, sondern in Instanzvariablen speichern, ist der Job ohne weitere Maßnahmen nicht multi-threading-fähig. Um dies zu demonstrieren, ersetzen Sie in ConcurrentJobConfiguration.java die Step-Bean:
@Bean public Step meinConcurrentStep() { return stepBuilderFactory.get( "meinConcurrentStep" ).tasklet( new Tasklet() { @Override public RepeatStatus execute( StepContribution sc, ChunkContext cc ) { String meinParm = cc.getStepContext().getStepExecution().getJobParameters().getString( MEIN_PARM_KEY ); ExecutionContext ec = cc.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); Object msg = ec.get( MEIN_MSG_KEY ); if( msg == null ) { msg = ""; } ec.put( MEIN_MSG_KEY, msg + meinParm ); return RepeatStatus.FINISHED; } } ).build(); }
durch:
// Achtung: fehlerhafte Implementierung: @Bean public Step meinConcurrentStep() { return stepBuilderFactory.get( "meinConcurrentStep" ).tasklet( new Tasklet() { String meinText = ""; @Override public RepeatStatus execute( StepContribution sc, ChunkContext cc ) { String meinParm = cc.getStepContext().getStepExecution().getJobParameters().getString( MEIN_PARM_KEY ); meinText += meinParm; cc.getStepContext().getStepExecution().getJobExecution().getExecutionContext().put( MEIN_MSG_KEY, meinText ); return RepeatStatus.FINISHED; } } ).build(); }
Als "Zustandsspeicher" für die String-Concatenation wird die Instanzvariable meinText verwendet. Das Ergebnis wird wie vorher in den ExecutionContext gespeichert.
Führen Sie wieder aus:
cd \MeinWorkspace\SpringBatchConcurrentDemo
mvn clean test
Der JUnit-Modultest meldet Fehler und Sie erhalten beispielsweise (gekürzt, die Buchstabenkombinationen wechseln):
... JobExecution 0: Job-ExecutionContext: Msg: baaba ... JobExecution 1: Job-ExecutionContext: Msg: baabab
Sie können deutlich erkennen, dass die beiden Job-Threads in dieselbe gemeinsame Instanzvariable schreiben, was zu fehlerhaften Ergebnissen führt. Der Grund ist, dass Spring-Beans defaultmäßig als Singleton instanziiert werden, und somit für alle Ausführungen dieselbe Bean-Instanz mit denselben Instanzvariablen verwendet wird.
Um trotz der Verwendung der Instanzvariable meinText Multi-threading-Sicherheit zu erlangen, können Sie statt der Instanziierung der Step-Bean als Singleton den Einsatzbereich der Bean mit einer Scope-Definition einschränken. Ersetzen Sie in ConcurrentJobConfiguration.java die folgenden zwei Zeilen der Step-Bean:
@Bean public Step meinConcurrentStep()
durch:
@Bean @Scope( value="job", proxyMode=ScopedProxyMode.INTERFACES ) public Step meinConcurrentStep()
Dadurch wird pro Job-Execution eine eigene getrennte Step-Bean erstellt,
so dass auch pro Job-Execution eine separate Instanzvariable verwendet wird.
Der ScopedProxyMode ist immer dann notwendig, wenn eine Bean mit zeitlich einschränkendem Scope
(wie die Step-Bean mit job-Scope)
in eine andere Bean mit Singleton-Scope (wie die Job-Bean) injiziert werden soll.
Abhängig von Ihrer Umgebung können Sie alternativ eventuell auch "@JobScope" verwenden.
Allerdings sind Sie dann auf den
ScopedProxyMode.TARGET_CLASS
festgelegt, der etwas weniger performant ist als
ScopedProxyMode.INTERFACES.
Sehen Sie sich an:
Bean scopes,
Job Scope,
@JobScope,
@Scope,
proxyMode,
ScopedProxyMode.INTERFACES,
Scoped-Proxy-Problem.
Führen Sie erneut aus:
cd \MeinWorkspace\SpringBatchConcurrentDemo
mvn clean test
Jetzt erhalten Sie wieder das korrekte Ergebnis (gekürzt):
... JobExecution 0: Job-ExecutionContext: Msg: aaa ... JobExecution 1: Job-ExecutionContext: Msg: bbb
Bevorzugen sollten Sie allerdings die zuerst gezeigte Variante ohne Instanzvariablen, und mit Speicherung des Zustands im ExecutionContext:
- Die Scope-Variante kann unübersichtlich und fehlerträchtig sein.
- Sie müssen sich selbst um korrekte Wiederanlauffähigkeit kümmern.
- Je nach Umgebung kann es sein, dass es nach Ablauf des Job-Scopes schwierig ist, an die Informationen zu den einzelnen Steps zu kommen.
Weiter unten ist beschrieben, wie Sie einige dieser Nachteile vermeiden können.
Die folgende Variante macht in diesem Beispiel keinen Sinn. Aber um die Scope-Definitionen besser zu verstehen, können Sie im letzten Beispiel die Zeile:
@Scope( value="job", proxyMode=ScopedProxyMode.INTERFACES )
testweise temporär ersetzen durch:
@Scope( value=ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode=ScopedProxyMode.INTERFACES )
Dadurch wird pro Step eine eigene getrennte Step-Bean erstellt, so dass die Instanzvariable nur für jeweils einen Step existiert.
Wenn Sie jetzt den JUnit-Modultest ausführen, erhalten Sie das unbrauchbare Ergebnis (gekürzt):
... JobExecution 0: Job-ExecutionContext: Msg: a ... JobExecution 1: Job-ExecutionContext: Msg: b
Um einige der Nachteile der Variante "Instanzvariable und Job-Scope am Step" zu vermeiden, sollte der Job-Scope nicht am Step, sondern stattdessen an einem Context-Holder-Objekt plaziert werden: Dies ist klarer, übersichtlicher und weniger fehlerträchtig. Und es gibt keine Schwierigkeiten beim Lesen der Step-Informationen nach Ablauf des Job-Scopes.
Ersetzen Sie in ConcurrentJobConfiguration.java die die komplette Step-Bean:
@Bean @Scope( value="job", proxyMode=ScopedProxyMode.INTERFACES ) public Step meinConcurrentStep() { ... }
durch:
@Autowired MeinContextHolder ctxHolder; public interface MeinContextHolder { String getText(); void setText( String text ); } public class MeinContextHolderImpl implements MeinContextHolder { private String text = ""; @Override public String getText() { return text; } @Override public void setText( String text ) { this.text = text; } } /** Context-Holder-Bean mit Job-Scope */ @Bean @Scope( value="job", proxyMode= ScopedProxyMode.INTERFACES ) public MeinContextHolder createMeinContextHolder() { return new MeinContextHolderImpl(); } /** Wenn der Job mehrfach gestartet wird, wird dieser Step multi-threaded parallel durchlaufen */ @Bean public Step meinConcurrentStep() { return stepBuilderFactory.get( "meinConcurrentStep" ).tasklet( new Tasklet() { @Override public RepeatStatus execute( StepContribution sc, ChunkContext cc ) { String meinParm = cc.getStepContext().getStepExecution().getJobParameters().getString( MEIN_PARM_KEY ); ctxHolder.setText( ctxHolder.getText() + meinParm ); cc.getStepContext().getStepExecution().getJobExecution().getExecutionContext().put( MEIN_MSG_KEY, ctxHolder.getText() ); return RepeatStatus.FINISHED; } } ).build(); }
Führen Sie erneut aus:
cd \MeinWorkspace\SpringBatchConcurrentDemo
mvn clean test
Jetzt erhalten Sie wieder das korrekte Ergebnis (gekürzt):
... JobExecution 0: Job-ExecutionContext: Msg: aaa ... JobExecution 1: Job-ExecutionContext: Msg: bbb
In dem folgenden Beispiel wird unterschieden zwischen:
Während "JobParameters" und "Application-Properties" von Spring standardmäßig verwendet werden,
werden die "Job-Properties" zusätzlich eingeführt.
Die Trennung von "Application-Properties" und "Job-Properties" kann in bestimmten Fällen Sinn machen,
beispielsweise wenn die "Application-Properties" von Spring automatisch beim Start der Anwendung geladen werden sollen,
aber die "Job-Properties" beispielsweise bei jedem Job-Lauf erneut geladen werden sollen,
oder womöglich vor jedem Job-Start angepasst werden müssen.
Je nachdem ob der Kommandozeilenparameter "--spring.config.location" gesetzt ist, werden zwei Fälle unterschieden:
Sie können das Beispiel wahlweise entweder downloaden oder wie beschrieben Schritt für Schritt aufbauen und ausführen.
Führen Sie folgende Schritte aus:
Wechseln Sie in Ihr Workspace-Verzeichnis (z.B. \MeinWorkspace) und führen Sie folgende Kommandos aus:
cd \MeinWorkspace
md SpringBatchPrintEnvDemo
cd SpringBatchPrintEnvDemo
md src\main\java\springbatchdemo
md src\test\java\springbatchdemo
md src\main\resources
md src\test\resources
md conf
tree /F
Erstellen Sie im SpringBatchPrintEnvDemo-Projektverzeichnis die schon bekannte Maven-Projektkonfigurationsdatei: pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>springbatchdemo</groupId> <artifactId>SpringBatchPrintEnvDemo</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.1.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-batch</artifactId> </dependency> <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Erläuterungen hierzu finden Sie weiter oben bei der SpringBatchItemChunkDemo-pom.xml.
Erzeugen Sie im src/main/resources-Verzeichnis die ebenfalls bekannte Logging-Konfiguration: logback.xml
<?xml version="1.0" encoding="UTF-8"?> <configuration> <logger name="org.springframework" level="error" /> </configuration>
Erzeugen Sie im src/test/resources-Verzeichnis eine Properties-Datei:
JobProps-Test.properties
Test-Prop-Key=Test-Prop-Value
Erzeugen Sie im conf-Verzeichnis zwei Properties-Dateien:
JobProps-Conf.properties
Test-Prop-Key=Conf-Prop-Value
application.properties
application.properties-Prop-Key=application.properties-Prop-Value
Erzeugen Sie im src/main/java/springbatchdemo-Verzeichnis die schon bekannte Main-Startklasse: MeineApplication.java
package springbatchdemo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class MeineApplication { public static void main( String[] args ) throws Exception { SpringApplication.run( MeineApplication.class, args ); } }
Erläuterungen hierzu finden Sie wieder weiter oben.
Erzeugen Sie im src/main/java/springbatchdemo-Verzeichnis die Klasse zurm Lesen der Job-Properties: JobProperties.java
package springbatchdemo; import org.springframework.core.env.Environment; import org.springframework.stereotype.Component; import java.io.*; import java.util.Properties; /** * Job-Properties. * Bitte Folgendes nicht verwechseln: * - JobParameters: * Von Spring Batch zur Laufzeit weitergereichte zu einer Job-Execution gehoerende Parameter * (beispielsweise fuer die jeweilige Job-Ausfuehrung zu verwendende Datumsbereiche). * - Job-Properties: * Nicht von Spring Batch benoetigte, sondern fuer die Jobs benoetigte Properties (z.B. E-Mail-Texte). * - Application-Properties: * Von Spring beim Start der Anwendung zur Initialisierung der Anwendung und zum Aufbau des * Spring-ApplicationContext verwendete Properties (z.B. DB-URL). * Diese Klasse sammelt Job-Properties. Dabei wird zwischen zwei Faellen unterschieden: * - Der Kommandozeilenparameter "--spring.config.location" ist nicht gesetzt: * Das ist ueblicherweise in JUnit-Modultests der Fall. * Dann werden die Job-Properties-Dateien im "src/test/resources"-Verzeichnis gelesen. * Dies testet der JUnit-Modultest "JobPropertiesTest". * - Der Kommandozeilenparameter "--spring.config.location" ist gesetzt: * Das ist in der Regel der Fall, wenn die Anwendung ausserhalb der Modultests betrieben wird, * beispielsweise in einem Batch-Webserver. * In diesem Fall werden die Job-Properties-Dateien in demselben Verzeichnis gelesen, in dem die per * "--spring.config.location" angegebene Application-Properties-Datei liegt. */ @Component public class JobProperties { /** * Lies Job-Properties aus Properties-Dateien. * @param env Spring-Environment (z.B. injiziert per: @Autowired Environment). * @param prefix "null" falls keine Filterung, sonst nur Properties-Dateien deren Name mit diesem Prefix beginnen. * @param postfix "null" falls keine Filterung, sonst nur Properties-Dateien deren Name mit diesem Postfix endet (vor der Dateiendung). * @param exclude "null" falls keine Filterung, sonst nur Properties-Dateien deren Name verschieden zu diesem Namen ist. * @return Job-Properties */ public static Properties getJobProperties( Environment env, final String prefix, final String postfix, final String exclude ) throws IOException { File parent = null; String springConfigLocation = env.getProperty( "spring.config.location" ); if( springConfigLocation != null && !springConfigLocation.isEmpty() ) { parent = (new File( springConfigLocation )).getAbsoluteFile().getParentFile(); } else { parent = (new File( "src/test/resources" )).getAbsoluteFile(); } return getProperties( parent, prefix, postfix, exclude ); } private static Properties getProperties( File parent, final String prefix, final String postfix, final String exclude ) throws IOException { FilenameFilter filenameFilter = new FilenameFilter() { @Override public boolean accept( File dir, String name ) { if( (exclude != null && name.equals( exclude )) || (!name.endsWith( ".properties" )) || (postfix != null && !name.endsWith( postfix + ".properties" )) || (prefix != null && !name.startsWith( prefix )) ) { return false; } return true; } }; Properties props = new Properties(); File[] files = parent.listFiles( filenameFilter ); for( File f : files ) { FileInputStream is = new FileInputStream( f ); try { Properties p = new Properties(); p.load( is ); props.putAll( p ); } finally { is.close(); } } return props; } }
Erzeugen Sie im src/main/java/springbatchdemo-Verzeichnis die Job-Konfigurationsklasse: PrintEnvJobConfiguration.java
package springbatchdemo; import org.springframework.batch.core.*; import org.springframework.batch.core.configuration.annotation.*; import org.springframework.batch.core.launch.support.RunIdIncrementer; import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.core.step.tasklet.Tasklet; import org.springframework.batch.repeat.RepeatStatus; import org.springframework.beans.factory.annotation.*; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.*; import org.springframework.core.env.*; import java.io.*; import java.util.*; /** * Job zur Ausgabe auf der Konsole von: * - Spring-Environment inklusive Application-Properties, * - alle JobProperties (aus eventuell mehreren Properties-Dateien) * - und gezielt eine einzelne JobProperty (falls deren Key angegeben wird). * Um eine einzelne JobProperty auszugeben, muss deren Key als JobParameter mit dem Key "PropKey" angegeben werden. * Angenommen der Job ist in einem Batch-Webserver mit REST-Schnittstelle integriert und * die URL zum Starten des Jobs entspricht der im Folgenden gezeigten (bitte anpassen), * dann kann dies bei laufendem Batch-Webserver folgendermassen per REST getestet werden, z.B. so: * Unter Linux: * curl --request POST localhost:8080/jobs/meinPrintEnvJob?Zeit=$(date -Iseconds),PropKey=Test-Prop-Key * Unter Windows: * curl --request POST localhost:8080/jobs/meinPrintEnvJob?Zeit=%time%,PropKey=Test-Prop-Key */ @Configuration @EnableBatchProcessing public class PrintEnvJobConfiguration { public static final String JOB_NAME = "meinPrintEnvJob"; @Autowired private ConfigurableApplicationContext ctx; @Autowired private JobBuilderFactory jobBuilderFactory; @Autowired private StepBuilderFactory stepBuilderFactory; @Bean public Job meinPrintEnvJob() throws Exception { return jobBuilderFactory.get( JOB_NAME ).incrementer( new RunIdIncrementer() ) .start( meinPrintEnvStep() ) .build(); } @Bean public Step meinPrintEnvStep() { return stepBuilderFactory.get( "meinPrintEnvStep" ).tasklet( new Tasklet() { @Override public RepeatStatus execute( StepContribution sc, ChunkContext cc ) throws IOException { ConfigurableEnvironment env = ctx.getEnvironment(); printSpringEnvironment( env, null ); File parent = null; String name = null; String springConfigLocation = env.getProperty( "spring.config.location" ); if( springConfigLocation != null ) { File f = (new File( springConfigLocation )).getAbsoluteFile(); parent = f.getParentFile(); name = f.getName(); } System.out.println( "" ); System.out.println( "spring.config.location-Parameter: " + springConfigLocation ); System.out.println( "spring.config.location-ParentDir: " + parent ); System.out.println( "\n-----------------------------------------\n" ); System.out.println( "JobProperties (eventuell aus mehreren Properties-Dateien):" ); Properties jobProps = JobProperties.getJobProperties( env, null, null, name ); TreeMap<Object,Object> mp = new TreeMap<Object,Object>( jobProps ); for( Map.Entry<Object,Object> e : mp.entrySet() ) { System.out.println( " " + e.getKey() + " = " + e.getValue() ); } System.out.println( "\n-----------------------------------------" ); String propKey = cc.getStepContext().getStepExecution().getJobParameters().getString( "PropKey" ); if( propKey != null ) { System.out.println( "Job-Property: " + propKey + " = " + jobProps.get( propKey ) ); System.out.println( "-----------------------------------------" ); } System.out.println( "\n" ); return RepeatStatus.FINISHED; } } ).build(); } /** * Anzeige von Spring-Environment-Properties (nur waehrend Entwicklung sinnvoll). * @param env ConfigurableEnvironment, z.B. mit: ConfigurableApplicationContext.getEnvironment() * oder ConfigFileApplicationListener.postProcessEnvironment() * oder ApplicationEnvironmentPreparedEvent.getEnvironment() * @param onlyPropSrcName Falls null: Anzeige aller Properties; sonst: nur Props aus PropertySource mit diesem Namen */ public static void printSpringEnvironment( ConfigurableEnvironment env, String onlyPropSrcName ) { for( Iterator<org.springframework.core.env.PropertySource<?>> itr = env.getPropertySources().iterator(); itr.hasNext(); ) { org.springframework.core.env.PropertySource<?> ps = itr.next(); if( onlyPropSrcName != null && ps.getName() != null && !ps.getName().contains( onlyPropSrcName ) ) { continue; } System.out.println( "-----------------------------------------" ); System.out.println( ps.getName() + " (" + ps.getClass() + ")" ); if( ps instanceof EnumerablePropertySource ) { EnumerablePropertySource<?> eps = (EnumerablePropertySource<?>) ps; String[] pns = eps.getPropertyNames(); Arrays.sort( pns ); for( String pn : pns ) { System.out.println( " " + pn + " = " + eps.getProperty( pn ) ); } } else { System.out.println( " PropertySource: " + ps.getSource().getClass() ); } } System.out.println( "-----------------------------------------" ); } }
Erzeugen Sie im src/test/java/springbatchdemo-Verzeichnis den JUnit-Modultest: JobPropertiesTest.java
package springbatchdemo; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import java.io.IOException; import java.util.Properties; @RunWith( SpringJUnit4ClassRunner.class ) @ContextConfiguration( classes = MeineApplication.class ) public class JobPropertiesTest { @Autowired Environment env; @Test public void testJobProperties() throws IOException { String propKey = "Test-Prop-Key"; Properties jobProps = JobProperties.getJobProperties( env, "JobProps", "Test", null ); System.out.println( "-----------------------------------------------" ); System.out.println( "Job-Properties: " + propKey + " = " + jobProps.get( propKey ) ); System.out.println( "-----------------------------------------------\n" ); Assert.assertEquals( "Test-Prop-Value", jobProps.get( propKey ) ); } }
Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):
[\MeinWorkspace\SpringBatchPrintEnvDemo] |- [conf] | |- application.properties | '- JobProps-Conf.properties |- [src] | |- [main] | | |- [java] | | | '- [springbatchdemo] | | | |- JobProperties.java | | | |- MeineApplication.java | | | '- PrintEnvJobConfiguration.java | | '- [resources] | | '- logback.xml | '- [test] | |- [java] | | '- [springbatchdemo] | | '- JobPropertiesTest.java | '- [resources] | '- JobProps-Test.properties '- pom.xml
Durch das spring-boot-maven-plugin wird eine ausführbare jar-Datei erzeugt, welche alle benötigten Abhängigkeiten beinhaltet.
Führen Sie im Kommandozeilenfenster aus:
cd \MeinWorkspace\SpringBatchPrintEnvDemo
mvn clean package
java -jar target/SpringBatchPrintEnvDemo-1.0-SNAPSHOT.jar --spring.config.location=conf/application.properties
Sie erhalten:
----------------------------------------- ... ... ----------------------------------------- applicationConfig: [file:conf/application.properties] (class org.springframework.core.env.PropertiesPropertySource) application.properties-Prop-Key = application.properties-Prop-Value ----------------------------------------- spring.config.location-Parameter: conf/application.properties spring.config.location-ParentDir: \MeinWorkspace\SpringBatchPrintEnvDemo\conf ----------------------------------------- JobProperties (eventuell aus mehreren Properties-Dateien): Test-Prop-Key = Conf-Prop-Value -----------------------------------------
Während im JUnit-Modultest die Properties-Datei src\test\resources\JobProps-Test.properties gelesen wurde, wird bei der Programmausführung mit --spring.config.location-Kommandozeilenparameter die Properties-Datei conf\JobProps-Conf.properties verwendet.
Spring Batch Admin
ist ein Spring-MVC-Aufsatz zu Spring Batch zur Verwaltung von Batch-Jobs, der wahlweise
Spring Batch Admin ermöglicht
Allerdings ist die Intention von Spring Batch Admin weniger eine fertige produktionstaugliche Anwendung zu bieten, sondern eher einen Prototyp zur technischen Demonstration, der weiter entwickelt werden kann.
Spring Batch Admin kann sowohl stand-alone betrieben werden als auch in andere Applikationen integriert werden.
Downloaden Sie Spring Batch Admin 1.3.1 wie unter Getting Started beschrieben, also entweder manuell von Spring Community Download oder GitHub: spring-projects/spring-batch-admin, oder, falls Sie curl installiert haben, beispielsweise folgendermaßen per Kommandozeile:
cd \MeinWorkspace
md SpringBatchAdminDemo
cd SpringBatchAdminDemo
jar xf spring-batch-admin-1.3.1.RELEASE.zip
xcopy spring-batch-admin-1.3.1.RELEASE\sample . /S
rmdir /S /Q spring-batch-admin-1.3.1.RELEASE
del spring-batch-admin-1.3.1.RELEASE.zip
Starten Sie im Kommandozeilenfenster den embedded Jetty-Webserver mit der Spring-Batch-Admin-Webanwendung:
cd \MeinWorkspace\SpringBatchAdminDemo\spring-batch-admin-sample
mvn jetty:run -Djetty.port=8080
Warten Sie bis "[INFO] Started Jetty Server" erscheint.
Öffnen Sie die Spring-Batch-Admin-Weboberfläche:
Eine Anleitung zur Benutzung mit vielen Screenshots finden Sie im:
Spring Batch Admin User Guide.
Die Beispielanwendung enthält bereits Demo-Jobs, die Sie testen können.
Stoppen Sie den Jetty-Webserver im Kommandozeilenfenster mit "Strg+C".
Den ersten eigenen Batch-Job wollen wir in Form von Sourcedateien zu Spring Batch Admin hinzufügen. Weiter unten werden weitere Batch-Jobs als Jar-Lib hinzugefügt.
Als eigener Batch-Job dient das oben erstellte Beispiel: Spring-Batch-Item/Chunk-Demo.
Kopieren Sie daraus Dateien in die Spring-Batch-Admin-Beispielanwendung:
cd \MeinWorkspace\SpringBatchAdminDemo\spring-batch-admin-sample
copy \MeinWorkspace\SpringBatchItemChunkDemo\src\main\resources\BeispielDaten*.csv src\main\resources
copy /Y \MeinWorkspace\SpringBatchItemChunkDemo\src\main\resources\schema-all.sql src\main\resources\business-schema-hsqldb.sql
xcopy \MeinWorkspace\SpringBatchItemChunkDemo\src\main\java\itemchunkdemo src\main\java\itemchunkdemo\
del src\main\java\itemchunkdemo\MeineApplication.java
Erstellen Sie im \MeinWorkspace\SpringBatchAdminDemo-Verzeichnis die Spring-Bean-Definitionsdatei: meinItemChunkJob.xml
<?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:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd"> <context:annotation-config /> <context:component-scan base-package="itemchunkdemo" /> </beans>
In diesem einfachen Fall enthält die XML-Datei lediglich einen Hinweis auf das zu scannende Package-Verzeichnis "itemchunkdemo", in dem sich die Jobdefinition in Form von Java-Klassen befindet. Alternativ könnte eine Jobdefinition in dieser XML-Datei erfolgen.
Bauen Sie das Projekt neu und rufen Sie wieder den embedded Jetty-Webserver und die Spring-Batch-Admin-Weboberfläche auf:
cd \MeinWorkspace\SpringBatchAdminDemo\spring-batch-admin-sample
mvn clean package
mvn jetty:run -Djetty.port=8080
start http://localhost:8080/spring-batch-admin-sample
Vorerst sehen Sie keine Änderung in der Spring-Batch-Admin-Weboberfläche. Der neue Job wird noch nicht angezeigt.
Wählen Sie im Spring-Batch-Admin-GUI:
"Files | Configuration | Register XML File | Durchsuchen... |
\MeinWorkspace\SpringBatchAdminDemo |
meinItemChunkJob.xml | Öffnen | Upload".
Unter dem Reiter "Jobs" wird der neue Job "meinItemChunkJob" angezeigt.
(Weiter unten
wird gezeigt, wie Sie diesen Schritt vermeiden können,
indem Sie die xml-Datei nach src\main\resources\META-INF\spring\batch\jobs kopieren.)
Klicken Sie auf den Jobnamen "meinItemChunkJob". Tragen Sie einen beliebigen Key/Value-Parameter als JobParameter ein, beispielsweise "x=y". Wählen Sie "Launch". Sie erhalten:
Und wenn Sie erneut auf den Reiter "Jobs" und den Jobnamen "meinItemChunkJob" klicken:
Um sich den "Step Execution Progress", die "History of Step Execution" und die "Details for Step Execution" anzusehen, klicken Sie auf: "Executions | 0 | COMPLETED".
Sehen Sie sich die Meldungen im Jetty-Kommandozeilenfenster an:
... INFO ... xml.XmlBeanDefinitionReader:315 - Loading XML bean definitions from meinItemChunkJob.xml ... ... INFO ... [FlowJob: [name=meinItemChunkJob]] launched with the following parameters: [{x=y, run.id=1}] ... ---- JobInstance.JobName: meinItemChunkJob JobExecution.Status: COMPLETED JobParameters: {x=y, run.id=1} ---- Datenbankinhalt: MeineDaten: {Anton, Alfa, 9 Buchstaben} MeineDaten: {Berta, Bravo, 10 Buchstaben} MeineDaten: {Caesar, Charlie, 13 Buchstaben} MeineDaten: {Dora, Delta, 9 Buchstaben} MeineDaten: {Emil, Echo, 8 Buchstaben} MeineDaten: {Felix, Foxtrot, 12 Buchstaben} ... INFO ... [FlowJob: [name=meinItemChunkJob]] completed with ... the following status: [COMPLETED]
Wiederholen Sie den Vorgang, aber geben Sie diesmal als JobParameter den Parameter "InputDateiPfad" mit einem gültigen relativen oder absoluten Dateipfad zu einer CSV-Datei in Ihrem Dateisystem an, beispielsweise "InputDateiPfad=src/main/resources/BeispielDaten2.csv". Sehen Sie sich wieder die Meldungen im Jetty-Kommandozeilenfenster an:
... INFO ... [FlowJob: [name=meinItemChunkJob]] launched with the following parameters: [{InputDateiPfad=src/main/resources/BeispielDaten2.csv, run.id=1}] ... ---- JobInstance.JobName: meinItemChunkJob JobExecution.Status: COMPLETED JobParameters: {InputDateiPfad=src/main/resources/BeispielDaten2.csv, run.id=1} ---- Datenbankinhalt: ... MeineDaten: {Otto, Olive, 9 Buchstaben}
Der "InputDateiPfad"-JobParameter wurde an den FlatFileItemReader in ItemChunkJobConfiguration.java übergeben, die BeispielDaten2.csv wurde gelesen, und es wurde ein weiterer Datensatz hinzugefügt: {Otto, Olive, 9 Buchstaben}.
Sie können Spring Batch Admin nicht nur über den Webbrowser bedienen, sondern auch über JSON-REST-Schnittstellen ansteuern und abfragen.
Sehen Sie sich die JSON-REST-Beispiele unter
Spring Batch Admin User Guide: JSON API an.
Eine Übersicht der REST-Kommandos zeigt Spring Batch Admin auf der Startseite
http://localhost:8080/spring-batch-admin-sample/home an.
Sie können die JSON-REST-Schnittstelle mit beliebigen REST-Tools verwenden (für die Abfrage-Kommandos genügt ein Webbrowser).
In den folgenden Beispiel-REST-Aufrufen wird
curl verwendet.
Abfrage aller Jobs:
curl http://localhost:8080/spring-batch-admin-sample/jobs.json
{"jobs" : { "resource" : "http://localhost:8080/spring-batch-admin-sample/jobs.json", "registrations" : { ... "meinItemChunkJob" : { "name" : "meinItemChunkJob", "resource" : "http://localhost:8080/spring-batch-admin-sample/jobs/meinItemChunkJob.json", "description" : "No description", "executionCount" : 1, "launchable" : true, "incrementable" : true }, ... } } }
Abfrage eines bestimmten Jobs:
curl http://localhost:8080/spring-batch-admin-sample/jobs/meinItemChunkJob.json
{"job" : { "resource" : "http://localhost:8080/spring-batch-admin-sample/jobs/meinItemChunkJob.json", "name" : "meinItemChunkJob", "jobInstances" : { "0" : { "resource" : "http://localhost:8080/spring-batch-admin-sample/jobs/meinItemChunkJob/0/executions.json", "executionCount" : 1, "lastJobExecution" : "http://localhost:8080/spring-batch-admin-sample/jobs/executions/0.json", "lastJobExecutionStatus" : "COMPLETED" } } } }
Abfrage der "jobInstance":
curl http://localhost:8080/spring-batch-admin-sample/jobs/meinItemChunkJob/0/executions.json
{"jobInstance" : { "id" : 0, "jobName" : "meinItemChunkJob", "jobExecutions" : { "0" : { "status" : "COMPLETED", "startTime" : "18:06:11", "duration" : "00:00:00", "resource" : "http://localhost:8080/spring-batch-admin-sample/jobs/executions/0.json", "jobParameters" : { "x" : "y", "run.id(long)" : "1" } } }} }
Abfrage der "jobExecution":
curl http://localhost:8080/spring-batch-admin-sample/jobs/executions/0.json
{"jobExecution" : { "resource" : "http://localhost:8080/spring-batch-admin-sample/jobs/executions/0.json", "id" : "0", "name" : "meinItemChunkJob", "status" : "COMPLETED", "startTime" : "18:06:11", "duration" : "00:00:00", "exitCode" : "COMPLETED", "exitDescription" : "", "jobInstance" : { "resource" : "http://localhost:8080/spring-batch-admin-sample/jobs/meinItemChunkJob/0.json" }, "stepExecutions" : { "meinStep" : { "status" : "COMPLETED", "exitCode" : "COMPLETED", "id" : "0", "resource" : "http://localhost:8080/spring-batch-admin-sample/jobs/executions/0/steps/0.json", "readCount" : "6", "writeCount" : "6", "commitCount" : "1", "rollbackCount" : "0", "duration" : "00:00:00" } } } }
Abfrage der "stepExecution":
curl http://localhost:8080/spring-batch-admin-sample/jobs/executions/0/steps/0.json
{"stepExecution" : { "id" : "0", "name" : "meinStep", "resource" : "http://localhost:8080/spring-batch-admin-sample/jobs/executions/0/steps/0/progress", "status" : "COMPLETED", "startTime" : "18:06:11", "duration" : "00:00:00", "readCount" : 6, "writeCount" : 6, "filterCount" : 0, "readSkipCount" : 0, "writeSkipCount" : 0, "processSkipCount" : 0, "commitCount" : 1, "rollbackCount" : 0, "exitCode" : "COMPLETED", "exitDescription" : "" }, "jobExecution" : { "id" : "0", "resource" : "http://localhost:8080/spring-batch-admin-sample/jobs/executions/0.json", "status" : "COMPLETED" } }
Sie können nicht nur Abfragen durchführen, sondern auch steuernde Kommandos ausführen.
Beispielsweise können Sie Jobs starten, etwa indem Sie vom meinItemChunkJob eine zweite "jobExecution" beauftragen:
curl --request POST -d jobParameters=x2=y2 http://localhost:8080/spring-batch-admin-sample/jobs/meinItemChunkJob.json
{"jobExecution" : { "resource" : "http://localhost:8080/spring-batch-admin-sample/jobs/executions/1.json", "id" : "1", "name" : "meinItemChunkJob", "status" : "STARTED", "startTime" : "", "duration" : "", "exitCode" : "UNKNOWN", "exitDescription" : "", "jobInstance" : { "resource" : "http://localhost:8080/spring-batch-admin-sample/jobs/meinItemChunkJob/1.json" }, "stepExecutions" : { } } }
Oben haben wir den Batch-Job Spring-Batch-Item/Chunk-Demo in Form von Sourcedateien zu Spring Batch Admin hinzugefügt. Im Folgenden werden zwei Batch-Jobs als Jar-Libs hinzugefügt.
Voraussetzung ist, dass die beiden oben gezeigten Spring-Batch-Job-Demos Spring-Batch-Tasklet-Demo und Spring-Batch-Conditional-Flow-Demo angelegt (oder downgeloadet) und gebaut wurden.
Kopieren Sie im \MeinWorkspace\SpringBatchAdminDemo-Verzeichnis die oben erstellte Spring-Bean-Definitionsdatei meinItemChunkJob.xml:
cd \MeinWorkspace\SpringBatchAdminDemo\spring-batch-admin-sample\src\main\resources\META-INF\spring\batch\jobs
copy \MeinWorkspace\SpringBatchAdminDemo\meinItemChunkJob.xml meineDemosJob.xml
Ersetzen Sie in der neuen XML-Datei meineDemosJob.xml die Zeile
<context:component-scan base-package="itemchunkdemo" />
durch
<context:component-scan base-package="springbatchdemo" />
Bauen Sie die beiden Spring-Batch-Job-Demos und kopieren Sie jeweils die erzeugte Jar-Lib:
cd \MeinWorkspace\SpringBatchAdminDemo\spring-batch-admin-sample
md src\main\webapp\WEB-INF\lib
copy ..\..\SpringBatchTaskletDemo\target\SpringBatchTaskletDemo-1.0-SNAPSHOT.jar.original src\main\webapp\WEB-INF\lib\SpringBatchTaskletDemo-1.0-SNAPSHOT.jar
copy ..\..\SpringBatchConditionalFlowDemo\target\SpringBatchConditionalFlowDemo-1.0-SNAPSHOT.jar.original src\main\webapp\WEB-INF\lib\SpringBatchConditionalFlowDemo-1.0-SNAPSHOT.jar
Die Projektstruktur enthält jetzt unter anderem folgende Dateien (überprüfen Sie es mit tree /F):
[\MeinWorkspace\MeinWorkspace\SpringBatchAdminDemo\spring-batch-admin-sample] |- [src] | |- [main] | | |- [java] | | | |- [itemchunkdemo] | | | | |- ItemChunkJobConfiguration.java | | | | |- JobCompletionNotificationListener.java | | | | |- Meinedaten.java | | | | '- MeinedatenItemProcessor.java | | | '- [org] | | | '- ... | | |- [resources] | | | |- [META-INF] | | | | '- [spring] | | | | '- [batch] | | | | |- [jobs] | | | | | |- ... | | | | | '- meineDemosJob.xml | | | | '- [servlet] | | | | '- ... | | | |- ... | | | |- BeispielDaten1.csv | | | |- BeispielDaten2.csv | | | |- business-schema-hsqldb.sql | | | '- ... | | '- [webapp] | | |- [WEB-INF] | | | |- [lib] | | | | |- SpringBatchConditionalFlowDemo-1.0-SNAPSHOT.jar | | | | '- SpringBatchTaskletDemo-1.0-SNAPSHOT.jar | | | '- web.xml | | '- index.jsp | |- [site] | | '- ... | '- [test] | '- ... |- ... '- pom.xml
Falls der embedded Jetty-Webserver noch läuft: Beenden Sie ihn mit Strg+C.
Bauen Sie das Spring-Batch-Admin-Projekt und starten Sie im Kommandozeilenfenster den embedded Jetty-Webserver mit der Spring-Batch-Admin-Webanwendung:
cd \MeinWorkspace\SpringBatchAdminDemo\spring-batch-admin-sample
mvn jetty:run -Djetty.port=8080
Warten Sie bis "[INFO] Started Jetty Server" erscheint und öffnen Sie die Spring-Batch-Admin-Weboberfläche:
Anders als oben beschrieben, brauchen Sie diesmal die XML-Jobbeschreibungen nicht zu registrieren, da sie bereits nach src\main\resources\META-INF\spring\batch\jobs kopiert wurden.
Wählen Sie im Spring-Batch-Admin-GUI den Reiter "Jobs": Die neuen Jobs "jobAbc" und "meinTaskletJob" werden angezeigt und können wie oben beschrieben ausgeführt und beobachtet werden.
Um die Übergabe von JobParametern zu testen, tragen Sie beim Launch beispielsweise folgende JobParameter ein
(Sie können mehrere Key/Value-Paare durch Komma getrennt eingeben):
- für meinItemChunkJob: InputDateiPfad=src/main/resources/BeispielDaten2.csv
- für meinTaskletJob: AnzahlSteps=11
- für meinConditionalFlowJob: OK_ODER_FEHLER=ok oder OK_ODER_FEHLER=Fehler
Sehen Sie sich die veränderten Ergebnisse im Jetty-Kommandozeilenfenster an.
Bauen Sie die Demo Spring-Batch-Demo "Stoppable Delayed Retry", und kopieren Sie die erzeugte Jar-Lib:
cd \MeinWorkspace\SpringBatchAdminDemo\spring-batch-admin-sample
copy ..\..\SpringBatchStoppableDelayedRetryDemo\target\SpringBatchStoppableDelayedRetryDemo-1.0-SNAPSHOT.jar.original src\main\webapp\WEB-INF\lib\SpringBatchStoppableDelayedRetryDemo-1.0-SNAPSHOT.jar
Außerdem muss wie oben beschrieben meineDemosJob.xml angelegt sein.
Wenn Sie jetzt Jetty starten, erhalten Sie die Exception:
...BeanCreationException: ...BeanDefinitionStoreException: ...
java.lang.NoClassDefFoundError: org/springframework/batch/core/step/tasklet/StoppableTasklet:
java.lang.ClassNotFoundException: org.springframework.batch.core.step.tasklet.StoppableTasklet
Der Grund ist, dass Spring Batch Admin 1.3.1 die veraltete Spring-Batch-Version 2.2.7 einsetzt, welche noch nicht das StoppableTasklet kennt.
Ändern Sie im SpringBatchAdminDemo\spring-batch-admin-parent-Verzeichnis in der pom.xml die Zeile
<spring.batch.version>2.2.7.RELEASE</spring.batch.version>
zu:
<spring.batch.version>3.0.7.RELEASE</spring.batch.version>
Jetzt können Sie Jetty fehlerfrei starten.
Falls der embedded Jetty-Webserver noch läuft: Beenden Sie ihn mit Strg+C. Starten Sie Jetty neu:
cd \MeinWorkspace\SpringBatchAdminDemo\spring-batch-admin-sample
mvn jetty:run -Djetty.port=8080
Warten Sie bis "[INFO] Started Jetty Server" erscheint und überprüfen Sie in einem zweiten Kommandozeilenfenster mit curl, dass der Job jobStoppableDelayedRetry in der Liste der bekannten Jobs enthalten ist:
curl http://localhost:8080/spring-batch-admin-sample/jobs.json
Testen Sie die Abbrechbarkeit. Um mehrfach testen zu können, können Sie folgende einfache Windows-Batchdatei starte-und-stoppe-jobStoppableDelayedRetry.bat erstellen:
@echo. @echo Job mit Fehler als Vorgabe starten, damit das WarteTasklet ausgefuehrt wird: curl --request POST -d jobParameters=OK_ODER_FEHLER=Fehler,ANZ_WIEDERHOL=5 ^ http://localhost:8080/spring-batch-admin-sample/jobs/jobStoppableDelayedRetry.json echo. @ping 1.1.1.1 -n 1 -w 900 >nul @echo. @echo Job vorzeitig abbrechen (Achtung: es werden alle laufenden Jobs abgebrochen!): curl --request DELETE http://localhost:8080/spring-batch-admin-sample/jobs/executions.json
Sie erhalten im Jetty-Fenster (gekürzt):
Executing step: [warteStep] WarteTasklet: Wiederholung nach Wartezeit . !!! Stopp-Signal durch JobExecution !!! ... JobExecution is stopped
Sie sehen, dass das "Stopp-Signal durch JobExecution" funktioniert,
aber das "Stopp-Signal durch StoppableTasklet" erscheint nicht.
Spring Batch Admin 1.3.1 unterstützt noch nicht den Abbruch per
StoppableTasklet.
Im folgenden Absatz sehen Sie, wie Sie das Ergebnis auch auf der Client-Seite des REST-Services erkennen können,
wenn Sie sich die jobExecution-id merken.
Wenn Sie nicht sämtliche Jobs stoppen wollen, sondern nur den gerade gestarteten, dann müssen Sie die jobExecution-id ermitteln und verwenden. Am einfachsten geht dies mit Kommandozeilen-JSON-Parsern wie beispielsweise jq (sowohl für Linux als auch für Windows). Falls Sie ohne jq auskommen wollen, kann das für Linux testweise beispielsweise so aussehen:
#!/bin/bash clear echo echo -------------------------------------------------------------------------------- echo Teste den Abbruch des Jobs jobStoppableDelayedRetry \(bei laufendem Batch-Server\) echo -------------------------------------------------------------------------------- echo echo Starte jobStoppableDelayedRetry _id=$(curl -s --request POST -d jobParameters=OK_ODER_FEHLER=Fehler,ANZ_WIEDERHOL=5,Zeit=$(date -Iseconds) \ localhost:8080/spring-batch-admin-sample/jobs/jobStoppableDelayedRetry.json \ | grep '"id"' | cut -d: -f2 | tr -d , | tr -d ' ' | tr -d '"') echo echo "jobExecution-id: $_id" echo curl localhost:8080/spring-batch-admin-sample/jobs/executions/$_id.json echo echo echo Warte 0,5 Sekunden sleep 0.5s echo echo Stoppe den Job mit: "curl --request DELETE localhost:8080/spring-batch-admin-sample/jobs/executions/$_id.json" echo curl --request DELETE localhost:8080/spring-batch-admin-sample/jobs/executions/$_id.json echo echo sleep 0.2s echo STOPPED-Ergebnis: echo curl localhost:8080/spring-batch-admin-sample/jobs/executions/$_id.json echo echo
Sie erhalten auf der REST-Client-Seite als Ergebnis des letzten curl-Aufrufs (gekürzt):
{"jobExecution" : { "resource" : "http://localhost:8080/spring-batch-admin-sample/jobs/executions/...json", "id" : "...", "name" : "jobStoppableDelayedRetry", "status" : "STOPPED", "startTime" : "...", "duration" : "00:00:01", "exitCode" : "STOPPED", "exitDescription" : "org.springframework.batch.core.JobInterruptedException; Stopp-Signal durch JobExecution", "jobInstance" : { "resource" : "http://localhost:8080/spring-batch-admin-sample/jobs/jobStoppableDelayedRetry/...json" }, "stepExecutions" : { ...
Die exitDescription "Stopp-Signal durch JobExecution" kann vom Client ausgewertet und angezeigt werden.
Auch die Vermeidung doppelter Jobläufe können Sie testen und Client-seitig erkennen. Führen Sie kurz hintereinander zwei Job-Starts mit etwas unterschiedlichen JobParametern aus, beispielsweise so:
curl --request POST -d jobParameters=OK_ODER_FEHLER=Fehler,ANZ_WIEDERHOL=10 http://localhost:8080/spring-batch-admin-sample/jobs/jobStoppableDelayedRetry.json
Und kurz danach:
curl --request POST -d jobParameters=OK_ODER_FEHLER=Fehler,ANZ_WIEDERHOL=11 http://localhost:8080/spring-batch-admin-sample/jobs/jobStoppableDelayedRetry.json
Wenn Sie sich vom letzten curl-Aufruf die jobExecution-id merken, und diese in folgendem Kommando statt $_id einsetzen:
curl localhost:8080/spring-batch-admin-sample/jobs/executions/$_id.json
Dann erhalten Sie auf der REST-Client-Seite (gekürzt):
{"jobExecution" : { "resource" : "http://localhost:8080/spring-batch-admin-sample/jobs/executions/...json", "id" : "...", "name" : "jobStoppableDelayedRetry", "status" : "FAILED", "startTime" : "...", "duration" : "00:00:00", "exitCode" : "FAILED", "exitDescription" : "Abbruch weil Job bereits laeuft", "jobInstance" : { "resource" : "http://localhost:8080/spring-batch-admin-sample/jobs/jobStoppableDelayedRetry/...json" }, "stepExecutions" : { "avoidDuplicateRun" : { "status" : "COMPLETED", "exitCode" : "FAILED", ...
Auch hier sehen Sie auf der REST-Client-Seite den Grund für den Job-Abbruch.
Falls Sie weitere ähnliche Tests auch mit anderen Jobs und Job-Parametern durchführen wollen, kann unter Linux das folgende Skript hilfreich sein, welches die jobExecution-id automatisch ermittelt, und damit eine Statusabfrage direkt nach Job-Start durchführt, und eine weitere nach einer vorgegebenen Wartezeit (z.B. wenn der Job fertig ist).
#!/bin/bash clear echo ------------------------------------------------------------------------------------------------ echo Teste die Ausfuehrung eines Jobs \(bei laufendem Batch-Server\). echo Zwei Parameter werden benoetigt: Jobname und JobParameter-String, echo als dritter Parameter kann die Wartezeit in Sekunden angegeben werden, echo z.B.: ./starte-und-checke-job.sh meinTaskletJob AnzahlSteps=11 2 echo oder: ./starte-und-checke-job.sh jobStoppableDelayedRetry OK_ODER_FEHLER=Fehler,ANZ_WIEDERHOL=3 3 echo ------------------------------------------------------------------------------------------------ _jobName=$1 _jobParm=$2 _pause=$3 if [ ! -n "$_jobName" ] ; then echo -e "\nFehler: Jobname-Parameter fehlt.\n" ; exit 1 ; fi if [ ! -n "$_jobParm" ] ; then echo -e "\nFehler: JobParameter-String fehlt.\n" ; exit 1 ; fi if [ ! -n "$_pause" ] ; then _pause=3 ; fi echo kommando="curl -s --request POST -d jobParameters=$_jobParm,Zeit=$(date -Iseconds) \ localhost:8080/spring-batch-admin-sample/jobs/$_jobName.json \ | grep '\"id\"' | cut -d: -f2 | tr -d , | tr -d ' ' | tr -d '\"'" echo ---- "$kommando" _id=$(eval "$kommando") echo echo "Jobname: $_jobName" echo "JobParameter: $_jobParm" echo "jobExecution-id: $_id" echo echo ---- JobExecution direkt nach Start: kommando="curl localhost:8080/spring-batch-admin-sample/jobs/executions/$_id.json" echo ---- "$kommando" eval "$kommando" echo echo echo ---- "Warte $_pause Sekunden" sleep ${_pause}s echo echo ---- JobExecution nach Wartezeit: kommando="curl localhost:8080/spring-batch-admin-sample/jobs/executions/$_id.json" echo ---- "$kommando" eval "$kommando" echo echo
Beachten Sie, dass die HSQLDB bislang nicht weiter konfiguriert wurde und deshalb als In-Memory-Datenbank verwendet wird, also bei jedem Lauf neu aufgesetzt wird, und am Ende alles vergisst. Für weitere Versuche sollten Sie entweder die HSQLDB als dateibasierte Datenbank konfigurieren, oder alternativ eine andere Datenbank verwenden. Sehen Sie sich hierzu die Vorbereitungen an in den Properties-Dateien in: spring-batch-admin-sample\src\main\resources.
Falls Sie Spring Batch Admin nicht in einen anderen Servlet-Container deployen, sondern den embedded Jetty-Server verwenden, und falls Sie dabei auf Dependency-Versionsprobleme stoßen: Defaultmäßig verwendet Spring Batch Admin 1.3.1 die sehr alte Jetty-Version 6.1.26, die beispielsweise nur Servlet 2.5 und noch nicht 3.x unterstützt. Sehen Sie sich hierzu an: Jetty-Versionen und korrespondierende Java- und Servlet-Versionen. Falls eine aktuelle Jetty-Version nicht in Frage kommt, kann die Version 8.1.16.v20140903 eine gute Wahl sein.
Falls Spring Batch Admin nicht Ihren Anforderungen entspricht: Sehen Sie sich alternativ den Spring-Boot-Starter spring-boot-starter-batch-web an: Batcharchitektur in the Enterprise, spring-boot-starter-batch-web, Tobias Flohre und Dennis Schulte (Blog, GitHub, Reference Guide). Falls Ihre Anwendung JSR-352-konform ist, kann auch die GlassFish Batch Job Admin Console interessant sein.
Falls Sie einen Scheduler benötigen: Sehen Sie sich beispielsweise an: Spring Batch Quartz Sample, Spring Batch & Quartz Scheduler Example und spring-batch-quartz-example.
Falls Sie die Annotation @EnableBatchProcessing verwenden, und falls Sie beim Hinzufügen Ihrer Java-Job-Configuration (wie z.B. obige ...JobConfiguration-Klassen) in eine Spring-Batch-Framework-Umgebung eine Fehlermeldung ähnlich zu folgender erhalten:
Injection of autowired dependencies failed; ... org.springframework.beans.factory.BeanCreationException: ... org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.batch.core.configuration.annotation.JobBuilderFactory]: Factory method 'jobBuilders' threw exception; nested exception is java.lang.ClassCastException: org.springframework.batch.core.repository.support.JobRepositoryFactoryBean$$... cannot be cast to org.springframework.batch.core.repository.JobRepository
Dann ist die Ursache wahrscheinlich, dass zwei verschiedene Stellen versuchen, jeweils einen StepScope einzurichten, aber mit unterschiedlichen Konzepten (z.B. dynamic Subclassing versus Java-Proxies).
In diesem Fall müssen Sie die Annotation @EnableBatchProcessing entfernen und dadurch fehlende Bean-Definitionen hinzufügen. Angenommen Ihre Java-Job-Configuration beginnt so:
@Configuration @EnableBatchProcessing public class MeineJobConfiguration { @Autowired private JobBuilderFactory jobBuilderFactory; @Autowired private StepBuilderFactory stepBuilderFactory; ...
Dann entfernen Sie @EnableBatchProcessing und erweitern Sie folgendermaßen die Definitionen:
@Configuration public class MeineJobConfiguration { @Autowired @Qualifier("meineJobBuilderFactory") private JobBuilderFactory jobBuilderFactory; @Autowired @Qualifier("meineStepBuilderFactory") private StepBuilderFactory stepBuilderFactory; @Bean public JobBuilderFactory meineJobBuilderFactory( JobRepository jobRepository ) { return new JobBuilderFactory( jobRepository ); } @Bean public StepBuilderFactory meineStepBuilderFactory( JobRepository jobRepository, PlatformTransactionManager transactionManager ) { return new StepBuilderFactory( jobRepository, transactionManager ); } ...
Falls Ihre vorhandenen Jobs von der JobRegistry nicht aufgelistet werden (z.B. mit: jobOperator.getJobNames()), und Sie eine Exception ähnlich zu dieser erhalten:
org.springframework.batch.core.launch.NoSuchJobException: No job configuration with the name [...] was registered
Dann könnte eine Registrierung der JobRegistry fehlen. Fügen Sie folgende Konfigurationsklasse hinzu: JobRegistryBeanPostProcessorConfiguration.java
package springbatchdemo; import org.springframework.batch.core.configuration.JobRegistry; import org.springframework.batch.core.configuration.support.JobRegistryBeanPostProcessor; import org.springframework.context.annotation.*; @Configuration public class JobRegistryBeanPostProcessorConfiguration { @Bean public JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor( JobRegistry jobRegistry ) { JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor = new JobRegistryBeanPostProcessor(); jobRegistryBeanPostProcessor.setJobRegistry( jobRegistry ); return jobRegistryBeanPostProcessor; } }
Falls in Ihrer Umgebung noch keine Scopes definiert sind, können Sie dies eventuell mit dieser Klasse nachholen: ScopeConfiguration.java
import org.springframework.batch.core.scope.*; import org.springframework.context.annotation.*; @Configuration class ScopeConfiguration { @Bean public JobScope jobScope() { JobScope jobScope = new JobScope(); jobScope.setAutoProxy( false ); return jobScope; } @Bean public StepScope stepScope() { StepScope stepScope = new StepScope(); stepScope.setAutoProxy( false ); return stepScope; } }
Achten Sie darauf, dass die korrekten Klassen verwendet werden:
org.springframework.batch.core.scope.JobScope
org.springframework.batch.core.scope.StepScope
Und nicht die Annotationen:
org.springframework.batch.core.configuration.annotation.JobScope
org.springframework.batch.core.configuration.annotation.StepScope
Falls Sie eine Fehlermeldung ähnlich zu folgender erhalten:
org.springframework.beans.factory.BeanCreationException: Scope 'step' is not active for the current thread;
consider defining a scoped proxy for this bean if you intend to refer to it from a singleton;
java.lang.IllegalStateException: No context holder available for step scope
Falls Sie diese Exception bei dem Versuch erhalten, beispielsweise mit "@Scope( "step" )" oder "@Scope( "job" )" eine für Multi-threading geeignete Stateful-Bean zu erstellen, dann versuchen Sie stattdessen eine Variante ähnlich zu diesen:
Falls jedesmal eine neue Bean instanziiert werden soll:
@Bean @Scope( value=ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode=ScopedProxyMode.INTERFACES )
Falls pro Job-Execution jeweils eine neue Bean instanziiert werden soll:
@Bean @Scope( value="job", proxyMode=ScopedProxyMode.INTERFACES )
Wie schon weiter oben beschrieben, ist der ScopedProxyMode immer dann notwendig,
wenn eine Bean mit zeitlich einschränkendem Scope (wie eine Step-Bean mit job-Scope)
in eine andere Bean mit Singleton-Scope (wie eine Job-Bean) injiziert werden soll.
Abhängig von Ihrer Umgebung können Sie alternativ eventuell auch "@JobScope" verwenden.
Allerdings sind Sie dann auf den
ScopedProxyMode.TARGET_CLASS
festgelegt, der etwas weniger performant ist als
ScopedProxyMode.INTERFACES.
Sehen Sie sich hierzu an: Multi-threaded stateful Step-Bean mit Zustand in Instanzvariable und Job-Scope am Step, Bean scopes, Job Scope, @JobScope, @Scope, proxyMode, ConfigurableBeanFactory.SCOPE_PROTOTYPE, ScopedProxyMode.INTERFACES.
Falls Sie eine Fehlermeldung ähnlich zu folgender erhalten:
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name '...WebSecurityConfigurerAdapter': Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire method: public void org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter.setObjectPostProcessor( org.springframework.security.config.annotation.ObjectPostProcessor ); nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [org.springframework.security.config.annotation.ObjectPostProcessor] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {}
Hierfür kommen verschiedene Ursachen in Frage. Falls Sie außer Spring Batch auch Spring Boot sowie eine Web-Konfiguration beispielsweise für REST-Services verwenden, und verschiedene Testarten in einem gemeinsamen Maven-Modul einsetzen, können Sie die oben gezeigte Exception normalerweise vermeiden, wenn Sie das Modul auf zwei getrennte Maven-Module aufteilen (zu den verschiedenen Testarten siehe auch Testing und Testing improvements):
Eine andere Möglichkeit ist eventuell, eine dieser Annotationen zu verwenden:
Angenommen es handelt sich um einen JUnit-Modultest und Ihre Testklasse beginnt so:
@RunWith( SpringJUnit4ClassRunner.class ) @ContextConfiguration( classes = MeineApplication.class ) public class MeinJobTest { ... }
Dann fügen Sie folgendermaßen beispielsweise @WebAppConfiguration hinzu:
@RunWith( SpringJUnit4ClassRunner.class ) @ContextConfiguration( classes = MeineApplication.class ) @WebAppConfiguration public class MeinJobTest { ... }
Falls Sie eine Fehlermeldung ähnlich zu folgender erhalten:
org.springframework.batch.core.step.FatalStepExecutionException: JobRepository failure forcing rollback,
org.springframework.dao.ConcurrencyFailureException: PreparedStatementCallback; SQL [UPDATE BATCH_STEP_EXECUTION ...]; serialization failure
Dann ist die Ursache wahrscheinlich, dass die verwendete Datenbank nicht für Multi-threading-Zugriffe konfiguriert ist.
Beispielsweise im Falle einer HSQL-DB muss für Multi-threading-Zugriffe der "MVCC Transaction Mode" konfiguriert werden
(Multiversion Concurrency Control).
Dies kann auf verschiedene Arten erfolgen:
Falls die HSQL-DB über eine URL definiert wird, dann kann diese URL um den Zusatz "hsqldb.tx=mvcc" erweitert werden, beispielsweise so:
url = "jdbc:hsqldb:mem:testdb;sql.enforce_strict_size=true;hsqldb.tx=mvcc"
Falls die HSQL-DB nicht über eine URL definiert wird, sondern über den Spring-EmbeddedDatabaseBuilder, dann kann folgendermaßen verfahren werden:
Fügen Sie eine DataSource-Konfigurationsklasse hinzu (z.B. bei den obigen Beispielen im src\test\java\springbatchdemo-Verzeichnis): HsqldbMvccConfiguration.java
package springbatchdemo; import javax.sql.DataSource; import org.springframework.context.annotation.*; import org.springframework.jdbc.datasource.embedded.*; @Configuration public class HsqldbMvccConfiguration { /** Vermeidung von: * org.springframework.batch.core.step.FatalStepExecutionException: JobRepository failure forcing rollback, * org.springframework.dao.ConcurrencyFailureException: PreparedStatementCallback; SQL [UPDATE BATCH_STEP_EXECUTION ...]; serialization failure */ @Bean public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType( EmbeddedDatabaseType.HSQL ) .addScript( "classpath:HSQLDB-MVCC.sql" ) .build(); } }
Fügen Sie dieses SQL-Skript hinzu (z.B. bei den obigen Beispielen im src\test\resources-Verzeichnis): HSQLDB-MVCC.sql
SET DATABASE TRANSACTION CONTROL MVCC;
Falls Jobs nach längerer Inaktivität verspätet oder gar nicht starten, oder falls Sie Verbindungsabbrüche beispielsweise zur Datenbank haben, kann die Ursache darin liegen, dass bei einer TCP-Verbindung nach längerer Datenübertragungspause die Verbindung einseitig vom Server oder durch ein drittes System wie eine Firewall getrennt wird, aber der Client weiterhin annimmt, die Verbindung würde bestehen. Dies kann zu verwirrenden Fehlermeldungen oder auch zu Aussetzern und Blockaden ganz ohne Fehlermeldung führen. Der konfigurierte Time-out beträgt häufig zwei Stunden.
Um der Fehlerursache auf die Spur zu kommen, ist es oft hilfreich, den Logging-Level für relevante Packages zu veringern. Falls Sie Log4j verwenden, beispielsweise so:
<logger name="org.springframework" additivity="false"> <priority value="TRACE"/> <appender-ref ref="appender_all"/> </logger> <logger name="org.glassfish.jersey" additivity="false"> <priority value="TRACE"/> <appender-ref ref="appender_all"/> </logger>
Kontrollieren Sie die Einstellungen zum "TCP Keepalive". Überprüfen Sie insbesondere die Firewall, die Betriebssystem-Konfiguration und die Konfiguration des Datenbanktreibers.
Siehe hierzu: TCP Keepalive HOWTO.
Zur Problembehebung gibt es zwei Ansätze:
- Sie können den konfigurierten Time-out erhöhen.
- Oder Sie sorgen dafür, dass innerhalb der konfigurierten Time-out-Zeit TCP-Keepalive-Pakete gesendet werden.
Passen Sie die Konfiguration im Betriebssystem an über die für Ihr Betriebssystem vorgesehenen Parameter.
Beispielsweise für Linux:
tcp_keepalive_time, tcp_keepalive_intvl und tcp_keepalive_probes.
Bzw. für Windows:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters.
Siehe hierzu: Using TCP keepalive under Linux und TCP Keepalives with Windows.
Passen Sie die Konfiguration Ihrer Datenbankverbindung an. Falls Sie eine Oracle-DB verwenden, können Sie "enable=broken" verwenden, beispielsweise so:
jdbc:oracle:thin:@(DESCRIPTION=(ENABLE=BROKEN)(ADDRESS=(PROTOCOL=TCP)(HOST=meinhost)(PORT=1521))(LOAD_BALANCE=no)(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=meinedb)))
Siehe hierzu: Oracle Database, Parameters, ENABLE=broken.
Speziell in Spring-Batch-Anwendungen können Sie TCP Keepalive aktivieren, indem Sie in der application.properties hinzufügen:
batch.jdbc.testWhileIdle=true batch.jdbc.validationQuery=select 1 from dual
Siehe hierzu: Apache Commons DBCP BasicDataSource Configuration Parameters.
Bei den bisherigen Beispielen wurden die Lib-Versionen überwiegend definiert durch den <spring-boot-starter-paren>-Eintrag in der pom.xml:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.1.RELEASE</version> </parent>
Mit dem Maven-Dependency-Kommando können Sie sich die resultierenden Abhängigkeiten für Ihr konkretes Projekt ansehen, beispielsweise für die obige SpringBatchConditionalFlowDemo:
mvn dependency:tree
springbatchdemo:SpringBatchConditionalFlowDemo:jar:1.0-SNAPSHOT +- org.springframework.boot:spring-boot-starter-batch:jar:1.4.1.RELEASE:compile | +- org.springframework.boot:spring-boot-starter:jar:1.4.1.RELEASE:compile | | +- org.springframework.boot:spring-boot:jar:1.4.1.RELEASE:compile | | +- org.springframework.boot:spring-boot-autoconfigure:jar:1.4.1.RELEASE:compile | | +- org.springframework.boot:spring-boot-starter-logging:jar:1.4.1.RELEASE:compile | | | +- ch.qos.logback:logback-classic:jar:1.1.7:compile | | | | \- ch.qos.logback:logback-core:jar:1.1.7:compile | | | +- org.slf4j:jcl-over-slf4j:jar:1.7.21:compile | | | +- org.slf4j:jul-to-slf4j:jar:1.7.21:compile | | | \- org.slf4j:log4j-over-slf4j:jar:1.7.21:compile | | \- org.yaml:snakeyaml:jar:1.17:runtime | +- org.springframework.boot:spring-boot-starter-jdbc:jar:1.4.1.RELEASE:compile | | +- org.apache.tomcat:tomcat-jdbc:jar:8.5.5:compile | | | \- org.apache.tomcat:tomcat-juli:jar:8.5.5:compile | | \- org.springframework:spring-jdbc:jar:4.3.3.RELEASE:compile | \- org.springframework.batch:spring-batch-core:jar:3.0.7.RELEASE:compile | +- com.ibm.jbatch:com.ibm.jbatch-tck-spi:jar:1.0:compile | | \- javax.batch:javax.batch-api:jar:1.0:compile | +- com.thoughtworks.xstream:xstream:jar:1.4.7:compile | | +- xmlpull:xmlpull:jar:1.1.3.1:compile | | \- xpp3:xpp3_min:jar:1.1.4c:compile | +- org.codehaus.jettison:jettison:jar:1.2:compile | +- org.springframework.batch:spring-batch-infrastructure:jar:3.0.7.RELEASE:compile | | \- org.springframework.retry:spring-retry:jar:1.1.4.RELEASE:compile | +- org.springframework:spring-aop:jar:4.3.3.RELEASE:compile | +- org.springframework:spring-beans:jar:4.3.3.RELEASE:compile | +- org.springframework:spring-context:jar:4.3.3.RELEASE:compile | | \- org.springframework:spring-expression:jar:4.3.3.RELEASE:compile | \- org.springframework:spring-tx:jar:4.3.3.RELEASE:compile +- org.hsqldb:hsqldb:jar:2.3.3:compile \- org.springframework.boot:spring-boot-starter-test:jar:1.4.1.RELEASE:test +- org.springframework.boot:spring-boot-test:jar:1.4.1.RELEASE:test +- org.springframework.boot:spring-boot-test-autoconfigure:jar:1.4.1.RELEASE:test +- com.jayway.jsonpath:json-path:jar:2.2.0:test | +- net.minidev:json-smart:jar:2.2.1:test | | \- net.minidev:accessors-smart:jar:1.1:test | | \- org.ow2.asm:asm:jar:5.0.3:test | \- org.slf4j:slf4j-api:jar:1.7.21:compile +- junit:junit:jar:4.12:test +- org.assertj:assertj-core:jar:2.5.0:test +- org.mockito:mockito-core:jar:1.10.19:test | \- org.objenesis:objenesis:jar:2.1:test +- org.hamcrest:hamcrest-core:jar:1.3:test +- org.hamcrest:hamcrest-library:jar:1.3:test +- org.skyscreamer:jsonassert:jar:1.3.0:test | \- org.json:json:jar:20140107:test +- org.springframework:spring-core:jar:4.3.3.RELEASE:compile \- org.springframework:spring-test:jar:4.3.3.RELEASE:test
Eventuell müssen Sie in Ihrem Projekt in der pom.xml bereits ein anderes Parent-Modul unter <parent> eintragen. Dann können Sie nicht den Spring-<parent>-Eintrag verwenden. In diesem Fall sollten Sie versuchen, den Spring-<parent>-Eintrag
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.1.RELEASE</version> </parent>
zu ersetzen durch diesen <dependencyManagement>-Eintrag (mit scope: import):
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>1.4.1.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
Diesen <dependencyManagement>-Block können Sie wahlweise in der anderen Parent-POM oder in der eigenen Projekt-POM hinzufügen.
Bei diesem Verfahren profitieren Sie weiterhin vom Spring Dependency Management, aber Sie verlieren den Komfort des Spring Plugin Managements. Siehe hierzu: Using Spring Boot without the parent POM.
Weil das Spring Plugin Management fehlt, müssen Sie eventuell bei einigen Plug-ins Konfigurationen hinzufügen. Beispielsweise beim obigen SpringBatchConditionalFlowDemo müssen Sie den Block
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin>
erweitern zu:
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin>
Wenn Sie weder den Spring-<parent>-Eintrag für spring-boot-starter-parent noch den Spring-<dependencyManagement>-Eintrag für spring-boot-dependencies verwenden wollen, können Sie die benötigten Dependencies natürlich auch explizit in Ihre Projekt-POM eintragen. Allerdings kann dies sehr mühsam werden und erfordert bei jedem Versions-Update erneute zusätzliche Arbeit. Außerdem variieren die benötigten Einträge auch, abhängig davon, welche Features Sie nutzen wollen.
Beispielsweise für die obige SpringBatchConditionalFlowDemo könnte eine minimale pom.xml folgendermaßen aussehen:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>springbatchdemo</groupId> <artifactId>SpringBatchConditionalFlowDemo</artifactId> <version>1.0-SNAPSHOT</version> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>4.3.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>4.3.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>4.3.3.RELEASE</version> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-batch</artifactId> <version>1.4.1.RELEASE</version> </dependency> <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> <version>2.3.4</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>1.4.1.RELEASE</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>