Spring Boot 3の新機能を使ってみよう! 2からアップグレードする手順、Observability機能、ネイティブイメージ化

Javaの開発フレームワークであるSpringの最新バージョンとして、Spring Boot 3が2022年11月にリリースされました。この記事ではSpring Boot 2で書かれたサンプルコードをSpring Boot 3にアップグレードしながら、考慮点や新機能を体感していただきます。ヴイエムウェア株式会社の星野真知さんによる解説です。

Spring Boot 3の新機能を使ってみよう! 2からアップグレードする手順、Observability機能、ネイティブイメージ化

Javaのエコシステム、その中でも世界で一番の人気を誇るのがJetBrains社の調査によると)Spring FrameworkおよびSpring Bootです。Spring Frameworkは軽量なJavaフレームワークとして2004年の1.0に始まり、2014年のSpring Boot 1.0のリリースを皮切りにクラウドネィティブへの対応がなされてきました。軽量なREST APIからエンタープライズ向けのバッチ処理まで、さまざまなシーンで使われています。

そんな中、2022年11月に大きな発表がありました。それがSpring Framework 6、そして若干のタイムラグの後に発表されたSpring Boot 3です。そのリリースノートには膨大な量の更新が記載されていました。

Spring Boot 3.0 Release Notes · spring-projects/spring-boot Wiki

中でも大きな変更ポイントとなるのが、以下の4つです。

  • ベースラインとしてJava 17を採用
  • Jakarta EE 9のベースライン化およびEE 10のサポート
  • 可観測性(Observability)の強化
  • GraalVMによるネイティブイメージのサポート

この記事ではSpring Boot 2で動作するサンプルコードを用意して、Spring Boot 3にアップグレードする手順を紹介します。実際のアップグレード作業を通して、上記の変更がどう影響しているのか? どういった新しいことができるのか? などを紹介します。 できればSpring Boot 2の経験があるとよいでしょう。

キューを読み書きするサンプルコードをSpring Boot 2で動作させる

以下のリポジトリに、本記事で使用するサンプルコードがあります。

mhoshi-vm/sb3-demo

手元のPCに適当な作業用のディレクトリを作成し、以下の手順でコードをダウンロードしてください。

$ git clone https://github.com/mhoshi-vm/sb3-demo

展開されるファイルはMaven Wrapper(mvnw)で起動するSpring Boot 2のプロジェクトとなっています。

なお、今回のサンプルを最後まで試すには、以下の環境が必要になります。

これらが揃っていればPCでもサーバーでも実施できます。

サンプルコードの構造

サンプルコードは、dataproducer(Data Producer)とdataconsumeandexpose(Data Consumer and Expose)という2つのWebサービスと、メッセージキュー(RabbitMQ)からなるシンプルな構造をしています。

  • dataproducerとして、REST API経由で取得したデータを、RabbitMQのキューに保管
  • dataconsumeandexposeが、RabbitMQ経由でデータを取得
  • dataconsumeandexposeが、ローカルのデータベースへデータを保管
  • dataconsumeandexposeが、REST API経由で全てのデータを表示

図式化すると以下のようになります。

boot1

RabbitMQの起動

サンプルコードを実行する前に、メッセージキュー(RabbitMQ)を起動しておく必要があります。

以降の解説では、RabbitMQが次の手順に従ってインストールされ、ユーザーとパスワードはデフォルトのまま(guest/guest)で起動されていることを想定しています。

The Homebrew RabbitMQ Formula — RabbitMQ

RabbitMQを起動すると、デフォルトでは次のURLで管理ダッシュボードにアクセスできます。

http://localhost:15672

管理ダッシュボードはこの後の解説でも参照します。

データをキューに入れるdataproducerの動作

サンプルコードを動作させてみます。まず、dataproducerを起動します。

$ cd dataproducer
$ ./mvnw spring-boot:run

サービスがlocalhostの8082番ポートで立ち上がるので、次のようにcurlを使って簡単なデータをREST APIで入力してみます。

$ curl http://localhost:8082/produce -H "Content-Type: text/plain" -d 'aaa' -X POST

データが作成され、以下のように表示されます。

Data Produced

RabbitMQの管理ダッシュボードにログインすると、以下のように「demoQueue」というキューが生成されています。またcurlを何度か実行すると、その回数に応じてreadyがカウントアップされるでしょう。

boot2

データをキューから取り出すdataconsumerandexposeの動作

次に、dataconsumerandexposeを起動します。

$ cd dataconsumerandexpose
$ ./mvnw spring-boot:run

サービスがlocalhostの8083番ポートで立ち上がるので、以下のコマンドでデータを取得します。

$ curl http://localhost:8083/get

先ほどのようにdataproducerに対してcurlを実行した直後であれば、以下のように出力されます。

[{"id":1,"data":"aaa"}]

RabbitMQの管理ダッシュボードでも、readyの値が0になります。このようにcurlの実行に応じてデータ数などが変わります。

boot3

このシンプルなコードを例に、Spring Boot 3に上げてみたいと思います。

いざSpring Boot 3にアップデート

まずは深く考えずに、このサンプルコードをそのままSpring Boot 3に上げてみましょう。それぞれのサービスのPOMファイルを修正します。

ベースラインとしてJava 17を採用

まず、dataproducerをSpring Boot 3にします。dataproducerディレクトリにあるpom.xmlを次のように書き換えます。versionでは、執筆時点でSpring Bootの最新バージョンである3.1.0を選択しています。

$ diff --git a/dataproducer/pom.xml b/dataproducer/pom.xml
index 9d8662e..076b1f3 100644
--- a/dataproducer/pom.xml
+++ b/dataproducer/pom.xml
@@ -5,7 +5,7 @@
     <parent>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-parent</artifactId>
-        <version>2.7.12</version>
+        <version>3.1.0</version>
         <relativePath/> <!-- lookup parent from repository -->
     </parent>
     <groupId>com.engineerhub</groupId>
@@ -14,7 +14,7 @@
     <name>dataproducer</name>
     <description>dataproducer</description>
     <properties>
-        <java.version>11</java.version>
+        <java.version>17</java.version>
     </properties>
     <dependencies>
         <dependency>

ここで注意点として、Spring Boot 3のベースラインにはJava 17が採用されました。Spring Boot 2まではJava 8からサポートされていましたが、今後の技術追従を見込み、Spring Boot 3からはリリース時点のLTS(ロングタームサポート)であるJava 17が最低サポートバージョンとなっています。上記のjava.versionにはそれが反映されています。

さて、この変更のみで起動してみます。

$ ./mvnw spring-boot:run

おそらくこの時点では普通に起動する上に、curlのコマンドなども問題なく動作すると思います。「なんだ、アップグレードも簡単じゃないか」。そう思うかもしれません。

Jakarta EE 9のベースライン化およびEE 10のサポート

続いて、dataconsumerandexposeディレクトリでも同じようにpom.xmlを書き換えて、アップグレードしていきましょう。

$ diff --git a/dataconsumeandexpose/pom.xml b/dataconsumeandexpose/pom.xml
index 1e853d9..fd1411b 100644
--- a/dataconsumeandexpose/pom.xml
+++ b/dataconsumeandexpose/pom.xml
@@ -5,7 +5,7 @@
     <parent>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-parent</artifactId>
-        <version>2.7.12</version>
+        <version>3.1.0</version>
         <relativePath/> <!-- lookup parent from repository -->
     </parent>
     <groupId>com.engineerhub</groupId>
@@ -14,7 +14,7 @@
     <name>dataconsumeandexpose</name>
     <description>dataconsumeandexpose</description>
     <properties>
-        <java.version>11</java.version>
+        <java.version>17</java.version>
     </properties>
     <dependencies>
         <dependency>

これもこのまま起動してみます。

$ ./mvnw spring-boot:run

正常に起動したdataproducerと異なり、以下のようなエラーが出るでしょう。

[INFO] -------------------------------------------------------------
[ERROR] COMPILATION ERROR :
[INFO] -------------------------------------------------------------
[ERROR] /Users/machih/intellij/sb3-demo/dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/SimpleEntity.java:[3,25] package javax.persistence does not exist
[ERROR] /Users/machih/intellij/sb3-demo/dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/SimpleEntity.java:[4,25] package javax.persistence does not exist
[ERROR] /Users/machih/intellij/sb3-demo/dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/SimpleEntity.java:[5,25] package javax.persistence does not exist
[ERROR] /Users/machih/intellij/sb3-demo/dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/SimpleEntity.java:[6,25] package javax.persistence does not exist

ポイントとなるのはjavax.persistence does not existというエラーです。この原因には、Spring Boot 3でJava EE 8のサポートを撤廃したことと、Jakarta EE 9/10のサポートが関わっています。

これはTomcat 10.1やJetty 11への技術追従(パッケージ名の変更を含む)を目的とした変更ですが、結果としてこれまでSpring Boot 2で使えていたパッケージが使えなくなるという大きな影響が出ました。今回のようにコード内でjavaxを指定しているものは使えなくなり、jakartaへの書き直しを余儀なくされます。

Spring Boot 3への変更に対応したマイグレーションの方法

ここまでに紹介した2つの大きな変更(Java 17とJakarta EE 9)については、公式ブログが2021年9月に取り上げています。

A Java 17 and Jakarta EE 9 baseline for Spring Framework 6

またこれ以外にも、さまざまな細かいコードの変更の必要性が日々報告されています。 それに対して実際のプロジェクトでは、次のマイグレーションガイドをもとにアップグレード作業を行います。

Spring Boot 3.0 Migration Guide · spring-projects/spring-boot Wiki

ただし、アップグレードはどうしても負荷が高い作業です。今回のようなシンプルなコードならまだ言われた通りに修正すればよいのですが、より複雑なコードを手動で変更していくには手間がかかります。

そこで、サンプルコードを機械的にSpring Boot 3へアップデートする仕組みを紹介します。 先ほど手動で行った変更は一度リセットし、Spring Boot 2へ戻しておきましょう。

$ git reset --hard origin/main

Spring Boot MigratorでSpring Boot 3へ自動変換

Spring Boot 2からSpring Boot 3への移行を支援するツールとして、Spring Boot Migratorがコミュニティから提供されています。

spring-projects-experimental/spring-boot-migrator: Spring Boot Migrator (SBM) is a tool for automated code migrations to upgrade or migrate to Spring Boot

このツールは大規模なコードリファクタリングを支援するOpenRewriteをベースに、さまざまなフレームワークからSpring Bootへの移行に加えて、Spring Boot 3へのマイグレーションも行うことができます。

最新リリースのダウンロードと起動

次のページから、最新のspring-boot-migrator.jarを任意のディレクトリへダウンロードします。

Releases · spring-projects-experimental/spring-boot-migrator

さっそく起動してみましょう。

$ java -jar spring-boot-migrator.jar

すると以下のようなプロンプトが表示されます。

migrator:>

このプロンプトで、さまざまなコマンドを実行することができます。

dataproducerのコードをスキャンして変換する

まずこのプロンプトから、dataproducerのコードをスキャンします。

> scan sb3-demo/dataproducer

実行結果から、このコードに対する実行可能なさまざまなレシピが確認できます。

boot4

この中で一番シンプルな一括変換のレシピ(boot-2.7-3.0-dependency-version-update)を実行します。

> apply boot-2.7-3.0-dependency-version-update

これにより、先ほどの例で取り上げたJava 17へのアップグレードや、javaxからjakartaパッケージへの変換だけでなく、コミュニティとして推奨されている変更まで含めて実施されます。

boot5

dataconsumeandexposeのコードもスキャンして変換する

続いてdataconsumeandexposeでも、同じようにスキャンと変換を実施します。

> scan sb3-demo/dataconsumeandexpose
> apply boot-2.7-3.0-dependency-version-update

ところでサンプルコードのリポジトリには、解説のセクションごとにそれぞれブランチを用意しています。このSpring Boot Migratorによる自動変換の作業は、次のブランチにまとまっています。

mhoshi-vm/sb3-demo at sb3migrate

例えば、上記のdataconsumeandexposeへのツールによる変換結果は、このコミットで参照できます。

このようなシンプルなコードではどうしても恩恵を感じにくいかもしれませんが、複雑なコードであればあるほど作業の簡略化が図れます。Spring Boot 3への移行を躊躇している場合は、強力なツールになり得ます。

とはいえ、執筆時点ではExperimentalなうえイシューも数多く上がっているので、自己責任で利用してください。新しい問題が見つかったときには、イシューを報告してコミュニティへ貢献するとよいでしょう。

さて、これでSpring Boot 3へのアップグレードが無事に完了しました。しかし、これだけではSpring Boot 3の新機能を実感しづらいでしょう。次のセクションから、Spring Boot 3ならではの機能を試していきます。

Spring Boot 3で強化されたObservability機能とは?

ここからは、アプリケーションの可観測性(Observability)を劇的に向上させる機能を紹介します。

Spring Boot 3から、Micrometerを中心としたこの機能が正式にGAされました。 これまでもSpring Actuatorによる監視が行えていましたが、Spring Boot 3になって以下の機能が強化されました。

  • Micrometer Tracingによるトレーシング機能
  • Micrometer Observabilityによるメトリクスとトレースの同時計測
  • Exemplarsによるメトリクスとスパンの関連付け

それぞれの機能を見ていきましょう。

監視基盤の立ち上げ

機能を紹介する前に、まず監視基盤を立ち上げます。監視基盤にはさまざまなオプションがありますが、今回は以下を組み合わせて実施します。

まず作業ディレクトリにdocker-compose.yamlを、以下のコンテンツで作成します。これは、先ほど挙げた3つのOSSを起動するDockerの設定ファイルです。

services:
  grafana:
    image: grafana/grafana
    extra_hosts:
    - host.docker.internal:host-gateway
    volumes:
      - ./docker/grafana/provisioning/datasources/:/etc/grafana/provisioning/datasources/:ro
    environment:
      - GF_AUTH_ANONYMOUS_ENABLED=true
      - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
      - GF_AUTH_DISABLE_LOGIN_FORM=true
    ports:
      - "3000:3000"

  prometheus:
    image: prom/prometheus
    extra_hosts:
    - host.docker.internal:host-gateway
    command:
      - --enable-feature=exemplar-storage
      - --config.file=/etc/prometheus/prometheus.yml
    volumes:
      - ./docker/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
    ports:
      - "9090:9090"

  zipkin:
    image: openzipkin/zipkin-slim
    ports:
      - "9411:9411"

ここで重要なのが、Prometheusの構成ファイルに設定されている--enable-feature=exemplar-storageです。これにより、後で取り上げるExemplarの取り扱いが可能になります。

Prometheusで監視対象を設定する

次に、./docker/prometheus/prometheus.ymlファイルを作成します。

scrape_configs:
    - job_name: 'apps'
      scrape_interval: 10s
      metrics_path: '/actuator/prometheus'
      static_configs:
        - targets: ['host.docker.internal:8082','host.docker.internal:8083']

内容は極めてシンプルですが、次の箇所で監視対象(今回はdataproducerとdataconsumeandexpose)の宛先を指定しているので、ポート番号の変更などを行った場合にはあわせて修正してください。

targets: ['host.docker.internal:8082','host.docker.internal:8083']

Grafanaの設定

もうひとつ、./docker/grafana/provisioning/datasources/datasources.yamlファイルを作成してください。

apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://host.docker.internal:9090
    editable: false
    jsonData:
      exemplarTraceIdDestinations:
        - name: trace_id
          datasourceUid: zipkin
  - name: Zipkin
    type: zipkin
    uid: zipkin
    url: http://host.docker.internal:9411
    readOnly: true

これもシンプルですが、Grafanaから見たPrometheusとZipkinの宛先を指定しているほか、後ほど説明するExemplar機能を使うためにexemplarTraceIdDestinations:で設定しています。

Docker Composeで起動

用意ができたらDocker Compose経由で起動します。

$ docker-compose up -d

docker psコマンドで起動したことを確認します。

CONTAINER ID   IMAGE                    COMMAND                  CREATED          STATUS                   PORTS                                       NAMES
e83944f82a04   grafana/grafana          "/run.sh"                14 seconds ago   Up 10 seconds            0.0.0.0:3000->3000/tcp, :::3000->3000/tcp   sb3-demo-grafana-1
f0b47c0cfb9b   openzipkin/zipkin-slim   "start-zipkin"           8 minutes ago    Up 7 minutes (healthy)   0.0.0.0:9411->9411/tcp, :::9411->9411/tcp   sb3-demo-zipkin-1
b7f4e3b8b0f9   prom/prometheus          "/bin/prometheus --e…"   8 minutes ago    Up 7 minutes             0.0.0.0:9090->9090/tcp, :::9090->9090/tcp   sb3-demo-prometheus-1

停止する際には、docker-compose downを実行してください。

Observability その1 ─ Spring Bootで有効化

監視基盤が立ち上がったので、サンプルコードにおいてSpring Observabilityを有効化していきます。

dataproducerとdataconsumeandexposeのpom.xmlを開き、以下を追記します。

  • spring-boot-starter-actuator ... 監視の本体
  • micrometer-tracing-bridge-brave ... トレース情報の送付
  • micrometer-registry-prometheus ... メトリクスをPrometheusへ転送
  • zipkin-reporter-brave ... Zipkinへのデータ転送

また、データベースへのトレースを有効化するため、以下も追加しています。

  • datasource-micrometer-spring-boot

dataproducerの設定変更

実際の変更後の差分イメージがこちらです。

$ diff --git a/dataproducer/pom.xml b/dataproducer/pom.xml
index 076b1f3..3a7f447 100644
--- a/dataproducer/pom.xml
+++ b/dataproducer/pom.xml
@@ -25,6 +25,29 @@
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.micrometer</groupId>
+            <artifactId>micrometer-tracing-bridge-brave</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.micrometer</groupId>
+            <artifactId>micrometer-registry-prometheus</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.zipkin.reporter2</groupId>
+            <artifactId>zipkin-reporter-brave</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>net.ttddyy.observation</groupId>
+            <artifactId>datasource-micrometer-spring-boot</artifactId>
+            <version>1.0.2</version>
+            <scope>runtime</scope>
+        </dependency>

         <dependency>
             <groupId>org.springframework.boot</groupId>

次に、設定をapplication.propertiesに追記していきます。

$ diff --git a/dataproducer/src/main/resources/application.properties b/dataproducer/src/main/resources/application.properties
index 3f51428..1694b02 100644
--- a/dataproducer/src/main/resources/application.properties
+++ b/dataproducer/src/main/resources/application.properties
@@ -4,3 +4,8 @@ spring.application.name=producer
 logging.pattern.dateformat=yyyy-MM-dd HH:mm:ss.SSS
 management.endpoints.jmx.exposure.include=*

+management.tracing.sampling.probability=1.0
+management.endpoints.web.exposure.include=prometheus
+
+management.metrics.distribution.percentiles-histogram.http.server.requests=true
+logging.pattern.level=%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]

dataproducerの起動と動作確認

この状態でdataproducerを起動してみます。

$ cd dataproducer/
$ ./mvnw spring-boot:run

起動が完了したらcurlコマンドを実行して、アプリケーションに通信を発生させます。

$ curl http://localhost:8082/produce -H "Content-Type: text/plain" -d 'aaa' -X POST

そして次のURLからZipkinにログインします。

http://localhost:9411/

「Run Query」を行い、curlを実行した回数分のクエリがあることを確認します。

boot6

dataconsumeandexposeの設定変更と起動確認

dataconsumeandexposeでも同じように変更していきます。pom.xmlapplication.propertiesともに同じ箇所を変更してください。

この状態で起動します。

$ cd dataconsumeandexpose/
$ ./mvnw spring-boot:run

再びZipkinにログインします。「Run Query」を行い、producerおよびconsumerともにデータが表示されていることを確認します。

boot7

最後に、次のURLでPrometheusにログインし、エンドポイントが2つとも起動していることを確認します。

http://localhost:9090/targets?search=

boot8

Observability その2 ─ RabbitMQで有効化

先ほどZipkinで確認したトレースですが、実は不完全です。 dataproducerとdataconsumeandexposeは本来なら関連したマイクロサービスのアプリケーションですが、Zipkinからは関連がないように見えています。

dataproducerとdataconsumeandexposeにコードを追加

これも若干のカスタマイズで、一連のマイクロサービスとして監視できます。 dataproducerに以下のコードを追加します。

dataproducer/src/main/java/com/engineerhub/dataproducer/ObservabilityCustomTemplate.java

package com.engineerhub.dataproducer;

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.autoconfigure.amqp.RabbitTemplateCustomizer;
import org.springframework.stereotype.Component;

@Component
public class ObservabilityCustomTemplate implements RabbitTemplateCustomizer {

    @Override
    public void customize(RabbitTemplate rabbitTemplate) {
        rabbitTemplate.setObservationEnabled(true);
    }
}

ポイントはRabbitTemplateCustomizerを実装し、rabbitTemplate.setObservationEnabled(true);を設定している点です。これによりrabbitTemplateと呼ばれるRabbitMQのクライアントのMicrometer Observabilityを有効にします。

これにより送信側のRabbitMQのObservabilityが有効化されましたが、受信側も別途対応する必要があります。dataconsumeandexposeに、以下のコードを追加します。

dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/ObservabilityConfig.java

package com.engineerhub.dataconsumeandexpose;

import org.springframework.amqp.rabbit.config.ContainerCustomizer;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = false)
public class ObservabilityConfig {
    @Bean
    ContainerCustomizer<SimpleMessageListenerContainer> containerCustomizer() {
        return (container) -> container.setObservationEnabled(true);
    }
}

これは、受け手側でのObservabilityを有効にし、具体的には@RabbitListnerアノテーションでくくられたコードを追跡するようになります。 この状態で2つのコードを立ち上げ直して、データを再度送ってみます。

一連の流れがつながって表示されることを確認

Zipkinにログインして、Dependenciesを見てください。「Run Query」すると、以下のようにつながった流れが見えるかと思います。

boot9

最新のトレースを「Show」してみてください。 すると、以下のように今回のアプリケーションの流れ(HTTPリクエスト → MQへの書き込み → MQの読み込み >→DBへの書き込み)を全て見ることができます。

boot10

今回はRabbitMQのトレースのカスタマイズ方法を紹介しましたが、Micrometerのその他のカスタマイズ方法は以下のサンプルに公開されています。ぜひ参考にしてください。

micrometer-metrics/micrometer-samples: Sample apps to demo Micrometer features

Observability その3 ─ Micrometerによるトレースとの同時計測

Spring Boot 3のObservabilityの面白い機能の1つが、メトリクスとトレースを同時に取得ができるようになった点です。この機能も紹介していきます。

dataconsumeandexposeのコードの修正

まず、先ほど作成したdataconsumeandexposeのObservabilityConfigに以下を追記します。

$ diff --git a/dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/ObservabilityConfig.java b/dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/ObservabilityConfig.java
index ae5845a..9f87ee6 100644
--- a/dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/ObservabilityConfig.java
+++ b/dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/ObservabilityConfig.java
@@ -1,5 +1,7 @@
 package com.engineerhub.dataconsumeandexpose;

+import io.micrometer.observation.ObservationRegistry;
+import io.micrometer.observation.aop.ObservedAspect;
 import org.springframework.amqp.rabbit.config.ContainerCustomizer;
 import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
 import org.springframework.context.annotation.Bean;
@@ -7,6 +9,12 @@ import org.springframework.context.annotation.Configuration;

 @Configuration(proxyBeanMethods = false)
 public class ObservabilityConfig {
+
+    @Bean
+    ObservedAspect observedAspect(ObservationRegistry observationRegistry) {
+        return new ObservedAspect(observationRegistry);
+    }
+
     @Bean
     ContainerCustomizer<SimpleMessageListenerContainer> containerCustomizer() {
         return (container) -> container.setObservationEnabled(true);

このBeanはこれから後に使う@Observedアノテーションを有効化するAspectを追加するためのものです。なお、この必要性はこのイシューによっては変わるかもしれません。

次に、dataconsumeandexposeに以下のコードを追加します。

dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/SimpleLogic.java

package com.engineerhub.dataconsumeandexpose;

import io.micrometer.observation.annotation.Observed;
import org.springframework.stereotype.Component;

@Component
public class SimpleLogic {

    @Observed
    public void logic() throws InterruptedException {
        Thread.sleep(10);
    }
}

見ての通り、Thread.sleepによる10ミリ秒の停止以外に何も入れていないシンプルなコードですが、@Observedによって囲われたメソッドのメトリクスとトレースを同時に取得することが可能になります。

最後に、SimpleServiceを以下のようにアップデートします。

$ diff --git a/dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/SimpleService.java b/dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/SimpleService.java
index 8a64670..cc785f8 100644
--- a/dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/SimpleService.java
+++ b/dataconsumeandexpose/src/main/java/com/engineerhub/dataconsumeandexpose/SimpleService.java
@@ -11,8 +11,11 @@ public class SimpleService {
     private static final Logger log = LoggerFactory.getLogger(SimpleService.class);
     SimpleRepository simpleRepository;

-    public SimpleService(SimpleRepository simpleRepository) {
+    SimpleLogic simpleLogic;
+
+    public SimpleService(SimpleRepository simpleRepository, SimpleLogic simpleLogic) {
         this.simpleRepository = simpleRepository;
+        this.simpleLogic = simpleLogic;
     }

     public Iterable<SimpleEntity> getAll(){
@@ -24,10 +27,11 @@ public class SimpleService {
     }

     @RabbitListener(queues = "demoQueue")
-    public void consume(String data){
+    public void consume(String data) throws InterruptedException {
         log.info("new data from queue");
         SimpleEntity simpleEntity = new SimpleEntity();
         simpleEntity.setData(data);
+        simpleLogic.logic();
         save(simpleEntity);
     }
 }

この状態でdataconsumeandexposeを再起動して、データを転送してみます。

PrometheusとZipkinで動作を確認

まず、以下のURLでPrometheusを見てみましょう。

http://localhost:9090/graph?g0.expr=method_observed_active_seconds_max&g0.tab=1&g0.stacked=0&g0.show_exemplars=0&g0.range_input=1h

すると@Observedで囲ったメソッドのメトリクスが収集されていることが分かります。

boot11

さらにZipkinも見てみましょう。

boot12

同じく、メソッドと同じ名前のスパン情報が追加されています。さらに、Thread.sleepで指定した10ミリ秒の時間もアプリケーションが停滞していることが分かります。

Observability その4 ─ Exemplarsによるスパンとの関連付け

Observabilityに関連して紹介する最後の機能は、Exemplarと呼ばれます。 この言葉にも馴染みがないかもしれませんが、OpenMetricsによって定義された概念であり、メトリクスとトレースをより関連付けることができる1つの機能です。

まず、以下のようなcurlを実行してみましょう。

$ curl -s 'http://localhost:8082/actuator/prometheus' | grep "trace_id"

特に何も表示されないと思います。次に以下を実行します。

$ curl -s 'http://localhost:8082/actuator/prometheus' -H 'Accept: application/openmetrics-text; version=1.0.0; charset=utf-8' | grep "trace_id"

すると、今度は多くの出力が得られます。以下は一例です。

http_server_requests_seconds_bucket{error="none",exception="none",method="POST",outcome="SUCCESS",status="200",uri="/produce",le="0.005592405"} 1.0 # {span_id="0789f112214e6089",trace_id="6470d1fc0ec0e0110789f112214e6089"} 0.005083116 1685115388.147

このメトリクス、厳密にはヒストグラムは、先ほどapplication.propertiesに追加した次の設定によって参照が可能になったものです。

management.metrics.distribution.percentiles-histogram.http.server.requests=true

本来ヒストグラムは、指定されたバケットに複数のデータが入るものですが、ポイントとなるのがそのバケット内の代表点のspan_idtrace_id、レスポンスタイムが記載されている点です。つまり上の結果では、/produceにPOSTでリクエストしたレスポンスタイムのヒストグラムで0.005592405[秒]以下のバケットにおける代表点0.005083116[秒])trace id6470d1fc0ec0e0110789f112214e6089であることが分かります。このtracd idを使うと、Zipkinで検索が可能になります。

Grafanaによる可視化

Grafanaでは、より面白い結果が見られます。以下の手順で実行していきます。

  1. http://localhost:3000にログイン
  2. Explore機能を選択
  3. Prometheusを選択
  4. Metrics Browserで次を入力
    histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket[$__rate_interval])) by (le))
  5. OptionsよりExemplarのトグルを有効化
  6. Run Queryの実行
  7. (若干見つけにくいですが)グラフ中のひし形にポインタを合わせる

すると以下の画面のようにメトリクスと連動したExemplarが確認できます。さらに「Query with Zipkin」を選択すると、Zipkin側のトレースも見ることができます。

boot13

先ほどの、Micrometer Observabilityとは違った角度とはいえ、両者共に、よりメトリクスとトレースを連動したものを作ろうとしているのが分かります。両者を連動させることでより関連性を明確にして、問題判別に役立てようという機能です。

以上が、Spring Boot 3で追加されたObservability機能の紹介です。

GraalVMを用いたネイティブイメージによる高速起動化

Spring Boot 3の解説もいよいよ佳境です。準備ができたこのJavaのコードをパッケージ化して世に公開する際に取ることができる1つの選択肢が、ネイティブイメージ化による高速起動化です。これまでネイティブイメージ対応はExperimental扱いでしたが、Spring Boot 3より正式にサポートされました。

ここまで修正したコードを、実際にネイティブイメージ化してみましょう。

GraalVMのインストールとネイティブ化のコンパイル

まず手順として、別途GraalVMを開発環境にインストールすることが必要です。執筆時点で最新のGraalVMを以下のコマンドでインストールできます。

$ sdk install java 22.3.r19-grl

インストールでき次第、以下のコマンドでdataproducerおよびdataconsumeandexposeをそれぞれパッケージ化します。

$ ./mvnw native:compile -Pnative

GraalVMによるネイティブイメージのコンパイルを初めて体験する方は驚くかもしれませんが、PCのスペックに依存するものの、今回のようなシンプルなコードでも5~10分近くのビルド時間が発生します。

完了後、以下の実行可能なファイルが生成されます。

./dataconsumeandexpose/target/dataconsumeandexpose
./dataproducer/target/dataproducer

そのままdataproducerを起動してみます。

$ ./dataproducer/target/dataproducer

通常のJVMによる起動に比べて、非常に高速に起動することが分かるかと思います。通常の起動だと3~5秒ほどかかるところが、以下の例では200ミリ秒以下で起動しています。

boot14

これがネイティブイメージ化の真髄であり、JVMによるアプリ起動と比較しても起動が速く、かつメモリの消費量も小さくなります。

さらにこの前章のObservabilityで紹介した監視がそのまま使えることも大きなポイントです。クラウドのサーバレス環境などでは、監視を設定する際に固有の設定を指示されるケースが多くあります。ネイティブイメージであれば、ローカル開発での監視の仕組みのまま、クラウドに持っていくことができます。

dataconsumeandexposeの起動エラーと対応

なお、執筆時点ではdataconsumeandexposeを起動しようとすると、以下のエラーになるはずです。

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory': No classes have been predefined during the image build to load from bytecodes at runtime.
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1770) ~[dataconsumeandexpose:6.0.9]
...

このエラーには次のイシューが影響しています。

Skip class transformer in PersistenceUnitInfoDescriptor for native images · Issue #30492 · spring-projects/spring-framework

今回のコードのワークアラウンドとして、推奨はされませんが、以下のようにHibernateを一時的にダウングレードする必要があります。

$ diff --git a/dataconsumeandexpose/pom.xml b/dataconsumeandexpose/pom.xml
index 1bdac74..40d544e 100644
--- a/dataconsumeandexpose/pom.xml
+++ b/dataconsumeandexpose/pom.xml
@@ -15,6 +15,7 @@
     <description>dataconsumeandexpose</description>
     <properties>
         <java.version>17</java.version>
+        <hibernate.version>6.1.7.Final</hibernate.version>
     </properties>
     <dependencies>
         <dependency>

ある意味でこれがネイティブイメージ化の弱点でもあり、コードを変更しなければならない場合があります。

なお、Spring Framework 6でAOT(Ahead-of-Time Processing)対応が入り、ビルド時にネイティブイメージ化しやすいソースコード・バイトコードに事前コンパイルされています。とはいえ、コンパイルにおいてのAOT理解が必須になります。

ビルド時間が長いことも、体感いただいた通り、もう1つの考慮点です。

これらを加味した上で、ネイティブイメージ化はコンテナやFaaS(Function as a Service)などの用途には非常に適していることが次のドキュメントで分かります。

GraalVM Native Image Support

なお、Spring Boot 3のネイティブイメージ対応は、可能な限りコード差異を意識することなくフレームワークレベルで抽象化されています。上のイシューがコミュニティですぐに修正されたように、最終形としては開発者がネイティブイメージにビルドするかどうかを意識することなく、開発に取り組めることが目標となっています。

まとめ

この記事では、Spring Boot 2で書かれたコードをアップグレードし、Spring Boot 3の新しい機能に触れていくということを解説しました。

前述したようにサンプルコードは以下のようなブランチに分かれていますので参考にしてください。

今回の記事が少しでもSpring Boot 3への興味や開発の意欲につながれば幸いです。

星野 真知(HOSHINO Machi)GitHub: mhoshi-vm

boot15
ヴイエムウェア株式会社 モダンアプリケーションプラットフォーム事業本部 VMware Tanzu Staff Solution Engineer
国際基督教大学卒業後、日本アイ・ビー・エム システムズ・エンジニアリング株式会社、ミランティス・ジャパン株式会社を経て現職。国内のみならずOpenStack Summit(現・OpenInfra Summit)など海外のセミナーでも講師も担当。OSSを中心としたITインフラソリューションで10年以上の経験を持ち、現在はCI/CDの設計からSpring BootやPostgresなどのアプリケーション分野に注力している。
VMware Japan Blog(企業ブログでの執筆記事)
blog.lespaulstudioplus.info(個人ブログ)

編集:中薗 昴
制作:はてな編集部

若手ハイキャリアのスカウト転職