Spring Boot で定期的にバッチ処理を実行する

定期的に一時ファイルを削除するとか、データを更新するとか、バッチ処理を実行する。

スケジューリングを有効にする。

@SpringBootApplication
@EnableScheduling
public class Application {

実際に実行したい処理は下記のような感じ。

@Component
public class XxxxTasks {

    @Scheduled(cron = "0 * * * * *")
    public void xxxxTask() {

これは毎分0秒に実行する例です。
このように cron で書く以外にも、
前の処理から5秒遅延させる
@Scheduled(fixedDelay=5000)
とか、5秒間隔で実行させるとか、
@Scheduled(fixedRate=5000)
初回の待機時間を設定するとか、
@Scheduled(initialDelay=1000, fixedRate=5000)
できるようです。

スケジューリングのデフォルトスレッドプールサイズが1のため、複数のタスクを同時実行させる場合は、スレッドプールのサイズを変更する必要があるようです。

Spring Boot でCORSを制御

Spring Boot でAPIなどを作っていて、他サイトからアクセスさせると、
Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
が発生する。

これを回避させるためにCORSを制御する。

@CrossOrigin(origins = "*", methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE})
public class XxxxxxController {

@CrossOrigin アノテーションで許可したいorigin、メソッドなどを設定する。

AWS の ElasticBeanstalk でログファイルを CloudWatch に転送する

SpringBootで作成したWebアプリケーションをElasticBeanstalkで運用していてAutoScalingなどでEC2が削除されると、EC2上に保存されているログファイルも当然削除されてしまう。
削除される前にログファイルをどこかに保存したいと思うのですが、その一つの方法としてCloudWatchに転送することにする。

ElasticBeanstalkにはCloudWatchにログファイルを連携する機能がありますが、今回はアプリケーション独自のログを連携したいのでこの機能(CloudWatch Logs へのインスタンスログのストリーミング)は使いません。

アプリケーションから出力した独自ログとapacheのログを転送することにします。
CloudWatchへの転送にはawslogsを使用します。
awslogsのインストールなどEC2への操作などはデプロイするWebアプリケーション内に設定ファイルとして追加します。
具体的には、アプリケーションの src/main/webapp/.ebextensions 以下に配置します。

.ebextensions 以下にconfigファイルを配置しますが、awslogsを使う場合は以下のような内容です。

packages:
  yum:
    awslogs: []
container_commands:
  1-cp-awscli_conf:
    command: cp -rf .ebextensions/awscli.conf /etc/awslogs/awscli.conf
  2-cp-awslogs_conf:
    command: cp -rf .ebextensions/awslogs.conf /etc/awslogs/awslogs.conf
  3-chkconfig-awslogs:
    command: /sbin/chkconfig awslogs on
  4-restert-awslogs:
    command: /sbin/service awslogs status; if [ $? -eq 0 ]; then /sbin/service awslogs restart; else /sbin/service awslogs start ; fi;

awslogsの設定ファイルも予め用意しておいて、そのファイルコピーします。
最終的なファイル構成はこんな感じです。

awscli.conf、awslogs.conf の内容も参考までに。

awscli.conf

[plugins]
cwlogs = cwlogs
[default]
region = ap-northeast-1

awslogs.conf

[general]
state_file = /var/lib/awslogs/agent-state

[/var/log/httpd/access_log]
log_group_name = sample_app
log_stream_name = {instance_id}_httpd
file = /var/log/httpd/access_log
datetime_format = %d/%b/%Y:%H:%M:%S
initial_position = start_of_file
buffer_duration = 5000

[/var/log/tomcat8/sample_app.log]
log_group_name = sample_app
log_stream_name = {instance_id}_app
file = /var/log/tomcat8/sample_app.log
datetime_format = %Y-%m-%d %H:%M:%S.
initial_position = start_of_file
buffer_duration = 5000

もちろんログのファイル名や、日付けフォーマットなどを合わせる必要があります。

Spring Boot でCSVダウンロード

データが少ない時

データが多くてメモリ不足になりそうなら、データが多い時の方法を検討。
それでもだめならバックグラウンドでcsvファイルを生成しておいて、あとでDLさせるとか検討。

    @RequestMapping(value = "csvdownload", method = RequestMethod.GET)
    public ResponseEntity<byte[]> download() throws IOException {
        HttpHeaders header = new HttpHeaders();
        header.add("Content-Type", "text/csv; charset=shift_jis");
        header.setContentDispositionFormData("filename", "sample.csv");
        String csv = "\"data1\",\"data2\"";
        return new ResponseEntity<>(csv.getBytes("shift_jis"), header, HttpStatus.OK);
    }

データが多い時

出力したいデータを全部取得するとメモリが足りなくなるケースがあるはずなので、順次取得しながら出力する。

    @RequestMapping(value = "csvdownload", method=RequestMethod.GET)
    public void csvdownload(HttpServletResponse response) {
        response.addHeader("Content-Disposition", "attachment; filename=\"sample.csv\"");
        response.setContentType("text/csv; charset=shift_jis");
        try (ServletOutputStream out = response.getOutputStream()) {
            for (int i = 0; i < 10000000; i++) {
                // ここから
                String csv = "\"data1\",\"data2\"";
                out.write(csv.getBytes("shift_jis"));
                // ここまでをデータ取得しながらデータ出力する
            }
            out.flush();
    } catch (Exception e) {
        e.printStackTrace();
        throw new RuntimeException(e);
    }

Spring Boot の認証にcookieを使う

Spring Boot でWebアプリを開発していて、少し規模が大きくなり複数のAppサーバーを使用するときにいくつか課題がある。
その一つとしてセッションの問題がある。

Spring Bootではデフォルトでセッションをサーバー側に保存するため、複数のサーバーを使用する場合、クライアントが異なるサーバーにアクセスするとセッションを維持できない。
そこで複数のサーバーで共有するなど、解決方法はいくつか考えられる。

  1. セッション情報をDBだったりRedisのようなKVSに保存する
  2. WebサーバーStickysessionを使う

「1.」 は良い方法ですが、セッション情報を保存するサーバーの運用、性能、冗長性など余計な管理が増えてしまう。
「2.」 は容易に実現できるが、同一のAppサーバーにアクセスする必要があるので、増減を動的に行うのが難しい。

そこでセッション情報をcookieのようなクライント側に保存する方法が良いのではと思う。
例えばPlay frameworkではセッションの実態はcookieを使用している。
https://www.playframework.com/documentation/2.6.x/SettingsSession#Configuring-the-session-cookie

これをSpring Bootで実現するときに参考にしたサイトをメモ。
http://www.svlada.com/jwt-token-authentication-with-spring-boot/
https://qiita.com/nyasba/items/f9b1b6be5540743f8bac