しおしお

IntelliJ IDEAのことなんかを書いてます

IntelliJ IDEA2020.3のEAPにしたらLombok使ってるプロジェクトのビルドができなくなったお話

IntelliJ IDEAのバージョン

2020.3 EAPの以下のビルドバージョン(これより前のビルドからこの問題が発生しています) f:id:sioiri:20201016061218p:plain

再現コード

pom.xml

  <dependencies>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.12</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>

コード

public class Main {

    public static void main(String[] args) {
        final Name name = new Name("name");
        System.out.println("name.value = " + name.getValue());
    }
    
    @Value
    static class Name {
        private final String value;
    }

}

ビルド結果

ビルド通るはずのコードなのに、IntelliJ IDEAでビルドするとでエラーになってしまいますね…

lombok-test/src/main/java/Main.java:6:27
java: クラス Main.Nameのコンストラクタ Nameは指定された型に適用できません。
  期待値: 引数がありません
  検出値: java.lang.String
  理由: 実引数リストと仮引数リストの長さが異なります
lombok-test/src/main/java/Main.java:7:50
java: シンボルを見つけられません
  シンボル:   メソッド getValue()
  場所: タイプMain.Nameの変数 name

ビルド時のログには、Lombokのワーニングが出力されています。

java: You aren't using a compiler supported by lombok, so lombok will not work and has been disabled.
  Your processor is: com.sun.proxy.$Proxy30
  Lombok supports: OpenJDK javac, ECJ

対応方法

[BUG] Lombok Does not work with IntelliJ EAP 2020.3 Build 203.4203.26 · Issue #2592 · rzwitserloot/lombok · GitHubにあるるように、 -Djps.track.ap.dependencies=false をオプションで指定するか、Lombokのバージョンを1.18.14に上げるかですね。

Lombokのバージョンを上げられない場合には、↓の手順でオプションを指定してあげます。 f:id:sioiri:20201016065800p:plain

オプションを実行後ビルドを実行すると、↓なワーニングがでますがLombokを使っているコードのビルドは通るようになります。

java: JPS incremental annotation processing is disabled. Compilation results on partial recompilation may be inaccurate. Use build process "jps.track.ap.dependencies" VM flag to enable/disable incremental annotation processing environment.

Spring Cloud Contractを試してみた

Spring Cloud Contractとは

Spring Cloud Contractは、Consumer-Driven Contract testing1を実現するためのフレームワークです。

お試し構成

Producerをservier-serviceとして、Consumerをclient-serviceとして構築しています。

f:id:sioiri:20200831091436p:plain

Producer(server-service)

build.gradle

dependenciesにspring-cloud-starter-contract-verifierをpluginにorg.springframework.cloud.contractを追加します。これで、契約を元にしたテストコードの生成と実行、Stubの生成などが行われるようになります。

設定を変更したい場合には、contractsに対してオプションを指定します。指定できるオプションは、Spring Cloud Contract: Configuration Optionsにまとまっています。 baseClassForTestsについては、生成されるテストコードの親クラスを指定します。

import org.springframework.cloud.contract.verifier.config.TestFramework

plugins {
  id "org.springframework.cloud.contract" version scContractVersion
}
apply plugin: 'maven'

dependencyManagement {
  imports {
    mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
  }
}

dependencies {
  testImplementation("org.springframework.cloud:spring-cloud-starter-contract-verifier")
}

contracts {
  testFramework = TestFramework.JUNIT5
  basePackageForTests = 'siosio'
  baseClassForTests = 'siosio.BaseTestClass'
}

アプリケーション側の実装

GET sample/{id}で受け取ったidnameを追加したjsonを返すだけのサーバサイドにしています。

package siosio

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.*

@SpringBootApplication
class ServerSideApplication

fun main(args: Array<String>) {
    runApplication<ServerSideApplication>(*args)
}

@RestController
@RequestMapping("/sample")
class SampleController(
    private val sampleService: SampleService
) {

    @GetMapping("{id:\\d+}")
    fun get(@PathVariable id: Int): SampleResponse {
        return sampleService.get(id)
    }
}

@Service
class SampleService {
    fun get(id: Int): SampleResponse {
        return SampleResponse(id, "name")
    }
}

data class SampleResponse(
    val id: Int,
    val name: String,
)

@ResponseStatus(HttpStatus.NOT_FOUND)
class NotFoundException : RuntimeException()

テストコードの親クラス

build.gradleで指定した親クラスの実装になります。 検証対象のエンドポイントを持つControllerがどれかを、RestAssuredMockMvc.standaloneSetupに設定します。 Controllerが依存しているServiceをモック化したい場合には、このクラスでモックを生成して設定してあげることができます。

abstract class BaseTestClass {

    private val mockService: SampleService = mockk()

    @BeforeEach
    open fun setup() {
        val slot = slot<Int>()
        every { mockService.get(capture(slot)) } answers {
            if (slot.captured == 999) throw NotFoundException()
            SampleResponse(slot.captured, "name_${slot.captured}")
        }
        RestAssuredMockMvc.standaloneSetup(SampleController(mockService))
    }
}

Contractの定義

Contractはデフォルト設定では、src/t/est/resources/contracts に定義します。今回は、正常系と404を返すケースのContractを定義してみます。 Contractの定義方法は、Contract DSLを見るとだいたいわかります。

正常系(src/t/est/resources/contracts/GetSample.groovy)

SampleControllerで定義しているエンドポイントを呼び出すrequestを定義しています。 responseでは、requestで送ったidnameが戻ってくることを定義します。

import org.springframework.cloud.contract.spec.Contract

Contract.make {
  priority 2

  request {
    method('GET')
    url "/sample/${regex('\\d+')}"
  }

  response {
    status(OK())

    headers {
      contentType(applicationJson())
    }
    
    body(
        id: fromRequest().path(1),
        name: "name_${fromRequest().path(1)}"
    )
  }
}

404系(src/t/est/resources/contractsGetSample_404.groovy)

期待するresponseとして404を定義します。priorityを正常系より高くして404を優先的に扱えるようにしてあげます。

import org.springframework.cloud.contract.spec.Contract

Contract.make {
  priority 1
  request {
    method('GET')
    url ("/sample/999")
  }

  response {
    status(NOT_FOUND())
  }
}

Stubの生成とMavenローカルリポジトリへのインストール

buildとインストールタスクを実行してStubの生成とMavenローカルリポジトリへのインストールを行います。 なお、Producer側のContractに従っているかの検証だけであれば、./gradlew testだけでOKです。

./gradlew  build install

テストコードが生成され、Contractに従っているかの検証が行われます。 今回は、正常系と404系の2ケースともContractに従っていることがわかります。

f:id:sioiri:20200901061109p:plain

Consumer(client-service)

build.gradle

dependenciesにspring-cloud-starter-contract-stub-runnerを追加します。これで、Producer側で生成したStubを使用したテストコードがかけるようになります。

dependencies {
  testImplementation("org.springframework.cloud:spring-cloud-starter-contract-stub-runner")
}

Producer側を呼び出すコード

RestTemplateを使ってエンドポイントを叩くだけの実装です。実際は、urlは設定ファイルに切り出したりとかが必要になると思いますが…

@Service
class SampleService(
    private val restTemplate: RestTemplate
) {
    fun get(id: Int): SampleApiResponse {
        return restTemplate.getForEntity("http://localhost:8080/sample/{id}", SampleApiResponse::class.java, id).body!!
    }
}

data class SampleApiResponse(
    val id: Int,
    val name: String,
)

テストコード

Producer側と違い、テストコードを作成する必要があります。

AutoConfigureStubRunnerアノテーションを使ってどのStubを使うのかを指定します。 idアノテーションにstubの情報を指定します。大事なのは、最後に指定するポート(8080の部分)は実際にProducerを呼び出す際に指定しているポートを指定します。 Mavenのローカルリポジトリを使っているので、stubsModeにはLOCALを指定します。 リモートリポジトリを使う場合には、REMOTEを設定しrepositoryRootを設定する必要があるようです。

package siosio

import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties
import org.springframework.test.context.TestConstructor
import org.springframework.test.context.junit.jupiter.SpringExtension
import org.springframework.web.client.HttpClientErrorException

@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
@ExtendWith(SpringExtension::class)
@SpringBootTest
@AutoConfigureStubRunner(ids = ["siosio:server-service:1.0.0:stubs:8080"], stubsMode = StubRunnerProperties.StubsMode.LOCAL)
internal class SampleServiceTest(
    private val sut: SampleService
) {

    @Test
    internal fun 正常系() {
        val actual = sut.get(100)
        Assertions.assertThat(actual)
            .isEqualTo(SampleApiResponse(100, "name_100"))
    }
    
    @Test
    internal fun `404系`() {
        Assertions.assertThatThrownBy { sut.get(999) }
            .isInstanceOf(HttpClientErrorException.NotFound::class.java)
    }
}

テスト結果

テスト成功ですね。これでConsumer側もContractに従っていることが検証できました。

f:id:sioiri:20200901062957p:plain

ソースコード全体

github.com

Spring Boot 2.3からのCloud Native Buildpacksを試してみる

Spring Boot 2.3からのCloud Native Buildpacksを試してみました。

プロジェクトの作成

Spring Initializrを使って、Spring Bootのバージョンを2.3以降にしてプロジェクトを生成します。
※お試しプロジェクトでは、Dependenciesに Spring WebSpring Boot Actuator を選択してGradleプロジェクトとしています。

Gradleのタスク一覧から、イメージ作成用のタスク(bootBuildImage)が追加されていることが確認できます。

./gradlew tasks --group build

> Task :tasks

------------------------------------------------------------
Tasks runnable from root project
------------------------------------------------------------

Build tasks
-----------
assemble - Assembles the outputs of this project.
bootBuildImage - Builds an OCI image of the application using the output of the bootJar task
bootJar - Assembles an executable jar archive containing the main classes and their dependencies.
build - Assembles and tests this project.
buildDependents - Assembles and tests this project and all projects that depend on it.
buildNeeded - Assembles and tests this project and all projects it depends on.
classes - Assembles main classes.
clean - Deletes the build directory.
jar - Assembles a jar archive containing the main classes.
testClasses - Assembles test classes.

タスクの詳細を確認してみると、オプションでイメージ名を指定できることがわかりますね。

./gradlew help --task bootBuildImage

> Task :help
Detailed task information for bootBuildImage

Path
     :bootBuildImage

Type
     BootBuildImage (org.springframework.boot.gradle.tasks.bundling.BootBuildImage)

Options
     --builder     The name of the builder image to use

     --imageName     The name of the image to generate

Description
     Builds an OCI image of the application using the output of the bootJar task

Group
     build

イメージを作って動かしてみる

bootBuildImageタスクを使うとさくっとイメージを作れます。

./gradlew bootBuildImage                                                                                                                                                                                                                                                                                                                                                                      ✘ 126 

> Task :bootBuildImage
Building image 'docker.io/library/buildpack-example:0.0.1-SNAPSHOT'

 > Pulling builder image 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' ..................................................
 > Pulled builder image 'gcr.io/paketo-buildpacks/builder@sha256:21a7235c03dbdfcc330e03183a85e0a36b4c0eee6904529e2376502e5e4a84a5'
 > Pulling run image 'gcr.io/paketo-buildpacks/run:base-cnb' ..................................................
 > Pulled run image 'gcr.io/paketo-buildpacks/run@sha256:fb49a85ddb10a93f194cc407b34d5c352def37693c23be5b9f9a9859e7526b78'
 > Executing lifecycle version v0.8.1
 > Using build cache volume 'pack-cache-3949d5df2001.build'

 > Running creator
    [creator]     ===> DETECTING
    [creator]     5 of 16 buildpacks participating
    [creator]     paketo-buildpacks/bellsoft-liberica 2.10.0
    [creator]     paketo-buildpacks/executable-jar    2.0.1
    [creator]     paketo-buildpacks/apache-tomcat     1.3.3
    [creator]     paketo-buildpacks/dist-zip          1.3.7
    [creator]     paketo-buildpacks/spring-boot       2.2.1
    [creator]     ===> ANALYZING
    [creator]     Previous image with name "docker.io/library/buildpack-example:0.0.1-SNAPSHOT" not found
    [creator]     ===> RESTORING
    [creator]     ===> BUILDING
    [creator]     
    [creator]     Paketo BellSoft Liberica Buildpack 2.10.0
    [creator]       https://github.com/paketo-buildpacks/bellsoft-liberica
    [creator]       Build Configuration:
    [creator]         $BP_JVM_VERSION              11.*            the Java version
    [creator]       Launch Configuration:
    [creator]         $BPL_JVM_HEAD_ROOM           0               the headroom in memory calculation
    [creator]         $BPL_JVM_LOADED_CLASS_COUNT  35% of classes  the number of loaded classes in memory calculation
    [creator]         $BPL_JVM_THREAD_COUNT        250             the number of threads in memory calculation
    [creator]       BellSoft Liberica JRE 11.0.8: Contributing to layer
    [creator]         Downloading from https://github.com/bell-sw/Liberica/releases/download/11.0.8+10/bellsoft-jre11.0.8+10-linux-amd64.tar.gz
    [creator]         Verifying checksum
    [creator]         Expanding to /layers/paketo-buildpacks_bellsoft-liberica/jre
    [creator]         Writing env.launch/JAVA_HOME.override
    [creator]         Writing env.launch/MALLOC_ARENA_MAX.override
    [creator]         Writing profile.d/active-processor-count.sh
    [creator]       Memory Calculator 4.1.0: Contributing to layer
    [creator]         Downloading from https://github.com/cloudfoundry/java-buildpack-memory-calculator/releases/download/v4.1.0/memory-calculator-4.1.0.tgz
    [creator]         Verifying checksum
    [creator]         Expanding to /layers/paketo-buildpacks_bellsoft-liberica/memory-calculator
    [creator]         Writing profile.d/memory-calculator.sh
    [creator]       Class Counter: Contributing to layer
    [creator]         Copying to /layers/paketo-buildpacks_bellsoft-liberica/class-counter
    [creator]       JVMKill Agent 1.16.0: Contributing to layer
    [creator]         Downloading from https://github.com/cloudfoundry/jvmkill/releases/download/v1.16.0.RELEASE/jvmkill-1.16.0-RELEASE.so
    [creator]         Verifying checksum
    [creator]         Copying to /layers/paketo-buildpacks_bellsoft-liberica/jvmkill
    [creator]         Writing env.launch/JAVA_OPTS.append
    [creator]       Link-Local DNS: Contributing to layer
    [creator]         Copying to /layers/paketo-buildpacks_bellsoft-liberica/link-local-dns
    [creator]         Writing profile.d/link-local-dns.sh
    [creator]       Java Security Properties: Contributing to layer
    [creator]         Writing env.launch/JAVA_OPTS.append
    [creator]         Writing env.launch/JAVA_SECURITY_PROPERTIES.override
    [creator]       Security Providers Configurer: Contributing to layer
    [creator]         Copying to /layers/paketo-buildpacks_bellsoft-liberica/security-providers-configurer
    [creator]         Writing profile.d/security-providers-classpath.sh
    [creator]         Writing profile.d/security-providers-configurer.sh
    [creator]       OpenSSL Certificate Loader: Contributing to layer
    [creator]         Copying to /layers/paketo-buildpacks_bellsoft-liberica/openssl-security-provider
    [creator]         Writing profile.d/openssl-certificate-loader.sh
    [creator]     
    [creator]     Paketo Executable JAR Buildpack 2.0.1
    [creator]       https://github.com/paketo-buildpacks/executable-jar
    [creator]         Writing env.launch/CLASSPATH
    [creator]       Process types:
    [creator]         executable-jar: java -cp "${CLASSPATH}" ${JAVA_OPTS} org.springframework.boot.loader.JarLauncher
    [creator]         task:           java -cp "${CLASSPATH}" ${JAVA_OPTS} org.springframework.boot.loader.JarLauncher
    [creator]         web:            java -cp "${CLASSPATH}" ${JAVA_OPTS} org.springframework.boot.loader.JarLauncher
    [creator]     
    [creator]     Paketo Spring Boot Buildpack 2.2.1
    [creator]       https://github.com/paketo-buildpacks/spring-boot
    [creator]       Build Configuration:
    [creator]         $BP_BOOT_NATIVE_IMAGE                  the build to create a native image (requires GraalVM)
    [creator]         $BP_BOOT_NATIVE_IMAGE_BUILD_ARGUMENTS  the arguments to pass to the native-image command
    [creator]       Launch Configuration:
    [creator]         $BPL_SPRING_CLOUD_BINDINGS_ENABLED     whether to auto-configure Spring Boot environment properties from bindings
    [creator]       Web Application Type: Contributing to layer
    [creator]         Servlet web application detected
    [creator]         Writing env.launch/BPL_JVM_THREAD_COUNT.default
    [creator]       Spring Cloud Bindings 1.4.0: Contributing to layer
    [creator]         Downloading from https://repo.spring.io/release/org/springframework/cloud/spring-cloud-bindings/1.4.0/spring-cloud-bindings-1.4.0.jar
    [creator]         Verifying checksum
    [creator]         Copying to /layers/paketo-buildpacks_spring-boot/spring-cloud-bindings
    [creator]         Writing profile.d/spring-cloud-bindings.sh
    [creator]       Image labels:
    [creator]         org.springframework.boot.spring-configuration-metadata.json
    [creator]         org.springframework.boot.version
    [creator]     ===> EXPORTING
    [creator]     Adding layer 'launcher'
    [creator]     Adding layer 'paketo-buildpacks/bellsoft-liberica:class-counter'
    [creator]     Adding layer 'paketo-buildpacks/bellsoft-liberica:java-security-properties'
    [creator]     Adding layer 'paketo-buildpacks/bellsoft-liberica:jre'
    [creator]     Adding layer 'paketo-buildpacks/bellsoft-liberica:jvmkill'
    [creator]     Adding layer 'paketo-buildpacks/bellsoft-liberica:link-local-dns'
    [creator]     Adding layer 'paketo-buildpacks/bellsoft-liberica:memory-calculator'
    [creator]     Adding layer 'paketo-buildpacks/bellsoft-liberica:openssl-security-provider'
    [creator]     Adding layer 'paketo-buildpacks/bellsoft-liberica:security-providers-configurer'
    [creator]     Adding layer 'paketo-buildpacks/executable-jar:class-path'
    [creator]     Adding layer 'paketo-buildpacks/spring-boot:spring-cloud-bindings'
    [creator]     Adding layer 'paketo-buildpacks/spring-boot:web-application-type'
    [creator]     Adding 1/1 app layer(s)
    [creator]     Adding layer 'config'
    [creator]     *** Images (ab7b253dc430):
    [creator]           docker.io/library/buildpack-example:0.0.1-SNAPSHOT

Successfully built image 'docker.io/library/buildpack-example:0.0.1-SNAPSHOT'

コンテナー起動後、actuator/envで確認するとJava11でアプリケーションが実行されているのが確認できますね。

curl http://localhost:8080/actuator/env
# 省略
{
  "value": "11.0.8+10-LTS"
}

Javaのバージョンは、GradleのtargetCompatibilityと連動しているっぽく、以下のようにJava14を指定すると自動的に起動時に使用するバージョンが変わるようです。

java {
    targetCompatibility = JavaVersion.VERSION_14
}

上の状態でイメージ作成後に確認してみると想定通りJava14で起動されていました。

curl http://localhost:8080/actuator/env
# 省略
{
  "value": "14.0.2+13"
}

起動時のオプションを変更してみる

コンテナ起動時に、JAVA_OPTS環境変数を設定することで起動時のJVMオプションを変更できるようです。

例えば、下のようにコンテナを起動するとhogeシステムプロパティにfugaが設定されます。

docker run -p "8080:8080" -e JAVA_OPTS="-Dhoge=fuga"  76747f99e0c7

起動後、actuator/envを叩くことで想定通りシステムプロパティが設定されていることが確認できます。

curl http://localhost:8080/actuator/env
{
  "activeProfiles": [],
  "propertySources": [
    {
      "name": "server.ports",
      "properties": {
        "local.server.port": {
          "value": 8080
        }
      }
    },
    {
      "name": "servletContextInitParams",
      "properties": {}
    },
    {
      "name": "systemProperties",
      "properties": {
        "hoge": {
          "value": "fuga"
        }
    }
  ]
}

イメージ名を指定してビルドしてみる

bootBuildImageタスクのimageNameオプションを指定することで、イメージ名を変更できます。 リモートリポジトリの名前に合わせる場合なんかに使うとよさげですかね。

./gradlew bootBuildImage --imageName siosio/sample:1.0.0

例えば、GitHub ActionsからECRにビルドしたイメージをpushする場合なんかは、こんな感じでいけるかなと思います。

    steps:
      - uses: actions/checkout@v2

      - name: Cache Gradle packages
        uses: actions/cache@v1
        with:
          path: ~/.gradle/caches
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
          restore-keys: ${{ runner.os }}-gradle

      - name: setup java
        uses: actions/setup-java@v1
        with:
          java-version: 11
          
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.DEV_AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.DEV_AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-1

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      - name: build image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: ${{ secrets.AWS_ECR_REPO_NAME }}
        run: |
          IMAGE_TAG=$(echo ${{ github.ref }} | sed 's/refs\/heads\///g' | sed 's/\//-/g')
          ./gradlew bootBuildImage --imageName $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG

IntelliJ IDEAのElasticsearchプラグインを試してみた

IntelliJ IDEA用のElasticsearchプラグインが最近登場したらしいのでお試ししてみたよ。

使ったバージョン

  • IntelliJ IDEA 2020.2 BETA
  • Elasticsearch plugin 0.1.7

インストール

Elasticsearchプラグインの以下ページやIntelliJのplugin settings画面からインストールできます。 plugins.jetbrains.com

Elasticsearchへの接続

Elasticsearchウィンドウを開いて、上の方にある から開くウィンドウで接続先の設定を行います。

f:id:sioiri:20200710062709p:plain

Elasticsearchウィンドウからできること

ホストを選択してコンテキストメニューで表示される内容になります。 インデックスの作成やステータスの確認が簡単にできそうですね。インデックスの作成時には、マッピング情報などの指定はできないみたいですが…

f:id:sioiri:20200710062953p:plain

インデックスを選択した場合は、インデックス関連のメニューに変わりますね

f:id:sioiri:20200710070405p:plain

検索処理をしてみよう

インデックスを選択してquery editor(下の画像の赤枠アイコン)を開くと検索処理が行えるようになります。

f:id:sioiri:20200710084643p:plain

デフォルトで、↓なクエリが表示されているのでこれを編集していくかんじになりますね。 クエリのJsonの組み立てはなにかサポート機能があるわけではないので、現状自力で頑張るしかないようです…

{
  "from": 0,
  "size": 20,
  "query": {
    "match_all": {}
  }
}

検索結果の表示は、下の画像のように tesxt or table で切り替えられるようになっています。

f:id:sioiri:20200710085811p:plain

table 表示に切り替えると検索結果めっちゃ見やすくなります。

f:id:sioiri:20200710085915p:plain

まとめ

クエリの履歴機能がないのはかなり不便かなと思いますが、検索結果をテーブルで表示できるのはめっちゃ見やすくてよいですね。 IntelliJさんから移動しなくてもさくっと、Elasticsearchにアクセスできるのはなかなか便利で良いです。

僕は、Elasticsearchにアクセスするときは、HTTP client in IntelliJ IDEA code editor - Help | IntelliJ IDEAを使うんですが、 現状だと保存したクエリをさくっと実行できるIntelliJさんのhttp clientのが便利かなーと感じました。(.httpファイルをコミットしておいてチームメンバーにも共有できますし)

まだ、公開されてから1ヶ月ぐらいしか立っていないので今後の進化に期待ですね。

Spring BootでDomaのCriteria APIをつかってみる

doma-spring-boot-starter1.4.0で簡単にCriteria APIが使えるようになったので早速つかってみました。

build.gradle

build.gradleに必要なライブラリを追加します。 doma2.30.0からcoreprocessorが分離されてたのも把握できてよかったです。

    implementation("org.seasar.doma.boot:doma-spring-boot-starter:1.4.0")
    implementation("org.seasar.doma:doma-core:2.35.0")
    kapt("org.seasar.doma:doma-processor:2.35.0")

Entity

  • Criteria APIのドキュメントにどおりに、@Entitymetamodelを設定します
@Entity(immutable = true, metamodel = Metamodel())
@Table(name = "user")
data class UserEntity(

        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        val id: Int,

        val name: String
)

Criteria APIを使ってみる

  • Criteria APIに必要なEntityqlをコンストラクタインジェクションでうけとります
    • Entityqldoma-spring-boot-starterがBean生成してくれるので特に設定せずに簡単に使えます
  • Entityqlには、Entityから生成したmetamodel classを渡します
    • Entityの例に登場したmetamodel = Metamodel()があることで、metamodel class(クラス名の最後にアンダースコアがついたもの)がコンパイル時に生成されます
  • 今回は、Entityのクラス名からmetamodel classを雑にひっぱってきて利用しています
  • あとは、ドキュメントどおりにCriteria APIを使っていくだけですね
import org.seasar.doma.jdbc.criteria.Entityql
import org.seasar.doma.jdbc.criteria.metamodel.EntityMetamodel
import siosio.demo.domain.User
import siosio.demo.domain.UserRepository

class DomaUserRepository(private val entityql: Entityql) : UserRepository {
    override fun findAll(): List<User> {
        return entityql.from(entityType<UserEntity>())
                .fetch()
                .map { User(it.id, it.name) }
    }

    override fun addUser(user: User) {
        entityql.insert(entityType<UserEntity>(), UserEntity(-1, user.name))
                .execute()
    }
}
private inline fun <reified ENTITY> entityType(): EntityMetamodel<ENTITY> {
    return Class.forName("${ENTITY::class.qualifiedName}_").getConstructor()
            .newInstance() as EntityMetamodel<ENTITY>
}

サンプルコード

github.com

Spring BootのテストでTestcontainersを使ってみる

Spring BootのテストでTestcontainersを使って、データベースをコンテナとして起動してテストを実行してみる感じです。 これを使うことで、開発で使っているデータベースを汚染せずに簡単にデータベースまで通しのテストができそうな気がしています。

Testcontainers関連のライブラリを追加

build.gradle.ktsTestcontainers関連のライブラリを追加しています。 今回は、MySqlで試したので、org.testcontainers:mysqlを入れています。

testImplementation(platform("org.testcontainers:testcontainers-bom:1.14.1"))
testImplementation("org.testcontainers:mysql")
testImplementation("org.testcontainers:junit-jupiter")

テスト対象

エンドポイント

テスト対象は、データベースに変更を加える登録処理とテーブルの全件を返すような一覧取得用のエンドポイントとしています。

private fun myRouter(
    userHandler: UserHandler
) = router {
    "/api".nest {
        POST("/users", userHandler::addUser)
        GET("/users", userHandler::findAll)
    }
}

テーブル定義

名前だけ持つ簡単なテーブルにしています。

create table user (
    id int not null auto_increment,
    name varchar(200) not null,
    primary key (id)
);

テストの準備

テストコード

  • @Testcontainersアノテーションをテストクラスに追加します
  • @Containerアノテーションをつけて、テストで使用するコンテナを指定します
    今回は、MySqlを使うのでMySQLContainerとなります
  • @DynamicPropertySourceをつけたメソッドで、コンテナで起動したデータベースのURLなどを設定値として登録します
  • BeforeEachでテスト対象のデータベースの内容をFlywayで初期化して登録のケースなどのデータベース変更の影響を受けないようにしています
    テストで使うデータは、test配下のdb/migrationにRepeatableなやつとしておいておいてバージョンなどの影響を受けずに必ず最後に実行するようにしています

アノテーションつけるだけでさくっとコンテナでテストできるのめっちゃ便利ですね。 考えないといけないのは、テストで使うデータのセットアップなどですね。

@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
@SpringBootTest
@AutoConfigureMockMvc
@Testcontainers
internal class UserHandlerTest(
        val mockMvc: MockMvc,
        val dataSource: DataSource,
        val flyway: Flyway
) {

    @BeforeEach
    internal fun setUp() {
        flyway.clean()
        flyway.migrate()
    }

    companion object {

        @JvmStatic
        @Container
        val container = MySQLContainer<Nothing>()

        @DynamicPropertySource
        @JvmStatic
        fun changeProperty(registry: DynamicPropertyRegistry): Unit {
            println("container.jdbcUrl = ${container.jdbcUrl}")
            registry.add("spring.datasource.url", container::getJdbcUrl)
            registry.add("spring.datasource.username", container::getUsername)
            registry.add("spring.datasource.password", container::getPassword)
        }
    }

    @Test
    internal fun ユーザが登録出来るよ() {
        val table = Table(dataSource, "user")
        val changes = Changes(table)
        changes.setStartPointNow()

        mockMvc.post("/api/users") {
            contentType = MediaType.APPLICATION_JSON
            content = """{"name": "siosio"}"""
        }.andExpect {
            status { isOk }
        }

        changes.setEndPointNow()
        Assertions.assertThat(changes)
                .hasNumberOfChanges(1)
                .changeOfCreation()
                .rowAtEndPoint().value("name").isEqualTo("siosio")
    }

    @Test
    internal fun ユーザ一覧が取得できるよ() {

        mockMvc.get("/api/users") {
        }.andExpect {
            status { isOk }
            content { contentType(MediaType.APPLICATION_JSON) }
            jsonPath("$.users.length()") { value(3) }
            jsonPath("$.users[*].name") { value(contains("user_1", "user_2", "user_3")) }
            jsonPath("$.users[*].id") { value(contains(1, 2, 3)) }
        }
    }
}

テスト用のデータベース用のデータ

Repeatableなマイグレーションファイルとしてtest/resources/db/migration/R__replace_user_table.sqlを作って、テスト対象テーブルのデータをセットアップしています。

truncate table user;
insert into user (name) values ('user_1');
insert into user (name) values ('user_2');
insert into user (name) values ('user_3');

テスト実行

こんな感じにログがでて、コンテナが起動されてテストが実行されます。 わりとさくっと、コンテナ使ってテスト実行が出来て便利な感じがあります。

07:10:33.048 [Test worker] DEBUG org.testcontainers.images.AbstractImagePullPolicy - Using locally available and not pulling image: mysql:5.7.22
07:10:33.048 [Test worker] DEBUG 🐳 [mysql:5.7.22] - Starting container: mysql:5.7.22
07:10:33.048 [Test worker] DEBUG 🐳 [mysql:5.7.22] - Trying to start container: mysql:5.7.22
07:10:33.049 [Test worker] DEBUG 🐳 [mysql:5.7.22] - Trying to start container: mysql:5.7.22 (attempt 1/3)
07:10:33.049 [Test worker] DEBUG 🐳 [mysql:5.7.22] - Starting container: mysql:5.7.22
07:10:33.049 [Test worker] INFO 🐳 [mysql:5.7.22] - Creating container for image: mysql:5.7.22

サンプルコード

サンプルプロジェクトは↓ github.com

IntelliJ IDEA系の自作プラグインでLanguage Injectionsの設定を行う方法

このメソッドのこの引数は自動的にHTMLやSQLとして認識させたいなーって思うことがあったりしますよね。 それを、自作のプラグインで自動的に設定してあげる手順的なやつです。

プラグインの設定手順

plugin.xmlの定義

org.intellij.intelliLang の拡張ポイントを使って、Language Injectionsの設定ファイルを認識させます。

plugin.xmlの内容的にはこんな感じになります。 この設定の場合は、プロジェクトのmain/resources/META-INF/domaInjection.xml(plugin.xmlと同じ場所)がLanguage Injectionsの設定ファイルとしてロードされます。 1

  <extensions defaultExtensionNs="org.intellij.intelliLang">
    <injectionConfig config="META-INF/domaInjection.xml" />
  </extensions>

Language Injectionsの設定ファイルを作る

Language Injectionsの設定ファイルを作って、plugin.xmlinjectionConfigに設定したパスに保存してあげます。

設定ファイルの内容的には、こんな感じですね。<component name="LanguageInjectionConfiguration">内にinjectionの設定をしてあげます。

<?xml version="1.0" encoding="UTF-8"?>
<component name="LanguageInjectionConfiguration">
  <injection language="SQL" injector-id="java">
    <display-name>Sql.value (org.seasar.doma.experimental)</display-name>
    <place><![CDATA[psiMethod().withName("value").withParameters().definedInClass("org.seasar.doma.experimental.Sql")]]></place>
  </injection>
  <injection language="SQL" injector-id="kotlin">
    <display-name>Sql.value (org.seasar.doma.experimental)</display-name>
    <place><![CDATA[psiMethod().withName("value").withParameters().definedInClass("org.seasar.doma.experimental.Sql")]]></place>
  </injection>
</component>

この設定ファイル、手で作るのはかなりしんどいのでIntelliJの画面で作ったxmlから設定内容を持ってくるのが楽で良いですね。 IntelliJの設定画面からは、↓な流れで設定できますね。

f:id:sioiri:20200226120615p:plain:w300

設定内容は、~/.IntelliJIdea2019.3/config/options/IntelliLang.xml(2020.1以降は、~/.config/JetBrains/IntelliJIdea2020.1/options/IntelliLang.xml)に保存されているので、そこから必要なinjectionタグを探して持ってくれば良いですね。

動かしてみると

↓のようにLanguage Injectionsの設定画面でプラグインで設定したやつが、Built-inスコープで登録されていれば成功ですね。

f:id:sioiri:20200226122019p:plain:w300


  1. IntelliJさんでこの設定を入れると、プラグイン開発用のプラグインorg.intellij.intelliLangに対応していないのかinjectionConfigが怒られますが気にしないで大丈夫です。