かんがるーさんの日記

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

Gradle で Multi-project を作成する ( その10 )( doma2lib+cmdapp+webapp編、sample-cmdapp プロジェクトを作成する2 )

概要

記事一覧はこちらです。

Gradle で Multi-project を作成する ( その9 )( doma2lib+cmdapp+webapp編、sample-cmdapp プロジェクトを作成する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Doma 2 の Entity、Dao を提供するライブラリ+Spring Boot ベースのコマンドラインアプリケーション+Spring Boot ベースの Web アプリケーションの Multi-project を作成します。
    • sample-cmdapp プロジェクトを作成します。前回からの続きで、今回はテストの作成、及びコマンドラインから実行した動作確認を行います。

参照したサイト・書籍

  1. Shortest way to get File object from resource in Groovy
    https://stackoverflow.com/questions/39245934/shortest-way-to-get-file-object-from-resource-in-groovy

  2. Spring start a transaction with object created by new
    https://stackoverflow.com/questions/4707193/spring-start-a-transaction-with-object-created-by-new

    • applicationContext.getAutowireCapableBeanFactory().applyBeanPostProcessorsAfterInitialization(...) で手動で Bean を登録する方法を参考にしました。

目次

  1. テストを作成する
  2. コマンドラインから実行して動作確認する
  3. 補足
    1. sample-cmdapp でも Flyway は動作している

手順

テストを作成する

最初にテストで利用する CSV ファイルを作成します。sample-cmdapp/src/test/resources/employee.csv を新規作成し、以下の内容を記述します。

"name","age","sex"
"高橋 蓮","15","男"
"渡辺 結月","24","女"

次に sample-cmdapp/src/main/java/ksbysample/app/samplecmdapp/EmployeeDataCsvToDbLoader.java のテストクラスを作成します。

@SpringBootTest アノテーションが付与されたクラスのテストメソッドを実行するとテストメソッド内の処理が実行される前に CommandLineRunner インターフェースを実装したクラスの run メソッドが実行されてしまうのですが、以下の方法で run メソッドが自動で実行されないようにします。

  • EmployeeDataCsvToDbLoader クラスに @ConditionalOnProperty(value = { "batch.execute" }, havingValue = "EmployeeDataCsvToDbLoader") アノテーションを付与して、batch.execute=EmployeeDataCsvToDbLoader が設定されない限り自動で Bean に登録されないようにします。
  • EmployeeDataCsvToDbLoader クラスのコンストラクタに渡すオブジェクト(Bean)は、テストクラスのフィールドに @Autowired アノテーションを付与して定義しておきます。
  • テストメソッドの最初で EmployeeDataCsvToDbLoader クラスのインスタンスを生成してから context.getAutowireCapableBeanFactory().applyBeanPostProcessorsAfterInitialization(...) を呼び出して手動で Bean に登録します。手動で登録すると CommandLineRunner#run メソッドは自動で実行されません。
f:id:ksby:20190501011141p:plainf:id:ksby:20190501011213p:plain

sample-cmdapp/src/test/groovy/ksbysample/app/samplecmdapp/EmployeeDataCsvToDbLoaderTest.groovy が新規作成されますので、以下の内容を記述します。

package ksbysample.app.samplecmdapp

import groovy.sql.Sql
import ksbysample.lib.doma2lib.dao.EmployeeDao
import org.modelmapper.ModelMapper
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.ApplicationContext
import spock.lang.Specification

import javax.sql.DataSource

// 通常は spring.profiles.active は IntelliJ IDEA の JUnit の Run/Debug Configuration と build.gradle に定義するが、
// 今回はテストが1つしかないので @SpringBootTest の properties 属性で指定する
@SpringBootTest(properties = ["spring.profiles.active=develop"])
class EmployeeDataCsvToDbLoaderTest extends Specification {

    @Autowired
    private ApplicationContext context

    @Autowired
    private DataSource dataSource

    @Autowired
    private final EmployeeDao employeeDao

    @Autowired
    private final ModelMapper modelMapper

    def sql

    void setup() {
        sql = new Sql(dataSource)
    }

    void cleanup() {
        sql.close()
    }

    def "EmployeeDataCsvToDbLoader.run メソッドを実行すると employee.csv のデータが employee テーブルに登録される"() {
        setup:
        // EmployeeDataCsvToDbLoader クラスは batch.execute=EmployeeDataCsvToDbLoader が指定されていないと Bean として
        // 登録されず run メソッドが自動で実行されない。テストでは手動で Bean に登録することで run メソッドが自動実行されることを
        // 回避する。
        EmployeeDataCsvToDbLoader employeeDataCsvToDbLoader =
                new EmployeeDataCsvToDbLoader(employeeDao, modelMapper)
        employeeDataCsvToDbLoader = (EmployeeDataCsvToDbLoader) context
                .getAutowireCapableBeanFactory()
                .applyBeanPostProcessorsAfterInitialization(employeeDataCsvToDbLoader, "employeeDataCsvToDbLoader")

        // src/test/resources の下の employee.csv の File オブジェクトを取得する
        def url = getClass().getResource("/employee.csv")
        def employeeCsvFile = new File(url.toURI())

        // employee テーブルからテストデータを削除する
        sql.execute("delete from employee where name in ('高橋 蓮', '渡辺 結月')")

        expect:
        employeeDataCsvToDbLoader.run("-csvfile=${employeeCsvFile.absolutePath}")
        def results = sql.rows("select name, age, sex from employee where name in ('高橋 蓮', '渡辺 結月')")
        results == [
                [name: "高橋 蓮", age: 15, sex: "男"],
                [name: "渡辺 結月", age: 24, sex: "女"]
        ]

        cleanup:
        sql.execute("delete from employee where name in ('高橋 蓮', '渡辺 結月')")
    }

}

テストを実行して成功することを確認します。

f:id:ksby:20190501013930p:plain

コマンドラインから実行して動作確認する

employee テーブルにテストデータが登録されていないことを確認した後、

f:id:ksby:20190501014632p:plain

sample-cmdapp-1.0.0-RELEASE.jar が生成されている D:\project-springboot\ksbysample-boot-miscellaneous\gradle-multiprj-doma2lib-cmdwebapp\sample-cmdapp\build\libs に移動してから java -Dspring.profiles.active=develop -Dbatch.execute=EmployeeDataCsvToDbLoader -jar sample-cmdapp-1.0.0-RELEASE.jar -csvfile=D:\project-springboot\ksbysample-boot-miscellaneous\gradle-multiprj-doma2lib-cmdwebapp\sample-cmdapp\src\test\resources\employee.csv コマンドを実行します。

f:id:ksby:20190501015315p:plain f:id:ksby:20190501015409p:plain

employee テーブルを確認するとテストデータが登録されています。

f:id:ksby:20190501015447p:plain

問題なさそうです。

最後に sample-cmdapp プロジェクトのディレクトリ構成を記載します。

f:id:ksby:20190501015848p:plain

sample-cmdapp-1.0.0-RELEASE.jar の中身を見てみると以下のようになっており doma2-lib-1.0.0-RELEASE.jar が lib ディレクトリの下に入っています。

f:id:ksby:20190501053130p:plain

補足

sample-cmdapp でも Flyway は動作している

現在の設定では sample-cmdapp を実行した時も Flyway は動作しています。確認してみます。

docker-compose down コマンドを実行後、docker/mysql/data の下をクリアします。

docker-compose.yml の flyway の設定をコメントアウトした後、

version: '3'

services:
  ..........

#  flyway:
#    image: boxfuse/flyway:${FLYWAY_VERSION}-alpine
#    container_name: flyway
#    environment:
#      - TZ=Asia/Tokyo
#    volumes:
#      - ./doma2-lib/src/main/resources/db/migration:/flyway/sql
#    command: -url=${FLYWAY_URL} -user=${FLYWAY_USER} -password=${FLYWAY_PASSWORD} -connectRetries=60 migrate
#    depends_on:
      - mysql
    # 下の3行は debug 用
    # うまく動かない時はコメントアウトを解除した後、
    # docker exec -it flyway /bin/sh
    # で接続してから
    # flyway <command に記述した文字列>
    # を実行してみる
    #
    # entrypoint: /bin/sh
    # stdin_open: true
    # tty: true

docker-compose up -d コマンドを実行します。

adminer にログインして sampledb を見るとテーブルは何もありませんが、

f:id:ksby:20190501021157p:plain

コマンドラインから java -Dspring.profiles.active=develop -Dbatch.execute=EmployeeDataCsvToDbLoader -jar sample-cmdapp-1.0.0-RELEASE.jar -csvfile=D:\project-springboot\ksbysample-boot-miscellaneous\gradle-multiprj-doma2lib-cmdwebapp\sample-cmdapp\src\test\resources\employee.csv を実行してから、

f:id:ksby:20190501021340p:plain f:id:ksby:20190501021435p:plain

sampledb を確認すると employee と flyway_schema_history の2つのテーブルが作成されており、

f:id:ksby:20190501021538p:plain

employee テーブルにも doma2-lib/src/main/resources/db/migration/V1__create_table.sql の2件と sample-cmdapp/src/test/resources/employee.csv の2件の合計4件のデータが登録されています。

f:id:ksby:20190501021718p:plain

履歴

2019/05/01
初版発行。

Gradle で Multi-project を作成する ( その9 )( doma2lib+cmdapp+webapp編、sample-cmdapp プロジェクトを作成する )

概要

記事一覧はこちらです。

Gradle で Multi-project を作成する ( その8 )( doma2lib+cmdapp+webapp編、doma2-lib プロジェクトを作成する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Doma 2 の Entity、Dao を提供するライブラリ+Spring Boot ベースのコマンドラインアプリケーション+Spring Boot ベースの Web アプリケーションの Multi-project を作成します。
    • 今回は sample-cmdapp プロジェクトを作成します。

参照したサイト・書籍

  1. univocity-parsers/src/test/java/com/univocity/parsers/examples/RoutineExamples.java
    https://github.com/uniVocity/univocity-parsers/blob/master/src/test/java/com/univocity/parsers/examples/RoutineExamples.java

目次

  1. Spring Initializr で sample-cmdapp プロジェクトを作成する
  2. sample-cmdapp の build.gradle を変更する
  3. gradle-multiprj-doma2lib-cmdwebapp の settings.gradle に include 'sample-cmdapp' を追加する
  4. gradle-multiprj-doma2lib-cmdwebapp の build.gradle の configure の適用対象に sample-cmdapp を追加する
  5. CSV ファイルを読み込んで DB のテーブルにデータを登録する機能を実装する
  6. 続く。。。

手順

Spring Initializr で sample-cmdapp プロジェクトを作成する

IntelliJ IDEA から Spring Initializr を利用して sample-cmdapp プロジェクトを作成します。

f:id:ksby:20190430184635p:plainf:id:ksby:20190430184741p:plain
f:id:ksby:20190430184829p:plainf:id:ksby:20190430184904p:plain

※dependencies のダイアログでは何もチェックしません。

f:id:ksby:20190430185000p:plain

作成後 IntelliJ IDEA のウィンドウが開きますが、何もせずに閉じます。

src ディレクトリと build.gradle だけ残して、それ以外のディレクトリファイルは全て削除します。

また sample-cmdapp/src/test/java/ksbysample/app/samplecmdapp/SampleCmdappApplicationTests.java も削除し、sample-cmdapp/src/test/java/ の下はクリアします。

sample-cmdapp の build.gradle を変更する

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

dependencies {
    implementation("org.springframework.boot:spring-boot-starter")

    implementation project(":doma2-lib")
    implementation("com.univocity:univocity-parsers:2.8.1")
    implementation("args4j:args4j:2.33")
    implementation("com.github.rozidan:modelmapper-spring-boot-starter:1.0.0")
}
  • doma2-lib プロジェクトへの依存関係と、CSV ファイルを処理するためのライブラリである univocity-parsers、コマンドラインオプションを処理するためのライブラリ args4j、ModelMapper を利用するためのライブラリ com.github.rozidan:modelmapper-spring-boot-starter への依存関係を追加します。

gradle-multiprj-doma2lib-cmdwebapp の settings.gradle に include 'sample-cmdapp' を追加する

settings.gradle に include 'sample-cmdapp' を追加します。

rootProject.name = 'gradle-multiprj-doma2lib-cmdwebapp'
include 'doma2-lib'
include 'sample-cmdapp'

gradle-multiprj-doma2lib-cmdwebapp の build.gradle の configure の適用対象に sample-cmdapp を追加する

gradle-multiprj-doma2lib-cmdwebapp の build.gradle の configure の適用対象に sample-cmdapp を追加します。

configure(subprojects.findAll { it.name ==~ /^(doma2-lib|sample-cmdapp)$/ }) {
    apply plugin: "org.springframework.boot"

    dependencyManagement {
        imports {
            mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
        }
    }

    ext {
        jdbcDriver = "mysql:mysql-connector-java:8.0.15"
        domaVersion = "2.24.0"
    }
    
    dependencies {
        testImplementation("org.springframework.boot:spring-boot-starter-test")

        runtimeOnly(jdbcDriver)
        implementation("org.seasar.doma.boot:doma-spring-boot-starter:1.1.1")
        implementation("org.seasar.doma:doma:${domaVersion}")
        implementation("org.flywaydb:flyway-core:5.2.4")
    }

}
  • /^(doma2-lib)$//^(doma2-lib|sample-cmdapp)$/ に変更します。

変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。Gradle Tool Window に sample-cmdapp が表示されます。

f:id:ksby:20190430190713p:plain

また Run/Debug Configuration の Spring Boot の所に doma2-lib と sample-cmdapp の2つの設定が追加されるので、IntelliJ IDEA の画面右下に「Run Dashboard」のダイアログが表示されます。「Show run configurations in Run Dashboard」リンクをクリックして Run Dashboard Tool Window を表示します。

f:id:ksby:20190430190448p:plain f:id:ksby:20190430190559p:plain

Doma2LibApplication の Run/Debug Configuration は不要なので削除します。

clean タスク実行 → Rebuild Project 実行 → build タスク実行を行い、警告・エラーが出ずに BUILD SUCCESSFUL が出力されることを確認します。

f:id:ksby:20190430192039p:plain

CSV ファイルを読み込んで DB のテーブルにデータを登録する機能を実装する

以下の仕様で実装します。

  • コマンドラインから java -jar -Dspring.profiles.active=<develop|product> -jar sample-cmdapp-1.0.0-RELEASE.jar -csvfile=<CSVファイルのパス> で実行します。
  • CSV ファイルは、
    • 文字コードUTF-8
    • 改行コードは CRLF
    • 1行目はヘッダ行。"name","age","sex"
    • 2行目以降はデータ行。
    • データは必ずダブルクォーテーションで囲む。
  • CSV ファイルのデータチェックは行いません。

doma2-lib プロジェクト内の ksbysample.lib パッケージの下も Component Scan の対象にするために sample-cmdapp/src/main/java/ksbysample/app/samplecmdapp/SampleCmdappApplication.java に @ComponentScan アノテーションを追加し、ksbysample.libksbysample.app の2つの package を指定します(ksbysample だけでも良いのですが複数指定するサンプルを作成したかったので2つ指定しています)。

package ksbysample.app.samplecmdapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.AutoConfigurationExcludeFilter;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.TypeExcludeFilter;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;

@SpringBootApplication
@ComponentScan(
        basePackages = {"ksbysample.lib", "ksbysample.app"},
        excludeFilters = {
                @ComponentScan.Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
                @ComponentScan.Filter(type = FilterType.CUSTOM,
                        classes = AutoConfigurationExcludeFilter.class)})
public class SampleCmdappApplication {

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

}

Lombok を使用したいので gradle-multiprj-doma2lib-cmdwebapp の build.gradle の configure の dependencies に追加します。

..........

configure(subprojects.findAll { it.name ==~ /^(doma2-lib|sample-cmdapp)$/ }) {
    apply plugin: "org.springframework.boot"

    dependencyManagement {
        imports {
            mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
        }
    }

    ext {
        jdbcDriver = "mysql:mysql-connector-java:8.0.15"
        domaVersion = "2.24.0"
    }
    
    dependencies {
        def lombokVersion = "1.18.6"

        testImplementation("org.springframework.boot:spring-boot-starter-test")

        runtimeOnly(jdbcDriver)
        implementation("org.seasar.doma.boot:doma-spring-boot-starter:1.1.1")
        implementation("org.seasar.doma:doma:${domaVersion}")
        implementation("org.flywaydb:flyway-core:5.2.4")

        // for lombok
        // testAnnotationProcessor、testCompileOnly を併記しなくてよいよう configurations で設定している
        annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
        compileOnly("org.projectlombok:lombok:${lombokVersion}")
    }

}
  • configure の dependencies に以下の3行を追加します。
    • def lombokVersion = "1.18.6"
    • annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
    • compileOnly("org.projectlombok:lombok:${lombokVersion}")

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

CSV ファイルの1レコードのデータをセットするための POJO クラスを実装します。Univocity Parsers で利用します。sample-cmdapp/src/main/java/ksbysample/app/samplecmdapp/EmployeeCsvRecord.java を新規作成し、以下の内容を記述します。

package ksbysample.app.samplecmdapp;

import com.univocity.parsers.annotations.Parsed;
import lombok.Data;

@Data
public class EmployeeCsvRecord {

    @Parsed(field = "name")
    private String name;

    @Parsed(field = "age")
    private Integer age;

    @Parsed(field = "sex")
    private String sex;

}

null のフィールドを insert 文に出力しないようにしたいので、doma2-lib/src/main/java/ksbysample/lib/doma2lib/dao/EmployeeDao.java の insert メソッドの @Insert アノテーションexcludeNull = true を追加します。この設定を行わないと insert 文で update_time のカラムに null がセットされて MySQL 側で日時をセットしてくれません。

@Dao
@ConfigAutowireable
public interface EmployeeDao {

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

    /**
     * @param entity
     * @return affected rows
     */
    @Insert(excludeNull = true)
    int insert(Employee entity);

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

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

CSV ファイルを読み込んで DB のテーブルにデータを登録する処理を実行するクラスを実装します。sample-cmdapp/src/main/java/ksbysample/app/samplecmdapp/EmployeeDataCsvToDbLoader.java を新規作成し、以下の内容を記述します。

package ksbysample.app.samplecmdapp;

import com.univocity.parsers.csv.CsvParserSettings;
import com.univocity.parsers.csv.CsvRoutines;
import ksbysample.lib.doma2lib.dao.EmployeeDao;
import ksbysample.lib.doma2lib.entity.Employee;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;
import org.modelmapper.ModelMapper;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.io.BufferedReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;

@Component
@ConditionalOnProperty(value = { "batch.execute" }, havingValue = "EmployeeDataCsvToDbLoader")
public class EmployeeDataCsvToDbLoader implements CommandLineRunner {

    @Option(name = "-csvfile", metaVar = "<path>", usage = "specifies a path to employee csv file")
    private String csvfile;

    private final EmployeeDao employeeDao;

    private final ModelMapper modelMapper;

    public EmployeeDataCsvToDbLoader(EmployeeDao employeeDao
            , ModelMapper modelMapper) {
        this.employeeDao = employeeDao;
        this.modelMapper = modelMapper;
    }

    @Override
    @Transactional
    public void run(String... args) throws Exception {
        // コマンドラインオプションを解析して @Option アノテーションを付加しているフィールドに値を設定する
        CmdLineParser cmdLineParser = new CmdLineParser(this);
        cmdLineParser.parseArgument(args);

        // Univocity Parses で CSV ファイルを読み込むための準備をする
        CsvParserSettings settings = new CsvParserSettings();
        settings.getFormat().setLineSeparator("\r\n");  // 改行コードは CRLF
        settings.setHeaderExtractionEnabled(true);      // ヘッダ行はスキップする
        CsvRoutines routines = new CsvRoutines(settings);

        // CSV ファイルを1行ずつ読み込み employee テーブルに insert する
        try (BufferedReader br = Files.newBufferedReader(Paths.get(this.csvfile), StandardCharsets.UTF_8)) {
            Employee employee = new Employee();
            for (EmployeeCsvRecord employeeCsvRecord : routines.iterate(EmployeeCsvRecord.class, br)) {
                modelMapper.map(employeeCsvRecord, employee);
                employeeDao.insert(employee);
            }
        }
    }

}

clean タスク実行 → Rebuild Project 実行 → build タスク実行を行い、警告・エラーが出ずに BUILD SUCCESSFUL が出力されることを確認します。

f:id:ksby:20190430221527p:plain

続く。。。

長くなったので一旦区切ります。次回は sample-cmdapp のテストの作成と、コマンドラインから実行した動作確認を行います。

履歴

2019/04/30
初版発行。

Gradle で Multi-project を作成する ( その8 )( doma2lib+cmdapp+webapp編、doma2-lib プロジェクトを作成する )

概要

記事一覧はこちらです。

Gradle で Multi-project を作成する ( その7 )( doma2lib+cmdapp+webapp編、Multi-project の設定ファイルと docker-compose.yml を作成する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Doma 2 の Entity、Dao を提供するライブラリ+Spring Boot ベースのコマンドラインアプリケーション+Spring Boot ベースの Web アプリケーションの Multi-project を作成します。
    • 今回は doma2-lib プロジェクトを作成します。

参照したサイト・書籍

  1. Spring Boot 2 Gradle plugin without executable jar
    https://stackoverflow.com/questions/49352837/spring-boot-2-gradle-plugin-without-executable-jar

  2. Spring Boot and multiple external configuration files
    https://stackoverflow.com/questions/25855795/spring-boot-and-multiple-external-configuration-files

  3. I am using a combination of @PropertySource and @ConfigurationProperties but I want to overwrite them with an external properties file
    https://stackoverflow.com/questions/47042237/i-am-using-a-combination-of-propertysource-and-configurationproperties-but-i-w

  4. Java11からMySQL8への接続時に起きた問題の解決
    https://qiita.com/talesleaves/items/d96ba7d74127799b1523

  5. MySQL 8.0は何が優れていて、どこに注意すべきか。データベース専門家が新機能を徹底解説
    https://employment.en-japan.com/engineerhub/entry/2018/09/18/110000

  6. Amazon RDS での MySQL
    https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/CHAP_MySQL.html

目次

  1. gradle-multiprj-doma2lib-cmdwebapp の build.gradle を変更する
  2. gradle-multiprj-doma2lib-cmdwebapp の settings.gradle に include 'doma2-lib' を追加する
  3. doma2-lib プロジェクトから不要なファイルを削除する
  4. doma2-lib の build.gradle を変更する
  5. ksbysample.lib.doma2lib パッケージを作成する
  6. domaGen タスクを実行して employee テーブルの Dao インターフェース、Entity クラスを生成する
  7. db-develop.properties、db-product.properties を作成する
  8. DataSourceConfig クラスを作成する
  9. EmployeeDao インターフェースのテストクラスを作成する
  10. clean タスク実行 → Rebuild Project 実行 → build タスク実行を行う
  11. メモ書き
    1. doma2-lib の build.gradle に bootJar { enabled = false }jar { enabled = true } を記述しないと build は失敗する

手順

gradle-multiprj-doma2lib-cmdwebapp の build.gradle を変更する

サブプロジェクト共通の設定を gradle-multiprj-doma2lib-cmdwebapp の build.gradle に記述します。

buildscript {
    repositories {
        mavenCentral()
        maven { url "https://plugins.gradle.org/m2/" }
        maven { url "https://repo.spring.io/release/" }
    }
    dependencies {
        classpath "io.spring.gradle:dependency-management-plugin:1.0.7.RELEASE"
        classpath "org.springframework.boot:spring-boot-gradle-plugin:2.1.4.RELEASE"
    }
}

allprojects {
    repositories {
        mavenCentral()
    }
}

subprojects {
    group "ksby.ksbysample-boot-miscellaneous"
    version "1.0.0-RELEASE"

    apply plugin: "java"
    apply plugin: "groovy"
    apply plugin: "io.spring.dependency-management"
    apply plugin: "idea"

    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11

    [compileJava, compileTestGroovy, compileTestJava]*.options*.encoding = "UTF-8"
    [compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = ["-Xlint:all,-options,-processing,-path"]

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

    configurations {
        // annotationProcessor と testAnnotationProcessor、compileOnly と testCompileOnly を併記不要にする
        testAnnotationProcessor.extendsFrom annotationProcessor
        testImplementation.extendsFrom compileOnly
    }

    dependencyManagement {
        imports {
            mavenBom("org.junit:junit-bom:5.4.2")
        }
    }

    dependencies {
        def assertjVersion = "3.12.2"
        def spockVersion = "1.3-groovy-2.5"

        // for Spock
        testImplementation("org.spockframework:spock-core:${spockVersion}")
        testImplementation("org.spockframework:spock-spring:${spockVersion}")

        // for JUnit 5 + AssertJ
        // junit-jupiter で junit-jupiter-api, junit-jupiter-params, junit-jupiter-engine の3つが依存関係に追加される
        testCompile("org.junit.jupiter:junit-jupiter")
        testRuntime("org.junit.platform:junit-platform-launcher")
        testImplementation("org.assertj:assertj-core:${assertjVersion}")
    }

    def jvmArgsDefault = [
            "-ea",
            "-Dfile.encoding=UTF-8",
            "-Dsun.nio.cs.map=x-windows-iso2022jp/ISO-2022-JP"
    ]
    def jvmArgsAddOpens = [
            "--add-opens=java.base/java.io=ALL-UNNAMED",
            "--add-opens=java.base/java.lang=ALL-UNNAMED",
            "--add-opens=java.base/java.lang.invoke=ALL-UNNAMED",
            "--add-opens=java.base/java.lang.ref=ALL-UNNAMED",
            "--add-opens=java.base/java.lang.reflect=ALL-UNNAMED",
            "--add-opens=java.base/java.net=ALL-UNNAMED",
            "--add-opens=java.base/java.security=ALL-UNNAMED",
            "--add-opens=java.base/java.util=ALL-UNNAMED"
    ]
    def printTestCount = { desc, result ->
        if (!desc.parent) { // will match the outermost suite
            println "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)"
        }
    }

    task testJUnit4AndSpock(type: Test) {
        jvmArgs = jvmArgsDefault +
                jvmArgsAddOpens

        testLogging {
            afterSuite printTestCount
        }
    }
    test.dependsOn testJUnit4AndSpock
    test {
        jvmArgs = jvmArgsDefault +
                jvmArgsAddOpens

        // for JUnit 5
        useJUnitPlatform()

        testLogging {
            afterSuite printTestCount
        }
    }
}

configure(subprojects.findAll { it.name ==~ /^(doma2-lib)$/ }) {
    apply plugin: "org.springframework.boot"

    dependencyManagement {
        imports {
            mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
        }
    }

    ext {
        jdbcDriver = "mysql:mysql-connector-java:8.0.15"
        domaVersion = "2.24.0"
    }
    
    dependencies {
        testImplementation("org.springframework.boot:spring-boot-starter-test")

        runtimeOnly(jdbcDriver)
        implementation("org.seasar.doma.boot:doma-spring-boot-starter:1.1.1")
        implementation("org.seasar.doma:doma:${domaVersion}")
        implementation("org.flywaydb:flyway-core:5.2.4")
    }

}
  • 今回作成するサブプロジェクトは全て Spring ベースで作成するので subprojects { ... } の中に設定を記述してもよいのですが、Spring ベースでないサブプロジェクトを作成することも考慮して Spring ベースの設定は configure(subprojects.findAll { it.name ==~ /^(...)$/ }) { ... } に記述します。
  • org.seasar.doma.boot:doma-spring-boot-starterorg.seasar.doma:doma:2.16.1 に依存していたので、Doma 2 の最新バージョンである 2.24.0 が入るように別途指定します。

gradle-multiprj-doma2lib-cmdwebapp の settings.gradle に include 'doma2-lib' を追加する

gradle-multiprj-doma2lib-cmdwebapp の settings.gradle に include 'doma2-lib' を追加します。

rootProject.name = 'gradle-multiprj-doma2lib-cmdwebapp'
include 'doma2-lib'

doma2-lib プロジェクトから不要なファイルを削除する

以下のファイルは不要なので削除します。

  • doma2-lib/src/main/java/ksbysample/lib/doma2lib/Doma2LibApplication.java
  • doma2-lib/src/main/resources/application.properties
  • doma2-lib/src/test/java/ksbysample/lib/doma2lib/Doma2LibApplicationTests.java
  • doma2-lib/src/test/java の下のディレクトリはクリアします。

doma2-lib の build.gradle を変更する

doma2-lib の build.gradle に記述します。

// for Doma 2
// JavaクラスとSQLファイルの出力先ディレクトリを同じにする
processResources.destinationDir = compileJava.destinationDir
// コンパイルより前にSQLファイルを出力先ディレクトリにコピーするために依存関係を逆転する
compileJava.dependsOn processResources

configurations {
    // for Doma 2
    domaGenRuntime
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter")

    annotationProcessor("org.seasar.doma:doma:${domaVersion}")
    domaGenRuntime("org.seasar.doma:doma-gen:${domaVersion}")
    domaGenRuntime("${jdbcDriver}")
}

bootJar {
    enabled = false
}
jar {
    enabled = true
}

// for Doma-Gen
task domaGen {
    doLast {
        // まず変更が必要なもの
        def rootPackageName = "ksbysample.lib.doma2lib"
        def daoPackagePath = "src/main/java/ksbysample/lib/doma2lib/dao"
        def dbUrl = "jdbc:mysql://localhost/sampledb"
        def dbUser = "sampledb_user"
        def dbPassword = "xxxxxxxx"
        def tableNamePattern = "employee"
//        def tableNamePattern = ".*"
        // おそらく変更不要なもの
        def importOfComponentAndAutowiredDomaConfig = "org.seasar.doma.boot.ConfigAutowireable"
        def workDirPath = "work"
        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()
        }

        // 生成された Dao インターフェースを作業用ディレクトリにコピーし、
        // @ComponentAndAutowiredDomaConfig アノテーションを付加する
        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@ConfigAutowireable")
            }
        }

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

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

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

void clearDir(String dirPath) {
    delete dirPath
}
  • Spring Boot 関連のライブラリに依存するライブラリ系のプロジェクトで build を成功させるために bootJar { enabled = false }jar { enabled = true } を記述します。

更新後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。Gradle Tool Window に doma2-lib が表示されます。

f:id:ksby:20190430112132p:plain

ksbysample.lib.doma2lib パッケージを作成する

doma2-lib/src/main/java の下に ksbysample.lib.doma2lib パッケージを作成します。

domaGen タスクを実行して employee テーブルの Dao インターフェース、Entity クラスを生成する

Gradle Tool Window から domaGen タスクを実行します。

f:id:ksby:20190430112643p:plain

f:id:ksby:20190430113027p:plain (.....途中省略.....) f:id:ksby:20190430113139p:plain

[ant:gen] Loading class com.mysql.jdbc.Driver. This is deprecated. The new driver class is com.mysql.cj.jdbc.Driver. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary. というメッセージが出力されますが、domaframework/doma-gen を見ると mysql の場合は com.mysql.jdbc.Driver を使用するようになっていました。JDBC Driver を別途指定できればいいのですが、方法が分かりませんでした。現時点では deprecated で使用できない訳ではないので、このまま進めることにします。

下の画像の赤文字のファイルが生成されます。

f:id:ksby:20190430113442p:plain

■ksbysample.lib.doma2lib.dao.EmployeeDao インターフェース

package ksbysample.lib.doma2lib.dao;

import ksbysample.lib.doma2lib.entity.Employee;
import org.seasar.doma.boot.ConfigAutowireable;
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
@ConfigAutowireable
public interface EmployeeDao {

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

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

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

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

■ksbysample.lib.doma2lib.entity.Employee クラス

package ksbysample.lib.doma2lib.entity;

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 = "employee")
public class Employee {

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

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

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

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

    /**  */
    @Column(name = "update_time")
    LocalDateTime updateTime;

    ..........

}

db-develop.properties、db-product.properties を作成する

src/main/resources の下に db-develop.properties、db-product.properties を新規作成し、以下の内容を記述します。DB の設定はこれらのファイルに記述し、sample-cmdapp、sample-webapp プロジェクト内では設定しません。

■db-develop.properties

doma.dialect=mysql

spring.datasource.hikari.jdbc-url=jdbc:mysql://localhost/sampledb
spring.datasource.hikari.username=sampledb_user
spring.datasource.hikari.password=xxxxxxxx
spring.datasource.hikari.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.leak-detection-threshold=60000
spring.datasource.hikari.register-mbeans=true

■db-product.properties

# 何も記述しないと git に commit できないので、コメントアウトした行を記述する

最後に動作確認する時に spring.profiles.active に指定する値を変更することで適用する設定ファイルを db-develop.properties か db-product.properties かを切り替えることが出来ることを確認するために2つファイルを用意しますが、今は db-product.properties には何も設定しません。

DataSourceConfig クラスを作成する

doma2-lib/src/main/java/ksbysample/lib/doma2lib の下に config パッケージを作成した後、その下に DataSourceConfig.java を新規作成し以下の内容を記述します。

package ksbysample.lib.doma2lib.config;

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jmx.export.MBeanExporter;

import javax.sql.DataSource;

@Configuration
@PropertySource(value = "classpath:db-${spring.profiles.active}.properties")
public class DataSourceConfig {

    private final MBeanExporter mbeanExporter;

    public DataSourceConfig(@Autowired(required = false) MBeanExporter mbeanExporter) {
        this.mbeanExporter = mbeanExporter;
    }

    @Bean
    @ConfigurationProperties("spring.datasource.hikari")
    public DataSource dataSource() {
        if (mbeanExporter != null) {
            mbeanExporter.addExcludedBean("dataSource");
        }
        return DataSourceBuilder.create()
                .type(HikariDataSource.class)
                .build();
    }

}

EmployeeDao インターフェースのテストクラスを作成する

@SpringBootApplication アノテーションを付与したクラスが1つもないと @SpringBootTest アノテーションを付与したテストが実行できないので、doma2-lib/src/test/main の下に ksbysample/lib/doma2lib パッケージを作成して TestApplication.java を新規作成し、以下の内容を記述します。生成する jar ファイルに TestApplication クラスを含めないようにするために src/test/main の下に作成しています。

package ksbysample.lib.doma2lib;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class TestApplication {

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

}

EmployeeDao インターフェースのテストクラスを Spock で作成します。

f:id:ksby:20190430131552p:plainf:id:ksby:20190430131625p:plain

doma2-lib/src/test/groovy/ksbysample/lib/doma2lib/dao/EmployeeDaoTest.groovy が新規作成されますので、以下の内容を記述します。

package ksbysample.lib.doma2lib.dao

import groovy.sql.Sql
import ksbysample.lib.doma2lib.entity.Employee
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import spock.lang.Specification
import spock.lang.Unroll

import javax.sql.DataSource

// 通常は spring.profiles.active は IntelliJ IDEA の JUnit の Run/Debug Configuration と build.gradle に定義するが、
// 今回はテストが1つしかないので @SpringBootTest の properties 属性で指定する
@SpringBootTest(properties = ["spring.profiles.active=develop"])
class EmployeeDaoTest extends Specification {

    static final String TESTDATA_NAME = "木村 太郎"

    @Autowired
    EmployeeDao employeeDao

    @Autowired
    private DataSource dataSource

    def sql

    void setup() {
        sql = new Sql(dataSource)
    }

    void cleanup() {
        sql.close()
    }

    @Unroll
    def "selectById メソッドのテスト(#id --> #name, #age, #sex)"() {
        setup:
        def result = employeeDao.selectById(id)

        expect:
        result.name == name
        result.age == age
        result.sex == sex

        where:
        id || name    | age | sex
        1  || "田中 太郎" | 20  | "男"
        2  || "鈴木 花子" | 18  | "女"
    }

    def "insert メソッドのテスト"() {
        setup:
        sql.execute("delete from employee where name = ${TESTDATA_NAME}")
        Employee employee = new Employee(id: null, name: "${TESTDATA_NAME}", age: 35, sex: "男", updateTime: null)
        employeeDao.insert(employee)

        expect:
        def result = sql.firstRow("select * from employee where name = ${TESTDATA_NAME}")
        result.name == TESTDATA_NAME
        result.age == 35
        result.sex == "男"

        cleanup:
        sql.execute("delete from employee where name = ${TESTDATA_NAME}")
    }

}

テストを実行して成功することを確認します。

f:id:ksby:20190430133242p:plain

clean タスク実行 → Rebuild Project 実行 → build タスク実行を行う

clean タスク実行 → Rebuild Project 実行 → build タスク実行を行い、エラーなしで doma2-lib-1.0.0-RELEASE.jar が生成されることを確認します。

f:id:ksby:20190430141210p:plain (.....途中省略.....) f:id:ksby:20190430141306p:plain

最後に BUILD SUCCESSFUL のメッセージは出力されましたが、途中で javax.net.ssl.SSLException: closing inbound before receiving peer's close_notify のメッセージが何度も出力されました。

原因を Web で検索してみたところ Java11からMySQL8への接続時に起きた問題の解決 という記事がありました。JDK 11 のバグらしいです。

JDBC の URL に sslMode=DISABLED を付けて SSL を使用しなければメッセージが出ないようにできるそうなので、今回はそれで対応します。また HikariCP を使用していると文字化けするという情報も記載されていたので characterEncoding=utf8 も付けることにします。

doma2-lib/src/main/resources/db-develop.properties を以下のように変更します。

doma.dialect=mysql

spring.datasource.hikari.jdbc-url=jdbc:mysql://localhost/sampledb?sslMode=DISABLED&characterEncoding=utf8
spring.datasource.hikari.username=sampledb_user
spring.datasource.hikari.password=xxxxxxxx
spring.datasource.hikari.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.leak-detection-threshold=60000
spring.datasource.hikari.register-mbeans=true

再度 clean タスク実行 → Rebuild Project 実行 → build タスク実行を行うと、今度は警告・エラーのメッセージは表示されずに BUILD SUCCESSFUL のメッセージが出力されました。

f:id:ksby:20190430142001p:plain

doma2-lib/build/libs の下に doma2-lib-1.0.0-RELEASE.jar が生成されています。現在のディレクトリ構成は以下のようになります。

f:id:ksby:20190430142333p:plain f:id:ksby:20190430142425p:plain

doma2-lib-1.0.0-RELEASE.jar の中身を見てみると以下のようになっています。

f:id:ksby:20190430142618p:plain

ここまでの感想ですが JDK 11+MySQL 8 の組み合わせはまだ安定していないようですね。。。 本番で使うなら MySQL は 5.7 を選択した方が良さそうです。

メモ書き

doma2-lib の build.gradle に bootJar { enabled = false }jar { enabled = true } を記述しないと build は失敗する

doma2-lib の build.gradle には bootJar { enabled = false }jar { enabled = true } を記述しましたが、これらを記述しないと bootJar タスクで実行可能 Jar を生成しようとしても Main class が存在しないため Main class name has not been configured and it could not be resolved のエラーメッセージが表示されて build タスクが失敗します。

f:id:ksby:20190430154132p:plain

履歴

2019/04/30
初版発行。

Gradle で Multi-project を作成する ( その7 )( doma2lib+cmdapp+webapp編、Multi-project の設定ファイルと docker-compose.yml を作成する )

概要

記事一覧はこちらです。

Gradle で Multi-project を作成する ( その6 )( Multi-project は settings.gradle に include を書くだけでもよいのでは? ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Doma 2 の Entity、Dao を提供するライブラリ+Spring Boot ベースのコマンドラインアプリケーション+Spring Boot ベースの Web アプリケーションの Multi-project を作成します。
    • Web とバッチで Doma 2 の Entity と Dao を共有したい場合を想定してみました。
    • 6~8回程度で書ければいいかな。。。

参照したサイト・書籍

  1. dockerhub - mysql https://hub.docker.com/_/mysql

  2. dockerhub - adminer https://hub.docker.com/_/adminer/

  3. MySQL8.0ではGRANT構文でユーザを作成できない
    https://www7390uo.sakura.ne.jp/wordpress/archives/456

  4. MySQL Innovation Day Tokyo で MySQL 8 の文字コードについて話した
    https://tmtms.hatenablog.com/entry/201805/mysql-innovation-day-tokyo

  5. ユーザーを作成する(CREATE USER文)
    https://www.dbonline.jp/mysql/user/index1.html

  6. MySQL 8.0 の AUTO_INCREMENT について
    https://www.s-style.co.jp/blog/2018/08/2284/

  7. MySQL入門】CREATE TABLE文でテーブルを作成する方法
    https://www.sejuku.net/blog/82708

  8. MySQL data types
    http://zetcode.com/databases/mysqltutorial/datatypes/

目次

  1. 方針
  2. gradle-multiprj-doma2lib-cmdwebapp ディレクトリ作成+Gradle Wrapper コピー+Gradle 5.4.1 バージョンアップ+gradlew init
  3. IntelliJ IDEA で gradle-multiprj-doma2lib-cmdwebapp プロジェクトをオープンする
  4. Spring Initializr で doma2-lib プロジェクトを作成する
  5. MySQL と Flyway を起動するための docmer-compose.yml を作成した後、サーバを起動してデータベースとテーブルを作成する

手順

方針

  • 以下のディレクトリ構成(必要最低限のディレクトリ・ファイルのみ記述しています)の Multi-project を作成します。
gradle-multiprj-doma2lib-cmdwebapp
├ doma2-lib    <-- Doma 2 の Entity、Dao を提供するライブラリの Project
│ ├ src
│ │ ├ main
│ │ │ └ java
│ │ │   └ ksbysample
│ │ │     └ lib
│ │ │       └ doma2lib
│ │ │         ├ dao
│ │ │         └ entity
│ │ └ resources
│ │   ├ db
│ │   │ ├ init
│ │   │ │ └ create_database.sql
│ │   │ └ migration
│ │   │   └ V1__create_table.sql    <-- Flyway 用 SQL ファイル
│ │   ├ db-develop.properties    <-- develop profile 用 DB 設定ファイル
│ │   └ db-product.properties    <-- product profile 用 DB 設定ファイル 
│ └ build.gradle
├ sample-cmdapp    <-- Spring Boot ベースのコマンドラインアプリケーションの Project
│ ├ src
│ │ ├ main
│ │ │ └ java
│ │ │   └ ksbysample
│ │ │     └ app
│ │ │       └ samplecmdapp
│ │ └ resources
│ └ build.gradle
├ sample-webapp    <-- Spring Boot ベースの Web アプリケーションの Project
│ ├ src
│ │ ├ main
│ │ │ └ java
│ │ │   └ ksbysample
│ │ │     └ app
│ │ │       └ samplewebapp
│ │ └ resources
│ └ build.gradle
├ build.gradle
└ settings.gradle
  • 今回は全てのプロジェクトを Spring Initializr で作成します。
  • develop と product の2つの Profile を作成・使用します。
  • テスティングフレームワークは全ての Project で JUnit 5 と Spock を使用できるようにします。
  • checkstyle, spotbugs, pmd, error-prone は導入しません。
  • Spring Boot ベースの Web アプリケーションでは 8080番ポートを使用します。
  • Spring Boot ベースのアプリケーションの build.gradle の共通の設定は各サブプロジェクトの build.gradle ではなく gradle-multiprj-doma2lib-cmdwebapp の build.gradle に記述します。
  • DB は MySQL のバージョン 8 を docker-compose で起動して利用します(これまで使ったことがなかったので)。
  • DB へのデータ投入には Flyway を使用します。Flyway に必要なファイルは doma2-lib プロジェクトの下に配置します。
  • コマンドラインアプリケーションでは CSV ファイルを読み込んで DB のテーブルにデータを登録し、Web アプリケーションでは DB のテーブルに登録されたデータを取得して表示します。
  • 最初に doma2-lib-1.0.0-RELEASE.jar を sample-cmdapp-1.0.0-RELEASE.jar、sample-webapp-1.0.0-RELEASE.jar の中に入れる方式(JarLauncher による起動)で一通り書いてから、doma2-lib-1.0.0-RELEASE.jar を外に出す方式(PropertiesLauncher による起動)を書くつもりです。
  • draw.io で全体図を描いてみました。

f:id:ksby:20190422070428p:plain

gradle-multiprj-doma2lib-cmdwebapp ディレクトリ作成+Gradle Wrapper コピー+Gradle 5.4.1 バージョンアップ+gradlew init

ksby/ksbysample-boot-miscellaneous の repository を checkout している D:\project-springboot\ksbysample-boot-miscellaneous の下に gradle-multiprj-doma2lib-cmdwebapp ディレクトリを作成します。

f:id:ksby:20190429010538p:plain

gradle-multiprj-lib-webapp2 プロジェクトから Gradle Wrapper のファイルをコピーします。

f:id:ksby:20190429010921p:plain

コマンドプロンプトから gradlew wrapper --gradle-version=5.4.1gradlew --versiongradlew wrapper コマンドを実行して Gradle を 5.4 → 5.4.1 へバージョンアップします。

f:id:ksby:20190429011306p:plain

gradle/wrapper/gradle-wrapper.properties を見ると 5.4.1 になっています。

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

gradlew init コマンドを実行します。選択肢は 1: basic1: groovy を選択し、Project name は何も入力せずに Enter キーを押します。

f:id:ksby:20190429011701p:plain f:id:ksby:20190429011756p:plain

IntelliJ IDEA で gradle-multiprj-doma2lib-cmdwebapp プロジェクトをオープンする

gradle-multiprj-doma2lib-cmdwebapp プロジェクトをオープンします。

f:id:ksby:20190429140849p:plain

f:id:ksby:20190429141235p:plainf:id:ksby:20190429141319p:plain

Spring Initializr で doma2-lib プロジェクトを作成する

Flyway で使用する SQL ファイルを doma2-lib プロジェクトの下に配置するので、最初に IntelliJ IDEA から Spring Initializr を利用して doma2-lib プロジェクトを作成しておきます。

f:id:ksby:20190429194516p:plainf:id:ksby:20190429194722p:plain
f:id:ksby:20190429194808p:plainf:id:ksby:20190429194858p:plain

※dependencies のダイアログでは何もチェックしません。

f:id:ksby:20190429195017p:plain

作成後 IntelliJ IDEA のウィンドウが開きますが、何もせずに閉じます。

src ディレクトリと build.gradle だけ残して、それ以外のディレクトリファイルは全て削除します。gradle-multiprj-doma2lib-cmdwebapp プロジェクトの settings.gradle にはまだ include 文は追加しません。

MySQL と Flyway を起動するための docmer-compose.yml を作成した後、サーバを起動してデータベースとテーブルを作成する

MySQL のデータファイルをローカルディレクトリに保存するために docker/mysql/data を作成します。

doma2-lib/src/main/resources の下に db/init、db/migration の2つのディレクトリを作成します。

doma2-lib/src/main/resources/db/init の下に create_database.sql を新規作成し、以下の内容を記述します。

create database if not exists sampledb character set utf8mb4 collate utf8mb4_ja_0900_as_cs_ks;

create user 'sampledb_user'@'%' identified by 'xxxxxxxx';
grant all privileges ON sampledb.* to 'sampledb_user'@'%' with grant option;
flush privileges;

doma2-lib/src/main/resources/db/migration の下に V1__create_table.sql を新規作成し、以下の内容を記述します。

create table employee (
    id int(11) not null auto_increment,
    name varchar(128) not null,
    age int(3) not null,
    sex enum('男', '女'),
    update_time datetime default current_timestamp,
    primary key (id)
);
insert into employee (name, age, sex) values ('田中 太郎', 20, '男');
insert into employee (name, age, sex) values ('鈴木 花子', 18, '女');

ルートディレクトリの下に .env を新規作成し、以下の内容を記述します。

MYSQL_VERSION=8.0.16
ADMINER_VERSION=4.7.1

FLYWAY_VERSION=5.2.4
FLYWAY_URL=jdbc:mysql://mysql/sampledb
FLYWAY_USER=sampledb_user
FLYWAY_PASSWORD=xxxxxxxx

ルートディレクトリの下に docker-compose.yml を新規作成し、以下の内容を記述します。

version: '3'

services:
  # 起動したコンテナに /bin/sh でアクセスする場合には以下のコマンドを実行する
  # docker exec -it mysql bash
  mysql:
    image: mysql:${MYSQL_VERSION}
    container_name: mysql
    command: --default-authentication-plugin=mysql_native_password
    restart: always
    ports:
      - 3306:3306
    environment:
      - TZ=Asia/Tokyo
      - MYSQL_ROOT_PASSWORD=xxxxxxxx
    volumes:
      - ./docker/mysql/data:/var/lib/mysql
      - ./doma2-lib/src/main/resources/db/init/create_database.sql:/docker-entrypoint-initdb.d/create_database.sql

  # URL
  # http://localhost:9080/
  adminer:
    image: adminer:${ADMINER_VERSION}
    container_name: adminer
    restart: always
    ports:
      - 9080:8080
    environment:
      - TZ=Asia/Tokyo
      - ADMINER_DEFAULT_SERVER=mysql

  flyway:
    image: boxfuse/flyway:${FLYWAY_VERSION}-alpine
    container_name: flyway
    environment:
      - TZ=Asia/Tokyo
    volumes:
      - ./doma2-lib/src/main/resources/db/migration:/flyway/sql
    command: -url=${FLYWAY_URL} -user=${FLYWAY_USER} -password=${FLYWAY_PASSWORD} -connectRetries=60 migrate
    depends_on:
      - mysql
    # 下の3行は debug 用
    # うまく動かない時はコメントアウトを解除した後、
    # docker exec -it flyway /bin/sh
    # で接続してから
    # flyway <command に記述した文字列>
    # を実行してみる
    #
    # entrypoint: /bin/sh
    # stdin_open: true
    # tty: true

これで必要なファイルを一通り作成しましたので、コマンドラインから docker-compose up -d コマンドを実行します。

f:id:ksby:20190429221827p:plain f:id:ksby:20190429222726p:plain

http://localhost:9080/ にアクセスして sampledb が作成されていること、emplayee テーブルにデータが登録されていることを確認します。

f:id:ksby:20190429222025p:plain f:id:ksby:20190430135340p:plain f:id:ksby:20190430111405p:plain f:id:ksby:20190430111441p:plain

履歴

2019/04/29
初版発行。
2019/04/30
* 「参照したサイト・書籍」に MySQL data types を追加した。
* V1__create_table.sql の create table employee 文に age, sex, update_time のカラムを追加し、投入するデータを日本語のものに変更した。
* create database の collate を utf8mb4_0900_as_cs → utf8mb4_ja_0900_as_cs_ks に変更した。

Gradle で Multi-project を作成する ( 番外編 )( Spring Actuator を利用してアプリ起動時にメールサーバに接続できない場合には起動を中断させる )

概要

記事一覧はこちらです。

本編とは全く関係ありません。。。

何となく思いついたことで Spring Actuator を入れると DB サーバやメールサーバの UP/DOWN を検知できますが、起動時にサーバに接続できなかったら起動を中断させることができたりするのかな?、と思って調べた時の内容です。

結論としては HealthChecker の実装に依存しますが、メールサーバは可能でした。

参照したサイト・書籍

  1. Programmatically shut down Spring Boot application
    https://stackoverflow.com/questions/22944144/programmatically-shut-down-spring-boot-application

目次

  1. 動作確認のための demo プロジェクトを作成する
  2. Spring Actuator でメールサーバをチェックするよう設定する
  3. メールサーバを起動せずに demo アプリを起動してみる
  4. 起動時にメールサーバに接続できなかったら起動を中断するための CustomMailHealthIndicator クラスを作成する
  5. 動作確認

手順

動作確認のための demo プロジェクトを作成する

IntelliJ IDEA から Spring Initializr を利用して demo プロジェクトを作成します。

f:id:ksby:20190426070614p:plainf:id:ksby:20190426070709p:plain
f:id:ksby:20190426070819p:plainf:id:ksby:20190426070854p:plain
f:id:ksby:20190426070931p:plainf:id:ksby:20190426071002p:plain

※DevTools、Web、Mail、Actuator をチェックします。

f:id:ksby:20190426071139p:plainf:id:ksby:20190426071356p:plain

IntelliJ IDEA のメイン画面が表示されたら、lombok@Slf4j アノテーションを使用したいので build.gradle の dependencies に lombok の依存関係を追加します。

plugins {
    id 'org.springframework.boot' version '2.1.4.RELEASE'
    id 'java'
}

apply plugin: 'io.spring.dependency-management'

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
    // annotationProcessor と testAnnotationProcessor、compileOnly と testCompileOnly を併記不要にする
    testAnnotationProcessor.extendsFrom annotationProcessor
    testImplementation.extendsFrom compileOnly
}

repositories {
    mavenCentral()
}

dependencies {
    def lombokVersion = "1.18.6"

    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-mail'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    runtimeOnly 'org.springframework.boot:spring-boot-devtools'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    // for lombok
    // testAnnotationProcessor、testCompileOnly を併記しなくてよいよう configurations で設定している
    annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
    compileOnly("org.projectlombok:lombok:${lombokVersion}")
}

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

Spring Actuator でメールサーバをチェックするよう設定する

Spring Actuator でメールサーバの health チェックを行うために application.properties に spring.mail.host=localhost の設定を追加します。

spring.mail.host=localhost

メールサーバを起動せずに demo アプリを起動してみる

メールサーバを起動しない状態で demo アプリを起動すると、Spring Actuator の MailHealthIndicator クラスから警告ログが出力されますがアプリは終了せず起動したままです。

f:id:ksby:20190426072646p:plain f:id:ksby:20190426072804p:plain

Health チェックの画面では mail が黄色アイコンで表示されています。

f:id:ksby:20190426072941p:plain

起動時にメールサーバに接続できなかったら起動を中断するための CustomMailHealthIndicator クラスを作成する

警告ログを出力している org.springframework.boot.actuate.mail.MailHealthIndicator クラスを見ると以下のように実装されています。

f:id:ksby:20190426080800p:plain

実際に health チェックしているのが doHealthCheck メソッドで、this.mailSender.testConnection(); で接続してみて例外が throw されなければ builder.up(); を呼び出してステータスを UP に変更する、という実装でした。

MailHealthIndicator クラスには @Component 等のアノテーションが付与されていないので Bean を定義しているクラスを探したところ、org.springframework.boot.actuate.autoconfigure.mail.MailHealthIndicatorAutoConfiguration クラスで定義されていました。

f:id:ksby:20190427015400p:plain

mailHealthIndicator という名前の Bean が存在しない時だけ MailHealthIndicatorAutoConfiguration クラスで Bean を生成しています。

MailHealthIndicator クラスを継承して CustomMailHealthIndicator クラスを作成し、一番最初の health チェックで例外が throw されたら Web アプリを強制終了させるようにしてみます。src/main/java/com/example/demo/CustomMailHealthIndicator.java クラスを新規作成した後、以下の内容を記述します。

package com.example.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ExitCodeGenerator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.mail.MailHealthIndicator;
import org.springframework.context.ApplicationContext;
import org.springframework.mail.javamail.JavaMailSenderImpl;

import java.util.concurrent.atomic.AtomicBoolean;

@Slf4j
@Component("mailHealthIndicator")
public class CustomMailHealthIndicator extends MailHealthIndicator {

    private final ApplicationContext context;

    private final AtomicBoolean isFirstTime = new AtomicBoolean(true);

    public CustomMailHealthIndicator(JavaMailSenderImpl mailSender, ApplicationContext context) {
        super(mailSender);
        this.context = context;
    }

    @Override
    protected void doHealthCheck(Health.Builder builder) throws Exception {
        try {
            super.doHealthCheck(builder);
        } catch (Exception e) {
            // 一番最初の health チェックでメールサーバに接続できなかった時にはアプリを強制終了させる
            if (isFirstTime.get()) {
                log.error("メールサーバが起動していないので強制終了します", e);
                int exitCode = SpringApplication.exit(context, (ExitCodeGenerator) () -> 1);
                System.exit(exitCode);
            }
            throw e;
        } finally {
            isFirstTime.compareAndExchange(true, false);
        }
    }

}

動作確認

先程と同様にメールサーバを起動しない状態で demo アプリを起動すると、最後に Process finished with exit code 1 のメッセージが出力されてアプリが終了しました。

f:id:ksby:20190427080959p:plain f:id:ksby:20190427081114p:plain

メールサーバ(smtp4dev)を起動してから demo アプリを起動すると、アプリは終了せずに起動したままとなり、

f:id:ksby:20190427081344p:plain

メールサーバを終了させると、health チェックのエラーログが出力されますがアプリは終了せずに起動したままでした。

f:id:ksby:20190427081532p:plain

想定通りの動きです。

履歴

2019/04/27
初版発行。

Gradle で Multi-project を作成する ( その6 )( Multi-project は settings.gradle に include を書くだけでもよいのでは? )

概要

記事一覧はこちらです。

Gradle で Multi-project を作成する ( その5 )( lib+webappx2編、Spring Boot ベースの Web アプリケーション(スタブ)のプロジェクトを作成する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 先に進める前に、Gradle の Multi-project は Gradle の project を1つのディレクトリの下に集めて、親ディレクトリに setting.gradle を作成して、その中に include 文を書けばそれで Multi-project として扱われるのではないだろうか。。。と思ったので試してみます。

参照したサイト・書籍

目次

  1. 2つ Multi-project を作成した上での考察。。。include だけでもいいのでは?
  2. gradle-multiprj-includeonly ディレクトリを作成する
  3. Gradle Wrapper のファイル一式をコピーする
  4. gradlew init コマンドを実行する
  5. IntelliJ IDEA で gradle-multiprj-includeonly プロジェクトを開く
  6. IntelliJ IDEA で sample-lib プロジェクトを新規作成する
  7. setting.gradle に include 'sample-lib' を追加する
  8. sample-lib プロジェクトに StrNumUtils クラスを追加する
  9. IntelliJ IDEA で sample-webapp プロジェクトを新規作成する
  10. setting.gradle に include 'sample-webapp' を追加する
  11. sample-webapp プロジェクトに SampleController クラスを追加する
  12. 動作確認
  13. まとめ

手順

2つ Multi-project を作成した上での考察。。。include だけでもいいのでは?

ここまで2つ Multi-project を作成してみましたが、そこまで凝る必要がなければ setting.gradle に include 文を記述するだけで十分な気がするんですよね。。。

たぶんこんな感じで作っても Multi-project になる気がします。

  • ルートディレクトリの直下に build.gradle を作成してサブプロジェクトの共通の設定を書けば確かに便利だが、実はルートディレクトリ直下には build.gradle はなくてもよい(サブプロジェクト毎に別々に build.gradle があるだけでOK)。
  • ルートディレクトリの下に作成したサブプロジェクトの中のディレクトリ・ファイルはそのままでもよい(何も削除する必要はない)。
  • ルートディレクトリ直下の setting.gradle に include '<サブプロジェクト名>' を記述すれば Multi-project になる。

試してみます。以下のような作り方でも Multi-project になるはずです。

  1. プロジェクトのルートディレクトリを作成する。
  2. Gradle Wrapper のファイル一式をコピーする。
  3. gradlew init コマンドを実行する。
  4. IntelliJ IDEA でプロジェクトを開く。
  5. 後は以下の作業の繰り返し。
    1. IntelliJ IDEA で Gradle プロジェクトか、Spring Boot のプロジェクト(Gradle 版)を作成する。
    2. ルートディレクトリ直下の setting.gradle に include '<サブプロジェクト名>' を記述する。
    3. Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新する。

gradle-multiprj-includeonly ディレクトリを作成する

D:\project-springboot\ksbysample-boot-miscellaneous の下に gradle-multiprj-includeonly ディレクトリを作成します。

f:id:ksby:20190423224300p:plain

Gradle Wrapper のファイル一式をコピーする

gradle-multiprj-lib-webapp2 プロジェクトから Gradle Wrapper のファイル一式をコピーします。

f:id:ksby:20190423224611p:plain

gradlew init コマンドを実行する

コマンドラインから gradlew init コマンドを実行します。

f:id:ksby:20190423224821p:plain

gradle-multiprj-includeonly ディレクトリ内に setting.gradle、build.gradle が作成されて、

f:id:ksby:20190423224912p:plain

setting.gradle には以下の内容が記述されています。コメントの部分は削除します。

/*
 * This file was generated by the Gradle 'init' task.
 *
 * The settings file is used to specify which projects to include in your build.
 *
 * Detailed information about configuring a multi-project build in Gradle can be found
 * in the user manual at https://docs.gradle.org/5.4/userguide/multi_project_builds.html
 */

rootProject.name = 'gradle-multiprj-includeonly'

build.gradle は以下の内容が記述されています。今回このファイルは何も変更しません。

/*
 * This file was generated by the Gradle 'init' task.
 *
 * This is a general purpose Gradle build.
 * Learn how to create Gradle builds at https://guides.gradle.org/creating-new-gradle-builds/
 */

IntelliJ IDEA で gradle-multiprj-includeonly プロジェクトを開く

IntelliJ IDEA で gradle-multiprj-includeonly プロジェクトを開きます。Project Tool Window と Gradle Tool Window は以下のように表示されます。

f:id:ksby:20190423225829p:plain f:id:ksby:20190423225925p:plain

IntelliJ IDEA で sample-lib プロジェクトを新規作成する

gradle-multiprj-includeonly ディレクトリの下に sample-lib プロジェクトを新規作成します。Gradle プロジェクトとして作成します。

f:id:ksby:20190423230209p:plain f:id:ksby:20190423230330p:plain

ディレクトリ・ファイルは以下のようになります。今回はこのディレクトリの中は何も削除しません(Gradle Wrapper も残したままです)。

f:id:ksby:20190423230522p:plain

setting.gradle に include 'sample-lib' を追加する

setting.gradle に include 'sample-lib' を追加します。

rootProject.name = 'gradle-multiprj-includeonly'
include 'sample-lib'

Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新すると、Gradle Tool Window に sample-lib プロジェクトが表示されます。

f:id:ksby:20190423231307p:plain

sample-lib プロジェクトに StrNumUtils クラスを追加する

sample-lib/src/main/java の下に ksbysample.lib.samplelib パッケージを作成した後、StrNumUtils.java を新規作成して以下の内容を記述します。

package ksbysample.lib.samplelib;

public class StrNumUtils {

    public static String plus(String v1, String v2) {
        return String.valueOf(Integer.parseInt(v1) + Integer.parseInt(v2));
    }

}

IntelliJ IDEA で sample-webapp プロジェクトを新規作成する

gradle-multiprj-includeonly ディレクトリの下に sample-webapp プロジェクトを新規作成します。Spring Initializr で作成します。

f:id:ksby:20190424001107p:plain f:id:ksby:20190424001246p:plain f:id:ksby:20190424001356p:plain f:id:ksby:20190424001725p:plain

ディレクトリ・ファイルは以下のようになります。このディレクトリの中は何も削除しません。

f:id:ksby:20190424002027p:plain

setting.gradle に include 'sample-webapp' を追加する

setting.gradle に include 'sample-webapp' を追加します。

rootProject.name = 'gradle-multiprj-includeonly'
include 'sample-lib'
include 'sample-webapp'

Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新すると、Gradle Tool Window に sample-webapp プロジェクトが表示されます。

f:id:ksby:20190424002334p:plain

sample-webapp プロジェクトに SampleController クラスを追加する

sample-webapp プロジェクトの build.gradle に sample-lib プロジェクトへの依存関係を追加します。

plugins {
    id 'org.springframework.boot' version '2.1.4.RELEASE'
    id 'java'
}

apply plugin: 'io.spring.dependency-management'

group = 'ksby.ksbysample-boot-miscellaneous'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    runtimeOnly 'org.springframework.boot:spring-boot-devtools'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    implementation project(":sample-lib")
}
  • dependencies block に implementation project(":sample-lib") を追加します。

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

sample-webapp/src/main/java/ksbysample/webapp/samplewebapp/SampleController.java を新規作成して以下の内容を記述します。

package ksbysample.webapp.samplewebapp;

import ksbysample.lib.samplelib.StrNumUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/sample")
public class SampleController {

    @RequestMapping
    @ResponseBody
    public String index() {
        return StrNumUtils.plus("1", "2");
    }

}

動作確認

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

f:id:ksby:20190424074417p:plain

Run/Debug Configuration に SampleWebappApplication が自動で登録されているので、ここから Tomcat を起動して、

f:id:ksby:20190424074557p:plain f:id:ksby:20190424074735p:plain

http://localhost:8080/sample にアクセスすると 3 と表示されます。

f:id:ksby:20190424074836p:plain

Project Tool Window で見ると以下のディレクトリ構成になっており、sample-lib-1.0-SNAPSHOT.jar、sample-webapp-0.0.1-SNAPSHOT.jar が生成されています。

f:id:ksby:20190424075014p:plain f:id:ksby:20190424075137p:plain

コマンドプロンプトから java -jar sample-webapp-0.0.1-SNAPSHOT.jar コマンドで Tomcat を起動してから、

f:id:ksby:20190424075452p:plain

http://localhost:8080/sample にアクセスしても 3 と表示されます。

f:id:ksby:20190424075540p:plain

まとめ

setting.gradle に include 文を書くだけでも Multi-project とみなされますね。プロジェクトが1つでも最初から Multi-project のディレクトリ・ファイル構成で作っておいて、開発途中にちょっとしたスタブが欲しい時にはサブプロジェクトを作成して setting.gradle に include を追加して対応する、というのもありなのかもしれません。

履歴

2019/04/24
初版発行。

Gradle で Multi-project を作成する ( その5 )( lib+webappx2編、Spring Boot ベースの Web アプリケーション(スタブ)のプロジェクトを作成する )

概要

記事一覧はこちらです。

Gradle で Multi-project を作成する ( その4 )( lib+webappx2編、Multi-project の設定ファイルと Spring Boot ベースの Web アプリケーション(メイン)のプロジェクトを作成する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Gradle で Spring を使用しないライブラリ+Spring Boot ベースの Web アプリケーション x 2(メインとスタブ)の Multi-project を作成します。
    • sample-stubapp プロジェクト(こちらが sample-lib の StrNumUtils クラスを呼び出します)を作成した後、sample-webapp の SampleController クラスを本実装して Multi-project を完成させます。

参照したサイト・書籍

  1. Spring @WebMvcTest with Spock Framework
    https://allegro.tech/2018/04/Spring-WebMvcTest-with-Spock.html

  2. Spring Boot RestTemplate POST JSON Example
    https://howtodoinjava.com/spring-boot2/resttemplate-post-json-example/

目次

  1. Spring Boot ベースの Web アプリケーション(スタブ)のプロジェクトを作成する
    1. IntelliJ IDEA で sample-stubapp プロジェクトを作成する
    2. sample-stubapp プロジェクトから不要なファイルを削除する
    3. sample-stubapp プロジェクトの build.gradle を変更する
    4. gradle-multiprj-lib-webapp2 プロジェクトの build.gradle を変更する
    5. settings.gradle に sample-stubapp プロジェクトの include 文を追加する
    6. application.properties に server.port=9080 を追加する
    7. StubWebapiController クラスを新規作成する
  2. sample-webapp の SampleController クラスを本実装する
  3. clean タスク実行 → Rebuild Project 実行 → build タスク実行を行う
  4. Run Dashboard から sample-webapp、sample-stubapp を起動して動作確認する
  5. sample-webapp、sample-stubapp を jar ファイルから起動して動作確認する
  6. まとめ

手順

Spring Boot ベースの Web アプリケーション(スタブ)のプロジェクトを作成する

IntelliJ IDEA で sample-stubapp プロジェクトを作成する

IntelliJ IDEA から Spring Initializr を利用して Spring Boot ベースの Web アプリケーションのプロジェクトを作成します。

f:id:ksby:20190421192134p:plain f:id:ksby:20190421192259p:plain f:id:ksby:20190421192417p:plain f:id:ksby:20190421192450p:plain ※DevTools と Web の2つをチェックします。

f:id:ksby:20190421192551p:plain f:id:ksby:20190421192629p:plain

作成後 IntelliJ IDEA のウィンドウが開きますが、何もせずに閉じます。

sample-stubapp プロジェクトから不要なファイルを削除する

D:\project-springboot\ksbysample-boot-miscellaneous\gradle-multiprj-lib-webapp2\sample-stubapp\ の下には以下のディレクトリ・ファイルがありますが、

f:id:ksby:20190421192923p:plain

src ディレクトリと build.gradle 以外を削除します。削除すると以下の状態になります。

f:id:ksby:20190421193047p:plain

sample-stubapp プロジェクトの build.gradle を変更する

sample-stubapp プロジェクトの build.gradle を以下の内容に変更します。必要な設定は gradle-multiprj-lib-webapp2 の build.gradle に記述したので、このファイルには sample-lib への依存関係だけ記述します。

dependencies {
    implementation project(":sample-lib")
}

gradle-multiprj-lib-webapp2 プロジェクトの build.gradle を変更する

gradle-multiprj-lib-webapp2 プロジェクトの build.gradle を以下のように変更します。

..........

configure(subprojects.findAll { it.name ==~ /^(sample-webapp|sample-stubapp)$/ }) {
    ..........
}
  • it.name ==~ /^(sample-webapp)$/it.name ==~ /^(sample-webapp|sample-stubapp)$/ に変更します。

settings.gradle に sample-stubapp プロジェクトの include 文を追加する

D:\project-springboot\ksbysample-boot-miscellaneous\gradle-multiprj-lib-webapp2 の下の settings.gradle に include 'sample-stubapp' を追加します。

rootProject.name = 'gradle-multiprj-lib-webapp2'
include 'sample-lib'
include 'sample-webapp'
include 'sample-stubapp'

追加後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新すると sample-stubapp が追加されます。

f:id:ksby:20190421194121p:plain

また IntelliJ IDEA の画面右下に「Run Dashboard」のダイアログが表示されます。「Show run configurations Run Dashboard」リンクをクリックすると Run Dashboard Tool Window が表示されます。

f:id:ksby:20190421202534p:plain f:id:ksby:20190421202743p:plain

※ちなみに Run Dashboard Tool Window は Run/Debug Configurations で Spring Boot に2つ以上設定を追加すれば表示させることができます。

f:id:ksby:20190421203153p:plain

clean タスク実行 → Rebuild Project 実行 → build タスク実行を行うと、BUILD SUCCESSFUL のメッセージが出力されました。

f:id:ksby:20190421204259p:plain f:id:ksby:20190421204421p:plain

Project Tool Window を見ると以下のディレクトリ構成になっています。sample-stubapp/build/libs の下に sample-stubapp-1.0.0-RELEASE.jar が生成されています。

f:id:ksby:20190421204810p:plain

application.properties に server.port=9080 を追加する

sample-stubapp/src/main/resources/application.properties に server.port=9080 を追加します。

server.port=9080

StubWebapiController クラスを新規作成する

以下の仕様の StubWebapiController クラスを作成します。

  • URL は /plus にする。
  • HTTP メソッドは POST のみ有効にする。
  • データは JSON で渡す。{ "v1": "<数値文字列>", "v2": "<数値文字列>" } のフォーマットする。
  • レスポンスは { "result": "<数値文字列>" } のフォーマットで返す。
  • エラー処理は考慮しない。

sample-stubapp/src/main/java/ksbysample/webapp/samplestubapp の下に PlusForm、PlusResponse、StubWebapiController クラスを新規作成し、以下の内容を記述します。

■PlusForm

package ksbysample.webapp.samplestubapp;

public class PlusForm {

    private String v1;

    private String v2;

    public String getV1() {
        return v1;
    }

    public void setV1(String v1) {
        this.v1 = v1;
    }

    public String getV2() {
        return v2;
    }

    public void setV2(String v2) {
        this.v2 = v2;
    }

}

■PlusResponse

package ksbysample.webapp.samplestubapp;

public class PlusResponse {

    private String result;

    PlusResponse() {
    }

    PlusResponse(String result) {
        this.result = result;
    }

    public String getResult() {
        return result;
    }

    public void setResult(String result) {
        this.result = result;
    }

}

■StubWebapiController

package ksbysample.webapp.samplestubapp;

import ksbysample.lib.samplelib.StrNumUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class StubWebapiController {

    @PostMapping("/plus")
    public PlusResponse plus(@RequestBody PlusForm plusForm) {
        String result = StrNumUtils.plus(plusForm.getV1(), plusForm.getV2());
        PlusResponse plusResponse = new PlusResponse(result);
        return plusResponse;
    }

}

テストを作成します。今回は Spock で作成します。

f:id:ksby:20190421210227p:plain f:id:ksby:20190421210259p:plain

sample-stubapp/src/test/groovy/ksbysample/webapp/samplestubapp/StubWebapiControllerTest.groovy が新規作成されますので、以下の内容を記述します。

package ksbysample.webapp.samplestubapp

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import spock.lang.Specification
import spock.lang.Unroll

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*

@SpringBootTest
@AutoConfigureMockMvc
class StubWebapiControllerTest extends Specification {

    @Autowired
    private MockMvc mvc

    @Unroll
    def "plus WebAPI のテスト(#v1, #v2 --> #result)"() {
        expect:
        mvc.perform(post("/plus")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content("""
                                    {
                                      "v1": ${v1},
                                      "v2": ${v2}
                                    }
                                """))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))
                .andExpect(jsonPath('$.result').value(result))

        where:
        v1    | v2  || result
        "1"   | "2" || "3"
        "999" | "1" || "1000"
    }

}

テストを実行すると成功しました。画面左側で WARNING が出ているのは IntelliJ IDEA の JUnit の Run/Debug Configuration に JDK 11 のオプションを 設定していないためです。

f:id:ksby:20190421221830p:plain

sample-stubapp/src/test/java/ksbysample/webapp/samplestubapp/SampleStubappApplicationTests.java は不要なので削除します。

sample-webapp の SampleController クラスを本実装する

sample-webapp/src/main/java/ksbysample/webapp/samplewebapp/SampleWebappApplication.java を以下のように変更します。

package ksbysample.webapp.samplewebapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
public class SampleWebappApplication {

    private final RestTemplateBuilder restTemplateBuilder;

    public SampleWebappApplication(RestTemplateBuilder restTemplateBuilder) {
        this.restTemplateBuilder = restTemplateBuilder;
    }

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

    @Bean
    public RestTemplate restTemplate() {
        return this.restTemplateBuilder
                .rootUri("http://localhost:9080")
                .build();
    }

}
  • private final RestTemplateBuilder restTemplateBuilder; とコンストラクタインジェクションの処理を追加します。
  • @Bean public RestTemplate restTemplate() { ... } を追加します。

sample-stubapp/src/main/java/ksbysample/webapp/samplestubapp の下の PlusForm、PlusResponse クラスを sample-webapp/src/main/java/ksbysample/webapp/samplewebapp の下にコピーします。

sample-webapp/src/main/java/ksbysample.webapp.samplewebapp.SampleController.java を以下の内容に変更します。

package ksbysample.webapp.samplewebapp;

import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.client.RestTemplate;

@Controller
@RequestMapping("/sample")
public class SampleController {

    private final RestTemplate restTemplate;

    public SampleController(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @RequestMapping
    @ResponseBody
    public String index() {
        PlusForm plusForm = new PlusForm();
        plusForm.setV1("1");
        plusForm.setV2("2");

        HttpHeaders httpHeaders = new HttpHeaders();
        HttpEntity<PlusForm> request = new HttpEntity<>(plusForm, httpHeaders);

        ResponseEntity<PlusResponse> response =
                restTemplate.postForEntity("/plus", request, PlusResponse.class);

        return response.getBody().getResult();
    }

}

テストを作成します。こちらも Spock で作成します。

f:id:ksby:20190421233800p:plain f:id:ksby:20190421233830p:plain

sample-webapp/src/test/groovy/ksbysample/webapp/samplewebapp/SampleControllerTest.groovy が新規作成されますので、以下の内容を記述します。

package ksbysample.webapp.samplewebapp

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.HttpMethod
import org.springframework.http.MediaType
import org.springframework.test.annotation.DirtiesContext
import org.springframework.test.web.client.MockRestServiceServer
import org.springframework.test.web.servlet.MockMvc
import org.springframework.web.client.RestTemplate
import spock.lang.Specification

import static org.springframework.test.web.client.match.MockRestRequestMatchers.method
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status

@SpringBootTest
@AutoConfigureMockMvc
@DirtiesContext
class SampleControllerTest extends Specification {

    @Autowired
    private MockMvc mvc

    @Autowired
    RestTemplate restTemplate

    def "/sample にアクセスすると 3 と表示される"() {
        setup:
        MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build()
        mockServer.expect(requestTo("http://localhost:9080/plus"))
                .andExpect(method(HttpMethod.POST))
                .andRespond(withSuccess('{"result": "3"}', MediaType.APPLICATION_JSON_UTF8))

        expect:
        mvc.perform(get("/sample"))
                .andExpect(status().isOk())
                .andExpect(content().string("3"))
    }

}

テストを実行すると成功しました。

f:id:ksby:20190422000502p:plain

sample-webapp/src/test/java/ksbysample/webapp/samplewebapp/SampleWebappApplicationTests.java は不要なので削除します。

clean タスク実行 → Rebuild Project 実行 → build タスク実行を行う

全ての実装が完了したので build タスクが正常に終了するか確認します。clean タスク実行 → Rebuild Project 実行 → build タスク実行を行うと、BUILD SUCCESSFUL のメッセージが出力されました。

f:id:ksby:20190422001946p:plain f:id:ksby:20190422002051p:plain

Run Dashboard から sample-webapp、sample-stubapp を起動して動作確認する

Run Dashboard から sample-webapp、sample-stubapp を起動します。Run Dashboard から起動すると使用されているポート番号が表示されるのが分かりやすくていいですね。

f:id:ksby:20190422002338p:plain f:id:ksby:20190422002450p:plain

ブラウザから http://localhost:8080/sample にアクセスすると画面上に "3" の文字が表示されました。

f:id:ksby:20190422002645p:plain

確認後、sample-webapp、sample-stubapp を停止します。

sample-webapp、sample-stubapp を jar ファイルから起動して動作確認する

今度は生成された jar ファイルから起動して確認してみます。コマンドプロンプトから jar ファイルが生成されている build/libs ディレクトリに移動した後、java -jar sample-webapp-1.0.0-RELEASE.jarjava -jar sample-stubapp-1.0.0-RELEASE.jar コマンドを実行します。

f:id:ksby:20190422003050p:plain

ブラウザから http://localhost:8080/sample にアクセスすると画面上に "3" の文字が表示されました。

f:id:ksby:20190422003131p:plain

確認後、sample-webapp、sample-stubapp を停止します。

まとめ

  • サブプロジェクトに Spring Boot ベースの Web アプリケーションを2つ以上作成する場合、共通の設定はプロジェクトのルートディレクトリの build.gradle にまとめられます。
  • Spring Boot ベースのサブプロジェクトと Spring Boot ベースではないサブプロジェクトが混在していて、Spring Boot ベースのサブプロジェクトの設定だけを プロジェクトのルートディレクトリの build.gradle にまとめる場合には configure(subprojects.findAll { it.name ==~ /^(sample-webapp|sample-stubapp)$/ }) { ... } のように記述して設定を適用するサブプロジェクトを指定できます。
  • IntelliJ IDEA の Run Dashboard を表示させる方法がよく分かっていなかったのですが、Run/Debug Configurations で Spring Boot の下に2つ以上設定を追加すれば自動で現れることが分かりました。
  • 簡単なサンプルを書いていたつもりでしたが、テストまで書くと意外に辛いですね。。。 書き方が分からなくて調べながらやっていて結構時間がかかりました。

履歴

2019/04/22
初版発行。