かんがるーさんの日記

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

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
初版発行。