かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その17 )( 入力画面1を作成する )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その16 )( H2 Database に Flyway でテーブルを作成する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • build すると Checkstyle, FindBugs, PMD が警告を出したので対応する
    • 入力画面1の作成

参照したサイト・書籍

目次

  1. build したら Checkstyle, FindBugs, PMD が警告を出したので対応する
    1. Javadoc タグには空でない説明文が必要です。
    2. Cannot open codebase filesystem:C:\project-springboot\ksbysample-boot-miscellaneous\boot-npm-geb-sample\build\classes\main\db\migration\V1__init.sql
    3. Too many fields
  2. input01.html 内の input タグに maxlength 属性を追加する
  3. Javascript 実装前に npm run springboot と Tomcat を起動して入力画面1を表示しようとしたら画面が表示されませんでした。。。
  4. 性別、職業の選択肢を表示する処理を追加する
  5. 続く。。。

手順

build したら Checkstyle, FindBugs, PMD が警告を出したので対応する

前回までの対応の後に build していなかったので build してみたところ、Checkstyle, FindBugs, PMD が警告を出したので対応します。

Javadoc タグには空でない説明文が必要です。

Doma-Gen で自動生成した Dao インターフェース src/main/java/ksbysample/webapp/bootnpmgeb/dao/InquiryDataDao.javaJavadoc のコメントで @param の説明文が空だったので警告が出ていました。

config/checkstyle/google_checks.xml に定義している module の <module name="NonEmptyAtclauseDescription"> ... </module> によるチェックの結果のようです。

Doma-Gen で自動生成する時に自動でパラメータ名と同じ文字列を説明文に記述するようにします。build.gradle の domaGen タスクを以下のように変更します。

// for Doma-Gen
task domaGen {
    doLast {
        ..........

        // 生成された Dao インターフェースを作業用ディレクトリにコピーし、
        // @ComponentAndAutowiredDomaConfig アノテーションを付加し、
        // Javadoc の @param に説明文を追加する
        copy() {
            from "${daoPackagePath}"
            into "${workDaoDirPath}/replace"
            filter {
                line ->
                    line.replaceAll('import org.seasar.doma.Dao;', "import ${importOfComponentAndAutowiredDomaConfig};\nimport org.seasar.doma.Dao;")
                            .replaceAll('@Dao', '@Dao\n@ComponentAndAutowiredDomaConfig')
                            .replaceAll('@param (\\S+)$', '@param $1 $1')
            }
        }

        ..........
    }
}
  • .replaceAll('@param (\\S+)$', '@param $1 $1') を追加します。

InquiryDataDao.java と InquiryData.java を削除してから Tomcat を起動し、domaGen タスクを実行します。

自動生成された src/main/java/ksbysample/webapp/bootnpmgeb/dao/InquiryDataDao.java を見ると、以下のようにパラメータ名と同じ文字列がセットされています。

@Dao
@ComponentAndAutowiredDomaConfig
public interface InquiryDataDao {

    /**
     * @param id id
     * @return the InquiryData entity
     */
    @Select
    InquiryData selectById(Integer id);

    ..........

Cannot open codebase filesystem:C:\project-springboot\ksbysample-boot-miscellaneous\boot-npm-geb-sample\build\classes\main\db\migration\V1__init.sql

build すると Flyway 用に追加した V1__init.sql が build/classes/main/db/migration/V1__init.sql にコピーされるのですが、build.gradle の tasks.withType(FindBugs) { ... } の中でこのファイルを FindBugs のチェック対象から除外していなかったため、以下のエラーが出ていました。

f:id:ksby:20170824000213p:plain

build.gradle の tasks.withType(FindBugs) { ... } を以下のように変更します。

tasks.withType(FindBugs) {
    // Gradle 3.3以降 + FindBugs Gradle Plugin を組み合わせると、"The following errors occurred during analysis:"
    // の後に "Cannot open codebase filesystem:..." というメッセージが大量に出力されるので、以下の doFirst { ... }
    // のコードを入れることで出力されないようにする
    doFirst {
        def fc = classes
        if (fc == null) {
            return
        }
        fc.exclude '**/*.properties'
        fc.exclude '**/*.sql'
        fc.exclude '**/*.xml'
        fc.exclude '**/META-INF/**'
        fc.exclude '**/static/**'
        fc.exclude '**/templates/**'
        classes = files(fc.files)
    }
    reports {
        xml.enabled = false
        html.enabled = true
    }
}
  • fc.exclude '**/*.sql' を追加します。

Too many fields

Doma-Gen で自動生成した Entity クラス src/main/java/ksbysample/webapp/bootnpmgeb/entity/InquiryData.java のフィールド数が多いので、PMD の <rule ref="rulesets/java/codesize.xml"> ... </rule> の TooManyFields で警告が出ていました。

Entity クラスを自動生成する時に @SuppressWarnings({"PMD.TooManyFields"}) を付加して PMD のチェック対象外になるようにします。build.gradle の domaGen タスクを以下のように変更します。

// for Doma-Gen
task domaGen {
    doLast {
        // まず変更が必要なもの
        def rootPackageName = 'ksbysample.webapp.bootnpmgeb'
        def rootPackagePath = 'src/main/java/ksbysample/webapp/bootnpmgeb'
        def dbUrl = 'jdbc:h2:tcp://localhost:9092/mem:bootnpmgebdb'
        def dbUser = 'sa'
        def dbPassword = ''
        def tableNamePattern = 'INQUIRY_DATA'
        // おそらく変更不要なもの
        def entityPackagePath = rootPackagePath + '/entity'
        def daoPackagePath = rootPackagePath + '/dao'
        def importOfComponentAndAutowiredDomaConfig = "${rootPackageName}.util.doma.ComponentAndAutowiredDomaConfig"
        def workDirPath = 'work'
        def workEntityDirPath = "${workDirPath}/entity"
        def workDaoDirPath = "${workDirPath}/dao"

        // 作業用ディレクトリを削除する
        clearDir("${workDirPath}")

        // 現在の Dao インターフェースのバックアップを取得する
        copy() {
            from "${daoPackagePath}"
            into "${workDaoDirPath}/org"
        }

        // Dao インターフェース、Entity クラスを生成する
        ant.taskdef(resource: 'domagentask.properties',
                classpath: configurations.domaGenRuntime.asPath)
        ant.gen(url: "${dbUrl}", user: "${dbUser}", password: "${dbPassword}", tableNamePattern: "${tableNamePattern}") {
            entityConfig(packageName: "${rootPackageName}.entity", useListener: false)
            daoConfig(packageName: "${rootPackageName}.dao")
            sqlConfig()
        }

        // 生成された Entity クラスを作業用ディレクトリにコピーし、
        // @SuppressWarnings({"PMD.TooManyFields"}) アノテーションを付加する
        copy() {
            from "${entityPackagePath}"
            into "${workEntityDirPath}/replace"
            filter {
                line ->
                    line.replaceAll('@Entity', '@SuppressWarnings({"PMD.TooManyFields"})\n@Entity')
            }
        }

        // @SuppressWarnings({"PMD.TooManyFields"}) アノテーションを付加した Entity クラスを
        // entity パッケージへ戻す
        copy() {
            from "${workEntityDirPath}/replace"
            into "${entityPackagePath}"
        }

        ..........
  • 以下の3行を追加します。
    • def rootPackagePath = 'src/main/java/ksbysample/webapp/bootnpmgeb'
    • def entityPackagePath = rootPackagePath + '/entity'
    • def workEntityDirPath = "${workDirPath}/entity"
  • daoPackagePath は rootPackagePath を使うよう変更し、記述する位置を // おそらく変更不要なもの の下へ移動します。
  • copy() { from "${entityPackagePath}" ... } を追加します。
  • copy() { from "${workEntityDirPath}/replace" ... } を追加します。

再度 InquiryDataDao.java と InquiryData.java を削除してから Tomcat を起動し直し、domaGen タスクを実行します。

自動生成された src/main/java/ksbysample/webapp/bootnpmgeb/entity/InquiryData.java を見ると、@SuppressWarnings({"PMD.TooManyFields"}) アノテーションが付加されています。

@SuppressWarnings({"PMD.TooManyFields"})
@Entity
@Table(name = "INQUIRY_DATA")
public class InquiryData {

以上の3点の警告を対応した後に build し直すと、今度は何も警告は表示されませんでした。

input01.html 内の input タグに maxlength 属性を追加する

INQUIRY_DATA テーブルのカラムに定義した文字数を src/main/resources/templates/web/inquiry/input01.html 内の <input type="text" .../> に maxlength 属性として追加します。

Javascript 実装前に npm run springbootTomcat を起動して入力画面1を表示しようとしたら画面が表示されませんでした。。。

Javascript の実装前にコマンドラインから npm run springboot を実行し、Tomcat を起動します。

ブラウザから http://localhost:9080/inquiry/input/01/ にアクセスして入力画面1が表示されることを確認しようとしたら、画面が表示されませんでした。。。

f:id:ksby:20170823074738p:plain

CSRF 絡みですね。おそらく H2 Console を表示するために設定を変更したのが原因でしょう。

調べたら原因は http.csrf().requireCsrfProtectionMatcher(...)CSRF対策の対象にしない URL を設定する時には一緒に GET オプション等で CSRF対策が実行されないよう設定しないといけないからでした。Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その13 )( Spring Session を使用する2 ) で書いていたのに忘れていました。

src/main/java/ksbysample/webapp/bootnpmgeb/config/WebSecurityConfig.java を以下のように変更します。

@Configuration
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private static final Pattern DISABLE_CSRF_TOKEN_PATTERN = Pattern.compile("(?i)^(GET|HEAD|TRACE|OPTIONS)$");
    private static final Pattern H2_CONSOLE_URI_PATTERN = Pattern.compile("^/h2-console");

    ..........

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ..........

        // spring.h2.console.enabled=true に設定されている場合には H2 Console を表示するために必要な設定を行う
        if (springH2ConsoleEnabled) {
            http.csrf()
                    .requireCsrfProtectionMatcher(request -> {
                        if (DISABLE_CSRF_TOKEN_PATTERN.matcher(request.getMethod()).matches()) {
                            // GET, HEAD, TRACE, OPTIONS は CSRF対策の対象外にする
                            return false;
                        } else if (H2_CONSOLE_URI_PATTERN.matcher(request.getRequestURI()).lookingAt()) {
                            // H2 Console は CSRF対策の対象外にする
                            return false;
                        }
                        return true;
                    });
            http.headers().frameOptions().sameOrigin();
        }
    }

}
  • private static final Pattern DISABLE_CSRF_TOKEN_PATTERN = Pattern.compile("(?i)^(GET|HEAD|TRACE|OPTIONS)$"); を追加します。
  • if (DISABLE_CSRF_TOKEN_PATTERN.matcher(request.getMethod()).matches()) { ... } を追加し、その次の if 文を else if に変更します。

Tomcat を再起動して http://localhost:9080/inquiry/input/01/ にアクセスすると今度は入力画面1が表示されました。

f:id:ksby:20170823080947p:plain

性別、職業の選択肢を表示する処理を追加する

src/main/java/ksbysample/webapp/bootnpmgeb の下に values パッケージを新規作成した後、ksbysample-webapp-lending から以下のファイルをコピーします。

  • src/main/java/ksbysample/webapp/bootnpmgeb/values/Values.java
  • src/main/java/ksbysample/webapp/bootnpmgeb/values/ValuesHelper.java
  • src/main/java/ksbysample/webapp/bootnpmgeb/values/validation/ValuesEnum.java
  • src/main/java/ksbysample/webapp/bootnpmgeb/values/validation/ValuesEnumValidator.java

src/main/resources/application-product.properties を以下のように変更します。

..........

valueshelper.classpath.prefix=BOOT-INF.classes.

  • ファイルの末尾に valueshelper.classpath.prefix=BOOT-INF.classes. を追加します。

まずは「性別」からです。src/main/java/ksbysample/webapp/bootnpmgeb/values の下に SexValues.java を新規作成し、以下の内容を記述します。

package ksbysample.webapp.bootnpmgeb.values;

import lombok.AllArgsConstructor;
import lombok.Getter;

@SuppressWarnings("MissingOverride")
@Getter
@AllArgsConstructor
public enum SexValues implements Values {

    MALE("1", "男性")
    , FEMALE("2", "女性");

    private final String value;
    private final String text;

}

src/main/resources/templates/web/inquiry/input01.html の「性別」の部分を以下のように変更します。

            <!-- 性別 -->
            <div class="form-group" id="form-group-sex">
              <div class="control-label col-sm-2">
                <label class="float-label">性別</label>
                <div class="label label-required">必須</div>
              </div>
              <div class="col-sm-10">
                <div class="row"><div class="col-sm-10">
                  <div class="radio-inline" th:each="sexValue : ${@vh.values('SexValues')}">
                    <label><input type="radio" name="sex" th:value="${sexValue.value}" th:text="${sexValue.text}"></label>
                  </div>
                </div></div>
                <div class="row hidden js-errmsg"><div class="col-sm-10"><p class="form-control-static text-danger"><small>ここにエラーメッセージを表示します</small></p></div></div>
              </div>
            </div>
  • <div class="radio-inline" ...>th:each="sexValue : ${@vh.values('SexValues')}" を追加します。
  • ラジオボタンの各選択肢を記述していた HTML を <label><input type="radio" name="sex" th:value="${sexValue.value}" th:text="${sexValue.text}"></label> に変更します。

次は「職業」です。src/main/java/ksbysample/webapp/bootnpmgeb/values の下に JobValues.java を新規作成し、以下の内容を記述します。

package ksbysample.webapp.bootnpmgeb.values;

import lombok.AllArgsConstructor;
import lombok.Getter;

@SuppressWarnings("MissingOverride")
@Getter
@AllArgsConstructor
public enum JobValues implements Values {

    EMPLOYEE("1", "会社員")
    , STUDENT("2", "学生")
    , OTHER("3", "その他");

    private final String value;
    private final String text;

}

src/main/resources/templates/web/inquiry/input01.html の「職業」の部分を以下のように変更します。

            <!-- 職業 -->
            <div class="form-group" id="form-group-job">
              <div class="control-label col-sm-2">
                <label class="float-label">職業</label>
              </div>
              <div class="col-sm-10">
                <div class="row"><div class="col-sm-10">
                  <select name="job" id="job" class="form-control" style="width: 250px;">
                    <th:block th:each="jobValue,iterStat : ${@vh.values('JobValues')}">
                    <option th:if="${iterStat.first}" value="">選択してください</option>
                    <option th:value="${jobValue.value}" th:text="${jobValue.text}">会社員</option>
                    </th:block>
                  </select>
                </div></div>
                <div class="row hidden js-errmsg"><div class="col-sm-10"><p class="form-control-static text-danger"><small>ここにエラーメッセージを表示します</small></p></div></div>
              </div>
            </div>
  • 以下の3行を追加します。
    • <th:block th:each="jobValue,iterStat : ${@vh.values('JobValues')}">
    • <option th:if="${iterStat.first}" value="">選択してください</option>
    • </th:block>
  • <option>th:value="${jobValue.value}" th:text="${jobValue.text}" を追加します。

画面を表示すると「性別」「職業」どちらも問題なく表示されています。

f:id:ksby:20170826004452p:plain

続く。。。

記事が長くなったので、あと1~2回くらいに分けて書きます。

最後に結構大きく修正した build.gradle の domaGen タスクを載せておきます。

ソースコード

build.gradle

// for Doma-Gen
task domaGen {
    doLast {
        // まず変更が必要なもの
        def rootPackageName = 'ksbysample.webapp.bootnpmgeb'
        def rootPackagePath = 'src/main/java/ksbysample/webapp/bootnpmgeb'
        def dbUrl = 'jdbc:h2:tcp://localhost:9092/mem:bootnpmgebdb'
        def dbUser = 'sa'
        def dbPassword = ''
        def tableNamePattern = 'INQUIRY_DATA'
        // おそらく変更不要なもの
        def entityPackagePath = rootPackagePath + '/entity'
        def daoPackagePath = rootPackagePath + '/dao'
        def importOfComponentAndAutowiredDomaConfig = "${rootPackageName}.util.doma.ComponentAndAutowiredDomaConfig"
        def workDirPath = 'work'
        def workEntityDirPath = "${workDirPath}/entity"
        def workDaoDirPath = "${workDirPath}/dao"

        // 作業用ディレクトリを削除する
        clearDir("${workDirPath}")

        // 現在の Dao インターフェースのバックアップを取得する
        copy() {
            from "${daoPackagePath}"
            into "${workDaoDirPath}/org"
        }

        // Dao インターフェース、Entity クラスを生成する
        ant.taskdef(resource: 'domagentask.properties',
                classpath: configurations.domaGenRuntime.asPath)
        ant.gen(url: "${dbUrl}", user: "${dbUser}", password: "${dbPassword}", tableNamePattern: "${tableNamePattern}") {
            entityConfig(packageName: "${rootPackageName}.entity", useListener: false)
            daoConfig(packageName: "${rootPackageName}.dao")
            sqlConfig()
        }

        // 生成された Entity クラスを作業用ディレクトリにコピーし、
        // @SuppressWarnings({"PMD.TooManyFields"}) アノテーションを付加する
        copy() {
            from "${entityPackagePath}"
            into "${workEntityDirPath}/replace"
            filter {
                line ->
                    line.replaceAll('@Entity', '@SuppressWarnings({"PMD.TooManyFields"})\n@Entity')
            }
        }

        // @SuppressWarnings({"PMD.TooManyFields"}) アノテーションを付加した Entity クラスを
        // entity パッケージへ戻す
        copy() {
            from "${workEntityDirPath}/replace"
            into "${entityPackagePath}"
        }

        // 生成された Dao インターフェースを作業用ディレクトリにコピーし、
        // @ComponentAndAutowiredDomaConfig アノテーションを付加し、
        // Javadoc の @param に説明文を追加する
        copy() {
            from "${daoPackagePath}"
            into "${workDaoDirPath}/replace"
            filter {
                line ->
                    line.replaceAll('import org.seasar.doma.Dao;', "import ${importOfComponentAndAutowiredDomaConfig};\nimport org.seasar.doma.Dao;")
                            .replaceAll('@Dao', '@Dao\n@ComponentAndAutowiredDomaConfig')
                            .replaceAll('@param (\\S+)$', '@param $1 $1')
            }
        }

        // @ComponentAndAutowiredDomaConfig アノテーションを付加した Dao インターフェースを
        // dao パッケージへ戻す
        copy() {
            from "${workDaoDirPath}/replace"
            into "${daoPackagePath}"
        }

        // 元々 dao パッケージ内にあったファイルを元に戻す
        copy() {
            from "${workDaoDirPath}/org"
            into "${daoPackagePath}"
        }

        // 作業用ディレクトリを削除する
        clearDir("${workDirPath}")
    }
}

履歴

2017/08/26
初版発行。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その16 )( H2 Database に Flyway でテーブルを作成する )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その15 )( Flyway のインストール + Spring Security 使用時に H2 Console に接続する + IntelliJ IDEA の Database Tools で in-memory モードの H2 Database に接続する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Tomcat 起動時に Flyway で in-memory モードの H2 Database にテーブルを作成します。
    • Doma-Gen で in-memory モードの H2 Database に作成したテーブルの entity クラス, dao インターフェースを生成します。

参照したサイト・書籍

  1. Spring Boot Flyway Sample
    https://github.com/spring-projects/spring-boot/tree/master/spring-boot-samples/spring-boot-sample-flyway

目次

  1. Database Tools でテーブルを作成する
  2. Tomcat 起動時に Flyway が実行するよう SQL ファイルを配置する
  3. 動作確認
  4. Doma-Gen で entity クラス, dao インターフェースを生成する
  5. 次回は。。。

手順

Database Tools でテーブルを作成する

IntelliJ IDEA の Database Tools でテーブルを作成して SQL を生成するのがお手軽なので、最初に Database Tools で今回使うテーブルを作成します。

in-memory モードの H2 Database に接続する必要があるので、Tomcat を起動します。

Database Tools で PUBLIC スキーマを選択してコンテキストメニューを表示した後、「New」-「Table」を選択します。

f:id:ksby:20170822003640p:plain

「Create New Table」ダイアログが表示されるので、カラムを追加します。

f:id:ksby:20170822005521p:plain

ダイアログの下の「SQL Script」に生成された SQL は以下のようになります。

CREATE TABLE INQUIRY_DATA
(
    id INT AUTO_INCREMENT PRIMARY KEY,
    lastname VARCHAR(20) NOT NULL,
    firstname VARCHAR(20) NOT NULL,
    lastkana VARCHAR(20) NOT NULL,
    firstkana VARCHAR(20) NOT NULL,
    sex VARCHAR(1) NOT NULL,
    age INT NOT NULL,
    job VARCHAR(1),
    zipcode1 VARCHAR(3) NOT NULL,
    zipcode2 VARCHAR(4) NOT NULL,
    address VARCHAR(256) NOT NULL,
    tel1 VARCHAR(5),
    tel2 VARCHAR(4),
    tel3 VARCHAR(4),
    email VARCHAR(256),
    type1 VARCHAR(1) NOT NULL,
    type2 VARCHAR(1),
    inquiry TEXT NOT NULL,
    survey VARCHAR(1) NOT NULL,
    update_date DATETIME NOT NULL
);

「Execute」ボタンをクリックするとテーブルが作成されて、Database Tools 上に INQUIRY_DATA テーブルが表示されます。

f:id:ksby:20170822010045p:plain

Tomcat 起動時に Flyway が実行するよう SQL ファイルを配置する

src/main/resources の下に db/migration ディレクトリを作成します。

src/main/resources/db/migration の下に V1__init.sql を新規作成し、上で記述した SQL をそのままコピー&ペーストします。

また IntelliJ IDEA のエディタ上に「Change dialect to…」リンクが表示されますのでクリックして、

f:id:ksby:20170822010851p:plain

SQL Dialects」ダイアログが表示されたら「Project SQL Dialect」で「H2」を選択して「OK」ボタンをクリックします。

f:id:ksby:20170822011101p:plain

Project Tool Window では以下のようなディレクトリ構成になります。

f:id:ksby:20170822015349p:plain

動作確認

Tomcat 起動時に INQUIRY_DATA テーブルが作成されるか確認します。

Tomcat を再起動するとコンソールに CREATE TABLE INQUIRY_DATA 文が出力されて、

f:id:ksby:20170822011940p:plain

Database Tools を更新すると INQUIRY_DATA テーブルが表示されました。問題なく作成されています。

f:id:ksby:20170822012928p:plain

Doma-Gen で entity クラス, dao インターフェースを生成する

build.gradle の domaGen タスクを変更し、Tomcat を起動した状態で entity クラス, dao インターフェースを生成します。

build.gradle を以下のように変更します。

dependencies {
    ..........

    // for Doma
    compile("org.seasar.doma:doma:${domaVersion}")
    domaGenRuntime("org.seasar.doma:doma-gen:${domaVersion}")
    domaGenRuntime("com.h2database:h2:1.4.192")

    ..........
}

// for Doma-Gen
task domaGen {
    doLast {
        // まず変更が必要なもの
        def rootPackageName = 'ksbysample.webapp.bootnpmgeb'
        def daoPackagePath = 'src/main/java/ksbysample/webapp/bootnpmgeb/dao'
        def dbUrl = 'jdbc:h2:tcp://localhost:9092/mem:bootnpmgebdb'
        def dbUser = 'sa'
        def dbPassword = ''
        def tableNamePattern = 'INQUIRY_DATA'
        // おそらく変更不要なもの
        def importOfComponentAndAutowiredDomaConfig = "${rootPackageName}.util.doma.ComponentAndAutowiredDomaConfig"
        def workDirPath = 'work'
        def workDaoDirPath = "${workDirPath}/dao"

        ..........
  • dependencies に domaGenRuntime("com.h2database:h2:1.4.192") を追加します。
  • domaGen タスクの以下の点を変更します。
    • dbUrljdbc:h2:mem:bootnpmgebdbjdbc:h2:tcp://localhost:9092/mem:bootnpmgebdb に変更します。
    • tableNamePattern.*INQUIRY_DATA に変更します。

変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

更新が終わったら Gradle Tool Window から domaGen タスクを実行します。"BUILD SUCCESSFUL" が表示されました。

f:id:ksby:20170822014140p:plain

Project Tool Window を見ると InquiryDataDao インターフェース、InquiryData クラスが作成されています。

f:id:ksby:20170822014456p:plain

生成されたソースは以下の通りです。TEXT 型にした inquiry カラムが Clob になっていますね。H2 Database の Data Types を見ると、たしかに TEXT 型は CLOB の1種類でした。

■ksbysample.webapp.bootnpmgeb.dao.InquiryDataDao

package ksbysample.webapp.bootnpmgeb.dao;

import ksbysample.webapp.bootnpmgeb.entity.InquiryData;
import ksbysample.webapp.bootnpmgeb.util.doma.ComponentAndAutowiredDomaConfig;
import org.seasar.doma.Dao;
import org.seasar.doma.Delete;
import org.seasar.doma.Insert;
import org.seasar.doma.Select;
import org.seasar.doma.Update;

/**
 */
@Dao
@ComponentAndAutowiredDomaConfig
public interface InquiryDataDao {

    /**
     * @param id
     * @return the InquiryData entity
     */
    @Select
    InquiryData selectById(Integer id);

    /**
     * @param entity
     * @return affected rows
     */
    @Insert
    int insert(InquiryData entity);

    /**
     * @param entity
     * @return affected rows
     */
    @Update
    int update(InquiryData entity);

    /**
     * @param entity
     * @return affected rows
     */
    @Delete
    int delete(InquiryData entity);
}

■ksbysample.webapp.bootnpmgeb.entity.InquiryData

package ksbysample.webapp.bootnpmgeb.entity;

import java.sql.Clob;
import java.time.LocalDateTime;
import org.seasar.doma.Column;
import org.seasar.doma.Entity;
import org.seasar.doma.GeneratedValue;
import org.seasar.doma.GenerationType;
import org.seasar.doma.Id;
import org.seasar.doma.Table;

/**
 * 
 */
@Entity
@Table(name = "INQUIRY_DATA")
public class InquiryData {

    /**  */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID")
    Integer id;

    /**  */
    @Column(name = "LASTNAME")
    String lastname;

    /**  */
    @Column(name = "FIRSTNAME")
    String firstname;

    /**  */
    @Column(name = "LASTKANA")
    String lastkana;

    /**  */
    @Column(name = "FIRSTKANA")
    String firstkana;

    /**  */
    @Column(name = "SEX")
    String sex;

    /**  */
    @Column(name = "AGE")
    Integer age;

    /**  */
    @Column(name = "JOB")
    String job;

    /**  */
    @Column(name = "ZIPCODE1")
    String zipcode1;

    /**  */
    @Column(name = "ZIPCODE2")
    String zipcode2;

    /**  */
    @Column(name = "ADDRESS")
    String address;

    /**  */
    @Column(name = "TEL1")
    String tel1;

    /**  */
    @Column(name = "TEL2")
    String tel2;

    /**  */
    @Column(name = "TEL3")
    String tel3;

    /**  */
    @Column(name = "EMAIL")
    String email;

    /**  */
    @Column(name = "TYPE1")
    String type1;

    /**  */
    @Column(name = "TYPE2")
    String type2;

    /**  */
    @Column(name = "INQUIRY")
    Clob inquiry;

    /**  */
    @Column(name = "SURVEY")
    String survey;

    /**  */
    @Column(name = "UPDATE_DATE")
    LocalDateTime updateDate;

    /** 
     * Returns the id.
     * 
     * @return the id
     */
    public Integer getId() {
        return id;
    }

    /** 
     * Sets the id.
     * 
     * @param id the id
     */
    public void setId(Integer id) {
        this.id = id;
    }

    /** 
     * Returns the lastname.
     * 
     * @return the lastname
     */
    public String getLastname() {
        return lastname;
    }

    /** 
     * Sets the lastname.
     * 
     * @param lastname the lastname
     */
    public void setLastname(String lastname) {
        this.lastname = lastname;
    }

    /** 
     * Returns the firstname.
     * 
     * @return the firstname
     */
    public String getFirstname() {
        return firstname;
    }

    /** 
     * Sets the firstname.
     * 
     * @param firstname the firstname
     */
    public void setFirstname(String firstname) {
        this.firstname = firstname;
    }

    /** 
     * Returns the lastkana.
     * 
     * @return the lastkana
     */
    public String getLastkana() {
        return lastkana;
    }

    /** 
     * Sets the lastkana.
     * 
     * @param lastkana the lastkana
     */
    public void setLastkana(String lastkana) {
        this.lastkana = lastkana;
    }

    /** 
     * Returns the firstkana.
     * 
     * @return the firstkana
     */
    public String getFirstkana() {
        return firstkana;
    }

    /** 
     * Sets the firstkana.
     * 
     * @param firstkana the firstkana
     */
    public void setFirstkana(String firstkana) {
        this.firstkana = firstkana;
    }

    /** 
     * Returns the sex.
     * 
     * @return the sex
     */
    public String getSex() {
        return sex;
    }

    /** 
     * Sets the sex.
     * 
     * @param sex the sex
     */
    public void setSex(String sex) {
        this.sex = sex;
    }

    /** 
     * Returns the age.
     * 
     * @return the age
     */
    public Integer getAge() {
        return age;
    }

    /** 
     * Sets the age.
     * 
     * @param age the age
     */
    public void setAge(Integer age) {
        this.age = age;
    }

    /** 
     * Returns the job.
     * 
     * @return the job
     */
    public String getJob() {
        return job;
    }

    /** 
     * Sets the job.
     * 
     * @param job the job
     */
    public void setJob(String job) {
        this.job = job;
    }

    /** 
     * Returns the zipcode1.
     * 
     * @return the zipcode1
     */
    public String getZipcode1() {
        return zipcode1;
    }

    /** 
     * Sets the zipcode1.
     * 
     * @param zipcode1 the zipcode1
     */
    public void setZipcode1(String zipcode1) {
        this.zipcode1 = zipcode1;
    }

    /** 
     * Returns the zipcode2.
     * 
     * @return the zipcode2
     */
    public String getZipcode2() {
        return zipcode2;
    }

    /** 
     * Sets the zipcode2.
     * 
     * @param zipcode2 the zipcode2
     */
    public void setZipcode2(String zipcode2) {
        this.zipcode2 = zipcode2;
    }

    /** 
     * Returns the address.
     * 
     * @return the address
     */
    public String getAddress() {
        return address;
    }

    /** 
     * Sets the address.
     * 
     * @param address the address
     */
    public void setAddress(String address) {
        this.address = address;
    }

    /** 
     * Returns the tel1.
     * 
     * @return the tel1
     */
    public String getTel1() {
        return tel1;
    }

    /** 
     * Sets the tel1.
     * 
     * @param tel1 the tel1
     */
    public void setTel1(String tel1) {
        this.tel1 = tel1;
    }

    /** 
     * Returns the tel2.
     * 
     * @return the tel2
     */
    public String getTel2() {
        return tel2;
    }

    /** 
     * Sets the tel2.
     * 
     * @param tel2 the tel2
     */
    public void setTel2(String tel2) {
        this.tel2 = tel2;
    }

    /** 
     * Returns the tel3.
     * 
     * @return the tel3
     */
    public String getTel3() {
        return tel3;
    }

    /** 
     * Sets the tel3.
     * 
     * @param tel3 the tel3
     */
    public void setTel3(String tel3) {
        this.tel3 = tel3;
    }

    /** 
     * Returns the email.
     * 
     * @return the email
     */
    public String getEmail() {
        return email;
    }

    /** 
     * Sets the email.
     * 
     * @param email the email
     */
    public void setEmail(String email) {
        this.email = email;
    }

    /** 
     * Returns the type1.
     * 
     * @return the type1
     */
    public String getType1() {
        return type1;
    }

    /** 
     * Sets the type1.
     * 
     * @param type1 the type1
     */
    public void setType1(String type1) {
        this.type1 = type1;
    }

    /** 
     * Returns the type2.
     * 
     * @return the type2
     */
    public String getType2() {
        return type2;
    }

    /** 
     * Sets the type2.
     * 
     * @param type2 the type2
     */
    public void setType2(String type2) {
        this.type2 = type2;
    }

    /** 
     * Returns the inquiry.
     * 
     * @return the inquiry
     */
    public Clob getInquiry() {
        return inquiry;
    }

    /** 
     * Sets the inquiry.
     * 
     * @param inquiry the inquiry
     */
    public void setInquiry(Clob inquiry) {
        this.inquiry = inquiry;
    }

    /** 
     * Returns the survey.
     * 
     * @return the survey
     */
    public String getSurvey() {
        return survey;
    }

    /** 
     * Sets the survey.
     * 
     * @param survey the survey
     */
    public void setSurvey(String survey) {
        this.survey = survey;
    }

    /** 
     * Returns the updateDate.
     * 
     * @return the updateDate
     */
    public LocalDateTime getUpdateDate() {
        return updateDate;
    }

    /** 
     * Sets the updateDate.
     * 
     * @param updateDate the updateDate
     */
    public void setUpdateDate(LocalDateTime updateDate) {
        this.updateDate = updateDate;
    }
}

次回は。。。

入力画面1~3を順に作成します。

履歴

2017/08/22
初版発行。

Java SE を 8u141 → 8u144 へ、IntelliJ IDEA を 2017.2.1 → 2017.2.2 へ、Git for Windows を 2.13.3 → 2.14.1 へバージョンアップ

※ksbysample-webapp-lending プロジェクトを開いた状態でバージョンアップしています。

Java SE を 8u141 → 8u144 へバージョンアップする

  1. OracleJava SE Downloads を見ると 8u144 がダウンロードできるようになっていました。まさかバージョン番号の下1桁だけ上がっているバージョンが出ているとは。。。

    8u144 へバージョンアップします。

  2. jdk-8u144-windows-x64.exe をダウンロードして C:\Java\jdk1.8.0_144 へインストールした後、環境変数 JAVA_HOME のパスを C:\Java\jdk1.8.0_144 へ変更します。

    コマンドプロンプトから java -version を実行し、1.8.0_144 に変更されていることを確認します。

    f:id:ksby:20170820203411p:plain

  3. IntelliJ IDEA を再起動した後、プロジェクトで使用する Java SE を 8u144 へ変更します。

  4. 開いているプロジェクトを閉じて「Welcome to IntelliJ IDEA」ダイアログを表示します。

  5. ダイアログ下部の「Configure」-「Project Defaults」-「Project Structure」を選択します。

    f:id:ksby:20170820203952p:plain

  6. 「Default Project Structure」ダイアログが表示されます。画面左側で「Project Settings」-「Project」を選択後、画面右側の「Project SDK」の「New…」ボタンをクリックし、表示されるメニューから「JDK」を選択します。

    f:id:ksby:20170820204241p:plain

  7. 「Select Home Directory for JDK」ダイアログが表示されます。C:\Java\jdk1.8.0_144 を選択した後、「OK」ボタンをクリックします。

    f:id:ksby:20170820204447p:plain

  8. 「Default Project Structure」ダイアログに戻るので、今度は「Project SDK」の「Edit」ボタンをクリックします。

    f:id:ksby:20170820204739p:plain

  9. 画面左側で「Platform Settings」-「SDKs」が選択された状態になるので、画面右上の入力フィールドで “1.8” → “1.8.0_144” へ変更します。

    f:id:ksby:20170820205042p:plain

  10. 次に中央のリストから「1.8.0_144」を選択した後、リストの上の「-」ボタンをクリックして削除します。

    f:id:ksby:20170820205233p:plain

  11. 「OK」ボタンをクリックして「Default Project Structure」ダイアログを閉じます。

  12. 「Welcome to IntelliJ IDEA」ダイアログに戻ったら、ksbysample-webapp-lending プロジェクトを開きます。

  13. IntelliJ IDEA のメイン画面が開いたら、メニューから「File」-「Project Structure…」を選択します。

  14. 「Project Structure」ダイアログが表示されます。以下の画像の状態になっているので、

    f:id:ksby:20170820205510p:plain

    「Project SDK」と「Project language level」を選択し直します。

    f:id:ksby:20170820205701p:plain

  15. 「OK」ボタンをクリックして「Project Structure」ダイアログを閉じます。

  16. メイン画面に戻ると画面右下に「Indexing…」の表示が出るので、終了するまで待ちます。

    f:id:ksby:20170820205900p:plain

  17. clean タスク実行 → Rebuild Project 実行 → build タスクを実行して、"BUILD SUCCESSFUL" のメッセージが出力されることを確認します。

    f:id:ksby:20170820210556p:plain

  18. Project Tool Window で src/test を選択した後、コンテキストメニューを表示して「Run ‘All Tests’ with Coverage」を選択し、テストが全て成功することを確認します。

    f:id:ksby:20170820211058p:plain

  19. 特に問題は発生しませんでした。8u144 で開発を進めます。

IntelliJ IDEA を 2017.2.1 → 2017.2.2 へバージョンアップする

IntelliJ IDEA の 2017.2.2 がリリースされたのでバージョンアップします。

※上の Java SE のバージョンアップからの続きで ksbysample-webapp-lending プロジェクトを開いた状態でバージョンアップしています。

  1. IntelliJ IDEA のメインメニューから「Help」-「Check for Updates…」を選択します。

  2. IDE and Plugin Updates」ダイアログが表示されます。左下に「Update and Restart」ボタンが表示されていますので、「Update and Restart」ボタンをクリックします。

    f:id:ksby:20170821030829p:plain

  3. Plugin の update も表示されました。「Error-prone Compiler Integration」はバージョンアップすると動かなくなりますので、これだけチェックを外して「Update and Restart」ボタンをクリックします。

    f:id:ksby:20170821031937p:plain

  4. Patch がダウンロードされて IntelliJ IDEA が再起動します。

  5. IntelliJ IDEA が起動すると画面下部に「Indexing…」のメッセージが表示されますので、終了するまで待機します。

    f:id:ksby:20170821032326p:plain

  6. IntelliJ IDEA のメインメニューから「Help」-「About」を選択し、2017.2.2 へバージョンアップされていることを確認します。

  7. Gradle Tool Window のツリーを見ると「Tasks」の下に「other」しかない状態になっているので、左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

    f:id:ksby:20170821032533p:plain

  8. clean タスク実行 → Rebuild Project 実行 → build タスクを実行して、"BUILD SUCCESSFUL" のメッセージが出力されることを確認します。

    f:id:ksby:20170821033151p:plain

  9. Project Tool Window で src/test を選択した後、コンテキストメニューを表示して「Run ‘All Tests’ with Coverage」を選択し、テストが全て成功することを確認します。

    f:id:ksby:20170821033740p:plain

Git for Windows を 2.13.3 → 2.14.1 へバージョンアップする

Git for Windows の 2.14.1 がリリースされていたのでバージョンアップします。

  1. https://git-for-windows.github.io/ の「Download」ボタンをクリックして Git-2.14.1-64-bit.exe をダウンロードします。

  2. Git-2.14.1-64-bit.exe を実行します。

  3. 「Git 2.14.1 Setup」ダイアログが表示されます。[Next >]ボタンをクリックします。

  4. 「Select Components」画面が表示されます。「Git LFS(Large File Support)」だけチェックした状態で [Next >]ボタンをクリックします。

  5. 「Adjusting your PATH environment」画面が表示されます。中央の「Use Git from the Windows Command Prompt」が選択されていることを確認後、[Next >]ボタンをクリックします。

  6. 「Choosing HTTPS transport backend」画面が表示されます。「Use the OpenSSL library」が選択されていることを確認後、[Next >]ボタンをクリックします。

  7. 「Configuring the line ending conversions」画面が表示されます。一番上の「Checkout Windows-style, commit Unix-style line endings」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  8. 「Configuring the terminal emulator to use with Git Bash」画面が表示されます。「Use Windows'default console window」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  9. 「Configuring extra options」画面が表示されます。「Enable file system caching」だけがチェックされていることを確認した後、[Install]ボタンをクリックします。

  10. インストールが完了すると「Completing the Git Setup Wizard」のメッセージが表示された画面が表示されます。中央の「View Release Notes」のチェックを外した後、「Finish」ボタンをクリックしてインストーラーを終了します。

  11. コマンドプロンプトを起動して git --version を実行し、git のバージョンが git version 2.14.1.windows.1 になっていることを確認します。

    f:id:ksby:20170821034746p:plain

  12. git-cmd.exe を起動して日本語の表示・入力が問題ないかを確認します。

    f:id:ksby:20170821034923p:plain

  13. 特に問題はないようですので、2.14.1 で作業を進めたいと思います。

Spring Boot + Spring Integration でいろいろ試してみる ( その23 )( http でリクエストを受信 → JdbcChannelMessageStore を使用した QueueChannel に Message を送信 → Message を受信して bat ファイルを実行する2 )

概要

記事一覧はこちらです。

参照したサイト・書籍

目次

  1. sleep.bat を作成する
  2. bat ファイルを起動する処理を作成する
  3. 動作確認
  4. バッチを複数起動可能にするには?
  5. 最後に

手順

sleep.bat を作成する

C:\eipapp\ksbysample-eipapp-batchexecutor\bat の下に sleep.bat を新規作成し、以下の内容を記述します。

@echo off
perl -sle "sleep $sleep_time" -- -sleep_time=%1
exit /B %ERRORLEVEL%

例えば sleep.bat 10 で起動すると、バッチを起動後 10秒 sleep してから終了します。

bat ファイルを起動する処理を作成する

InputStream 等の close 処理に Apache Commons IO を使いたいのと、ログの出力に lombok を使いたいので、build.gradle を以下のように変更します。

dependencies {
    def lombokVersion = "1.16.18"
    def errorproneVersion = "2.0.15"

    // dependency-management-plugin によりバージョン番号が自動で設定されるもの
    // Appendix A. Dependency versions ( http://docs.spring.io/platform/docs/current/reference/htmlsingle/#appendix-dependency-versions ) 参照
    ..........
    compile("org.springframework.integration:spring-integration-jdbc")
    compile("commons-io:commons-io")
    testCompile("org.springframework.boot:spring-boot-starter-test")

    ..........
    compile("com.h2database:h2:1.4.192")

    // for lombok
    compileOnly("org.projectlombok:lombok:${lombokVersion}")
    testCompileOnly("org.projectlombok:lombok:${lombokVersion}")

    // for Error Prone ( http://errorprone.info/ )
    ..........
}
  • compile("commons-io:commons-io") を追加します。Apache Commons IO は Spring IO Platform の Appendix A. Dependency versions に最新バージョンの 2.5 で定義されているので、バージョン番号の記述は不要です。
  • lombok をインストールするのに以下の3行を追加します。
    • def lombokVersion = "1.16.18"
    • compileOnly("org.projectlombok:lombok:${lombokVersion}")
    • testCompileOnly("org.projectlombok:lombok:${lombokVersion}")

変更した後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

src/main/java/ksbysample/eipapp/batchexecutor/FlowConfig.java を以下のように変更します。

@Slf4j
@Configuration
public class FlowConfig {

    ..........

    @Bean
    public IntegrationFlow httpBatchFlow() {
        return IntegrationFlows.from(
                // http://localhost:8080/batch?sleep=... でリクエストを受信して
                // sleep の値を payload にセットした Message を生成する
                Http.inboundGateway("/batch")
                        .requestMapping(r -> r
                                .methods(HttpMethod.GET)
                                .params("sleep"))
                        .payloadExpression("#requestParams.sleep[0]"))
                .handle((p, h) -> {
                    log.info("★★★ リクエストを受信しました ( sleep = {} )", p);
                    return p;
                })
                ..........
                .get();
    }

    @Bean
    public IntegrationFlow executeBatchFlow() {
        return IntegrationFlows.from(executeBatchQueueChannel())
                // QueueChannel に Message が送信されているか1秒毎にチェックする
                .bridge(e -> e.poller(Pollers.fixedDelay(1000)))
                // sleep.bat を実行する
                .handle((p, h) -> {
                    log.info("●●● sleep.bat {} を実行します", p);
                    ProcessBuilder builder = new ProcessBuilder();
                    builder.command(COMMAND_SLEEP_BAT, (String) p, ">", "NUL")
                            .directory(new File(WORKDIR_SLEEP_BAT))
                            .redirectErrorStream(true);

                    Process process = null;
                    try {
                        process = builder.start();
                        int exitCode = process.waitFor();
                        log.info("●●● sleep.bat {} が終了しました ( exitCode = {} )", p, exitCode);
                    } catch (IOException | InterruptedException e) {
                        throw new RuntimeException(e);
                    } finally {
                        if (process != null) {
                            IOUtils.closeQuietly(process.getInputStream());
                            IOUtils.closeQuietly(process.getOutputStream());
                            IOUtils.closeQuietly(process.getErrorStream());
                            process.destroy();
                        }
                    }

                    return null;
                })
                .get();
    }

}
  • クラスに @Slf4j アノテーションを付加します。
  • httpBatchFlow メソッド内に .handle((p, h) -> { log.info("★★★ リクエストを受信しました ( sleep = {} )", p); return p; }) を追加して、リクエストを受信したことが分かるようにします。
  • executeBatchFlow メソッド内の .handle((p, h) -> { ... }) に上記の sleep.bat を起動する処理を記述します。

動作確認

動作確認します。jar ファイルは作成せず、IntelliJ IDEA から起動して確認します。

アプリケーションを起動後、以下3つの URL で連続してアクセスします。

  • http://localhost:8080/batch?sleep=10
  • http://localhost:8080/batch?sleep=8
  • http://localhost:8080/batch?sleep=6

f:id:ksby:20170820172310p:plain

コンソールに出力されたログを見ると以下のことが分かります。想定通り動作しています。

  • http で送信したリクエストはバッチの実行中でも受信して処理されています。http 受信とバッチ実行は別のスレッドで処理されています。
  • リクエストは1件ずつ処理されてバッチを実行しています。実行されているバッチが終了しないと次のバッチは実行されていません。
  • バッチは起動した後、URL の sleep パラメータで指定された秒数待機して終了しています。

バッチを複数起動可能にするには?

バッチを複数起動可能にするには src/main/java/ksbysample/eipapp/batchexecutor/FlowConfig.java を以下のように変更します。

    @Bean
    public IntegrationFlow executeBatchFlow() {
        return IntegrationFlows.from(executeBatchQueueChannel())
                // QueueChannel に Message が送信されているか1秒毎にチェックする
                .bridge(e -> e.poller(Pollers.fixedDelay(1000)))
                // バッチを最大3スレッドで起動する
                // 最大数の制限を設けないのであれば Executors.newCachedThreadPool() にする 
                .channel(c -> c.executor(Executors.newFixedThreadPool(3)))
                // sleep.bat を実行する
                .handle((p, h) -> {
                    ..........
  • executeBatchFlow メソッドで Message を受信した後に .channel(c -> c.executor(Executors.newFixedThreadPool(3))) を追加すると最大3スレッドでバッチを実行するようになります。Executors#newFixedThreadPool メソッドに渡す値を変更すれば起動するバッチ数を変更できます。また .channel(c -> c.executor(Executors.newCachedThreadPool())) と記述すればリクエストが送信された分だけバッチが起動します。

アプリケーションを起動し直して sleep パラメータの値を 10, 9, 8, 7, 6 の5つに変えて連続してアクセスすると、

f:id:ksby:20170820180001p:plain

  • sleep = 10, 9, 8 の3つはリクエスト受信後にバッチが起動されています(3つ同時に実行されています)。またこの3つはそれぞれ別のスレッドで実行されています。
  • sleep = 7 は sleep = 9 のバッチが終了した後に起動され、sleep = 6 のバッチは sleep = 10 のバッチが終了した後に起動されています。

最後に

HTTP Support の方はサンプル通りなので難しくありませんでしたが、JdbcChannelMessageStore の方が少し分かりづらいですね。出力されたエラーメッセージと stackoverflow の QA を見てやっと実装できました。ちょっとした永続的な QueueChannel が使いたい時には、データベースの QueueChannel が使えると便利かもしれません。

最後に最終形のソースを掲載しておきます。

ソースコード

Application.java

package ksbysample.eipapp.batchexecutor;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ImportResource;

import javax.sql.DataSource;

/**
 * Application 用メイン + JavaConfig 用クラス
 */
@SpringBootApplication(exclude = {JpaRepositoriesAutoConfiguration.class, HibernateJpaAutoConfiguration.class})
@ImportResource("classpath:applicationContext.xml")
public class Application {

    /**
     * メインクラス
     * @param args アプリケーション起動時のオプション
     */
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    /**
     * @return Tomcat JDBC Connection Pool の DataSource オブジェクト
     */
    @Bean
    @ConfigurationProperties("spring.datasource.tomcat")
    public DataSource dataSource() {
        return DataSourceBuilder.create()
                .type(org.apache.tomcat.jdbc.pool.DataSource.class)
                .build();
    }

}

application.properties

spring.datasource.tomcat.url=jdbc:h2:file:C:/eipapp/ksbysample-eipapp-batchexecutor/db/batchexecutordb
spring.datasource.tomcat.username=sa
spring.datasource.tomcat.password=
spring.datasource.tomcat.driverClassName=org.h2.Driver
spring.datasource.tomcat.initialSize=2
spring.datasource.tomcat.maxActive=2
spring.datasource.tomcat.maxIdle=2
spring.datasource.tomcat.minIdle=2
spring.datasource.tomcat.testOnBorrow=true
spring.datasource.tomcat.validationQuery=select 1
spring.datasource.tomcat.validationQueryTimeout=5
spring.datasource.tomcat.removeAbandoned=true
spring.datasource.tomcat.removeAbandonedTimeout=30
spring.datasource.tomcat.jdbc-interceptors=SlowQueryReport(threshold=5000)
# spring.datasource.jmx-enabled は spring.datasource.tomcat.jmx-enabled と書かないこと。
# spring.datasource.tomcat.jmx-enabled だと機能しない。
spring.datasource.jmx-enabled=true

applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/integration"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xmlns:beans="http://www.springframework.org/schema/beans"
             xmlns:jdbc="http://www.springframework.org/schema/jdbc"
             xsi:schemaLocation="http://www.springframework.org/schema/beans
                                 http://www.springframework.org/schema/beans/spring-beans.xsd
                                 http://www.springframework.org/schema/integration
                                 http://www.springframework.org/schema/integration/spring-integration.xsd
                                 http://www.springframework.org/schema/jdbc
                                 http://www.springframework.org/schema/jdbc/spring-jdbc.xsd">

    <!--
        アプリケーション起動時に schema-h2.sql を実行して JdbcChannelMessageStore で使用する
        INT_CHANNEL_MESSAGE テーブルを新規作成する
        また、既に INT_CHANNEL_MESSAGE テーブルがあると schema-h2.sql 実行時にエラーが出るが、
        ignore-failures="ALL" を付与して無視するようにする
    -->
    <jdbc:initialize-database ignore-failures="ALL">
        <jdbc:script location="classpath:org/springframework/integration/jdbc/store/channel/schema-h2.sql"/>
    </jdbc:initialize-database>

</beans:beans>

FlowConfig.java

package ksbysample.eipapp.batchexecutor;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.dsl.channel.MessageChannels;
import org.springframework.integration.dsl.core.Pollers;
import org.springframework.integration.dsl.http.Http;
import org.springframework.integration.jdbc.store.JdbcChannelMessageStore;
import org.springframework.integration.jdbc.store.channel.H2ChannelMessageStoreQueryProvider;
import org.springframework.integration.store.PriorityCapableChannelMessageStore;
import org.springframework.messaging.MessageChannel;

import javax.sql.DataSource;
import java.io.File;
import java.io.IOException;

/**
 * バッチ起動リクエスト受信+バッチ起動 Flow 設定用 JavaConfig クラス
 */
@Slf4j
@Configuration
public class FlowConfig {

    private static final String WORKDIR_SLEEP_BAT = "C:/eipapp/ksbysample-eipapp-batchexecutor/bat";
    private static final String COMMAND_SLEEP_BAT = WORKDIR_SLEEP_BAT + "/sleep.bat";

    private final DataSource dataSource;

    /**
     * コンストラクタ

     * @param dataSource {@link DataSource} オブジェクト
     */
    public FlowConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    /**
     * {@link FlowConfig#executeBatchQueueChannel()} の ChannelMessageStore を H2 Database
     * にするための {@link JdbcChannelMessageStore}
     *
     * @return {@link PriorityCapableChannelMessageStore} オブジェクト
     */
    @Bean
    public PriorityCapableChannelMessageStore jdbcChannelMessageStore() {
        JdbcChannelMessageStore messageStore = new JdbcChannelMessageStore(this.dataSource);
        messageStore.setChannelMessageStoreQueryProvider(new H2ChannelMessageStoreQueryProvider());
        messageStore.setPriorityEnabled(true);
        return messageStore;
    }

    /**
     * bat ファイル実行指示用の QueueChannel
     *
     * @return {@link MessageChannel} オブジェクト
     */
    @Bean
    public MessageChannel executeBatchQueueChannel() {
        return MessageChannels.queue(jdbcChannelMessageStore(), "EXECUTE_BATCH")
                .get();
    }

    /**
     * http://localhost:8080/batch?sleep=... でリクエストを受信して、sleep パラメータの値を
     * payload にセットした Message を bat ファイル実行指示用の QueueChannel に送信した後、
     * sleep パラメータの文字列をそのままテキストとしてレスポンスとして返す
     *
     * @return {@link IntegrationFlow} オブジェクト
     */
    @Bean
    public IntegrationFlow httpBatchFlow() {
        return IntegrationFlows.from(
                // http://localhost:8080/batch?sleep=... でリクエストを受信して
                // sleep の値を payload にセットした Message を生成する
                Http.inboundGateway("/batch")
                        .requestMapping(r -> r
                                .methods(HttpMethod.GET)
                                .params("sleep"))
                        .payloadExpression("#requestParams.sleep[0]"))
                .handle((p, h) -> {
                    log.info("★★★ リクエストを受信しました ( sleep = {} )", p);
                    return p;
                })
                // message を bat ファイル実行指示用の QueueChannel に送信する
                // .wireTap(...) を利用することで、処理をここで中断せず次の .handle(...) が実行されるようにする
                .wireTap(f -> f.channel(executeBatchQueueChannel()))
                // http のレスポンスには sleep パラメータの文字列をそのままテキストで返す
                .handle((p, h) -> p)
                .get();
    }

    /**
     * bat ファイル実行指示用の QueueChannel に Message が送信されているか1秒毎にチェックし、
     * 送信されている場合には受信して sleep.bat を実行する。連続して Message を送信しても
     * 多重化していないので前のバッチが終了しないと次のバッチは実行されない。
     *
     * @return {@link IntegrationFlow} オブジェクト
     */
    @Bean
    public IntegrationFlow executeBatchFlow() {
        return IntegrationFlows.from(executeBatchQueueChannel())
                // QueueChannel に Message が送信されているか1秒毎にチェックする
                .bridge(e -> e.poller(Pollers.fixedDelay(1000)))
                // バッチを最大3スレッドで起動する
                // 最大数の制限を設けないのであれば Executors.newCachedThreadPool() にする
                // .channel(c -> c.executor(Executors.newFixedThreadPool(3)))
                // sleep.bat を実行する
                .handle((p, h) -> {
                    log.info("●●● sleep.bat {} を実行します", p);
                    ProcessBuilder builder = new ProcessBuilder();
                    builder.command(COMMAND_SLEEP_BAT, (String) p, ">", "NUL")
                            .directory(new File(WORKDIR_SLEEP_BAT))
                            .redirectErrorStream(true);

                    Process process = null;
                    try {
                        process = builder.start();
                        int exitCode = process.waitFor();
                        log.info("●●● sleep.bat {} が終了しました ( exitCode = {} )", p, exitCode);
                    } catch (IOException | InterruptedException e) {
                        throw new RuntimeException(e);
                    } finally {
                        if (process != null) {
                            IOUtils.closeQuietly(process.getInputStream());
                            IOUtils.closeQuietly(process.getOutputStream());
                            IOUtils.closeQuietly(process.getErrorStream());
                            process.destroy();
                        }
                    }

                    return null;
                })
                .get();
    }

}

履歴

2017/08/20
初版発行。

Spring Boot + Spring Integration でいろいろ試してみる ( その22 )( http でリクエストを受信 → JdbcChannelMessageStore を使用した QueueChannel に Message を送信 → Message を受信して bat ファイルを実行する )

概要

記事一覧はこちらです。

  • Spring Integration DSL で以下の処理を行う常駐型アプリケーションを作成します。
    • http://localhost:8080/batch?sleep=... でリクエストを受信します。17. HTTP Support の機能を利用して WebAPI を作成します。
    • 渡された sleep パラメータの値を payload に設定した Message を生成し、H2 Database を MessageStore とする QueueChannel に送信します。
    • H2 Database は今回作成する常駐型アプリケーションから組み込みモードで起動し、データは in-memory ではなくファイルに保存します。
    • QueueChannel を1秒間隔でチェックし、Message が送信されていたら受信して Windows の bat ファイルを実行します。
    • bat は同時に1つしか起動しないようにします。
    • bat ファイルは引数を1つ受け取り、引数で渡された値の秒数 sleep してから、引数の値を戻り値として返します。bat ファイル名は sleep.bat とし、sleep.bat [秒数] で起動します。sleep 機能は ActivePerl をインストールして Perlワンライナースクリプトで実装します。
  • 新しく使った機能としては、以下の2つです。
    • HTTP Support を使用した WebAPI の作成
    • データベースを MessageStore に利用した QueueChannel の作成
  • 2~3回に分けて書く予定です。

参照したサイト・書籍

  1. Spring Integration Java DSL Reference - Using Protocol Adapters
    https://github.com/spring-projects/spring-integration-java-dsl/wiki/spring-integration-java-dsl-reference#adapters

  2. Spring Integration Reference Manual - 17.4.6 URI Template Variables and Expressions
    http://docs.spring.io/spring-integration/docs/4.3.11.RELEASE/reference/htmlsingle/#_uri_template_variables_and_expressions

    • SpEL で #requestParams が使用できることはここに書かれています。
  3. Spring integration flow respond with HTTP 400
    https://stackoverflow.com/questions/42631872/spring-integration-flow-respond-with-http-400

    • 今回は使用していませんが、レスポンスの HTTP ステータスコードを変えたい場合の方法が書かれていたのを見つけたので、メモとして残しておきます。
  4. TransactionalPoller is SpringIntegration
    https://stackoverflow.com/questions/41184992/transactionalpoller-is-springintegration

  5. Jdbc initialize-database with Java config
    https://stackoverflow.com/questions/37971721/jdbc-initialize-database-with-java-config

  6. How can I run jdbc:initialize-database using condition?
    https://stackoverflow.com/questions/24308731/how-can-i-run-jdbcinitialize-database-using-condition

  7. Spring Integration Reference Manual - 18.4.2 Backing Message Channels
    http://docs.spring.io/spring-integration/docs/4.3.11.RELEASE/reference/htmlsingle/#jdbc-message-store-channels

    • INT_CHANNEL_MESSAGE の説明が記載されています。

目次

  1. ksbysample-eipapp-batchexecutor プロジェクトを作成する
  2. http://localhost:8080/batch?sleep=... でリクエストを受信する処理を作成する
  3. 渡された sleep パラメータの値を H2 Database の JdbcChannelMessageStore を使用した QueueChannel に送信する処理を作成する
  4. QueueChannel を1秒毎にチェックする処理を作成する
  5. 次回ヘ続く

手順

ksbysample-eipapp-batchexecutor プロジェクトを作成する

IntelliJ IDEA で Gradle プロジェクトを作成します。

  • プロジェクトは C:\project-springboot\ksbysample-boot-integration\ksbysample-eipapp-batchexecutor に作成します。
  • ArtifactId は ksbysample-eipapp-batchexecutor にします。

build.gradle を以下の内容に変更した後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

group 'ksbysample'
version '1.0.0-RELEASE'

buildscript {
    ext {
        springBootVersion = '1.5.6.RELEASE'
    }
    repositories {
        mavenCentral()
        maven { url "https://plugins.gradle.org/m2/" }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        // for Error Prone ( http://errorprone.info/ )
        classpath("net.ltgt.gradle:gradle-errorprone-plugin:0.0.11")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'groovy'
apply plugin: 'net.ltgt.errorprone'
apply plugin: 'checkstyle'
apply plugin: 'findbugs'
apply plugin: 'pmd'

sourceCompatibility = 1.8
targetCompatibility = 1.8

task wrapper(type: Wrapper) {
    gradleVersion = '3.5'
}

[compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = ['-Xlint:all,-options,-processing,-path']
compileJava.options.compilerArgs += ['-Xep:RemoveUnusedImports:WARN']

idea {
    module {
        inheritOutputDirs = false
        outputDir = file("$buildDir/classes/main/")
    }
}

checkstyle {
    configFile = file("${rootProject.projectDir}/config/checkstyle/google_checks.xml")
    toolVersion = '8.1'
    sourceSets = [project.sourceSets.main]
}

findbugs {
    toolVersion = '3.0.1'
    sourceSets = [project.sourceSets.main]
    ignoreFailures = true
    effort = "max"
    excludeFilter = file("${rootProject.projectDir}/config/findbugs/findbugs-exclude.xml")
}

tasks.withType(FindBugs) {
    // Gradle 3.3以降 + FindBugs Gradle Plugin を組み合わせると、"The following errors occurred during analysis:"
    // の後に "Cannot open codebase filesystem:..." というメッセージが大量に出力されるので、以下の doFirst { ... }
    // のコードを入れることで出力されないようにする
    doFirst {
        def fc = classes
        if (fc == null) {
            return
        }
        fc.exclude '**/*.properties'
        fc.exclude '**/*.xml'
        fc.exclude '**/META-INF/**'
        fc.exclude '**/static/**'
        fc.exclude '**/templates/**'
        classes = files(fc.files)
    }
    reports {
        xml.enabled = false
        html.enabled = true
    }
}

pmd {
    toolVersion = "5.8.1"
    sourceSets = [project.sourceSets.main]
    ignoreFailures = true
    consoleOutput = true
    ruleSetFiles = rootProject.files("/config/pmd/pmd-project-rulesets.xml")
    ruleSets = []
}

repositories {
    mavenCentral()
}

dependencyManagement {
    imports {
        // BOM は https://repo.spring.io/release/io/spring/platform/platform-bom/Brussels-SR4/
        // の下を見ること
        mavenBom("io.spring.platform:platform-bom:Brussels-SR4")
    }
}

bootRepackage {
    mainClass = 'ksbysample.eipapp.batchexecutor.Application'
}

dependencies {
    def errorproneVersion = "2.0.15"

    // dependency-management-plugin によりバージョン番号が自動で設定されるもの
    // Appendix A. Dependency versions ( http://docs.spring.io/platform/docs/current/reference/htmlsingle/#appendix-dependency-versions ) 参照
    compile("org.springframework.boot:spring-boot-starter-integration")
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("org.springframework.boot:spring-boot-starter-data-jpa")
    compile("org.springframework.integration:spring-integration-http")
    compile("org.springframework.integration:spring-integration-jdbc")
    testCompile("org.springframework.boot:spring-boot-starter-test")

    // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    compile("org.springframework.integration:spring-integration-java-dsl:1.2.2.RELEASE")
    compile("com.h2database:h2:1.4.192")

    // for Error Prone ( http://errorprone.info/ )
    errorprone("com.google.errorprone:error_prone_core:${errorproneVersion}")
    compileOnly("com.google.errorprone:error_prone_annotations:${errorproneVersion}")
}
  • Spring Boot は 1.5.6、Spring IO Platform は Brussels-SR4 を使用します。
  • CheckstyleFindBugs、PMD、Error Prone は全て入れます。config ディレクトリは ksbysample-webapp-lending プロジェクトの config ディレクトリ をそのまま持ってきます。
  • Gradle は 3.5 を使用します。
  • 17. HTTP Support の機能を利用して WebAPI を作成するので、以下の2行を記述します。
    • compile("org.springframework.boot:spring-boot-starter-web")
    • compile("org.springframework.integration:spring-integration-http")
  • H2 Database と JdbcChannelMessageStore を利用するので、以下の3行を記述します。作成したデータベースに IntelliJ IDEA の Database Tools から接続するために com.h2database:h2 は Database Tools が使用するクライアントライブラリのバージョンと同じバージョンを指定します。
    • compile("org.springframework.boot:spring-boot-starter-data-jpa")
    • compile("org.springframework.integration:spring-integration-jdbc")
    • compile("com.h2database:h2:1.4.192")

src/main/java の下に ksbysample.eipapp.batchexecutor パッケージを作成し、その下に Application.java を新規作成して以下の内容を記述します。

package ksbysample.eipapp.batchexecutor;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;

import javax.sql.DataSource;

/**
 * Application 用メイン + JavaConfig 用クラス
 */
@SpringBootApplication(exclude = {JpaRepositoriesAutoConfiguration.class, HibernateJpaAutoConfiguration.class})
public class Application {

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

    /**
     * @return Tomcat JDBC Connection Pool の DataSource オブジェクト
     */
    @Bean
    @ConfigurationProperties("spring.datasource.tomcat")
    public DataSource dataSource() {
        return DataSourceBuilder.create()
                .type(org.apache.tomcat.jdbc.pool.DataSource.class)
                .build();
    }

}

src/main/resources の下に application.properties を新規作成し、以下の内容を記述します。

spring.datasource.tomcat.url=jdbc:h2:file:C:/eipapp/ksbysample-eipapp-batchexecutor/db/batchexecutordb
spring.datasource.tomcat.username=sa
spring.datasource.tomcat.password=
spring.datasource.tomcat.driverClassName=org.h2.Driver
spring.datasource.tomcat.initialSize=2
spring.datasource.tomcat.maxActive=2
spring.datasource.tomcat.maxIdle=2
spring.datasource.tomcat.minIdle=2
spring.datasource.tomcat.testOnBorrow=true
spring.datasource.tomcat.validationQuery=select 1
spring.datasource.tomcat.validationQueryTimeout=5
spring.datasource.tomcat.removeAbandoned=true
spring.datasource.tomcat.removeAbandonedTimeout=30
spring.datasource.tomcat.jdbc-interceptors=SlowQueryReport(threshold=5000)
# spring.datasource.jmx-enabled は spring.datasource.tomcat.jmx-enabled と書かないこと。
# spring.datasource.tomcat.jmx-enabled だと機能しない。
spring.datasource.jmx-enabled=true

アプリケーションの実行環境として以下のディレクトリを作成し、H2 Database のデータベースファイルは C:\eipapp\batch-executor\db の下に作成する想定です。

C:\eipapp\ksbysample-eipapp-batchexecutor
├ bat
│ └ sleep.bat
└ db
   ├ batchexecutordb.mv.db
   └ batchexecutordb.trace.db

現在のプロジェクトの構成は以下のようになります。

f:id:ksby:20170820020957p:plain

http://localhost:8080/batch?sleep=... でリクエストを受信する処理を作成する

src/main/java/ksbysample/eipapp/batchexecutor の下に FlowConfig.java を新規作成し、以下の内容を記述します。

package ksbysample.eipapp.batchexecutor;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.dsl.http.Http;

@Configuration
public class FlowConfig {

    /**
     * http://localhost:8080/batch?sleep=... でリクエストを受信して、sleep パラメータの値を 
     * payload にセットした Message を bat ファイル実行指示用の QueueChannel に送信した後、
     * sleep パラメータの文字列をそのままテキストとしてレスポンスとして返す
     * 
     * @return {@link IntegrationFlow} オブジェクト
     */
    @Bean
    public IntegrationFlow httpBatchFlow() {
        return IntegrationFlows.from(
                // http://localhost:8080/batch?sleep=... でリクエストを受信して
                // sleep の値を payload にセットした Message を生成する
                Http.inboundGateway("/batch")
                        .requestMapping(r -> r
                                .methods(HttpMethod.GET)
                                .params("sleep"))
                        .payloadExpression("#requestParams.sleep[0]"))
                // http のレスポンスには sleep パラメータの文字列をそのままテキストで返す
                .handle((p, h) -> p)
                .get();
    }

}

アプリケーションを実行して http://localhost:8080/batch?sleep=10 でアクセスすると、画面上に 10 の文字が表示されます。

f:id:ksby:20170820035004p:plain

渡された sleep パラメータの値を H2 Database の JdbcChannelMessageStore を使用した QueueChannel に送信する処理を作成する

まず H2 Database を JdbcChannelMessageStore とする QueueChannel を定義します。src/main/java/ksbysample/eipapp/batchexecutor/FlowConfig.java に以下の記述を追加します。

@Configuration
public class FlowConfig {

    private final DataSource dataSource;

    public FlowConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    /**
     * {@link FlowConfig#executeBatchQueueChannel()} の ChannelMessageStore を H2 Database
     * にするための {@link JdbcChannelMessageStore}
     *
     * @return {@link PriorityCapableChannelMessageStore} オブジェクト
     */
    @Bean
    public PriorityCapableChannelMessageStore jdbcChannelMessageStore() {
        JdbcChannelMessageStore messageStore = new JdbcChannelMessageStore(this.dataSource);
        messageStore.setChannelMessageStoreQueryProvider(new H2ChannelMessageStoreQueryProvider());
        messageStore.setPriorityEnabled(true);
        return messageStore;
    }

    /**
     * bat ファイル実行指示用の QueueChannel
     *
     * @return {@link MessageChannel} オブジェクト
     */
    @Bean
    public MessageChannel executeBatchQueueChannel() {
        return MessageChannels.queue(jdbcChannelMessageStore(), "EXECUTE_BATCH")
                .get();
    }

    ..........
  • jdbcChannelMessageStore メソッド内で messageStore.setPriorityEnabled(true); と記述していますが、この行は必須です。引数に false は指定できません。この行を書かなかったり、false を指定した場合には、アプリケーション起動時に java.lang.IllegalArgumentException: When using priority, the 'PriorityCapableChannelMessageStore' must have priority enabled. というエラーが出てアプリケーションが起動しません。

次に FlowConfig#httpBatchFlow メソッドを以下のように変更します。

    @Bean
    public IntegrationFlow httpBatchFlow() {
        return IntegrationFlows.from(
                // http://localhost:8080/batch?sleep=... でリクエストを受信して
                // sleep の値を payload にセットした Message を生成する
                Http.inboundGateway("/batch")
                        .requestMapping(r -> r
                                .methods(HttpMethod.GET)
                                .params("sleep"))
                        .payloadExpression("#requestParams.sleep[0]"))
                // message を bat ファイル実行指示用の QueueChannel に送信する
                // .wireTap(...) を利用することで、処理をここで中断せず次の .handle(...) が実行されるようにする
                .wireTap(f -> f.channel(executeBatchQueueChannel()))
                // http のレスポンスには sleep パラメータの文字列をそのままテキストで返す
                .handle((p, h) -> p)
                .get();
    }
  • .wireTap(f -> f.channel(executeBatchQueueChannel())) を追加します。

アプリケーションを起動し直して http://localhost:8080/batch?sleep=10 の URL にアクセスするとエラーが発生しました。。。 画面のエラーメッセージを見ると Table "INT_CHANNEL_MESSAGE" not found と表示されています。INT_CHANNEL_MESSAGE というテーブルが必要なようですが、自動では作成してくれないようです。

f:id:ksby:20170820041438p:plain

INT_CHANNEL_MESSAGE を作成する方法を調べてみると、stackoverflow の以下の QA がヒットしました。

また Spring Integration Reference Manual では以下の場所に INT_CHANNEL_MESSAGE の記載がありました。

内容をまとめると、

  • classpath:org/springframework/integration/jdbc/store/channel/schema-h2.sql を実行すれば INT_CHANNEL_MESSAGE テーブルを作成してくれます。
  • XML ベースの Spring Integration の場合、XML ファイルに <jdbc:initialize-database><jdbc:script location="..."/></jdbc:initialize-database> を記述すると、アプリケーションの起動時に SQL ファイルを実行してくれます。
  • 1度アプリケーションを実行して INT_CHANNEL_MESSAGE テーブルを作成した後アプリケーションを落として再度起動すると、 INT_CHANNEL_MESSAGE テーブルが既に存在するため schema-h2.sql 実行時にエラーが出るのですが(アプリケーション自体は起動します)、<jdbc:initialize-database>ignore-failures="ALL" 属性を追加するとエラーが出なくなります(無視されます)。

今回は以下のように対応します。

  • classpath:org/springframework/integration/jdbc/store/channel/schema-h2.sqlXML ファイルを作成して実行するようにします。
  • XML ファイルは src/main/resources の下に applicationContext.xml というファイル名で作成し、Application クラスに @ImportResource("classpath:applicationContext.xml") アノテーションを付加して読み込みます。

src/main/resources の下に applicationContext.xml を新規作成し、以下の内容を記述します。

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/integration"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xmlns:beans="http://www.springframework.org/schema/beans"
             xmlns:jdbc="http://www.springframework.org/schema/jdbc"
             xsi:schemaLocation="http://www.springframework.org/schema/beans
                                 http://www.springframework.org/schema/beans/spring-beans.xsd
                                 http://www.springframework.org/schema/integration
                                 http://www.springframework.org/schema/integration/spring-integration.xsd
                                 http://www.springframework.org/schema/jdbc
                                 http://www.springframework.org/schema/jdbc/spring-jdbc.xsd">

    <!--
        アプリケーション起動時に schema-h2.sql を実行して JdbcChannelMessageStore で使用する 
        INT_CHANNEL_MESSAGE テーブルを新規作成する 
        また、既に INT_CHANNEL_MESSAGE テーブルがあると schema-h2.sql 実行時にエラーが出るが、 
        ignore-failures="ALL" を付与して無視するようにする
    -->
    <jdbc:initialize-database ignore-failures="ALL">
        <jdbc:script location="classpath:org/springframework/integration/jdbc/store/channel/schema-h2.sql"/>
    </jdbc:initialize-database>

</beans:beans>

src/main/java/ksbysample/eipapp/batchexecutor/Application.java を以下のように変更します。

@SpringBootApplication(exclude = {JpaRepositoriesAutoConfiguration.class, HibernateJpaAutoConfiguration.class})
@ImportResource("classpath:applicationContext.xml")
public class Application {
  • @ImportResource("classpath:applicationContext.xml") アノテーションを Application クラスに付加します。

アプリケーションを起動し直して http://localhost:8080/batch?sleep=10 の URL にアクセスすると、今度はエラーにならず画面上に 10 と表示されました。

f:id:ksby:20170820090351p:plain

Tomcat を停止して、データベースに Message が入っていることを確認します。

IntelliJ IDEA の Database Tools で「+」-「Data Source」-「H2」を選択して、

f:id:ksby:20170820090618p:plain

「Data Sources and Drivers」ダイアログが表示されたら以下の値を入力して「OK」ボタンを押します。

f:id:ksby:20170820090934p:plain

  • 「URL」の一番右側のドロップダウンリストで「URL only」を選択します。
  • 「Name」に file:batchexecutordb を入力します。file: を付けているのは in-memory や tcp 接続と区別するためです。
  • 「User」に sa を入力します。
  • 「URL」に jdbc:h2:file:C:/eipapp/ksbysample-eipapp-batchexecutor/db/batchexecutordb を入力します。
  • 「Test Connection」ボタンをクリックして Successful が表示されることを確認します。

f:id:ksby:20170820091331p:plain

  • 「Schemas」タブをクリックした後、「All schemas」をチェックします。

Database Tools に戻ると「file:batchexecutordb」が追加されており、展開すると INT_CHANNEL_MESSAGE テーブルも表示されています。

f:id:ksby:20170820091536p:plain

INT_CHANNEL_MESSAGE テーブルの中のデータを表示すると、1件データが登録されていました。

f:id:ksby:20170820093547p:plain

登録されていたデータは一旦削除します。

QueueChannel を1秒毎にチェックする処理を作成する

src/main/java/ksbysample/eipapp/batchexecutor/FlowConfig.java に以下の記述を追加します。

@Configuration
public class FlowConfig {

    ..........

    /**
     * bat ファイル実行指示用の QueueChannel に Message が送信されているか1秒毎にチェックし、
     * 送信されている場合には受信して sleep.bat を実行する。連続して Message を送信しても
     * 多重化していないので前のバッチが終了しないと次のバッチは実行されない。
     *
     * @return {@link IntegrationFlow} オブジェクト
     */
    @Bean
    public IntegrationFlow executeBatchFlow() {
        return IntegrationFlows.from(executeBatchQueueChannel())
                // QueueChannel に Message が送信されているか1秒毎にチェックする
                .bridge(e -> e.poller(Pollers.fixedDelay(1000)))
                // まだバッチ実行処理は実装しない
                // メッセージを受信したことが分かるようログを出力する
                .log()
                // sleep.bat を実行する
                .handle((p, h) -> {
                    return null;
                })
                .get();
    }

}
  • executeBatchFlow メソッドを追加します。

アプリケーションを起動し直して http://localhost:8080/batch?sleep=10http://localhost:8080/batch?sleep=9http://localhost:8080/batch?sleep=8 の URL に連続してアクセスしてみると、payload=10、payload=9、payload=8 の3つの Message のログが出力されました。

f:id:ksby:20170820143350p:plain

送信された Message を見ると payload 以外に header にもいろいろ情報をセットしており、http://localhost:8080/batch?sleep=10 の URL にアクセスした時に送信されたメッセージを見ると以下の値がセットされていました。

GenericMessage [
    payload=10
    , headers={
        http_requestMethod=GET
        , accept-language=ja-JP,en;q=0.5
        , cookie=Idea-283d915c=f5d44479-0894-4fff-95d8-ec599ffec940
        , accept=[text/html, application/xhtml+xml, */*]
        , JdbcChannelMessageStore.CREATED_DATE=1503207113688
        , host=localhost:8080
        , http_requestUrl=http://localhost:8080/batch?sleep=10
        , connection=Keep-Alive
        , id=c30d9152-90d9-a804-3c58-ca489111d6aa
        , JdbcChannelMessageStore.SAVED=true
        , accept-encoding=gzip
        , deflate
        , user-agent=Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko
        , timestamp=1503207113688
    }
]

次回ヘ続く

長くなったので一旦ここで止めて、次回へ続きます。

履歴

2017/08/20
初版発行。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その15 )( Flyway のインストール + Spring Security 使用時に H2 Console に接続する + IntelliJ IDEA の Database Tools で in-memory モードの H2 Database に接続する )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その14 )( browser-sync –> Tomcat 連携してファイル変更時に自動リロードで反映される環境を構築してみる ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 本当は Flyway をインストールして H2 Database にテーブルを作成するのをさっと終わらせるだけのつもりでいたのですが、他のことで以外に苦労しました。
    • 以下の内容を記述しています。
      • Flyway のインストールと、Tomcat 起動時の Flyway の動作の確認
      • Spring Security を使用している時に H2 Console に接続する方法
      • IntelliJ IDEA の Database Tools で、Spring Boot の Tomcat から in-memory モードで起動した H2 Database に接続する方法
    • Flyway で H2 Database にテーブルを作成するのは次回に持ち越しです。

参照したサイト・書籍

  1. Flyway
    https://flywaydb.org/

  2. H2 Database Engine
    http://www.h2database.com/html/main.html

  3. Spring Boot Reference Guide - 78. Database initialization - 78.5.1 Execute Flyway database migrations on startup
    https://docs.spring.io/spring-boot/docs/current/reference/html/howto-database-initialization.html#howto-execute-flyway-database-migrations-on-startup

  4. Spring Boot Flyway Sample
    https://github.com/spring-projects/spring-boot/tree/v1.5.6.RELEASE/spring-boot-samples/spring-boot-sample-flyway

  5. SpringBoot:H2DBに接続
    http://web-dev.hatenablog.com/entry/spring-boot/intro/connect-h2db

  6. Spring Boot /h2-console throws 403 with Spring Security 1.5.2
    https://stackoverflow.com/questions/43794721/spring-boot-h2-console-throws-403-with-spring-security-1-5-2

  7. H2 Database Console
    https://springframework.guru/using-the-h2-database-console-in-spring-boot-with-spring-security/

  8. Connect to H2 database using IntelliJ database client
    https://stackoverflow.com/questions/28940912/connect-to-h2-database-using-intellij-database-client

  9. QUERYING THE EMBEDDED H2 DATABASE OF A SPRING BOOT APPLICATION
    https://techdev.de/querying-the-embedded-h2-database-of-a-spring-boot-application/

  10. org.h2.jdbc.JdbcSQLException: Connection is broken: “unexpected status 5505072”
    https://groups.google.com/forum/#!topic/h2-database/qZ5bpboIC6U

目次

  1. Flyway をインストールする
  2. Tomcat を起動してみるが起動せず。。。connection pool の initialSize を 1→2 に変更する
  3. Tocmat 起動時の Flyway の動作を見てみる
  4. IntelliJ IDEA の Database Tools から H2 Database へ接続する。。。が何かおかしい
  5. H2 Console に接続する
  6. in-memory モードで起動している H2 Database に別プロセスから接続するには?
  7. 再び Database Tools から H2 Database へ接続してみる

手順

Flyway をインストールする

Spring IO Platform Reference Guide を見ると org.flywaydb:flyway-core の記述がありますが、バージョンは 3.2.1 でした。Flyway を見ると最新バージョンは 4.2.0 でしたので、バージョン番号を指定して 4.2.0 をインストールします。

build.gradle を以下のように変更します。

dependencies {
    ..........

    // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    compile("com.integralblue:log4jdbc-spring-boot-starter:1.0.1")
    compile("org.flywaydb:flyway-core:4.2.0")
    testCompile("org.dbunit:dbunit:2.5.3")
    testCompile("com.icegreen:greenmail:1.5.5")
    testCompile("org.assertj:assertj-core:3.8.0")
    testCompile("org.spockframework:spock-core:${spockVersion}")
    testCompile("org.spockframework:spock-spring:${spockVersion}")
    testCompile("com.google.code.findbugs:jsr305:3.0.2")

    ..........
}
  • dependencies に compile("org.flywaydb:flyway-core:4.2.0") を追加します。

変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

Tomcat を起動してみるが起動せず。。。connection pool の initialSize を 1→2 に変更する

Flyway をインストールしただけの状態で Tomcat が起動するか確認します。

Tomcat を起動すると Caused by: org.apache.tomcat.jdbc.pool.PoolExhaustedException: [main] Timeout: Pool empty. Unable to fetch a connection in 30 seconds, none available[size:1; busy:1; idle:0; lastwait:30000]. というエラーログが出力されて起動できませんでした。application.properties に spring.datasource.tomcat.initialSize=1 と設定していますが、1 つでは足りないようです。

f:id:ksby:20170813150540p:plain

initialSize の数値を変更して試してみたところ、最低 2 で設定していないと起動できませんでした。application.properties、application-product.properties の設定を以下のように変更します。

spring.datasource.tomcat.initialSize=2
spring.datasource.tomcat.maxActive=2
spring.datasource.tomcat.maxIdle=2
spring.datasource.tomcat.minIdle=2

再度試すと Tomcat が無事起動しました。

f:id:ksby:20170813151035p:plain

Tocmat 起動時の Flyway の動作を見てみる

Log4jdbc Spring Boot Starter により Tomcat 起動時に Flyway で実行される SQL 文がコンソールに出力されていたので、追ってみました。

  1. Connection.new
  2. CALL SCHEMA()
  3. SELECT COUNT(*) FROM INFORMATION_SCHEMA.schemata WHERE schema_name=‘PUBLIC’
  4. SET SCHEMA “PUBLIC”
  5. SELECT COUNT(*) FROM INFORMATION_SCHEMA.schemata WHERE schema_name=‘PUBLIC’
  6. SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLEs WHERE TABLE_schema = ‘PUBLIC’ AND TABLE_TYPE = ‘TABLE’
  7. CALL SCHEMA()
  8. CREATE TABLE “PUBLIC”.“schema_version” ( “installed_rank” INT NOT NULL, “version” VARCHAR(50), “description” VARCHAR(200) NOT NULL, “type” VARCHAR(20) NOT NULL, “script” VARCHAR(1000) NOT NULL, “checksum” INT, “installed_by” VARCHAR(100) NOT NULL, “installed_on” TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, “execution_time” INT NOT NULL, “success” BOOLEAN NOT NULL )
  9. ALTER TABLE “PUBLIC”.“schema_version” ADD CONSTRAINT “schema_version_pk” PRIMARY KEY (“installed_rank”)
  10. CREATE INDEX “PUBLIC”.“schema_version_s_idx” ON “PUBLIC”.“schema_version” (“success”)
  11. select * from “PUBLIC”.“schema_version” for update
  12. SELECT “installed_rank”,“version”,“description”,“type”,“script”,“checksum”,“installed_on”,“installed_by”,“execution_time”,“success” FROM “PUBLIC”.“schema_version” WHERE “installed_rank” > -1 ORDER BY “installed_rank”
  13. SET SCHEMA “PUBLIC”
  14. Connection.close()

schema_version というテーブルがないと create table しています。おそらくこのテーブルでどのバージョンの SQL まで実行済なのかを管理しているのでしょう。

“flyway schema_version” で Google で検索すると How Flyway works のページがヒットしました。中の記述を読むと確かに schema_version というテーブルでバージョン管理されていました。

IntelliJ IDEA の Database Tools から H2 Database へ接続する

IntelliJ IDEA の Database Tool Window の左上の「+」ボタンをクリックして「Data Source」-「H2」を選択します。

f:id:ksby:20170813194830p:plain

「Data Sources and Drivers」ダイアログが表示されますので、以下の値を入力して「OK」ボタンをクリックします。

f:id:ksby:20170813195119p:plain

  • URL に jdbc:h2:mem:bootnpmgebdb を入力します。
  • 「Test Connection」ボタンをクリックして “Successful” が表示されることを確認します。

f:id:ksby:20170813195330p:plain

  • 「Schemas」タブに切り替えて「All schemas」をチェックします。

Database Tool Window に戻ると「bootnpmgebdb」が追加されています。が、Flyway が PUBLIC の下に schema_version テーブルを作成したはずなのに何も表示されていませんね。。。?

f:id:ksby:20170813195555p:plain

H2 Console に接続する

H2 Database には H2 Console という Web アプリケーションがありますので、そちらに接続しても schema_version テーブルが見えないのか確認してみます。

H2 Console はデフォルトでは起動していないので、src/main/resources/application-develop.properties の最後に以下の設定を追加して develop 環境の時だけ起動するようにします。

spring.h2.console.enabled=true

Tomcat を再起動してからブラウザで http://localhost:8080/h2-console にアクセスすると、Basic 認証のダイアログ?が表示されました。これは Spring Security でしょうか?

f:id:ksby:20170813225043p:plain

Web で調べてみると以下の記事が見つかりました。

まずは Spring Security の Basic 認証を無効にします。src/main/resources/application-develop.properties に以下の設定を追加します。

spring.h2.console.enabled=true
security.basic.enabled=false
  • security.basic.enabled=false を追加します。

次に src/main/java/ksbysample/webapp/bootnpmgeb/config/WebSecurityConfig.java を以下のように変更します。

package ksbysample.webapp.bootnpmgeb.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

import java.util.regex.Pattern;

/**
 * ???
 */
@Configuration
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private static final Pattern DISABLE_CSRF_TOKEN_PATTERN = Pattern.compile("(?i)^(GET|HEAD|TRACE|OPTIONS)$");
    private static final Pattern H2_CONSOLE_URI_PATTERN = Pattern.compile("^/h2-console");

    @Value("${spring.h2.console.enabled:false}")
    private boolean springH2ConsoleEnabled;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 認証の対象外にしたいURLがある場合には、以下のような記述を追加します
                // 複数URLがある場合はantMatchersメソッドにカンマ区切りで対象URLを複数列挙します
                // .antMatchers("/country/**").permitAll()
                //
                // この Web アプリケーションでは Spring Security を CSRF対策で使用したいだけなので、
                // 全ての URL を認証の対象外にする
                .antMatchers("/**").permitAll()
                .anyRequest().authenticated();

        // spring.h2.console.enabled=true に設定されている場合には H2 Console を表示するために必要な設定を行う
        if (springH2ConsoleEnabled) {
            http.csrf()
                    .requireCsrfProtectionMatcher(request -> {
                        if (DISABLE_CSRF_TOKEN_PATTERN.matcher(request.getMethod()).matches()) {
                            // GET, HEAD, TRACE, OPTIONS は CSRF対策の対象外にする
                            return false;
                        } else if (H2_CONSOLE_URI_PATTERN.matcher(request.getRequestURI()).lookingAt()) {
                            // H2 Console は CSRF対策の対象外にする
                            return false;
                        }
                        return true;
                    });
            http.headers().frameOptions().sameOrigin();
        }
    }

}
  • private static final Pattern DISABLE_CSRF_TOKEN_PATTERN = Pattern.compile("(?i)^(GET|HEAD|TRACE|OPTIONS)$"); を追加します。
  • private Pattern H2_CONSOLE_URI_PATTERN = Pattern.compile("^/h2-console"); を追加します。
  • @Value("${spring.h2.console.enabled:false}") private boolean springH2ConsoleEnabled; を追加します。
  • configure メソッド内に if (springH2ConsoleEnabled) { ... } の処理を追加します。/h2-console の URI に対する CSRF対策を無効にし、X-Frame-Options レスポンスヘッダに DENY ではなく SAMEORIGIN がセットされるようにします。

再び Tomcat を再起動してからブラウザで http://localhost:8080/h2-console にアクセスすると今度はログイン画面が表示されました。

f:id:ksby:20170814003240p:plain

JDBC URL」に jdbc:h2:mem:bootnpmgebdb を、「User Name」に sa を入力して「Connect」ボタンをクリックします。

H2 Console の画面が表示されました。画面左側を見ると schema_version テーブルが表示されています。

f:id:ksby:20170814003458p:plain

Database Tools で接続しても schema_version テーブルが見えない原因がなんとなく分かりました。おそらく起動しているプロセスが別なので、見ている in-memory データベースが別々になっているからですね。

in-memory モードで起動している H2 Database に別プロセスから接続するには?

そんな方法があるのかな。。。と思いつつ Web で調べてみると、以下の記事が見つかりました。

これらの記事によると TCP サーバを起動して接続すればいいらしいです。試してみます。

src/main/java/ksbysample/webapp/bootnpmgeb/config の下に H2DatabaseConfig.java を新規作成し、以下の内容を記述します。

package ksbysample.webapp.bootnpmgeb.config;

import org.h2.tools.Server;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

import java.sql.SQLException;

/**
 * Tomcat 内で in-memory モードで起動した H2 Database に外部のプロセスから接続するための
 * TCP サーバを起動する JavaConfig クラス。develop 環境でのみ起動する。
 */
@Configuration
@Profile("develop")
public class H2DatabaseConfig {

    // TCP port for remote connections, default 9092
    @Value("${h2.tcp.port:9092}")
    private String h2TcpPort;

    /**
     * TCP connection to connect with SQL clients to the embedded h2 database.
     * Connect to "jdbc:h2:tcp://localhost:9092/mem:testdb", username "sa", password empty.
     */
    @Bean
    public Server h2TcpServer() throws SQLException {
        return Server.createTcpServer("-tcp", "-tcpAllowOthers", "-tcpPort", h2TcpPort).start();
    }

}
  • TCP サーバだけあればよいので、Web サーバは記述していません。
  • develop 環境でのみ起動します。

Tomcat を再起動します。

H2 Console から接続を試みてみます。「JDBC URL」に jdbc:h2:tcp://localhost:9092/mem:bootnpmgebdb と入力して「Connect」ボタンをクリックすると、

f:id:ksby:20170814072125p:plain

接続できました。scheme_version テーブルも表示されています。

f:id:ksby:20170814072245p:plain

再び Database Tools から H2 Database へ接続してみる

Database Tool Window から「Data Sources and Drivers」ダイアログを表示して、以下のように設定しますが。。。

f:id:ksby:20170815001348p:plain

  • 「URL」の右側のドロップダウンリストで「Remote」を選択した後、jdbc:h2:tcp://localhost:9092/mem:bootnpmgebdb を入力します。
  • 「User」に sa を入力します。
  • 「Test Connection」ボタンをクリックして “Successful” のメッセージが表示されることを確認します。

「Test Connection」は成功しているのに、ダイアログの下に「Error: [90067][90067]接続が壊れています: “unexpected status 256"」のメッセージが表示されます。「Database」のところに mem:bootnpmgebdb ではなく mem としか表示されていないからでしょうか。。。

「Schemas」タブをクリックすると何もスキーマが表示されていません。

f:id:ksby:20170815001746p:plain

「Data Sources and Drivers」ダイアログの設定をしばらく見ていたら「URL」の右側のドロップダウンリストに「URL only」という選択肢があることに気づきました。

f:id:ksby:20170815002414p:plain

「URL only」で設定すると今度は「URL」のところに赤縦線は表示されず「Test Connection」も成功しているのですが、「接続が壊れています」のメッセージが相変わらず表示されています。

f:id:ksby:20170815002557p:plain

原因がまるで分かりません。。。

Web でエラーメッセージからいろいろ調べてみたら以下のやり取りを見つけました。サーバとクライアントで H2 のバージョンが違うからではないか?、と書かれていますね。

確認してみます。Project Tool Window の「External Libraries」でサーバのバージョンを見ると 1.4.195 でした。

f:id:ksby:20170815003730p:plain

Database Tool Window から「Data Sources and Drivers」ダイアログを表示して H2 のクライアントのバージョンを見ると latest と表示されていますが、この表示だと使われているのは 1.4.192 でしょう。

f:id:ksby:20170815004126p:plain

確かに違いました。H2 のサーバのバージョンを 1.4.192 に変更して問題が解消されるか試してみます。build.gradle を以下のように変更します。

dependencies {
    ..........
    compile("org.codehaus.janino:janino")
    testCompile("org.springframework.boot:spring-boot-starter-test")
    ..........

    // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    compile("com.integralblue:log4jdbc-spring-boot-starter:1.0.1")
    compile("org.flywaydb:flyway-core:4.2.0")
    compile("com.h2database:h2:1.4.192")
    testCompile("org.dbunit:dbunit:2.5.3")
    ..........
}
  • compile("com.h2database:h2") を下に移動して compile("com.h2database:h2:1.4.192") に変更します。

変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

Tomcat を再起動した後、「Data Sources and Drivers」ダイアログを表示すると今度は「接続が壊れています」のメッセージが表示されていません。「Test Connection」も成功します。

f:id:ksby:20170815005353p:plain

「Schemas」タブをクリックするとスキーマ一覧も表示されています。「All schemas」をチェックします。

f:id:ksby:20170815071508p:plain

「OK」ボタンをクリックしてダイアログを閉じた後 Database Tool Window を見ると、今度は schema_version テーブルが表示されていました。接続できたようです。

f:id:ksby:20170815012438p:plain

履歴

2017/08/16
初版発行。
2017/08/17
* WebSecurityConfigurerAdapter クラスに追加した H2_CONSOLE_URI_PATTERN 定数に static final が抜けていたので追加した。
* H2DatabaseConfig クラスにクラスコメントが抜けていたので追加した。
2017/08/23
* WebSecurityConfig.javahttp.csrf().requireCsrfProtectionMatcher(...) を設定する場合、GET メソッド等で CSRF対策が実行されないようにする設定も追加する必要があったが、入れるのを忘れていたので追加した。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( 番外編 )( browser-sync + http-proxy-middleware で https の環境を構築する )

概要

記事一覧はこちらです。

Spring Boot で開発中に https を使用したい場合、keytool コマンドで key-store を作成して application.properties の server.ssl.* に設定する方法がありますが、browser-sync + http-proxy-middleware を使っても https の環境を構築できるのでそのメモ書きです。

browser-sync の https 機能を利用すれば key-store を作成する必要がないのが便利です。

bs-springboot-config.js を変更して https 環境を構築する

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その14 )( browser-sync –> Tomcat 連携してファイル変更時に自動リロードで反映される環境を構築してみる ) で作成した bs-springboot-config.js の設定を変更して https 環境を構築してみます。

bs-springboot-config.js の以下の点を変更します。

var httpProxyMiddleware = require('http-proxy-middleware');
var proxy = httpProxyMiddleware(
    [
        // /css, /js, /vendor と *.html は Tomcat に転送しない
        "!/css/**/*",
        "!/js/**/*",
        "!/vendor/**/*",
        "!/**/*.html",
        "/**/*"
    ],
    {
        target: "http://localhost:8080",
        xfwd: true
    }
);

..........
module.exports = {
    ..........
    "serveStatic": [],
    "https": true,
    "ghostMode": {
        "clicks": true,
        "scroll": true,
        "location": true,
        "forms": {
            "submit": true,
            "inputs": true,
            "toggles": true
        }
    },
    ..........
  • xfwd: true を追加します。Tomcat 転送時に X-Forward 系の HTTP Header が付加されるようになります。
  • "https": true を追加します。

これだけです。尚、"https": true を設定すると https しか受け付けなくなります。

動作確認してみます。Tomcagt を起動した後、npm run springboot コマンドを実行します。browser-sync が起動すると https で待ち受けていることが分かります。

f:id:ksby:20170813062828p:plain

IE から https://localhost:9080/inquiry/input/01/ にアクセスすると、自己証明書なので以下の画面が表示されます。「このサイトの閲覧を続行する (推奨されません)。」リンクをクリックします。

f:id:ksby:20170813063102p:plain

入力画面1が表示されます。自己証明書なのでアドレスバーが赤く表示されています。

f:id:ksby:20170813063256p:plain

「次へ」ボタンを押すと入力画面2へ遷移します。リダイレクトが行われていますが、プロトコルhttps)やドメイン名(localhost:9080)は引き継がれています。

f:id:ksby:20170813063437p:plain

ちなみに bs-springboot-config.js に xfwd: true を設定しないと入力画面1→入力画面2へ遷移しようとすると以下のようにプロトコルhttps)が引き継がれません。

f:id:ksby:20170813063742p:plain f:id:ksby:20170813063836p:plain

http を 80番ポートで、https を 443番ポートで待ち受ける環境を構築するには?

browser-sync の設定ファイルを2つ作って、2つ同時に起動すれば構築可能です。

先程の bs-springboot-config.js の設定変更を元に戻してから、以下のように変更します。

module.exports = {
    ..........
    "proxy": false,
    "port": 80,
    "middleware": false,
    ..........
  • "port": 9080"port": 80 に変更します。

bs-springboot-config.js をコピーして bs-springboot-https-config.js を作成し、以下の内容に変更します。

var httpProxyMiddleware = require('http-proxy-middleware');
var proxy = httpProxyMiddleware(
    [
        // /css, /js, /vendor と *.html は Tomcat に転送しない
        "!/css/**/*",
        "!/js/**/*",
        "!/vendor/**/*",
        "!/**/*.html",
        "/**/*"
    ],
    {
        target: "http://localhost:8080",
        xfwd: true
    }
);

/*
 |--------------------------------------------------------------------------
 | Browser-sync config file
 |--------------------------------------------------------------------------
 |
 | For up-to-date information about the options:
 |   http://www.browsersync.io/docs/options/
 |
 | There are more options than you see here, these are just the ones that are
 | set internally. See the website for more info.
 |
 |
 */
module.exports = {
    "ui": false,
    "files": [
        "./build/classes/**/*.class",
        "./build/classes/**/*.html",
        "./static/**/*",
        "./src/main/resources/static/**/*"
    ],
    "watchEvents": [
        "change"
    ],
    "watchOptions": {
        "ignoreInitial": true,
        "ignorePermissionErrors": true
    },
    "server": {
        "baseDir": [
            "./static",
            "./src/main/resources/static"
        ],
        "middleware": [proxy]
    },
    "proxy": false,
    "port": 443,
    "middleware": false,
    "serveStatic": [],
    "https": true,
    "ghostMode": {
        "clicks": true,
        "scroll": true,
        "location": true,
        "forms": {
            "submit": true,
            "inputs": true,
            "toggles": true
        }
    },
  • xfwd: true を追加します。
  • "ui": {"port": 3001, "weinre": {"port": 9081}}"ui": false に変更します。ui を起動しないようにします。
  • "port": 9080"port": 443 に変更します。
  • "https": true を追加します。
  • "open": "local""open": false に変更します。browser-sync 起動時にブラウザを起動しないようにします。

package.json を以下のように変更します。

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "postinstall": "run-s clean:static-dir copy:all",
    "clean:static-dir": "rimraf src/main/resources/static/*",
    "copy:all": "run-p copy:bootstrap copy:admin-lte copy:font-awesome copy:ionicons",
    "copy:bootstrap": "cpx node_modules/bootstrap/dist/**/* src/main/resources/static/vendor/bootstrap",
    "copy:admin-lte": "cpx node_modules/admin-lte/dist/**/* src/main/resources/static/vendor/admin-lte",
    "copy:font-awesome": "cpx node_modules/font-awesome/{css,fonts}/**/* src/main/resources/static/vendor/font-awesome",
    "copy:ionicons": "cpx node_modules/ionicons/dist/{css,fonts}/**/* src/main/resources/static/vendor/ionicons",
    "postcss:watch": "postcss src/main/assets/css/**/* -d src/main/resources/static/css -x .min.css -w --poll",
    "webpack": "webpack",
    "webpack:watch": "webpack --watch",
    "browser-sync": "browser-sync",
    "browser-sync:start": "browser-sync start --config bs-config.js",
    "browser-sync:springboot": "browser-sync start --config bs-springboot-config.js",
    "browser-sync:springboot:https": "browser-sync start --config bs-springboot-https-config.js",
    "server": "run-p postcss:watch webpack:watch browser-sync:start",
    "springboot": "run-p postcss:watch webpack:watch browser-sync:springboot browser-sync:springboot:https"
  },
  • "browser-sync:springboot:https": "browser-sync start --config bs-springboot-https-config.js" を追加します。
  • "springboot" の最後に browser-sync:springboot:https を追加します。

Tomcat を起動します。

コマンドプロンプトを「管理者として実行…」で起動した後、npm run springboot コマンドを実行します。http で 80番、https で 443番で待ち受けていることが表示されます。

f:id:ksby:20170813091832p:plain

ブラウザを起動して http://localhost/inquiry/input/01/ にアクセスして入力画面1を表示してから、「次へ」ボタンをクリックすると入力画面2が表示されます。URL は http://localhost/inquiry/input/02/ になっています。

f:id:ksby:20170813092005p:plain f:id:ksby:20170813092215p:plain

https://localhost/inquiry/input/01/ にアクセスして入力画面1を表示してから、「次へ」ボタンをクリックすると入力画面2が表示されます。URL は https://localhost/inquiry/input/02/ になっています。

f:id:ksby:20170813092329p:plain f:id:ksby:20170813092424p:plain

localhost ではなくドメイン名(www.test.co.jp)にしても問題ないか確認してみます。hosts ファイルに以下の定義を追加します。192.168.56.1 は自分の PC に割り当てられている IPアドレスです。

192.168.56.1    www.test.co.jp

http://www.test.co.jp/inquiry/input/01/ にアクセスして入力画面1を表示してから、「次へ」ボタンをクリックすると入力画面2が表示されます。URL は http://www.test.co.jp/inquiry/input/02/ になっています。

f:id:ksby:20170813093033p:plain f:id:ksby:20170813093123p:plain

https://www.test.co.jp/inquiry/input/01/ にアクセスして入力画面1を表示してから、「次へ」ボタンをクリックすると入力画面2が表示されます。URL は https://www.test.co.jp/inquiry/input/02/ になっています。

f:id:ksby:20170813093259p:plain f:id:ksby:20170813093343p:plain