Sicherer Datenversand mit SFTP und Spring Batch

Keine Kommentare

Eine gängige Anforderung in Projekten ist der Datenaustausch mit Fremdsystemen und externen Partnern. IT-Systeme arbeiten schon lange nicht mehr isoliert vom Rest der Welt sondern sind oft hochintegriert, genau wie die Geschäftsprozesse, die sie unterstützen. Obwohl die Technologie der Wahl für den Austausch momentan eher Webservices oder REST-Services sein dürften, ist man als Entwickler nicht immer frei in der Wahl der Kommunikationsmittel. Für manche Zwecke haben eher althergebrachte Technologien wie FTP/SFTP oder auch E-Mail (siehe Versand von E-Mails mit Spring Batch) durchaus ihre Daseinsberechtigung.

In diesem Artikel wird gezeigt, wie sich mit Hilfe von Spring Batch und Spring Integration ein SFTP-Upload realisieren lässt.

Wie in Spring Batch üblich, benötigt man einen Marshaller mit Reader und Writer. Die Zuständigkeit des Readers ist es, die zu transferierenden Daten aus der Datenbank auszulesen. Dazu bedient er sich eines RowMappers, der die Daten, die die Datenbank-Abfrage als Resultat liefert, in ein Domänen-Objekt transferiert.

Der Reader:

<bean id="sftpFileReader">
  <property name="dataSource" ref="dataSource" />
  <property name="sql" value="SELECT * FROM table" />
  <property name="rowMapper">
    <bean class="de.package.rowmapper.SftpFileRowMapper" />
  </property>
</bean>

Der RowMapper, welcher vom Reader verwendet wird:

package de.package.rowmapper;
 
import java.sql.ResultSet;
import java.sql.SQLException;
 
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.jdbc.core.RowMapper;
 
import de.package.domainObjects.SftpFileObject;
 
public class SftpFileRowMapper implements RowMapper {
 
    public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
        SftpFileObject fileLine = new SftpFileObject();
        try {
            fileLine.setDbField1(rs.getString("dbField1"));
            fileLine.setDbField2(rs.getString("dbField2"));
            ...
        } catch (SQLException e) {
            System.out.println("Can't create data row for export File.");
        }
        return fileLine;
    }
}

Das Domänen-Objekt, welches vom Reader verwendet wird:

package de.package.domainObjects;
 
public class SftpFileObject implements java.io.Serializable {
 
    private static final long serialVersionUID = 1L;
 
    public String dbField1;
    public String dbField2;
    ...
 
    public String getDbField1() {
        return dbField1;
    }
 
    public void setDbField1(String dbField1) {
        this.dbField1= dbField1;
    }
 
    public String getDbField2() {
        return dbField2;
    }
 
    public void setDbField2(String dbField2) {
        this.dbField2= dbField2;
    }
 
    ...
}

Der Writer schreibt die Daten der Domänen-Objekte in eine CSV-Datei (hier sind natürlich auch beliebige andere Formate denkbar). Hierzu benutzt er den DelimitedLineAggregator aus dem Spring Batch Framework. Damit die CSV-Datei auch eine Kopfzeile bekommt, benutzen wir zusätzlich das Property headerCallback.

Der Writer:

<bean id="sftpFileWriter" scope="step">
  <property name="resource" value="file:path/to/file/filename.csv" />
  <property name="encoding" value="ISO-8859-1" />
  <property name="headerCallback">
    <bean class="de.package.helper.HeaderCallback" />
  </property>
  <property name="lineAggregator">
    <bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
      <property name="delimiter" value=";" />
      <property name="fieldExtractor">
        <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor">
          <property name="names" value="dbField1, dbField2, ..." />
        </bean>
      </property>
    </bean>
  </property>
</bean>

Für die Erzeugung der Kopfzeile (s. o.) benutzen wir folgenden, denkbar einfachen Code:

package de.package.helper;
 
import java.io.IOException;
import java.io.Writer;
import org.springframework.batch.item.file.FlatFileHeaderCallback;
 
public class HeaderCallback implements FlatFileHeaderCallback {
 
    @Override
    public void writeHeader(Writer writer) throws IOException {
        writer.write("Header1;Header2; ...");
    }
}

Der eigentliche Versand per SFTP schließlich wird durch die SftpSessionFactory geregelt, welche auch die Zugangsdaten für den SFTP-Server als Properties bekommt.

<bean id="sftpSessionFactory" class="org.springframework.integration.sftp.session.DefaultSftpSessionFactory">
  <property name="host" value="host.of.receiver"/>
  <property name="user" value="username"/>
  <property name="password" value="secureSftpPassword"/>
  <property name="port" value="22"/>
</bean>

Des Weiteren benötigt man einen Channel, welcher für den Versand der Daten verwendet wird. Das channel-Tag kommt aus dem Spring Integration Namensraum, der im XML-Header der applicationContext.xml deklariert werden muss (s. u.).

<int:channel id="outputChannel" />

Damit Spring Batch auch weiß, auf welchem Weg der Versand erfolgen soll, muss man dies Spring Batch mitteilen. Hierfür verwendet man einen „outbound-channel-adapter“, welcher den Versandweg, die Referenz zur SftpSessionFactory und den Dateinamen für den Zielserver (remote-filename-generator) beinhaltet.

<int-sftp:outbound-channel-adapter id="sftpOutboundAdapter"
  session-factory="sftpSessionFactory"
  channel="outputChannel"
  charset="UTF-8"
  remote-directory="/target"
  remote-filename-generator="fileNameGenerator" />

Wenn man es einfach halten möchte, speichert man die Datei auf dem Zielserver mit dem gleichen Namen wie auf dem sendenden Server. Hierfür benötigt man nur den DefaultFileNameGenerator von Spring Integration.

<bean id="fileNameGenerator" class="org.springframework.integration.file.DefaultFileNameGenerator" />

Um die Datei nun wirklich zu versenden, benötigt man noch ein Tasklet und den eigentlichen Batch Job. Für das Tasklet kann man eine kleine Java Klasse verwenden, welcher man den Dateinamen und den Channel als Parameter mitgibt.

Deklaration des Tasklets:

<bean id="sftpJobTasklet" class="de.package.tasklets.SftpTasklet">
  <property name="fileName" value="path/to/file/filename.csv" />
  <property name="sftpChannel" ref="outputChannel" />
</bean>

Der Javacode des Tasklets:

package de.package.tasklets;
 
import java.io.File;
import org.springframework.batch.core.StepContribution;
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.integration.Message;
import org.springframework.integration.MessageChannel;
import org.springframework.integration.support.MessageBuilder;
 
public class SftpTasklet implements Tasklet {
 
    private String fileName;
    private MessageChannel sftpChannel;
 
    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
 
        File file = new File(fileName);
 
        if (file.exists()) {
            Message&lt;File&gt; message = MessageBuilder.withPayload(file).build();
            try {
                sftpChannel.send(message);
            } catch (Exception e) {
                System.out.println("Could not send file per SFTP: " + e);
            }
        } else {
            System.out.println("File does not exist.");
        }
 
    return RepeatStatus.FINISHED;
 
  }
 
  public String getFileName() {
    return fileName;
  }
 
  public void setFileName(String fileName) {
    this.fileName = fileName;
  }
 
  public MessageChannel getSftpChannel() {
    return sftpChannel;
  }
 
  public void setSftpChannel(MessageChannel sftpChannel) {
    this.sftpChannel = sftpChannel;
  }
}

Und zu guter Letzt der XML-Code des Batch-Jobs:

<batch:job id="sftpJob" restartable="false">
  <batch:step id="sftpFileGenerateStep" next="sftpFileSendStep">
    <batch:tasklet>
      <batch:chunk reader="sftpFileCreator" writer="sftpFileWriter" commit-interval="100" />
      <batch:listeners>
        <batch:listener ref="fileNameListener" />
      </batch:listeners>
    </batch:tasklet>
  </batch:step>
  <batch:step id="sftpFileSendStep">
    <batch:tasklet ref="sftpJobTasklet" />
  </batch:step>
</batch:job>

Diesen Batch Job kann man nun wie gewohnt von der Kommandozeile (also z. B. auch als cronjob) starten.

Zur Vollständigkeit hier noch der Header-Teil der applicationContext.xml mit allen benötigten Namespace-Deklarationen:

<xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:batch="http://www.springframework.org/schema/batch"
xmlns:int="http://www.springframework.org/schema/integration"
xmlns:int-file="http://www.springframework.org/schema/integration/file"
xmlns:int-sftp="http://www.springframework.org/schema/integration/sftp"
xmlns:int-stream="http://www.springframework.org/schema/integration/stream"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch-2.1.xsd
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/integration/file http://www.springframework.org/schema/integration/file/spring-integration-file-2.0.xsd
http://www.springframework.org/schema/integration/sftp http://www.springframework.org/schema/integration/sftp/spring-integration-sftp-2.0.xsd
http://www.springframework.org/schema/integration/stream http://www.springframework.org/schema/integration/stream/spring-integration-stream-2.0.xsd">

Kommentieren

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