Spring Boot + Spring Batch + Java 6 でバッチを実装した話

今回、Spring Boot + Spring Batch + Java 6 でバッチを実装する機会があったので、もろもろを備忘録としてまとめる。

開発環境

Spring Tool Suite v3.8.3 を使用。Spring Starter Project からプロジェクトを作成する。

  • Java Version 1.6
  • Spring Boot 1.4.3
  • I/O > Batch

Dependencies で Batch をチェックしてプロジェクトを作成すると、spring-boot-starter-batch の依存が追加される。

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-batch</artifactId>
</dependency>

プロジェクトを作成して、Run Spring Boot App すると以下のエラーが出る。

... nested exception is java.lang.UnsupportedClassVersionError: org/apache/tomcat/jdbc/pool/DataSource : Unsupported major.minor version 51.0

Tomcat JDBCJava 6 に対応していない模様。今回、Tomcat やデータベースを使う予定がなかったので、思い切って Tomcat JDBC を依存から除外する。

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-batch</artifactId>
  <exclusions>
    <exclusion>
      <groupId>org.apache.tomcat</groupId>
      <artifactId>tomcat-jdbc</artifactId>
    </exclusion>
  </exclusions>
</dependency>

とりあえず、Java 6 で Spring Boot + Spring Batch が動作することを確認。

Tasklet

今回は割とシンプルなアプリケーションだったので、Tasklet のみで構成。main メソッドからの流れは基本的に本家のクイックスタートの通り。> Spring Batch

プロパティファイル

今回、src/main/resources 下の application.properties の他に、エンドユーザが設定するプロパティファイルとして application-env.properties を用意しました。(アプリ内部で利用するプロパティとエンドユーザが設定するプロパティをファイルとして分割しようと思って)

application-env.properties は config ディレクトリに配置して、jar を実行するときに -Dspring.config.location でプロパティファイルのパスを指定すれば読み込める模様。尚、Linux 環境では --spring.config.location なので注意。これで30分くらいハマった…。

java -jar batch.jar -Dspring.config.location=config/application-env.properties

-Dspring.config.location で外部のプロパティファイルを指定したとき、クラスパス上の application.properties は読み込まれるのか不安でしたが、普通に読み込めました。このあたりは別エントリにまとめました。

Spring Boot で複数の @ConfigurationProperties のプロパティを読み込む - kntmr-blog

プロパティは @ConfigurationProperties を付けた Bean 経由で読み込む。@ConfigurationProperties は、カンマ区切りのプロパティを自動的に List<T> に格納してくれたり、Bean Validation と組み合わせてプロパティをチェックできるので便利。
以下の依存を追加。(@NotBlank とかを使おうとすると、STS が追加してくれる)

<dependency>
  <groupId>javax.validation</groupId>
  <artifactId>validation-api</artifactId>
  <version>1.1.0.Final</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

@NotNulljavax.validation.constraints パッケージで提供されているアノテーション@NotBlank, @NotEmptyHibernate Validator が提供するアノテーション。それぞれのおおまかな違いは以下を参考にさせていただきました。

@NotNull/@NotEmpty/@NotBlankの違い - 見習いプログラミング日記

Hibernate Validator のバリデーションメッセージは、デフォルトでは「may not be empty」のような英語メッセージとなる。今回、バリデーションメッセージを日本語化するために ValidationMessages.properties を追加したが、ログに出力されるメッセージが文字化ける…。アノテーションの message 属性に直接日本語を書くと問題ないんだけど。
で、ソースコードに直接メッセージを書きたくなかったのと、「may not be empty」でもまぁ問題ないかなということで、バリデーションメッセージの日本語化は諦めることに。

Logback

ロギングは Logback を使う。Logbackを使う場合は src/main/resources 配下に logback-spring.xml を配置する。今回は、コンソールとファイルに同じ内容を出力して、ファイルサイズでローテートするようにしている。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE logback>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml" />
    <include resource="org/springframework/boot/logging/logback/console-appender.xml" />
    <!--<include resource="org/springframework/boot/logging/logback/file-appender.xml" />-->
    
    <property name="LOG_FILE" value="../logs/batch.log" />
    
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_FILE}</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
            <fileNamePattern>${LOG_FILE}.%i</fileNamePattern>
            <minIndex>1</minIndex>
            <maxIndex>5</maxIndex>
        </rollingPolicy>
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <maxFileSize>100KB</maxFileSize>
        </triggeringPolicy>
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
        </encoder>
    </appender>
    
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="FILE" />
    </root>
</configuration>

メッセージプロパティ

メッセージプロパティを使う場合は、src/main/resources の application.properties に以下の設定を追加する。

spring.messages.basename=messages
spring.messages.cache-seconds=-1
spring.messages.encoding=UTF-8

ソースコードからは MessageSource クラス経由で messages.properties のメッセージを取得する。

// MessageSource クラスを DI
@Autowired
private MessageSource messageSource;

// メッセージを取得
messageSource.getMessage(code, args, Locale.JAPAN);

Apache Commons IO

今回、あるディレクトリ配下のファイルを読み込んでごにょごにょする、みたいな要件がありました。Java 7 以降であれば、Files クラスとか Paths クラスとか何かと便利な API が使えていいんですが、今回は如何せん Java 6 …。というわけで、Apache Commons IO 2.3 を使う。

<dependency>
  <groupId>commons-io</groupId>
  <artifactId>commons-io</artifactId>
  <version>2.3</version>
</dependency>

あと、やっぱり try-with-resources が使えないのが地味にツライ。

パッケージング

パッケージングは mvn package するだけ。

ステータスコード

今回、Spring Batch で作成した jar をシェルスクリプトからキックして、RETVAL=$?ステータスコードを取得する想定でした。

当初、StepContribution#setExitStatus メソッドに ExitStatus.COMPLETED or ExitStatus.FAILED を渡して正常終了か異常終了を判別してみたものの、異常終了の場合でもステータスコードが 0 になる…。

// 正常終了
contribution.setExitStatus(ExitStatus.COMPLETED);
// 異常終了
contribution.setExitStatus(ExitStatus.FAILED);

というわけで、ChunkContext クラスから StepExecution を辿って、StepExecution#setStatus メソッドに BatchStatus.COMPLETED or BatchStatus.FAILED を渡すことで正常終了か異常終了を判別することができた。
ちなみに BatchStatus.FAILED を渡すと、ステータスコードに 5 が返る。

// 正常終了
chunkContext.getStepContext().getStepExecution().setStatus(BatchStatus.COMPLETED);
// 異常終了
chunkContext.getStepContext().getStepExecution().setStatus(BatchStatus.FAILED);

ちなみに本家のクイックスタートにも記載されているが、ステータスコードを返すときは main メソッドで以下のようにして System#exit を呼び出す。

public static void main(String[] args) {
    System.exit(SpringApplication.exit(SpringApplication.run(FooBatchApplication.class, args)));
}

Spring Boot で複数の @ConfigurationProperties のプロパティを読み込む

src/main/resources に application.properties があって、プロジェクト直下に config/application-env.properties があるとする。このとき、両方のプロパティファイルを読み込むことを検証したい。

プロパティファイルの内容は次の通り。

  • src/main/resources/application.properties
app.foo=foofoo
app.bar=barbar
  • config/application-env.properties
env.hoge=hogehoge
env.fuga=fugafuga

それぞれのプロパティファイルに対応する Bean を用意する。

  • application.properties に対応する Bean
@Component
@ConfigurationProperties(prefix = "app")
public class ApplicationProperties {
    private String foo;
    private String bar;
    // getter/setter は省略
}
  • application-env.properties に対応する Bean
@Component
@ConfigurationProperties(prefix = "env")
public class ApplicationEnvProperties {
    private String hoge;
    private String fuga;
    // getter/setter は省略
}

プロパティを使うときは、それぞれの Bean を DI して使う。

@Autowired
private ApplicationProperties appProp;
@Autowired
private ApplicationEnvProperties envProp;

...

appProp.getFoo()); // foofoo
appProp.getBar()); // barbar
envProp.getHoge()); // hogehoge
envProp.getFuga()); // fugafuga

尚、application-env.properties を読み込むには、アプリケーションの実行時に -Dspring.config.location=config/application-env.propertiesVM 引数に渡す必要がある。

まとめ

VM 引数に -Dspring.config.location=config/application-env.properties を指定した場合、application-env.properties しか読み込まれないかと思っていたら、クラスパス上にある application.properties も普通に読み込まれていたので、特に深く考える必要はなかった模様...。

Bootstrap を利用して簡単なモックアップを作る

仕事で提案活動向けに画面のモックアップを作る機会がありまして、今回は Bootstrap ベースで作りました。

で、せっかくの機会なので Bootstrap の調査を兼ねて Bootstrap の機能をいくつか盛り込んだサンプルみたいなものを作りました。ログインから検索、参照、更新まで基本的な画面遷移をひと通り実装しています。

github.com

全体的に以下の日本語リファレンスを参考にしています。

また、以下を利用してサイドメニューを実装しています。(スタイルは若干変更)

アイコンはここ。

ご参考まで。

JSON Server でダミーサーバを手軽に構築する

JSON を返すダミー API サーバを構築する際、以下が手軽で便利そうだったので使ってみる。

github.com


npm コマンドでインストール。

npm install -g json-server

APIJSON ファイルで作成する。次の場合、exampleAPI のエンドポイントとなり、value のオブジェクトがレスポンスボディとなる。

{
  "example": {
    "values": [
      { "id": "1", "name": "foo" },
      { "id": "2", "name": "bar" },
      { "id": "3", "name": "baz" },
      { "id": "4", "name": "hoge" },
      { "id": "5", "name": "fuga" }
    ]
  }
}

JSON Server を起動する。--watch オプションを付けると JSON ファイルの変更を監視してくれる。便利。

json-server --watch example.json

http://localhost:3000/example にアクセスすると以下のレスポンスが返る。

{
  "values": [
    {
      "id": "1",
      "name": "foo"
    },
    {
      "id": "2",
      "name": "bar"
    },
    {
      "id": "3",
      "name": "baz"
    },
    {
      "id": "4",
      "name": "hoge"
    },
    {
      "id": "5",
      "name": "fuga"
    }
  ]
}

JSON Server 起動時に js ファイルを指定して、動的にレスポンスを生成することもできる。(未検証)

(2019/10/01 追記)

ルーティングについて。

JSON Server ではエンドポイントの設定に / を含めることができない。例えば、/example/10001 のような記述を db.json に書くことはできない。

そこで、次のような routes.json を用意する。

{
  "/example/10001": "/example1",
  "/example/10002": "/example2"
}

これに合わせて次のような db.json を用意。

{
  "example1": {
    "values": [
      // ...
    ]
  },
  "example2": {
    "values": [
      // ...
    ]
  }
}

JSON Server 起動。

json-server --watch db.json --routes routes.json

http://localhost:3000/example/10001 にアクセスすると内部で /example1 にルーティングされて、db.jsonexample1 の内容が返却される。

参考) Add custom routes - typicode/json-server

jQuery プラグインを使ってキーワードサジェストを実装してみる

Google 検索のようなキーワードサジェストを実装する際、以下の jQuery プラグインが使いやすくてよかったです。というメモ。

www.devbridge.com

ソースコードはこちら

github.com

利用方法

プラグインを読み込む。

<script src="js/jquery.autocomplete.min.js"></script>

キーワードサジェストを適用するテキストボックスを用意。

<input type="text" id="autocomplete">

今回はサンプルということで、Ajax ではなく内部に埋め込んであるデータを読み込む。

$(function() {
  var values = [
    { value: 'foo', id: '1', name: 'FOO' },
    { value: 'bar', id: '2', name: 'BAR' },
    { value: 'baz', id: '3', name: 'BAZ' },
    { value: 'hoge', id: '4', name: 'HOGE' },
    { value: 'fuga', id: '5', name: 'FUGA' }
  ];
  $('#autocomplete').autocomplete({
    lookup: values,
    onSelect: function(suggestion) {
      console.log('id: ' + suggestion.id + ', name: ' + suggestion.name);
    }
  });
});

$('#autocomplete').autocomplete({ ... でテキストボックスに対してキーワードサジェストを適用する。lookup プロパティには、サジェストするデータを格納したオブジェクトを指定する。内部的にはオブジェクトの value プロパティの値がマッチングに使われる模様。

onSelect プロパティには、サジェストされた候補から選択したときにコールバックされる関数を指定する。

表示データを Ajax で取得する場合

serviceUrlAjax のリクエスト URLを指定する。レスポンスは JSON 形式であること。

$('#autocomplete').autocomplete({
  serviceUrl: '<ajax-url>',
  onSelect: function(suggestion) {
    console.log('id: ' + suggestion.id + ', name: ' + suggestion.name);
  }
});

Ajax のレスポンスをごにょごにょする場合は以下でイケる。

$('#autocomplete').autocomplete({
  lookup: function(query, done) {
    $.ajax({
      type: 'GET',
      url: '<ajax-url>',
      data: 'query=' + query,
      dataType: 'json',
      success: function(res) {
        // レスポンスをごにょごにょして done に渡す
        // res = { suggestions: [ { value: 'foo', id: '1', name: 'FOO' }, ... ] };
        done(res);
      },
    });
  },
  onSelect: function(suggestion) {
    console.log('id: ' + suggestion.id + ', name: ' + suggestion.name);
  }
});

(2017/01/18 追記)
Ajax の動作を手軽に試すなら JSON Server が便利。

kntmr.hatenablog.com

2017年の行動指針

早くも2017年の1.9%が過ぎ去ろうとしていますが、年末年始を帰省先でだらだらと過ごした影響で、未だに怠け癖が抜けない今日この頃です。このまま貴重な時間を浪費するのはよろしくないので、きちんと計画というか行動指針みたいなものを決めておこうかと思います。

基本的に毎日勉強することが目標。

平日の通勤時間を利用して英語を勉強する。当面は、zuknow アプリを利用して英単語を覚える。その他、英文法やリスニングをやる。

http://blog.zuknow.net/

仕事は、本質的な作業に集中することを意識する。「神は細部に宿る」と言いますが、いつもどうでもいい細かいところばかりに気を取られる傾向にあるので、ある程度のアウトプットを出したら早く家に帰るようにしよう。

帰宅したら素早く家事を片付けて子供を寝かし付け、22~24時を自分の時間として確保できるようにする。ただし、疲れているときは無理せず寝る。翌早朝に起きて朝活するのもありだが、過去の朝活経験からすると継続することは期待できない...。

当面は、Oracle Certified Java Programmer, Gold SE 8 認定資格に向けて勉強する。あと、JavaScript フレームワークを勉強したい。仕事で Angular + Spring の組み合わせを使う可能性があるので、Angular 2 あたりかな。今さら感あるけども。Angular 以外だと React か Vue.js あたりか。

とりあえずしばらくはこんな感じで。

Apache HttpClient の HttpRequestRetryHandler でリトライをカスタマイズするサンプル

Apache HttpClient の DefaultHttpRequestRetryHandler は ConnectTimeoutException のときはリトライしない - kntmr-blog

これの続き。

雑ですが、HttpRequestRetryHandler でリトライをカスタマイズするサンプルを書いてみました。ついでにリトライの間隔を指定できるようにしました。

gist.github.com

使うときはこんな感じ。

MyRetriableHttpClient client = new MyRetriableHttpClient(url).setTimeout(5000, 5000).setRetry(3, 3000);
try {
    Optional<String> resp = client.execute();
    resp.ifPresent(System.out::println);
} catch (IOException e) {
    // error
}