Spring Bootのサンプルパッケージ構成の考古学

Spring Framework/Spring Bootでは(Java EEとかでもそうだけど)、かつて"controller", "service", "repository"といったレイヤーによってパッケージを分ける構成が主流でした。この"package by layer"の手法はドメインの要求よりもフレームワークといった技術的な制約の中で生まれたものです。というのも、レイヤードアーキテクチャへの過度な適応であったり、パッケージ=レイヤーのほうがクラススキャン・AOPにとって都合が良かったりしたという経緯がありました。

ですが、ドメイン駆動設計(ないしドメインを重要視する志向)の影響もあり、機能やドメインでパッケージをまとめる構成(package by feature)が普及していきます。実際にSpring Bootのリファレンスでは、前述の技術的な制約がなくなってきたこともあり、package by featureを典型的(typical)な例として紹介しています。

ですが、このサンプルはあくまで例とは言え若干の違和感があります。それはプレゼンテーション層となるControllerがドメインクラスと一緒のパッケージに置かれている点です。

そもそもSpringのリファレンスがどのような思想でこのサンプルにしたか、歴史的経緯を探ってみました。

実は、このサンプルのパッケージ構成は最初からこうなっていたわけではなく、Spring Boot 2.0.x(2018年)からのことです。その前のSpring Boot 1.5ではなんとpackage by layer的なサンプルになっていたのです。

なぜ2018年にpackage by layerからpackage by featureに変更したのか。その理由がこのIssueに書いてありました。

github.com

現在のサンプルの元ネタは2016年当時PivotalのMike Gehard氏による、"The Journey From Monolith to Microservices: A Guided Adventure"という発表で紹介されていたパッケージ構成(vertical slices)でした。タイトルから分かる通りモノリスからマイクロサービスへの移行という観点で、featureごとのパッケージがbounded contextとなり、その単位でマイクロサービスへの分割が容易になる、という趣旨です。(一応書くと、Mike Gehard氏がpackage by feature/vertical slicesの発案者ということではありません。)

余談ですが、2018年ごろからSimon Brown氏による講演や、Jimmy Bogard氏による記事に見られるようにモジュラモノリスに関する議論が盛り上がりを見せ始め、Shopifyの事例によって一気に一般に広がります。この当時の大きな課題意識として、モノリスからマイクロサービスへの移行、またはマイクロサービス化を見据えたモノリス(=モジュラモノリス)があったのだと分かります。

こういった経緯を踏まえると、Controllerとドメインクラスが一緒のパッケージに置かれた理由が見えてきます。Springのサンプルでは、featureごとのパッケージは単独でマイクロサービスに切り出し可能であるほど独立性が高くあるべきという前提があったのです。

もちろん、サンプルにそこまでの含意があったかは分かりませんし(むしろ単純に脱package by layerしたかっただけなんじゃないかと思う)、これが唯一の正解のパッケージ構成というわけではありません。背景や歴史的経緯は横目に見つつ、目の前の状況に合わせてエッセンスを盗むくらいでいいんじゃないかなーと思います。

勝手にツイートを引用させていただいたnagiseさん、asano_koさん、masuda220さんに感謝申し上げます。

Spring BootでJEP 512 (Instance Main Method)を試してみた

Java 25リリースおめでとうございます!Javaのバージョンに年齢を超されるまでもう10年かからなくなって来ました。

Java 25の新機能として、JEP512:Compact Source Files and Instance Main Methodsがあります。

gihyo.jp

皆さんおなじみの呪文、 public static void main(String[] args)void main() と書くだけで良くなったということで、初学者の方のJavaを学ぶハードルが下がることが期待されています。(ただ個人的な見方ですが、mainメソッドなんてIDEで一発で作成できるうえに、入門書の分量が増えて覚えることが多くなっただけでは…?という気がしています)

さて、お仕事でコードを書く中でmainメソッドを作る機会はあまり無いのが実際のところですが、その数少ない機会の一つがSpring Bootのエントリポイントとしてのmainメソッドです。

package com.github.rshindo.java25spring;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Java25springApplication {

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

このmainメソッドをインスタンスメソッドにするとSpring Bootは正常に起動するのでしょうか?やってみましょう。

@SpringBootApplication
public class Java25springApplication {

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

ログ

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.5.5)

2025-09-17T21:23:56.802+09:00  INFO 22548 --- [java25spring] [           main] c.g.r.j.Java25springApplication          : Starting Java25springApplication using Java 25 with PID 22548 (/Users/shindo/git/rshindo/java25spring/target/classes started by shindo in /Users/shindo/git/rshindo/java25spring)
2025-09-17T21:23:56.802+09:00  INFO 22548 --- [java25spring] [           main] c.g.r.j.Java25springApplication          : No active profile set, falling back to 1 default profile: "default"
2025-09-17T21:23:56.963+09:00  INFO 22548 --- [java25spring] [           main] c.g.r.j.Java25springApplication          : Started Java25springApplication in 0.326 seconds (process running for 0.601)

Process finished with exit code 0

問題なく起動しました。これにて完!!




というだけではつまらないので、もうちょっと遊んでみます。

mainメソッドをインスタンスメソッドにできるということは、クラスのインスタンスができているはずです。さらに @SpringBootApplication がついたクラスのインスタンスはSpring BeanとしてApplicationContextに登録されます。これらのインスタンスは同一でしょうか?

コードを次のように変更して確認してみます。せっかくなので、Java 25で追加されたjava.lang.IOも使いましょう。static importすれば println() だけで標準出力することができます。素敵ですね。

package com.github.rshindo.java25spring;

import static java.lang.IO.println;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class Java25springApplication implements CommandLineRunner {

    @Autowired
    ApplicationContext applicationContext;

    void main(String[] args) {
        println("main: " + this);
        println("applicationContext: " + applicationContext);
        run(args);
        SpringApplication.run(Java25springApplication.class, args);
    }

    @Override
    public void run(String... args) {
        println("run: " + this);
        println("applicationContext: " + applicationContext);
    }
}

ログ(関係する部分だけ抜き出し)

# mainメソッド
main: com.github.rshindo.java25spring.Java25springApplication@60c6f5b
applicationContext: null

# mainから呼び出したrunメソッド
run: com.github.rshindo.java25spring.Java25springApplication@60c6f5b
applicationContext: null

# CommandLineRunnerで呼び出したrunメソッド
run: com.github.rshindo.java25spring.Java25springApplication@b0964b2
applicationContext: org.springframework.context.annotation.AnnotationConfigApplicationContext@73cd37c0, started on Wed Sep 17 21:33:56 JST 2025

ハッシュ値を見るとmainが呼ばれたthisと、CommandLineRunnerでrunを呼ばれたthisが異なるインスタンスであることがわかります。さらに、当然のことながらmainが呼ばれた段階ではApplicationContextのAutowiredは働かずnullのままです。

ちなみに、コンストラクタインジェクションの場合どうなるんだ?という疑問ですが、mainをインスタンスメソッドにする場合は引数なしのコンストラクタが必要になるため、そもそもコンストラクタインジェクションにすることはできません。

ということでJEP512を試してみましたが、Spring Bootでインスタンスmainメソッドにしても良いか?ということについては個人的には否定的です。上記のコードの例のように、mainメソッドから他のインスタンスメソッドを呼び出せてしまうのが微妙ですし、psvmが短く書けたところでさほどメリットがないと考えてます。

JEP512を少し悪く言う感じになってしまいましたが、Java 25にはScoped Valuesなどかなり使えそうな機能が沢山あるのでどんどん使っていきましょう〜

Spring Boot, Testcontainers, ローカル環境の作り方

この記事はChatGPTを利用して執筆されて…ません。

コンテキスト

現代(と言ってもしばらく前からですが)的なアプリケーションは、単一のアプリ(+RDB)だけで完結するものではなく、多種多様なアプリやミドルウェアと協調して動作することが多々あります。そこで問題の一つとして挙がってくるのが、ローカル環境構築の複雑化です。

依存しているミドルウェアをインストールし、他サービスをコードから実行またはAPIスタブを起動し、必要な初期データを投入し…長大で複雑な手順は初期構築コスト、管理コストの増大を招きます。

コンテナを使うことで環境構築コストは下がりましたが、コンテナ起動 -> アプリ実行という順序を開発者自身が気をつけなければならないケースは多いと思います。(何度アプリ実行してからコンテナ起動していないことに気づいたことか…)

Spring Boot, そしてTestcontainers

話は変わり、Spring Boot 3.0からTestcontainersサポートが入りました。Testcontainersを使うことで、例えばPostgreSQLのコンテナをテスト実行前に自動的に起動し、テスト終了後に自動的に終了する、といったことが可能になります。言わばテストのための使い捨てコンテナの自動化です。

例として、Spring Initializrで Testcontainers, PostgreSQL Driver, JDBC API を追加してプロジェクトを出力してみます。するとtest sourcesにTestcontainers用のConfigurationが追加されているのが見えます。

package com.example.demo;

import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.utility.DockerImageName;

@TestConfiguration(proxyBeanMethods = false)
class TestcontainersConfiguration {

  @Bean
  @ServiceConnection
  PostgreSQLContainer<?> postgresContainer() {
    return new PostgreSQLContainer<>(DockerImageName.parse("postgres:latest"));
  }

}

テストを実行してみると、PostgreSQLのコンテナが自動的に起動していることが分かります。なお、初回実行時にはイメージがpullされるログも出るはずです。

2025-05-29T00:41:39.609+09:00  INFO 82565 --- [demo] [           main] com.example.demo.DemoApplicationTests    : Starting DemoApplicationTests using Java 21.0.2 with PID 82565 (started by shindo in /Users/shindo/git/20250529/demo)
2025-05-29T00:41:39.609+09:00  INFO 82565 --- [demo] [           main] com.example.demo.DemoApplicationTests    : No active profile set, falling back to 1 default profile: "default"
2025-05-29T00:41:39.812+09:00  INFO 82565 --- [demo] [           main] org.testcontainers.images.PullPolicy     : Image pull policy will be performed by: DefaultPullPolicy()
2025-05-29T00:41:39.813+09:00  INFO 82565 --- [demo] [           main] o.t.utility.ImageNameSubstitutor         : Image name substitution will be performed by: DefaultImageNameSubstitutor (composite of 'ConfigurationFileImageNameSubstitutor' and 'PrefixingImageNameSubstitutor')
2025-05-29T00:41:39.827+09:00  INFO 82565 --- [demo] [           main] org.testcontainers.DockerClientFactory   : Testcontainers version: 1.21.0
2025-05-29T00:41:40.019+09:00  INFO 82565 --- [demo] [           main] o.t.d.DockerClientProviderStrategy       : Found Docker environment with Docker accessed via Unix socket (/Users/shindo/.docker/run/docker.sock)
2025-05-29T00:41:40.020+09:00  INFO 82565 --- [demo] [           main] org.testcontainers.DockerClientFactory   : Docker host IP address is localhost
2025-05-29T00:41:40.034+09:00  INFO 82565 --- [demo] [           main] org.testcontainers.DockerClientFactory   : Connected to docker: 
  Server Version: 28.1.1
  API Version: 1.49
  Operating System: Docker Desktop
  Total Memory: 7937 MB
  Labels: 
    com.docker.desktop.address=unix:///Users/shindo/Library/Containers/com.docker.docker/Data/docker-cli.sock
2025-05-29T00:41:40.045+09:00  INFO 82565 --- [demo] [           main] tc.testcontainers/ryuk:0.11.0            : Creating container for image: testcontainers/ryuk:0.11.0
2025-05-29T00:41:40.144+09:00  INFO 82565 --- [demo] [           main] tc.testcontainers/ryuk:0.11.0            : Container testcontainers/ryuk:0.11.0 is starting: fad4b10c28361ca998c772451b23e777f31f666362e8a4181e8de05e4aa4cf5b
2025-05-29T00:41:40.296+09:00  INFO 82565 --- [demo] [           main] tc.testcontainers/ryuk:0.11.0            : Container testcontainers/ryuk:0.11.0 started in PT0.251254S
2025-05-29T00:41:40.299+09:00  INFO 82565 --- [demo] [           main] o.t.utility.RyukResourceReaper           : Ryuk started - will monitor and terminate Testcontainers containers on JVM exit
2025-05-29T00:41:40.299+09:00  INFO 82565 --- [demo] [           main] org.testcontainers.DockerClientFactory   : Checking the system...
2025-05-29T00:41:40.299+09:00  INFO 82565 --- [demo] [           main] org.testcontainers.DockerClientFactory   : ✔︎ Docker server version should be at least 1.6.0
2025-05-29T00:41:40.299+09:00  INFO 82565 --- [demo] [           main] tc.postgres:latest                       : Creating container for image: postgres:latest
2025-05-29T00:41:40.322+09:00  INFO 82565 --- [demo] [           main] tc.postgres:latest                       : Container postgres:latest is starting: 3c4c90377981a5472e05c651d5ccef0088bd0bf4467af8c1528f7155a00962d2
2025-05-29T00:41:41.032+09:00  INFO 82565 --- [demo] [           main] tc.postgres:latest                       : Container postgres:latest started in PT0.732824S
2025-05-29T00:41:41.033+09:00  INFO 82565 --- [demo] [           main] tc.postgres:latest                       : Container is started (JDBC URL: jdbc:postgresql://localhost:50859/test?loggerLevel=OFF)
2025-05-29T00:41:41.144+09:00  INFO 82565 --- [demo] [           main] com.example.demo.DemoApplicationTests    : Started DemoApplicationTests in 1.648 seconds (process running for 2.166)

なお、Testcontainersで立ち上げたコンテナに接続するためのプロパティ(JDBC URL)などを自動設定するService Connectionという仕組みもSpring Bootにあるのですが、説明は省略します。

この仕組みで起動したコンテナは使い捨てのため、Spring BootのApplicationContextの起動にフックすれば初期データ投入を手動で自由度高く、かつ他のテスト実行に干渉しないかたちで行うことができます。例えば、CSVファイルを読み込んでDBに投入するなど。データ投入されたコンテナはテスト終了時に破棄されるため、次のテスト実行の際にはきれいまっさらな状態のコンテナを使うことができるわけです。

ローカル環境構築

Spring Initializrで出力したプロジェクトのtest sourcesに、見慣れないクラスが一つあります。

package com.example.demo;

import org.springframework.boot.SpringApplication;

public class TestDemoApplication {

    public static void main(String[] args) {
        SpringApplication.from(DemoApplication::main).with(TestcontainersConfiguration.class).run(args);
    }

}

このクラスを実行すると、Spring Bootアプリが起動されると同時に、Testcontainersにより依存コンテナも自動的に起動されます。話がローカル環境構築に戻り、このクラスによってdocker-compose.yamlを書くことなく、mainメソッドを実行するだけでアプリ実行することが可能です。

今までの方式を図にするとこんな感じです。

それがTestcontainersによりこうなるわけです。

このやり方でローカル実行することで、次のようなメリットがあります。

  • リポジトリをpullしてTest~~Applicationをmain実行するだけでコンテナと一緒にローカル環境構築できてしまう
  • ローカル実行時のみ必要な処理(初期データ投入など)、コンポーネント(モックなど)をtestスコープに閉じ込めることができる

特に2番目のメリットは大きく、productionコードからローカルDBの接続先といったプロパティも排除することができます。デプロイ時に環境変数の指定が漏れていて、デフォルトのローカル用プロパティが使われていたみたいな事故も起きなくなりますね。

大規模なアプリ、システムだと初期データ、テストデータの管理など追加で考えるべき課題は出てくると思いますが、今のところこのやり方でかなり上手く行っているので、Testcontainersおすすめです。

Spring Boot 3.4の構造化ロギングを試してみる

こちらはJava Advent Calendar 2024の8日目です。昨日はkumazoさんの「Java Foreign Function & Memory API で WebGPU を叩く」でした。

Spring Boot 3.4で構造化ロギングのサポートが入りました。以前は構造化ログにするためにはecs-logging-javalogstash-logback-encoderを使う必要がありましたが、とうとうSpring Bootの依存のみで可能になったわけです。

Springレベル3くらいのプログラマとして早速試してみます。

なおリファレンスはこちら。

docs.spring.io

ロギングフォーマット

構造化ログフォーマットは以下の3つがサポートされています。

  • Elastic Common Schema (ECS)
  • Graylog Extended Log Format (GELF)
  • Logstash

ECSはElasticがログ解析用に策定したJSON仕様ですね。Logstashはlogstash-logback-encoderのフォーマットのようですが、 @timestamp などECSと一部共通している部分もあります。

SLF4J + Logback

まずはSpring BootデフォルトのLogbackで試してみます。

構造化ロギングを有効化するにはプロパティの logging.structured.format.console を変更します。

logging:
  structured:
    format:
      console: ecs

ログ出力のJavaコードは特別な設定を必要としませんが、せっかくなのでSLF4J 2.0から追加されたFluent APIを使い、独自定義のフィールドに値を出力してみます。

package com.example.springmvcexample;

import java.util.concurrent.atomic.AtomicInteger;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(path = "/greeting")
public class GreetingController {

    final AtomicInteger counter = new AtomicInteger();

    private static final Logger log = LoggerFactory.getLogger(GreetingController.class);

    @GetMapping
    public String greet(@RequestParam String name) {
        int count = counter.getAndIncrement();
        log.atInfo()
                .addKeyValue("greeting.count", count)
                .log("this is slf4j logger");
        return "Hello " + name + "!";
    }
}

これを実行すると以下のJSONが出力されます。(見やすいように改行を入れています)

{
    "@timestamp": "2024-12-08T04:07:40.882835Z",
    "log.level": "INFO",
    "process.pid": 37694,
    "process.thread.name": "http-nio-8080-exec-1",
    "service.name": "spring-mvc-example",
    "log.logger": "com.example.springmvcexample.GreetingController",
    "message": "this is slf4j logger",
    "greeting.count": 0,
    "ecs.version": "8.11"
}

@timestamp, message, greeting.count 以外にも各メタデータが自動的に追加されているのが分かります。

SLF4J + Log4j2 (は上手くいかない)

Spring BootはLogback以外にもLog4j2をサポートしています。Log4j2を入れるにはpom.xmlを以下のように編集します。

    <dependencies>
        ...

+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-log4j2</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-starter-logging</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>

    </dependencies>

コードは先程と同じくSLF4Jを使います。再度実行してみると…

{
    "@timestamp": "2024-12-08T04:14:55.193894Z",
    "log.level": "INFO",
    "process.pid": 37864,
    "process.thread.name": "http-nio-8080-exec-1",
    "service.name": "spring-mvc-example",
    "log.logger": "com.example.springmvcexample.GreetingController",
    "message": "this is slf4j logger",
    "greeting.count": "0",
    "ecs.version": "8.11"
}

一見問題ないように見えますが、greeting.countがnumberではなくstringになってしまっています。例えばElasticsearchでこのフィールドのsumを取りたい場合に、フィールドのtypeがstringだと集計できないという問題が発生してしまいます。

SLF4JがLog4j2にブリッジする際にaddKeyValue(key, value)で追加された値をLog4j2のThreadContextMapに入れているのですが、ThreadContextMapがKey-ValueをString-Stringで保持しているため元のvalueがIntegerなどでもtoStringされてしまうのが原因で、この問題が発生しているようです。(Issueもありますが、特に動きはない状況です。)

ではSLF4Jを使わずLog4j2のAPIを直接呼び出した場合はどうでしょうか。StringMapMessageを使って同様のログを出してみます。

package com.example.springmvcexample;

import java.util.concurrent.atomic.AtomicInteger;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.message.StringMapMessage;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(path = "/greeting")
public class GreetingController {

    final AtomicInteger counter = new AtomicInteger();

    private static final org.apache.logging.log4j.Logger log4jLogger = LogManager.getLogger();

    @GetMapping
    public String greet(@RequestParam String name) {
        int count = counter.getAndIncrement();
        log4jLogger.info(new StringMapMessage()
                .with("message", "this is log4j logger")
                .with("greeting.count", count));
        return "Hello " + name + "!";
    }

}

今度は次のJSONが出力されました。message フィールドが、StringMapMessageJSONシリアライズしたオブジェクトになってしまっています。

{
    "@timestamp": "2024-12-08T04:27:56.847793Z",
    "log.level": "INFO",
    "process.pid": 37970,
    "process.thread.name": "http-nio-8080-exec-1",
    "service.name": "spring-mvc-example",
    "log.logger": "com.example.springmvcexample.GreetingController",
    "message": {
        "greeting.count": 0,
        "message": "this is log4j logger"
    },
    "ecs.version": "8.11"
}

同じコードでも ecs-logging-javaのlog4j2-ecs-layoutを使うと問題なく出力されるのですが、Spring Bootだとまだそこまではサポートされていないようです。

まとめ

これから新規で作るSpring Bootプロジェクトは、依存ライブラリを減らすために構造化ロギングサポートを使ったほうが良いです。一方で、すでに3rd-partyライブラリを使って構造化ロギングを実現している場合は、上記のLog4j2のような非互換性の問題が出てくる可能性があるため、無理に乗り換える必要は無いと思います。

明日は@Telethaさんの記事です。それでは良い年末を!

Rustに入門してみた その1

ふと思い立ってRustを触ってみることにした。

そこまで強い目的意識は無いが、FaaS/サーバーレス環境で起動のオーバーヘッドが少ない言語の選択肢を持っておきたいという気持ちはある。Goも使ってはいるが、いまいち肌に合わないというかフラストレーションが溜まるポイントがいくつかあるので、Rustに目を向けてみたという経緯だ。

とりあえず書籍『実践Rustプログラミング入門』の内容を一通り試すことを目標にした。正直どの本が良いか皆目分からなかったけど、手を動かすこと優先の構成は自分に合っていると思う。

www.shuwasystem.co.jp

環境構築

公式ページのコマンドでRustツールチェインを一発でインストールできる。(Mac OS)

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

はじめに - Rustプログラミング言語

本に記載の通り、 cargo new でプロジェクトを作成して cargo run で実行してみる。なおcargoはRust標準のパッケージマネージャー兼ビルドツールとのこと。ビルドツールがいまいちで乗り換えもできないとその言語を使う気持ちが削がれるが、手軽さ考えると言語標準があったほうが良いとは思う。

$ cargo new hello
     Created binary (application) `hello` package
$ cd hello/
$ cargo run
   Compiling hello v0.1.0 (/~~~/hello)
    Finished dev [unoptimized + debuginfo] target(s) in 4.22s
     Running `target/debug/hello`
Hello, world!

本の指示の通りcargo-editを入れることに。cargo-editはパッケージの追加・削除・アップグレード等をコマンドで実行できるツール。

インストール時に以下の記事と同じ問題が発生したが、opensslをインストールすることで解決。 cargo-editインストール時のエラー

$ brew install openssl
$ cargo install cargo-edit
(中略)
   Installed package `cargo-edit v0.12.2` (executables `cargo-add`, `cargo-rm`, `cargo-set-version`, `cargo-upgrade`)

IDEIntelliJ IDEAにRustプラグインを入れて使うことにした。Ultimateライセンスをすでに持っているのもあるが、Language Serverを入れるのが面倒というのが大きい。(初学者なのに周辺ツールの問題でつまづきたくない)

文法(基本的な型、制御構文)

文法に関しては感想だけ。

  • Tuple, Option, ResultなどScalaとかでよく見たやつがあるのは嬉しい
  • 可変長のリストの名前がVec(ベクタ型)なのはJavaVectorを思い出してしまいちょっとだけ混乱する
  • 固定サイズの値はスタックに格納できるが、可変サイズの場合は Box 型を使って明示的にヒープに置く必要がある。文法レベルでメモリ管理と言った低レイヤのことを意識しないといけない。

今日はここまで。

Spring BootでWebアプリを作るときの第一歩

Spring Bootで初めてWebアプリを作ろうとすると最初につまづくのが「どういうライブラリ組み合わせればええんや…」というポイントなので、自分の中にある選定パターンをメモします。ここで説明するのはWeb層とDBアクセス層をどうするかという部分だけです。

あまり真面目な記事ではないので、ちゃんとした情報を知りたい人はSpring Academyとかを見るのが良いと思います。

マイ・パターン

灰色になっているのは仕事では使ったことのない組み合わせです。

Spring WebFlux + Spring Data R2DBC

Spring初心者は無視して良いです。自分も使ったことがあるのはDBアクセスなしのWebFluxだけです。 Microservicesで言うところのAPI Gatewayだったりリバースプロキシだったり、特定のユースケースのときしか選択肢に上がりません。

Spring MVC

@Controllerとか@RestControllerとか使ういつものアレです。なお「Springってアノテーションだらけの奴でしょ?」という発言には「アノテーションレスのSpring Web MVC.fnとかあるが?」というツッコミが飛んでくるらしいです。

Spring JDBC

Springプロジェクトの一つで、JdbcTemplateなどが使えるライブラリです。javax.sqlをラップした程度の機能しかありませんが、Spring本体と何の問題もなく組み合わせることができますし、小規模なPJであればこれで十分というケースは多いです。

Spring Framework 6.1 (Spring Boot 3.2) からはJdbcClientというFluent APIを使うことができて、ちょっとだけおしゃれに書くことができます。

Spring Data JPA

JPA使いたいときはこれ。逆に言うとJPAじゃなくても良いときは選ばないほうが良いです。

Spring Data JPAJPAとしての機能のほか、Repositoryインタフェースに findByUserId(long userId) みたいなメソッドを宣言するとメソッド名から自動的にクエリを生成してくれるという黒魔術が使えます。

Spring Data JDBC

非常に紛らわしいですが、Spring JDBCとは異なります。

出始めの頃に一度触ってみましたが、最近はどうなっているのかあまり詳しくありません。

Spring Data JPAからJPAを抜いたような感じ(?)で、名前のスタンダード感に反して結構クセが強い印象です。

Doma

ここから先はサードパーティライブラリです。

最近よく使ってるやつ。元Seasarプロジェクトらしく、SQLテンプレートを書くことができます。 カラムの値をドメインと呼ばれるJavaオブジェクトとして扱えるのが大きな特徴です。(例えば、Eメールアドレスを EmailAddress クラスのオブジェクトのまま扱ったり、区分値をEnumのまま扱ったり)

ビルド時にDaoインタフェースの実装クラスを自動生成するので、それに抵抗感がなければ結構おすすめです。

MyBatis

SQLを直接かけるやつ第2弾。

こちらは.sqlファイルではなくXMLファイルにSQLを書きます。(なので">"をエスケープしなければならないのが若干面倒)XMLの設定は柔軟で割りと何でもできますが、やりすぎるとXML地獄になるので注意。

ネット上の情報量が多く、コードジェネレーターなどの周辺ツールも充実しているので安牌と考える人は多いと思います。

まとめ

きちんと比較検討したい人は多田さんのスライドを読むのが良いと思います。今読んでも参考になります。

Java ORマッパー選定のポイント #jsug | ドクセル

DBアクセス周りはR2DBCなどを除いて枯れ気味の分野なので、たぶんしばらくはこのパターンから変わることはないんじゃないかなあ。