Spring Batch

+ andere TechDocs
+ Spring-Projekte
+ Spring DI und AOP
+ Spring Boot
+ Spring Batch
+


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.

 


Inhalt

  1. Begriffe im Batch-Umfeld
  2. Spring-Batch-Item/Chunk-Demo
  3. Spring-Batch-Tasklet-Demo
  4. Spring-Batch-Conditional-Flow-Demo
  5. Spring-Batch-Demo mit mehreren Jobdefinitionen in einer Job-Konfiguration
  6. Spring-Batch-Demo mit mehrfacher Conditional-Flow-Verzweigung
  7. Spring-Batch-Demo mit paralleler Ausführung von Steps per "Split Flow"
  8. Spring-Batch-Demo mit JobOperator im JUnit-Modultest
  9. Spring-Batch-Demo "Stoppable Delayed Retry"
  10. Spring-Batch-Demo mit multi-threaded stateful Step-Bean
  11. Spring-Batch-Demo zur Anzeige des Spring-Environments und von Job-Properties
  12. Spring Batch Admin als Web-GUI und für JSON-REST-Schnittstellen
  13. Eventuell notwendige Problemlösungen
  14. Spring Batch ohne Spring-Parent-POM
  15. Doku zu Spring, Spring Boot, Spring Batch und Spring Data



Begriffe im Batch-Umfeld

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.



Spring-Batch-Item/Chunk-Demo

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:

  1. JDK und Maven müssen installiert sein.

  2. 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.

  3. 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

  4. 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:

  5. 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
    
  6. 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
    );
    
  7. 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}") : "}");
       }
    }
    
  8. 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.

  9. 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").

  10. 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.

  11. 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.

  12. 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 ) );
       }
    }
    
  13. 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>
    
  14. 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
    
  15. 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

  16. 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}
         ...
    
  17. Sehen Sie sich die verwendeten Libs an:

    mvn dependency:tree

  18. 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" } );

  19. 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.



Spring-Batch-Tasklet-Demo

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:

  1. JDK und Maven müssen installiert sein.

  2. 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

  3. 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.

  4. 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>
    
  5. 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.

  6. 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.

  7. 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 );
          }
       }
    }
    
  8. 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
    
  9. 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

  10. 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"
    


Spring-Batch-Conditional-Flow-Demo

Das folgende einfache Beispiel demonstriert:

Sie können das Beispiel wahlweise entweder downloaden oder wie beschrieben Schritt für Schritt aufbauen und ausführen:

  1. Voraussetzung für dieses Beispiel ist das vorherige Beispiel Spring-Batch-Tasklet-Demo.

  2. 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

  3. Ersetzen Sie im neuen SpringBatchConditionalFlowDemo-Projektverzeichnis in der pom.xml die artifactId SpringBatchTaskletDemo durch SpringBatchConditionalFlowDemo.

  4. 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.

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

  8. 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
    


Spring-Batch-Demo mit mehreren Jobdefinitionen in einer Job-Konfiguration

Das folgende Beispiel demonstriert:

Sie können das Beispiel wahlweise entweder downloaden oder wie beschrieben Schritt für Schritt aufbauen und ausführen:

  1. Voraussetzung für dieses Beispiel ist das vorherige Beispiel Spring-Batch-Conditional-Flow-Demo.

  2. 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

  3. 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.

  4. 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

  5. Ä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() );
       }
    
  6. 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.

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



Spring-Batch-Demo mit mehrfacher Conditional-Flow-Verzweigung

Das folgende Beispiel demonstriert:

Sie können das Beispiel wahlweise entweder downloaden oder wie beschrieben Schritt für Schritt aufbauen und ausführen:

  1. Voraussetzung für dieses Beispiel ist das vorherige Beispiel SpringBatchMehrereJobsDemo.

  2. 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();
       }
    
  3. 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();
       }
    
  4. 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.*;
    
  5. 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() );
          }
       }
    
  6. 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.



Spring-Batch-Demo mit paralleler Ausführung von Steps per "Split Flow"

Das folgende Beispiel demonstriert:

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:

  1. JDK und Maven müssen installiert sein.

  2. 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

  3. 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.

  4. 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>
    
  5. 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.

  6. 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.

  7. 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 );
       }
    }
    
  8. 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
    
  9. 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.



Spring-Batch-Demo mit JobOperator im JUnit-Modultest

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:

  1. 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.

  2. 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 ) );
       }
    }
    
  3. Führen Sie aus:

    mvn clean test

  4. 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

  5. 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;
       }
    }
    
  6. Jetzt erhalten Sie bei Ausführung der Tests die Meldung:

    ---- Vorhandene Jobs: [jobAbc, jobXyz]

    und die JUnit-Modultests laufen fehlerfrei.

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

  8. Falls Ihnen der RunIdIncrementer nicht gefällt, können Sie beliebige eigene JobParametersIncrementer definieren, beispielsweise einen UuidIncrementer mit der Methode uuid = UUID.randomUUID().



Spring-Batch-Demo "Stoppable Delayed Retry"

Das folgende Beispiel demonstriert:

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:

  1. JDK und Maven müssen installiert sein.

  2. 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

  3. 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.

  4. 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>
    
  5. 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.

  6. 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.

  7. 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() );
             }
          }
       }
    }
    
  8. 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;
       }
    }
    
  9. 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
    
  10. 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.

  11. 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.

  12. 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).



Spring-Batch-Demo mit multi-threaded stateful Step-Bean

Das folgende Beispiel demonstriert:

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:

  1. JDK und Maven müssen installiert sein.

  2. 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

  3. 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.

  4. 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>
    
  5. 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;
    
  6. 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.

  7. 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();
       }
    }
    
  8. 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() );
             }
          }
       }
    }
    
  9. 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;
       }
    }
    
  10. 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();
       }
    }
    
  11. 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
    

Multi-threaded stateful Step-Bean mit Zustand im ExecutionContext

  1. 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.

  2. 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.

  3. Führen Sie im Kommandozeilenfenster aus:

    cd \MeinWorkspace\SpringBatchConcurrentDemo

    mvn clean test

  4. Sie erhalten (gekürzt):

    ...
    JobExecution  0:
    Job-ExecutionContext: Msg: aaa
    ...
    JobExecution  1:
    Job-ExecutionContext: Msg: bbb
    

Multi-threaded stateful Step-Bean mit fehlerhafter Implementierung des Zustands in Instanzvariable

  1. 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.

  2. Führen Sie wieder aus:

    cd \MeinWorkspace\SpringBatchConcurrentDemo

    mvn clean test

  3. 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.

Multi-threaded stateful Step-Bean mit Zustand in Instanzvariable und Job-Scope am Step

  1. 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.

  2. Führen Sie erneut aus:

    cd \MeinWorkspace\SpringBatchConcurrentDemo

    mvn clean test

  3. Jetzt erhalten Sie wieder das korrekte Ergebnis (gekürzt):

    ...
    JobExecution  0:
    Job-ExecutionContext: Msg: aaa
    ...
    JobExecution  1:
    Job-ExecutionContext: Msg: bbb
    
  4. 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.

  5. 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
    

Multi-threaded stateful Step-Bean mit Zustand in einem Context-Holder-Objekt mit Job-Scope

  1. 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.

  2. 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();
       }
    
  3. Führen Sie erneut aus:

    cd \MeinWorkspace\SpringBatchConcurrentDemo

    mvn clean test

  4. Jetzt erhalten Sie wieder das korrekte Ergebnis (gekürzt):

    ...
    JobExecution  0:
    Job-ExecutionContext: Msg: aaa
    ...
    JobExecution  1:
    Job-ExecutionContext: Msg: bbb
    


Spring-Batch-Demo zur Anzeige des Spring-Environments und von Job-Properties

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:

  1. JDK und Maven müssen installiert sein.

  2. 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

  3. 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.

  4. 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>
    
  5. Erzeugen Sie im src/test/resources-Verzeichnis eine Properties-Datei:
    JobProps-Test.properties

    Test-Prop-Key=Test-Prop-Value
    
  6. 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
    
  7. 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.

  8. 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;
       }
    }
    
  9. 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( "-----------------------------------------" );
       }
    }
    
  10. 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 ) );
       }
    }
    
  11. 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
    
  12. 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 als Web-GUI und für JSON-REST-Schnittstellen

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.

Basisinstallation für erste Versuche

  1. 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

    curl -O http://s3.amazonaws.com/dist.springframework.org/release/BATCHADM/spring-batch-admin-1.3.1.RELEASE.zip

    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

  2. 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

  3. Warten Sie bis "[INFO] Started Jetty Server" erscheint.
    Öffnen Sie die Spring-Batch-Admin-Weboberfläche:

    start http://localhost:8080/spring-batch-admin-sample

  4. 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.

  5. Stoppen Sie den Jetty-Webserver im Kommandozeilenfenster mit "Strg+C".

Den Batch-Job "meinItemChunkJob" hinzufügen und über das Admin-Web-GUI ausführen

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.

  1. Als eigener Batch-Job dient das oben erstellte Beispiel: Spring-Batch-Item/Chunk-Demo.

  2. 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

  3. 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.

  4. 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

  5. Vorerst sehen Sie keine Änderung in der Spring-Batch-Admin-Weboberfläche. Der neue Job wird noch nicht angezeigt.

  6. 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.)

  7. 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:

  8. Um sich den "Step Execution Progress", die "History of Step Execution" und die "Details for Step Execution" anzusehen, klicken Sie auf: "Executions | 0 | COMPLETED".

  9. 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]
    
  10. 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}.

Abfragen und Kommandos per JSON-REST-Schnittstelle

Sie können Spring Batch Admin nicht nur über den Webbrowser bedienen, sondern auch über JSON-REST-Schnittstellen ansteuern und abfragen.

  1. 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.

  2. 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
            },
            ...
         }
      }
    }
    
  3. 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"
            }
         }  }
    }
    
  4. 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"
                }
            }
        }}
    }
    
  5. 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"
                }
        }
      }
    }
    
  6. 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"
      }
    }
    
  7. 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" : {
        }
      }
    }
    

Weitere Batch-Jobs als Jar-Lib hinzufügen

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.

  1. 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.

  2. 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

  3. Ersetzen Sie in der neuen XML-Datei meineDemosJob.xml die Zeile

       <context:component-scan base-package="itemchunkdemo" />
    

    durch

       <context:component-scan base-package="springbatchdemo" />
    
  4. 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

  5. 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
    
  6. 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:

    start http://localhost:8080/spring-batch-admin-sample

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

  8. 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.

  9. 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.

Besonderheit beim Hinzufügen des Jobs "Stoppable Delayed Retry"

  1. 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

  2. Außerdem muss wie oben beschrieben meineDemosJob.xml angelegt sein.

  3. 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.

  4. Ä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.

  5. 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

  6. 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
    
  7. 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.

  8. 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.

  9. 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.

  10. 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
    

Weitere Optionen



Eventuell notwendige Problemlösungen

StepScope-/ClassCastException-Problem

JobRegistry-Registrierungs-Problem

Fehlende Scopes

Scoped-Proxy-Problem

ConcurrencyFailureException-Problem

WebSecurityConfigurerAdapter/ObjectPostProcessor-Problem



Spring Batch ohne Spring-Parent-POM

Dependencies mit Spring-Parent-POM

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

Mit Spring-Dependency-Management statt Spring-Parent-POM

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>

Mit expliziten Dependency-Definitionen

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>


Doku zu Spring, Spring Boot, Spring Batch und Spring Data

Spring:
Spring Getting Started Guides
Spring Framework Reference Documentation
Spring in Action von Craig Walls
Spring DI und AOP
Spring Boot:
Spring Boot 1.4.4
Spring Boot 1.5.1
Spring Boot Getting Started
Spring Boot Reference Guide
Spring Boot Samples
Spring Boot Maven Plugin Reference Guide
Spring Boot Maven Plugin Doku
Spring Boot in Action von Craig Walls
Spring Boot Cookbook von Alex Antonov
Spring Batch:
Spring Batch Getting Started
Spring Batch Reference Documentation
Spring Batch Samples
Spring Batch Essentials von P. Raja Malleswara Rao
Spring Batch in Action von Arnaud Cogoluegnes, Thierry Templier, Gary Gregory, Olivier Bazoud
Batchverarbeitung in JEE7, Christoph Schmidt-Casdorff
Batch-Computing in Java, Phillip Ghadir und Frank Hinkel
Spring Batch Admin Web-GUI
Spring Batch Admin User Guide
Spring Batch Admin Tutorial, Michael Fanous
Spring Batch Admin – Spring Boot, spring-batch-admin-spring-boot, Thomas Bosch (GitHub)
Batcharchitektur in the Enterprise, spring-boot-starter-batch-web, Tobias Flohre und Dennis Schulte (Blog, GitHub, Reference Guide)
Spring Data:
Spring Data
Spring Data Commons - Reference Documentation
Spring Data JPA - Reference Documentation
Spring Data Examples




Weitere Themen: andere TechDocs | Spring DI und AOP | Spring Boot
© 2017 Torsten Horn, Aachen