kagamihogeの日記

kagamihogeの日記です。

gradle initの複数subproject構成を見る(6.8.3)

動機

gradle勉強のためgradle initが生成する複数のsubproject構成を見る。基本的には https://docs.gradle.org/6.8.3/samples/sample_building_java_applications_multi_project.html と書いてる事は変わらない。

複数のsubprojectを作る場合はネットをぐぐるallprojectssubprojectsを使う方法が多く出てくる。対してgradle initプラグインによる設定共有でこれを実現している。なのでその辺を学ぶのが目的。

環境

6.8.3

ソースコード

まずsettings.gradleを見る。

rootProject.name = 'sample'
include('app', 'list', 'utilities')

各プロジェクト間の関係は後々それぞれのbuild.gradleで見るが以下になっている。

app -> utilities -> list

次にbuildSrc/src/main/groovyを見る。ここには、このプロジェクト内で使用するgradleのプラグインが配置されている。プラグインに共通設定を記述し、各プロジェクトからそのプラグインを使用する、という構成となっている。

まずは各プラグインファイルの中身を見る。

sample.java-common-conventions.gradle

plugins {
    id 'java'
}

repositories {
    jcenter()
}

dependencies {
    constraints {
        implementation 'org.apache.commons:commons-text:1.9'
    }
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.2'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
}

tasks.named('test') {
    useJUnitPlatform()
}

sample.java-application-conventions.gradle

plugins {
    id 'sample.java-common-conventions'
    id 'application'
}

sample.java-library-conventions.gradle

plugins {
    id 'sample.java-common-conventions'
    id 'java-library'
}

プラグイン自体にも参照関係があり以下になっている。これも後に各プロジェクトのbuild.gradleで見るが、どのプロジェクトがどのプラグイン使うのかはファイル名からなんとなく予想がつく。

application -+
|-common
ilbrary-----+

commonは全プラグインの共通設定がある。id 'java'とかjcenter()とかどのプロジェクトでも使用するdependenciesとかそのあたり。

applicationはmainのあるプロジェクトを想定しているので、commonにプラスでid 'application'を含む。

libraryapplicationとは対照的にcommonプラスid 'java-library'を含む。

buildSrc/build.gradleには以下の記述がある。

plugins {
    id 'groovy-gradle-plugin'
}

repositories {
    gradlePluginPortal()
}

id 'groovy-gradle-plugin'はconvention pluginsというもので、名前の通り、上記で見たプラグインファイルを命名規則に従って自動的に登録してくれる。なのでbuild.gradleに適切なプラグイン名さえ書けばビルド時によしなにやってくれる。

というわけで各プロジェクトのbuild.gradleを見ていく。ポイントはplugins {...}のところ。

app

plugins {
    id 'sample.java-application-conventions'
}

dependencies {
    implementation 'org.apache.commons:commons-text'
    implementation project(':utilities')
}

application {
    // Define the main class for the application.
    mainClass = 'sample.app.App'
}

utilities

plugins {
    id 'sample.java-library-conventions'
}

dependencies {
    api project(':list')
}

list

plugins {
    id 'sample.java-library-conventions'
}

appはmainを置くアプリケーション本体なのでそれ用の設定が書かれたプラグインid 'sample.java-application-conventions'を使用する。

utilities, listは被使用側のプロジェクトなのでプラグインid 'sample.java-library-conventions'を使用する。

まとめ

といった感じで、gradle initが生成するプロジェクトは、そのプロジェクト用のプラグインを作成してそこに共通設定を記述、そして各プロジェクトからそれを参照、という作りになっている。

なお、更に複雑なプロジェクトの場合は https://docs.gradle.org/6.8.3/userguide/structuring_software_products.html という方法もあるらしい。

なんにしてもプロジェクトの複雑度に応じて方法を使い分けてね、という事なんだと思う。

EnvironmentPostProcessorでEnvironment初期化処理をカスタマイズ

spring-bootのEnvironmentPostProcessorを実装する事でEnvironmentの初期化処理に後処理を追加可能になる。これでプロパティの初期化をカスタマイズできる。大抵のケースでExternalized Configurationを利用すればプロパティは問題無いが、それでは機能不足する場合などに利用する。

以下の例では実行時に現在時刻(EnvironmentPostProcessor実行時点)のプロパティを追加する。

plugins {
    id 'org.springframework.boot' version '2.4.3'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

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

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
    useJUnitPlatform()
}
import java.time.LocalDateTime;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.logging.DeferredLogFactory;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;

@Order(Ordered.LOWEST_PRECEDENCE)
public class MyEnvPostProcessor implements EnvironmentPostProcessor {

    public MyEnvPostProcessor(DeferredLogFactory factory, Log log, ConfigurableBootstrapContext context) {
        log.info("log MyEnvPostProcessor");
    }

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        PropertySource<?> property = new MapPropertySource("myEnvPost", Map.of("current.local-date-time", LocalDateTime.now().toString()));
        environment.getPropertySources().addLast(property);
    }

}

current.local-date-timeという名前のプロパティを追加している。

@Orderは任意だが用途を考えると基本的には低優先度を設定すると思われる。

また、任意でコンストラクタにはDeferredLogFactory, org.apache.commons.logging.Log, ConfigurableBootstrapContextを指定できる。

次にこの実装クラスをMETA-INF/spring.factoriesで登録する。src/main/resources/META-INF/spring.factoriesを作成する。

org.springframework.boot.env.EnvironmentPostProcessor=kagaimhoge.sample.MyEnvPostProcessor

最後に動作確認用のMainクラス。

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Main implements CommandLineRunner {

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

    @Value("${current.local-date-time}")
    String now;

    @Override
    public void run(String... args) throws Exception {
        System.out.println(now);
    }
}

Testcontainers + Spring Boot + Oracle XE

Testcontainersを使用し、spring-bootのテスト開始時にoracle xeのdockerコンテナを起動してテスト終了時に停止する。

環境

  • Windows 10 Pro x64
  • docker desktop 3.1.0

事前準備

oracleのdocker

まずdockerでoracleを起動可能にする。

kagamihoge.hatenablog.com

oracle xeのpre-builtのdocker準備

oracleの起動は時間かかるので少しでも早くするために行う。もし手動でなく自動テストで起動時間が多少かかっても問題無いならしなくても良い。

kagamihoge.hatenablog.com

ソースコード

spring-bootであればSpring Initializrで作成するのが楽。以下ツイートで教えて頂きました。

plugins {
  id 'org.springframework.boot' version '2.4.3'
  id 'io.spring.dependency-management' version '1.0.11.RELEASE'
  id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '15'
configurations {
  compileOnly {
    extendsFrom annotationProcessor
  }
}
repositories {
  mavenCentral()
}
ext {
  set('testcontainersVersion', "1.15.1")
}
dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
  compileOnly 'org.projectlombok:lombok'
  developmentOnly 'org.springframework.boot:spring-boot-devtools'
  annotationProcessor 'org.projectlombok:lombok'
  testImplementation 'org.springframework.boot:spring-boot-starter-test'
  testImplementation 'org.testcontainers:junit-jupiter'
  testImplementation 'org.testcontainers:oracle-xe'
}
dependencyManagement {
  imports {
    mavenBom "org.testcontainers:testcontainers-bom:${testcontainersVersion}"
  }
}
test {
  useJUnitPlatform()
}

今回はoracle-xeを使うのでorg.testcontainers:oracle-xeを追加している。

まず、動作確認用のrepositoryを作っておく。

src/main/resources/application.propertiesはいつも通りにoracleを起動済みの前提で接続設定などを記述する。

spring.datasource.url=jdbc:oracle:thin:@//localhost:1521/XE
spring.datasource.username=system
spring.datasource.password=oracle
import javax.persistence.Entity;
import javax.persistence.Id;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
public class Item  {
    @Id
    String id;
}
import org.springframework.data.jpa.repository.JpaRepository;

public interface ItemRepository extends JpaRepository<Item, String> {
}

以下がメインのテストクラス。

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.jdbc.Sql;
import org.testcontainers.containers.OracleContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@Sql(scripts = "classpath:schema-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Testcontainers
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
class DemoApplicationTest {

    @Container
    static OracleContainer oracle = new OracleContainer("oracle/db-prebuilt:18.4.0-xe")
            .withUsername("system")
            .withPassword("oracle");

    @DynamicPropertySource
    static void registerProperties(DynamicPropertyRegistry registry) {
        System.out.println(oracle.getJdbcUrl());
        System.out.println(oracle.getUsername());
        System.out.println(oracle.getPassword());

        registry.add("spring.datasource.url", () -> oracle.getJdbcUrl());
        registry.add("spring.datasource.username", () -> oracle.getUsername());
        registry.add("spring.datasource.password", () -> oracle.getPassword());
    }

    @Autowired
    JdbcTemplate template;

    @Autowired
    ItemRepository repository;

    @Test
    void test() {
        System.out.println(template.queryForObject("select 1 from dual", Long.class));
        System.out.println(repository.count());
    }

}

以下はsrc/test/resources/schema-test.sql

CREATE TABLE item (id VARCHAR2(10) NOT NULL);

@Testcontainersにより@Containerで定義するコンテナが自動的に有効になる。今回はoracle-xeなのでtestcontainersのOracleContainerを使用する。javadoc見ると@Testcontainers is a JUnit Jupiter extensionとか書いてあるのでそれ以前のバージョンでは使えないと思われる。

OracleContainerにはoracleのdockerイメージ名とusernameとpasswordを指定している。これらの値は今回pre-built作成時に事前指定する想定なので、その時に設定した値を指定する。

@DynamicPropertySourceを使用してテスト起動時に動的にspring-bootのプロパティを書き換える*1。docker起動後にOracleContainerから各種値が取得できるのでそれをspring.datasource.*に設定する。なお、この例ではusernameやpasswordは固定なので動的に指定しなくてもよい。ポートは上記のコードだとランダムになるのでこうしている。環境によってはポートを固定値にしても良いと思う。

@AutoConfigureTestDatabase(replace = Replace.NONE)は組み込みDBの自動起動が不要なのでこうしている。

@Sql(scripts = "classpath:schema-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)はテスト起動時にテーブルなど作成SQLを実行する。自分の知ってる方法がこれなので、他にやり方あるかもしれない。

起動時の様子

テストコード起動すると下記ような感じに自動的にコンテナが起動&停止する。

docker ps -a
CONTAINER ID   IMAGE                          COMMAND                  CREATED          STATUS                            PORTS                                                                       NAMES
bf02cc55e92f   oracle/db-prebuilt:18.4.0-xe   "/bin/sh -c 'exec $O…"   10 seconds ago   Up 8 seconds (health: starting)   0.0.0.0:49200->1521/tcp, 0.0.0.0:49199->5500/tcp, 0.0.0.0:49198->8080/tcp   zealous_noether
555a00685b91   testcontainers/ryuk:0.3.0      "/app"                   11 seconds ago   Up 10 seconds                     0.0.0.0:49197->8080/tcp                                                     testcontainers-ryuk-9bdd7420-07f0-43ad-848f-fe060aec3298

参考