日々常々

ふつうのプログラマがあたりまえにしたいこと。

イニシャライザのどうでもいい話

すごくどうでもいいんだけど、Javaにはインスタンスイニシャライザとコンストラクタというのがあって。

class Hoge {
  // これがインスタンスイニシャライザ
  {
    int i = 0;
    i++;
    i++;
    i++;
  }

  // これがコンストラクタ
  Hoge() {
    long l = 0;
    l--;
    l--;
    l--;
  }
}

これを javap するとこうなって、インスタンスイニシャライザはみあたらない。

% javap -p Hoge
Compiled from "Hoge.java"
class Hoge {
  Hoge();
}

別に消えてるのではなくて、コンストラクタに展開されるん。 javap -c すると

% javap -c Hoge
Compiled from "Hoge.java"
class Hoge {
  Hoge();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: iconst_0
       5: istore_1
       6: iinc          1, 1
       9: iinc          1, 1
      12: iinc          1, 1
      15: lconst_0
      16: lstore_1
      17: lload_1
      18: lconst_1
      19: lsub
      20: lstore_1
      21: lload_1
      22: lconst_1
      23: lsub
      24: lstore_1
      25: lload_1
      26: lconst_1
      27: lsub
      28: lstore_1
      29: return
}

べろっと。 iinc ならんでるとこがインスタンスイニシャライザ由来で、 lsub とかがコンストラクタ由来なのは雰囲気感じられればOK。

これだけだとふーんって感じだけど、コンストラクタはオーバーロードできるんだよね。

class Hoge {
  // これがインスタンスイニシャライザ
  {
    int i = 0;
    i++;
    i++;
    i++;
  }

  // これがコンストラクタ
  Hoge() {
    long l = 0;
    l--;
    l--;
    l--;
  }

+ // オーバーロードしたコンストラクタ
+ Hoge(Object o) {
+   o.toString();
+  }
}

こうしてもどちらのコンストラクタが実行されるときもインスタンスイニシャライザは実行されるんだけど、コンストラクタに展開されるってことはー

% javap -c Hoge
Compiled from "Hoge.java"
class Hoge {
  Hoge();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: iconst_0
       5: istore_1
       6: iinc          1, 1
       9: iinc          1, 1
      12: iinc          1, 1
      15: lconst_0
      16: lstore_1
      17: lload_1
      18: lconst_1
      19: lsub
      20: lstore_1
      21: lload_1
      22: lconst_1
      23: lsub
      24: lstore_1
      25: lload_1
      26: lconst_1
      27: lsub
      28: lstore_1
      29: return

  Hoge(java.lang.Object);
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: iconst_0
       5: istore_2
       6: iinc          2, 1
       9: iinc          2, 1
      12: iinc          2, 1
      15: aload_1
      16: invokevirtual #7                  // Method java/lang/Object.toString:()Ljava/lang/String;
      19: pop
      20: return
}

なるほど、イニシャライザが複製されておる。

で、コンストラクタはチェーンできるんだけど

class Hoge {
  // これがインスタンスイニシャライザ
  {
    int i = 0;
    i++;
    i++;
    i++;
  }

  // これがコンストラクタ
  Hoge() {
    long l = 0;
    l--;
    l--;
    l--;
  }

  // オーバーロードしたコンストラクタ
  Hoge(Object o) {
+   this();
    o.toString();
  }
}

これで展開されてるとイニシャライザが2回実行されることになるので、当然

% javap -c Hoge
Compiled from "Hoge.java"
class Hoge {
  Hoge();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: iconst_0
       5: istore_1
       6: iinc          1, 1
       9: iinc          1, 1
      12: iinc          1, 1
      15: lconst_0
      16: lstore_1
      17: lload_1
      18: lconst_1
      19: lsub
      20: lstore_1
      21: lload_1
      22: lconst_1
      23: lsub
      24: lstore_1
      25: lload_1
      26: lconst_1
      27: lsub
      28: lstore_1
      29: return

  Hoge(java.lang.Object);
    Code:
       0: aload_0
       1: invokespecial #7                  // Method "<init>":()V
       4: aload_1
       5: invokevirtual #10                 // Method java/lang/Object.toString:()Ljava/lang/String;
       8: pop

きえるわけだ。

これ自体は明示的にコンストラクタ呼び出しをすることで親クラスのコンストラクタ呼び出しとかも消えるし、特に違和感があるでもない。 けど、Javaコードが増えてバイトコードが減るのが面白いなぁって。

Hoge.java Hoge.class
this()なし 310 394
this()あり 322 372

インスタンスイニシャライザなんて書かないだろから通常のアプリケーション開発ではほんとどうでもいいことなんだろけど、任意のバイトコードをいじったりするツールとか作るなら気にしなきゃなのかなぁ。

私個人だと 昔から「コンストラクタはチェーンしよう」って言ってるし、イニシャライザを書くこともそんなにないからこの状況になることもそうないんだけども。

なお実行環境は以下の通り。あんま関係ないと思う。

% java -version
openjdk version "21.0.7" 2025-04-15 LTS
OpenJDK Runtime Environment Temurin-21.0.7+6 (build 21.0.7+6-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.7+6 (build 21.0.7+6-LTS, mixed mode, sharing)'

追記:バイトコード上での名前とかstaticイニシャライザとかの話

もともとこれ見る発端が、コンストラクタもインスタンスイニシャライザもコンパイルすると <init> って名前でクラスファイルに入ることから、両方あったらどうなるんだっけ?とみたものです。 javap で見るところでは「コンストラクタでしょ?」とJavaコードで書くコンストラクタのように示されていますが、たとえば // Method java/lang/Object."<init>":()V などで「親クラスのコンストラクタ」とかが出てきています。この <init> がコンストラクタおよびインスタンスイニシャライザです。 で、名前が同じもんだから、バイトコードだけで見るとコンストラクタとインスタンスイニシャライザの区別がつかなかったりするわけです。実害はない。

なおstaticイニシャライザでは <clinit> という別の名前が割り当てられるので、混ざったり展開されたりはしないようです。

複数アプリケーションを育てていくための共通化戦略 #jjug_ccc

2025-06-07に開催された JJUG CCC 2025 Spring で話しました。

speakerdeck.com

内容については全部スライドに書く形式にしましたので、資料を見ていただければと思います。

JJUG CCCについて

今回の参加登録が久々に1000人を超えました。 ハイブリッド開催など試行錯誤を繰り返されていましたが、大台は参加者としても感慨深いです。

どうやら今回でJJUG CCC10回目の登壇だったようです。

(ページ数が多い&内容に関係がないので共有スライドからは除外したページ)

拙いCfPを毎回読んでいただいて&聞きに来ていただいてありがとうございます。

今回のCCCは8トラックで最後の枠は交流会準備などで減る時間帯。 被った人たちで「そっち見に行きたいんだけどなぁ」みたいなのを言い合っていました。

複数トラックイベントはどのセッションを聞くかを選ぶのを迷うのがよくて(でないと偏ってしまいますしね)、 聞いた感じ各部屋立ち見もなく、タイムテーブルの妙、素晴らしい仕事だったと思います。

セッションについて

前日にこれで

この時点で80ページ程度。そこから言っている通り付録(若干緑がかったSpringのとこ)を丸々足したので、45分に収まるわけもなく。 あとパターン言語に寄せているところはもっと文字数多かったです。(ここは減らす努力をした。)

とはいえ最終セッションとはいえオーバーするなんてトリを任せて頂いたCCC運営の皆様に失礼だと思い、なんとかしようとは思っていました。 ただ特に前々回のセッションは早送りが過ぎたなぁあれは無いわと反省もしていたものでして。

そんなこんなで方針を決めたら

某ゆ: どうせ終わらないんでしょ?

結果は45分ピッタリで終わったはずです。10秒程度は超えてるかもしれませんが、超えたとは言われてない。

ちゃんとおわる #とは

セッションスタイル

内容だけで伝わるものはブログとか動画とかでいいと思うんです。

オフラインイベント復帰後は特に、オフラインの対面でやるからこそのセッションをしたいとは思っていまして。

その結果「資料に書いたことは読まず、力点をピックアップして伝える」(要約というか、これもモデリングです)というスタイルを検証中です。 話ながら、聞いてくれている人の反応を見ながら、当日の他のセッションとかの流れを取り入れながら。

関ジャバでのLT もその練習みたいなとこはあります。 普通に考えてLTで話すネタじゃないよなぁ。

聞いてる側もスライドをしっかり読もうとしたら話についてこれないだろうと思いましたし、「あとで公開します」だと忘れちゃうかもなので、セッション前に公開としました。

セッション前に資料できてたのは初めてかもしれない

私的にはそれなりに手応えを感じているんですが、聞いている方が実際どう感じているのかはわかりません。CCCのアンケートでも他でも、フィードバックくれると嬉しいです。

おまけ

ぅぉぃ!

Spring Bootの開発時サービスサポート(Testcontainers & Docker Compose)のサンプル実装と所感

Spring Boot 3.1でTestcontainersとDocker Composeのサポートが導入されました。 導入時はそれぞれ独立した項目でしたが、現在(Spring Boot 3.4.4)では 開発時のサービスDevelopment-time Services)としてまとめられています。

TestcontainersやDocker ComposeはそれぞれSpring Bootのサポートがなくても使われてきましたが、 サポートが導入されたことで非常に使いやすくなりました。

Spring Bootがサポートしてくれることを簡単に言うと「テストやローカル実行時に勝手にOCIコンテナを立ち上げて接続設定も面倒見てくれる」という機能です。 これがなければ自身(もしくは外部の仕組み)でコンテナを立ち上げて、その設定に従ってSpring Boot側に接続設定を行う必要がありました。 ちょっと楽になります。このちょっとさがとても大事。(だからと言って「テストでもどんどんコンテナ使いましょう!!」とはならない点には注意が必要ですが、それは別の話。)

それぞれが独立したツールであり、実際に併用されてきため、動かす方法は幾通りもあります。 それだけに情報も錯綜するため、何をどう使ったら動くか(また動かないか)がわかりづらくもあります。

なので極力シンプルかつ実プロダクトで使う場合のイメージで動くものを書いておこうと思いました。

本体は以下のリポジトリにあります。このブログはREADMEの抜粋です。

github.com

Testcontainers

ざっくり導入手順

  • 依存に testAndDevelopmentOnly("org.springframework.boot:spring-boot-testcontainers") を追加
  • 使用するコンテナに応じたTestcontainersモジュールを依存に追加
    • ここでは org.testcontainers:mysql を追加
  • @TestConfigurationServiceConnection をBean定義して、テストで使う
    • @Bean
      @ServiceConnection
      MySQLContainer<?> mysql() {
          return new MySQLContainer<>("mysql:lts");
      }
      

メモ

Spring Bootではよくあることなのですが、Spring Bootサポートがある場合、使用しているツールの標準の方法より簡潔な選択肢が出てきます。

TestcontainersではJUnit5サポート( org.testcontainers:junit-jupiter )やJDBCサポートとしてもJDBC URLとコンテナオブジェクト( MySQLContainer など)が提供されています。 JDBCサポートではJDBC URLのほうがURLを変えるだけで使用できる高レベルAPIになるため、TestcontainersでJDBC接続先のコンテナのみを使用する場合はJDBC URLが推奨されていたりします。

しかしSpring Bootのテストでは、このサンプルで示しているようにJDBC URLもJUnit5サポート機能( org.testcontainers:junit-jupiter )も使用せず、 ここで実装しているようにコンテナオブジェクトを @ServiceConnection で使用し、合成アノテーションを作成するのが適切です。

Spring Bootのリファレンスでは @Container@ImportTestcontainers を使用するものも紹介されていますが、 ちゃんと読めばこれはTestcontainersを昔から使っているなど、この使い方にこだわりのある方向けだということがわかります。ちゃんと読むの難しいとかは、まぁ、わかる。

ローカル実行はJDBCだけでコンテナを使用するのであればJDBC URLで良いと思います。 TestMyApplication の作りは癖がありますし、ローカル実行とはいえテストリソースがクラスパスに含まれる点に若干の懸念があるためです。 JDBC以外も使うのであればコンテナオブジェクト+@ServiceConnection に寄せるか、他のコンテナ起動方法にした方がいいと思います。

Docker Compose

ざっくり導入手順

  • compose.yml を用意
  • 依存に testAndDevelopmentOnly("org.springframework.boot:spring-boot-docker-compose") を追加
  • テストで使う場合は spring.docker.compose.skip.in-tests=false を指定

メモ

compose.yml の検出はworking directoryを起点に行いますが、working directoryは実行の仕方によって変わります。 たとえばMyApplicationIDEで実行した場合、未設定だとルートプロジェクトがworking directoryになったりしますが、 テストを実行した場合はプロジェクトルートがworking directoryになったりします。

このサンプルではマルチプロジェクト構成で動作を安定させるために spring.docker.compose.file=classpath:dev-compose.yml のようにクラスパスで指定しています。

テストでは classpath:test-compose.yml を使用しているのは別設定も使用できることを示していますが、Docker Composeの設定が複数になるのも微妙ですし、 同じにできるなら同じものを使う方が好ましいです。 クラスパスに含める形にする場合、 main に置くとリリースモジュールに含まれることになるのは少し気になるところです。 動きはしないので害はありませんが、気になるなら除外しましょう。

Docker Composeを開発時のアプリケーション起動やテスト時以外でも起動するのであればデフォルトのままworking directoryを使用するとして、 うまく動作しない時はworking directoryを設定するか、 spring.docker.compose.file絶対パスで指定するのが良いでしょう。

所感

「ざっくり導入手順」を見て分かるように、簡単に使えるのはDocker Composeの方です。 一方でTestcontainersの方が柔軟で痒いところにも手が届きそうな感じです。

Docker Composeサポートはシンプルケースでは org.springframework.boot:spring-boot-docker-compose を依存に入れるだけで済み、 ServiceConnection などは表に出てきません。 すでにDocker Composeを使っている場合、依存を追加するだけで動作しかねません。 楽なのはいいですが、設定が必要な状況になると急に難度が上がるように感じるかもしれません。

TestcontainersサポートはTestcontainersの Container を使って @ServiceConnection も書くので、うまくいかなくてもこの延長線上でなんとかなる感はあります。 スライスをテストする場合などは 必要なコンテナのみを使用するのも簡単です。 Testcontainers単体で使う場合でも複数通りの使い方があり、さらにSpring Bootのドキュメントでもいくつかの使い方が示されているため、 どうやったら動くのかわかりづらいかもしれませんし、効果のない組み合わせで使ったりとかも散見されます。

ローカル実行はサポート機能は使わなくていいかなぁというのが正直なところですが、使うならDocker Composeかなと思います。 ただテストで使っていない場合にローカル実行のために依存を追加するかというと正直微妙なところ。 TestcontainersはJDBC URLなら使いたいですが、JDBC以外には使えませんし、名前に冠している通りテスト向きで、ローカル実行ではどうしてもぎこちない感じが拭えません。とはいえ併用も微妙なので、テストでTestcontainersを使っているなら使ってもいいかなぁ、くらい。忌避するほどのものではない感覚です。

補足

  • TestcontainersのバージョンもSpring BootでManagementされています。Testcontainersに限らず、Spring BootがManagementしているバージョンはよほどの事情がない限り自分で指定しない方がいいです。
  • Gradleの testAndDevelopmentOnlySpring Boot 3.2で追加されました。Testcontainersのためだそうですが、他でも使えるかもですね。

あとがき

本体がコードとREADMEなのでGitHubにあげるだけで満足なんだけど、ブログにも書いてみるってのはどうかなと。

新しい(って言っても3.1は2024-11-14なので半年前。……新しい、でいいか。)機能は機能自身も情報も安定しませんし、なかなか取り扱いが難しいものです。 開発時サービス(TestcontainersやDocker Compose)は冒頭で書いているようにSpring Bootに機能がなくとも使われてきたものですが、Spring Bootサポートなしでの使い方とありでの使い方が入り混じったり、なかなかにわかりづらい状態。説明するにも動くコードが居るなぁと。

3.4まできてそれなりに実績と落ち着きも見えてきていると思います。この辺りで一度足場固めをして、半端な状態の使い方を脱するのがよいんじゃないでしょうか。動くものだけを見れば、どちらであってもだいぶシンプルなものです。動くものだけを見れば。実務で使うならそれぞれREADMEに書いたくらいは理解して使って欲しく思います。

自分たちで使う道具の点検について書いている本ってあるっけーと本棚を眺めていたらプロダクティブプログラマが目に入りました。

パラパラ読み返すと十五章で紹介されている「イカダのイカリ」アンチパターン(余計な荷物を背負っている状態。ゴールデンハンマー症候群とかも近いやもしれない。)などがうっすら関連するでしょうか。15年以上前の書籍になりますが、あまり年代は気にしなくていい本の一つです。

アプレンティスシップ・パターンでは「具体的スキル」あたりでしょうか。

「なんとなくやったら動いた」で一旦よしとしてしまいがちなものたちですが、足場になるところはしっかり固めていきたいなぁ。って感じです。