[Java]FTPs通信のアイドルタイムアウト回避

## 起きた問題

`spring-integration` のFTPSアダプター（`MessageSource` および `OutboundGateway`）を利用したFTPs通信を行うバッチ処理で、巨大なファイルの送受信を行うと **送受信完了後に例外が発生** して異常終了した。

## 直接原因

FTPsの **制御チャネルのアイドル状態が長時間化** し、通信経路中にある **ロードバランサのアイドルタイムアウトに抵触** したため。

## 根本原因

`DefaultFtpsSessionFactory#createClientInstance()` で生成される `FTPSClient` のインスタンスには、*controlKeepAliveTimeout* （※）が設定されていないため。

※ controlKeepAliveTimeout を設定すると、一定間隔で制御チャネルから無害なコマンド（NOOP）を送信するようになり、通信経路中にあるネットワーク機器のアイドルタイマーをリセットする効果がある

## 回避方法

`DefaultFtpsSessionFactory` の拡張クラスで *postProcessClientBeforeConnect* メソッドをオーバーライドして、その中で *controlKeepAliveTimeout* を設定する。

```java:FtpsSessionFactory.java
// 要lombok.jar

import org.apache.commons.net.ftp.FTPSClient;
import org.springframework.integration.ftp.session.DefaultFtpsSessionFactory;
import org.springframework.integration.util.JavaUtils;
import lombok.Setter;

public class FtpsSessionFactory extends DefaultFtpsSessionFactory {

    @Setter
    private Long controlKeepAliveTimeout;

    @Override
    protected void postProcessClientBeforeConnect(FTPSClient ftpsClient) {
        super.postProcessClientBeforeConnect(ftpsClient);
        JavaUtils.INSTANCE
                .acceptIfNotNull(this.controlKeepAliveTimeout,
                        ftpsClient::setControlKeepAliveTimeout);
    }

}
```

```java:FtpsConfig.java
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.file.remote.session.CachingSessionFactory;
import org.springframework.integration.file.remote.session.SessionFactory;

@Configuration
public class FtpsConfig {

    @Value("${ftps.config.host:127.0.0.1}")
    private String host;
    @Value("${ftps.config.port:21}")
    private int port;
    @Value("${ftps.config.connectTimeout:1000}")
    private int connectTimeout;
    @Value("${ftps.config.defaultTimeout:1000}")
    private int defaultTimeout;
    @Value("${ftps.config.dataTimeout:1000}")
    private int dataTimeout;
    @Value("${ftps.config.controlKeepAliveTimeout:5}")
    private long controlKeepAliveTimeout;
    @Value("${ftps.config.username:hoge}")
    private String username;
    @Value("${ftps.config.password:none}")
    private String password;
    @Value("${ftps.config.usePassive:true}")
    private boolean usePassive;

    @Bean
    public SessionFactory<FTPFile> ftpsSessionFactory() {
        FtpsSessionFactory factory = new FtpsSessionFactory();
        factory.setHost(host);
        factory.setPort(port);
        factory.setConnectTimeout(connectTimeout);
        factory.setDefaultTimeout(defaultTimeout);
        factory.setDataTimeout(dataTimeout);
        factory.setControlKeepAliveTimeout(controlKeepAliveTimeout);
        factory.setUsername(username);
        factory.setPassword(password);
        if (usePassive) {
            factory.setClientMode(FTPClient.PASSIVE_LOCAL_DATA_CONNECTION_MODE);
        }
        return new CachingSessionFactory<>(factory);
    }

}
```

## 参考

https://commons.apache.org/proper/commons-net/javadocs/api-3.6/org/apache/commons/net/ftp/FTPClient.html

```text: Control channel keep-alive feature: より
During file transfers, the data connection is busy, but the control connection is idle. FTP servers know that the control connection is in use, so won't close it through lack of activity, but it's a lot harder for network routers to know that the control and data connections are associated with each other. Some routers may treat the control connection as idle, and disconnect it if the transfer over the data connection takes longer than the allowable idle time for the router.
One solution to this is to send a safe command (i.e. NOOP) over the control connection to reset the router's idle timer.
```