すきま風

勉強したことのメモとか

Spring Boot × Gradleで自作ライブラリを作る

Spring Bootで自作ライブラリを1から作ったことがなかったので試してみました。

作成するライブラリ

文字列を引数で与えるとPrefixをつけて返してくれるMyTextDecoratorを作成します。
さらにadditionalを定義することで追加で文字列を付けてくれます。

github star 5,000超え待ったなしのスーパーライブラリですよこいつはぁ。

利用方法

# application.yml
mylibrary:
  text:
    prefix: my-prefix
    additional:
      enabled: true
      prefix: additional
@Service
class DemoService(
    private val myTextDecorator: MyTextDecorator
) : DemoUseCase {
    override suspend fun getSampleModel(): DemoUseCase.Output {
        val text = myTextDecorator.decorate("demo")
        println(text)  // -> my-prefixadditionaldemo
        return DemoUseCase.Output(text)
    }
}

やること

  • mylibrary.text.prefix を読み込んでBeanを生成する
  • mylibrary.text.additional.prefix を読み込んで条件に応じてBeanを生成する
  • MyTextDecorator.ktをBeanに登録する
  • spring.factoriesを用意する
  • jarに固める
  • 利用者側に取り込む


ソフトウェアバージョン

software version
OS MacOS Catalina
Spring Boot 2.2.2
Java Corretto-11.0.4.11.1
Kotlin 1.3.61
Gradle 6.0.1

my-library 構成

my-library
├── build.gradle.kts
├── settings.gradle.kts
└── src
    └── main
        ├── kotlin
        │   └── com
        │       └── example
        │           └── mylibrary
        │               ├── MyLibraryApplication.kt
        │               └── autoconfigure
        │                   ├── TextAdditionalPrefixConfiguration.kt
        │                   ├── TextDecoratorConfiguration.kt
        │                   └── TextDefaultPrefixConfiguration.kt
        └── resources
            ├── META-INF
            │   └── spring.factories
            └── application.properties

TextDefaultPrefixConfiguration.kt

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(TextDefaultPrefixProperty::class)
class TextDefaultPrefixConfiguration(
    private val textDefaultPrefixProperty: TextDefaultPrefixProperty
) {
    @Bean
    fun defaultPrefix() = DefaultPrefix(textDefaultPrefixProperty.prefix)
}

@ConfigurationProperties("mylibrary.text")
@ConstructorBinding
class TextDefaultPrefixProperty(
    val prefix: String
)

data class DefaultPrefix(val value: String)

TextAdditionalPrefixConfiguration.kt

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(TextAdditionalPrefixProperty::class)
class TextAdditionalPrefixConfiguration(
    private val textAdditionalPrefixProperty: TextAdditionalPrefixProperty
) {
    // mylibrary.text.additional.enabled=true の場合にBeanにセットする。
    // enabledが無い場合もBeanに入る
    @Bean
    @ConditionalOnProperty(prefix = "mylibrary.text.additional", name = ["enabled"], matchIfMissing = true)
    fun additionalPrefix() = AdditionalPrefix(textAdditionalPrefixProperty.prefix)
}

@ConfigurationProperties("mylibrary.text.additional")
@ConstructorBinding
class TextAdditionalPrefixProperty(
    // mylibrary.text.additional.prefixはoptionalなので default valueをセットしておく
    // application.ymlに存在しない場合はBlank
    val prefix: String = ""
)

data class AdditionalPrefix(
    val value: String = ""
)

TextDecoratorConfiguration.kt

@Configuration(proxyBeanMethods = false)
class TextDecoratorConfiguration(
    private val defaultPrefix: DefaultPrefix,
    // additionalPrefixはBeanに登録していない場合もあるのでnullableにする
    private val additionalPrefix: AdditionalPrefix?
) {
    @Bean
    fun decorator() = MyTextDecorator(defaultPrefix.value + (additionalPrefix?.value ?: ""))
}

class MyTextDecorator(private val value: String) {
    fun decorate(str: String)  = value + str
}

spring.factories

https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-developing-auto-configuration

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.mylibrary.autoconfigure.TextDefaultPrefixConfiguration,\
com.example.mylibrary.autoconfigure.TextAdditionalPrefixConfiguration,\
com.example.mylibrary.autoconfigure.TextDecoratorConfiguration

build.gradle.kts

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.springframework.boot") version "2.2.2.RELEASE"
    id("io.spring.dependency-management") version "1.0.8.RELEASE"
    kotlin("jvm") version "1.3.61"
    kotlin("plugin.spring") version "1.3.61"

    // IDEにmeta-dataを理解してもらう。tutorialsの下の方に記載
    // https://spring.io/guides/tutorials/spring-boot-kotlin/
    kotlin("kapt") version "1.3.61"
  
    // maven pluginを使いたい場合
    java
    maven
}

group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    testImplementation("org.springframework.boot:spring-boot-starter-test") {
        exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
    }

    kapt("org.springframework.boot:spring-boot-configuration-processor")
}

tasks.withType<Test> {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs = listOf("-Xjsr305=strict")
        jvmTarget = "1.8"
    }
}

// Jarを作れるようにする
tasks.withType<Jar> {
    enabled = true
}

// maven pluginを利用する場合
// https://docs.gradle.org/current/userguide/maven_plugin.html
tasks.named<Upload>("uploadArchives") {
    repositories.withGroovyBuilder {
        "mavenDeployer" {
            "repository"("url" to "file://path/to/foo/bar/")
            "pom" {
                setProperty("version", version)
            }
        }
    }
}

Library User側のbuild.gradle.kts

jarをlibsに入れて参照するだけにする

dependencies {
    implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
}

Call

@Service
class DemoService(
    private val myTextDecorator: MyTextDecorator
) : DemoUseCase {
    override suspend fun getSampleModel(): DemoUseCase.Output {
        val text = myTextDecorator.decorate("demo")
        return DemoUseCase.Output(text)
    }
}

まとめ

ConditionalOnとかAutoConfigureOrderとかの動きを知りたくて作ってみたのですがAutoConfigureOrderを利用しなかったな😑

Spring Boot 2.2から @ConstructorBinding が利用できるようになったのでProperty classを表現しやすくなりました。

参考

https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-developing-auto-configuration
https://docs.gradle.org/current/userguide/maven_plugin.html
https://qiita.com/daisuzz/items/158e34e86096beae84af