ソフトウェアエンジニアの日常の雑記

日々思ったことをまとめます

OpenRewriteでマルチプロジェクト構成のSpringBoot2のマイグレーションを行う

本エントリーはJava Advent Calendarの21日目です。昨日はHatanoさんの文鳥は家鴨の夢を見るか #Java - Qiitaでした。

OpenRewriteを使用して、MultiProject構成のSpringBoot2をSpringBoot3にマイグレーションを行います。所属している事業部ではよく用いられているSpringBoot x Gradle x マルチプロジェクト 構成に対して、マイグレーションを実施します。弊社でもOpenRewriteを使用してマイグレーションを行っています。

docs.openrewrite.org

ディレクト

appディレクトリとserviceディレクトリの2つのマルチプロジェクト構成になっています。

├── app
│   ├── build
│   ├── build.gradle
│   └── src
├── build.gradle
├── gradlew
├── gradlew.bat
├── service
│   ├── build
│   ├── build.gradle
│   └── src
└── settings.gradle

build.gradle

build.gradleは、下記にようになっており、SpringBoot2.7.4Java17となっています。

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

group = 'jp.co.excite'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

subprojects {

    apply plugin: 'java'
    apply plugin: 'io.spring.dependency-management'
    apply plugin: 'org.springframework.boot'

    configurations {
        compileOnly {
            extendsFrom annotationProcessor
        }
    }

    repositories {
        mavenCentral()
    }

    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter'
        implementation 'org.springframework.boot:spring-boot-starter-validation'

        compileOnly 'org.projectlombok:lombok'
        developmentOnly 'org.springframework.boot:spring-boot-devtools'
        annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
        annotationProcessor 'org.projectlombok:lombok'

        testImplementation 'org.springframework.boot:spring-boot-starter-test'
        testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    }
    tasks.named('test') {
        useJUnitPlatform()
    }

}

project(":app") {
    dependencies {
        implementation project(":service")
        implementation 'org.springframework.boot:spring-boot-starter-web'
    }
}

project(":service") {
    repositories {
        mavenCentral()
    }
}

コード

入力文字を大文字にするAPIを用意します。

コントローラは下記のようになります。

package jp.co.excite.adventcalendar.controller;

import jp.co.excite.adventcalendar.SampleService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.NotBlank;


@RestController
@RequestMapping("/")
public class RootController {

    private final SampleService sampleService;

    public RootController(SampleService sampleService) {
        this.sampleService = sampleService;
    }

    @RequestMapping("upper")
    public String upper(@NotBlank String text) {
        return sampleService.upper(text);
    }
}

サービスは下記のようになります。

package jp.co.excite.adventcalendar;

import org.springframework.stereotype.Service;


public interface SampleService {
    String upper(String text);
}

@Service
class SampleServiceImpl implements SampleService {

    @Override
    public String upper(String text) {
        return String.format("input: %s", text.toUpperCase());
    }
}

上記のAPIにOpenRewriteでマイグレーションをかけていきます。

build.gradleにOpenRewriteの設定を追加

OpenRewriteの設定を追加します。 マルチプロジェクト構成でも、rootprojectsubprojectsのディレクティブの中に書くのではなく、トップ階層に書くのがポイントです。

plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.4'  apply false
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'  apply false

    id 'org.openrewrite.rewrite' version '6.28.3' apply false    // <-- 追加
}

/* 下記を追加 */

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

apply plugin: 'org.openrewrite.rewrite'

rewrite {
    activeRecipe("org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_3")
    activeRecipe("org.openrewrite.java.migrate.UpgradeToJava21")
}

repositories {
    mavenCentral()
}

dependencies {
    rewrite("org.openrewrite.recipe:rewrite-spring:5.25.0")
    rewrite("org.openrewrite.recipe:rewrite-migrate-java:2.31.0")
}

/* ここまで追加 */

今回は、SpringBoot3.3にするのに追加して、Java21にもアップデートします。 設定は、 rewriteの中に適用するレシピを記述します。今回は、UpgradeSpringBoot_3_3UpgradeToJava21を適用します。dependenciesには、それぞれの依存関係を書いておきます。

今回使用したレシピ以外にも、他の言語のものもあったりするので、探してみてください。

docs.openrewrite.org

OpenRewriteを実行する

./gradlew rewriteDryRun   // <-- コードは変更されません
./gradlew rewriteRun  // <-- コードが変更されます

実行後の差分

キャプチャで見づらいかもですが、SpringBootのバージョン変更、java tool chain のバージョン変更、javax -> jakarta パッケージの変更や、String.format -> "text".formatted など変更されています。

SpringBoot3へのマイグレーション

まとめ

小さいサンプルコードなので変更は少ないですけど、実際は、2000ファイルや4000ファイルくらい変更がある場合もあります。そして、ほぼすぐ動作しますので単体テストを通して、ある程度負荷とテストとかして1週間もあればマイグレーションはできると思います。こういうツールが充実しているのはサービスを運営していく身としては大変助かります。

JavaのEnumとSwitch式の良い関係

はじめに

今回は、私が好きで多用しているEnumとSwitch式の良い関係を書きます。

enum

Enumは、Java1.5(Tiger)のときに入った機能で、EffectiveJava(第3版)でも、第6章 enumとアノテーションアノテーションとセットですが、1章を割かれるくらいの機能になります。他の言語だとenumにはあまりスポットがあたっているのを見かけませんが、最近だとFlutter3にJavaと同じようなenumの機能が入っていました。

enumのよく使うケース

システム上で固定で判定できるものは、ほとんどenumで対応したりしています。下記はシステム上で使用するステータスを管理するenumになります。

public enum StatusType {
    TODO("作業前")
    , DOING("作業中")
    , CHECK("確認中")
    , DONE("完了")
    , OTHER("その他");

    private static final Map<String, StatusType> STATUS_TYPE_MAP = Arrays.stream(StatusType.values()).collect(Collectors.toMap(e -> e.name(), e -> e));  // 初期化時にMapに入れて取り出しやすくします
    
    private final String displayName;

    StatusType(String displayName) {
        this.displayName = displayName;
    }

    // データベース等に入っている文字列をこのメソッドを通すことで、enum型に変換します
    public StatusType getStatusType(String name) {
        return STATUS_TYPE_MAP.getOrDefault(name.toUpperCase(), StatusType.OTHER);
    }
}

このような感じのenumをシステムの挙動にあわせて、結構作成します。アプリケーション側ではenumの値によって処理を変えるということをやるときには下記のようになります。

class Sample {

    List<String> index(StatusType statusType){
        return switch (statusType) {
            case TODO -> todo();
            case DOING -> doing();
            case CHECK -> check();
            case DONE -> done();
            case OTHER -> other();
        }
    }
    
    // 各メソッドのみ記述(処理は省きます)
    private List<String> todo(){ return List.of();}
    private List<String> doing(){ return List.of();}
    private List<String> check(){ return List.of();}
    private List<String> done(){ return List.of();}
    private List<String> other(){ return List.of();}
}

enumのステータスが追加されることになったとき

上記のようなenumのステータスごとに分岐する処理があちらこちらに書いてあるとします。そのときに、enumの値が追加されると漏れなく全てを書き直さないといけません。enumとswitch式を使用している、enumに定義している定数を追加した際に、switch式の定義が足りないとコンパイルエラーが発生します。

public enum StatusType {
    TODO("作業前")
    , DOING("作業中")
    , CHECK("確認中")
    , CHECKED("確認済")    // <- ステータスの追加を行う
    , DONE("完了")
    , OTHER("その他");

(中略)

}

上記のように、ステータスの追加を行ってコンパイルをしてみましょう。

エラー: switch式がすべての可能な入力値をカバーしていません
        return switch (statusType) {
               ^

switch式がenumの全て要素の処理が書かれていないので、コンパイルエラーになります。これで、ステータスが追加されても処理の抜け漏れがなくて安全にステータスの追加や削除が行なえます。安心安全の型システムの恩恵を享受できています。

まとめ

enumは列挙型であるとともに、switch式と組み合わせると網羅性も担保されており、ドメイン知識が多いシステムのときにぬけもれがなく安心して開発ができるとても手軽で便利な機能だと思います。最近では、recordsealedを組み合わせて、enumでなくても網羅性のあるコードが書けるようになってきています。頑強なシステムを開発するのに重要な機能だと思いますので使いこなしていきたいです。

※この網羅性の機能はswitch式のみで使用できる機能であり、switch文では使用できません。

さいごに

久しぶりに個人ブログを書きました。しばらくは、所属会社の方のテックブログを頑張っていたのですが、これを機に個人ブログも再開しようかと思います。来年も良いJava開発ライフであると良いなと思います。

良いお年を!

MavenプロジェクトをGradleプロジェクトに変換する

MavenのプロジェクトをGradleのプロジェクトに変換したいときがあります。

変換する

下記のディレクトリをGradleプロジェクトに変換したい

$ ls
README.md   cdk.json    pom.xml     src
  • 変換する pom.xmlがあるディレクトリで、 gradle init を実行する。 pom.xmlが存在すると、Maven プロジェクトをみつけて、変換するかを選択できる。
$ gradle init

Welcome to Gradle 6.5!

Here are the highlights of this release:
 - Experimental file-system watching
 - Improved version ordering
 - New samples

For more details see https://docs.gradle.org/6.5/release-notes.html

Starting a Gradle Daemon (subsequent builds will be faster)

Found a Maven build. Generate a Gradle build from this? (default: yes) [yes, no] yes     // <- yes と入力する

Mavenにしかないものとかは動かないと思うので、それは一つずつ対応していくいかないです。

flywayのmultiple databases migration の設定メモ

本家はこちら

flyway でdb migrateしているが、公式ドキュメントではmultiple schemaは記述の仕方でなんとかなるんだけど、DBが分かれた場合は、地道にタスクを書くしかないっぽくて、そんなの嫌なのでちょっとメモ。

全体構成

一旦こんな感じのディレクトリ構成にしてみる。

├── build.gradle
├── conf
│   ├── example-admin.conf
│   └── example.conf
└── src
    └── main
        └── resources
            ├── example
            │   └── migration
            │       ├── V201801060359__create_account_table.sql
            │       └── V201801070137__create_table.sql
            └── example-admin
                └── migration
                    ├── V201801060359__create_account_table.sql
                    └── V201801070137__create_table.sql

resourcesディレクト

SQLはsrc/main/resources配下にexampleとexample-adminの配下にマイグレーション用のSQLファイルがおいてある。

build.gradle

汚いけど、taskを動的生成する

build.gradle

repositories {
    jcenter()
}

plugins {
    id 'org.flywaydb.flyway' version '6.4.1'
        id 'java'
}

["example", "example-admin"].each { name ->
    task  "${name}FlywayMigrate" (type: org.flywaydb.gradle.task.FlywayMigrateTask) {
        configFiles = ["conf/${name}.conf"]
    }

    task "${name}FlywayBaseline"(type: org.flywaydb.gradle.task.FlywayBaselineTask) {
        configFiles = ["conf/${name}.conf"]
    }

    task  "${name}FlywayClean"(type: org.flywaydb.gradle.task.FlywayCleanTask) {
        configFiles = ["conf/${name}.conf"]
    }

    task  "${name}FlywayInfo"(type: org.flywaydb.gradle.task.FlywayInfoTask) {
        configFiles = ["conf/${name}.conf"]
    }
}

こんな感じで、タスクを動的生成するようにする

confディレクト

ここにはDBとスキーマの定義ファイルがおいてある confファイルはこんな感じ

conf/example.conf

flyway.url=jdbc:postgresql://localhost:5432/example
flyway.user=example
flyway.password=example
flyway.schemas=public
flyway.locations=filesystem:src/main/resources/example/migration

conf/example-admin.conf

記事の効率的な読み方を試行錯誤

本家はこちら

最近、追う記事が多くてキャッチしきれなくなってきたので、スタイルを変えてみた。

もともと

TechCrunchとかはてぶとかTwitterとかで流れているものを、いちいち中身を追ってました。しかし、この方法だとどんどん流れるし、読んでて調べないとわからないことを調べているとどんどん流れていってしまいます。見る範囲が狭いときは、この方法でもよかったが、プログラミング、AWS、AI、マーケティング、英語、ビジネス、IR... とか増えていったときに、もう多すぎて流れていってしまって見落としが多いことに気づき、やり方を見直しを行いました。

Pocket

まず、読むだろう記事をPocketにストックしていくようにしました。以前はPocketを使っていたんですが、ここ2,3年全然つかっていなく、アップデートを確認してなかったんですが、音声再生の機能が格段にあがってました。コードが貼ってあるような記事はちょっと辛いのですが、それ以外のビジネス関係の読み物にはかなりあっていて、ほとんどこれで再生して聞いています。聞いている間に別の記事をストックしていくという感じで、短縮もできています。

Evernote or Notion

Pocketで格納した記事で残したいものは、さらにEvernoteとかNotionにシェアします。そうするとEvernote or NotionでWebクリップをしてくれるので残せます。

まとめ

気になったタイトルとかキーワードの記事はさっとPocketにためて、Pocketからまとめて読んだり、音声再生をして聞いたりして、残したいものはEvernoteかNotionにWebクリップすれば、かなりいいスクラップブックになるかなと思いました。他にもいいやり方があれば教えていただけるとー。

MyBatisが色々てんこ盛りになっていい感じになっている件

本家はこちら

久しぶりにSpringBootの環境構築をやろうかと思い、ORMをどうするか選定していたところ、MyBatisが結構進化していたので、メモ。

MyBatis3DynamicSql

こんな感じでかけます。

        List<TableCode> records = mapper.selectByExample()
                .where(id, isEqualTo(3))
                .build()
                .execute();       

        List<TableCode> records = mapper.selectByExample()
                .where(id, isEqualTo(3))
                .or(description, isLike("f%"))
                .build()
                .execute();    

        List<TableCode> records = mapper.selectByExample()
                .where(id, isLessThan(10), and(description, isEqualTo("foo")))
                .or(description, isLike("b%"))
                .orderBy(id.descending())
                .build()
                .execute(); 

JOOQみたいに、タイプセーフでSQLがかけるのはとてもすごい進化だと思います。

FreeMarker Tempalte

前に書いたとおりのFreeMarker Templateは健在。XMLがいやでFreeMarkerにしたけど、複雑なクエリはやはりJavaコードじゃなくて、素のSQLの方がいいイメージなので、継続して使用します。

sample.ftl

select
 *
from
  sample_table
where
  id = <@p name="param.id" />

こんな感じでかける。

from句とかを別のftlファイルにして、includeして使うことも可能です。

Thymeleaf Template

Doma2とかでやっている2WaySQLがThymeleaf Templateで実現している。Doma2より書き方は野暮ったいけど、一応2waySQLはできる。

SELECT * FROM names
  WHERE 1 = 1
  /*[# th:if="${not #lists.isEmpty(ids)}"]*/
    AND id IN (/*[# mb:p="ids"]*/ 1 /*[/]*/)
  /*[/]*/
  ORDER BY id

MyBatis Migrate

これは前からあったみたいなんだが、Flywayの陰に隠れて知らなかった。 機能的には十分っぽいので、試してみる。

詳細 => https://mybatis.org/migrations/

Mybatis Log Plugin (Intellijのみ)

これがあると、条件分岐があって、引数のバインドが全部終わったSQLを出力してくれるので、デバッグに便利です。 必需品かと思います。(Domaは標準でありますが)

https://plugins.jetbrains.com/plugin/10065-mybatis-log-plugin

最後に

タイプセーフでSQLかけるし、2WaySQLあるし、Migrateあるし、なんかMyBatisで全部OKなんじゃないかとおもいました。おそらくThymeleafは

GraalVMのnative-imageを少し試してみる

本家はこちら

GraalVMのnative-imageがそろそろ色々使えそうなので、試してみる。

Javaやってる人は SDK Man は入ってると思うので、それ前提でメモします。 (SDK Man のインストールはこちら -> https://sdkman.io/ )

GraalVM のインストール

まずは、GraalVMのインストールを行う。

$ sdk install java 20.0.0.r11-grl

GraalVMを使ってnative-imageをinstallする

GraalVMのパッケージツールでnative-imageをinstallする。

$  gu install native-image

これでnative-imageのinstallが完了。

かんたんなJavaコードを書く

Sample.java

class Sample {
  public static void main(String[] args) {
   System.out.println("Hello World");
  }
}

Javaコンパイルする。

$ javac Sample.java

コンパイルすると、classファイルができる。

Sample.class

native-imageでコンパイルする


$ native-image Sample sample // 最後のファイル名を指定しないと、デフォルトで小文字になる

下記、ファイルができる。

Sample

このファイルは実行できる。

./sample

比較する

JavaVMでの実行とnative-imageでの実行を比較する

$ time ./sample
real    0m0.007s
user    0m0.003s
sys     0m0.003s

======

$ time javac Sample
real    0m0.179s
user    0m0.110s
sys     0m0.035s

JavaVMの起動速度が入ってしまっていると思うが、結構違う。 JVMの起動後にどのくらい違うのかはまた次回。 あと、こんな1ファイルで使う人もいないと思うので、Jar での作成方法もまた次回。