Spring Framework について。当初は Spring Framework のコア機能だけを取り上げるつもりだったが、地味な話ばかりになってしまいそうだったので、流行りモノであるところの Spring Boot の話を盛り込み、コア機能は IoC コンテナの話題に絞ることにした。

Spring Boot

最近の Spring Framework ベースのアプリケーション開発で欠かせないのが Spring Boot である。Spring Boot とは、公式によれば:

Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can "just run". We take an opinionated view of the Spring platform and third-party libraries so you can get started with minimum fuss. Most Spring Boot applications need very little Spring configuration.

であるとのこと。Spring Boot にはいろんな機能が備わっているわけだが、これらはひとえに Spring Framework ベースのアプリケーションをかんたんに作り、かんたんに運用するために用意されている。それでいて出来上がるアプリケーションはオモチャのようなものではなく、紛れもない本物の Spring Framework ベースのアプリケーションである。

Spring Boot ベースのかんたんな Web アプリケーションを作ってみることにする。

プロジェクトを作る

Spring Initializr でプロジェクトのひな形を作ることができる。

ここでは

  • Maven Project
  • Spring Boot 1.5.2
  • Project Metadata
    • group: org.creasys
    • artifact: fortune
  • Dependencies
    • web
    • devtools

として、ひな形を作る。fortune.zip が落ちてくるので、適当な場所に展開する。

$ unzip fortune.zip
$ tree fortune
fortune
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── org
    │   │       └── creasys
    │   │           └── FortuneApplication.java
    │   └── resources
    │       ├── application.properties
    │       ├── static
    │       └── templates
    └── test
        └── java
            └── org
                └── creasys
                    └── FortuneApplicationTests.java

12 directories, 6 files

Eclipse 用のプロジェクト設定を生成する。

$ cd fortune
$ ./mvnw eclipse:eclipse

Maven をインストールしていなくても、そのラッパースクリプトである mvnw を使うことで、自動的に Maven が導入される。mvnw は Spring からは独立したプロダクトなので、どんなプロジェクトにも導入できる。

起動する

プロジェクトを Eclipse で開き、 org.creasys.fortune.FortuneApplication を Java Application として実行する。

15:58:55.047 [main] DEBUG org.springframework.boot.devtools.settings.DevToolsSettings - Included patterns for restart : []
15:58:55.051 [main] DEBUG org.springframework.boot.devtools.settings.DevToolsSettings - Excluded patterns for restart : [/spring-boot-starter/target/classes/, /spring-boot-autoconfigure/target/classes/, /spring-boot-starter-[\w-]+/, /spring-boot/target/classes/, /spring-boot-actuator/target/classes/, /spring-boot-devtools/target/classes/]
15:58:55.052 [main] DEBUG org.springframework.boot.devtools.restart.ChangeableUrls - Matching URLs for reloading : [file:/home/kazuki/sources/writing/spring/fortune/target/test-classes/, file:/home/kazuki/sources/writing/spring/fortune/target/classes/]

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.2.RELEASE)

2017-03-11 15:58:55.478  INFO 10029 --- [  restartedMain] org.creasys.FortuneApplication             : Starting FortuneApplication on carrot with PID 10029 (/home/kazuki/sources/writing/spring/fortune/target/classes started by kazuki in /home/kazuki/sources/writing/spring/fortune)
2017-03-11 15:58:55.479  INFO 10029 --- [  restartedMain] org.creasys.FortuneApplication             : No active profile set, falling back to default profiles: default
2017-03-11 15:58:55.571  INFO 10029 --- [  restartedMain] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@38bed225: startup date [Sat Mar 11 15:58:55 JST 2017]; root of context hierarchy
2017-03-11 15:58:57.303  INFO 10029 --- [  restartedMain] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)

(snip)

2017-03-11 15:58:58.780  INFO 10029 --- [  restartedMain] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2017-03-11 15:58:58.785  INFO 10029 --- [  restartedMain] org.creasys.FortuneApplication             : Started FortuneApplication in 3.707 seconds (JVM running for 4.236)

最後のメッセージが表示されれば、アプリケーションの起動が完了している。組み込み Tomcat が :8080 で待ち構えていることが分かる。また、メインスレッドの名前が restartedMain になっていることから、spring-boot-devtools が効いていることが分かる。

Spring Boot ベースの Web アプリケーションは、組み込みの Servlet コンテナを使って動作するよう設定されている。そのため Eclipse で起動するときに別途 Servlet コンテナを立てる必要もないし、通常の Java アプリケーションとして実行できる。

コントローラを追加する

Web アプリケーションの MVC (俗に MVC2 と呼ばれるらしい。軽くググってみたが、出典は分からなかった) で言うところのコントローラを追加し、HTTP 経由でアプリケーションを叩けるようにする。

@RestController
public class FortuneController {
    @GetMapping
    public String index() {
        return LocalDateTime.now().toString();
    }
}

Spring MVC の機能を使ってコントローラを定義する。@RestController は、当該クラスが REST API を提供するコントローラであることを表現するアノテーションである。@GetMapping は、当該メソッドが GET リクエストを処理するメソッドであることを表現するアノテーションである。上記の例では未指定だが、@GetMapping にはパスを指定することができ、URL とメソッドを対応付けることができる。未指定の場合は空文字列と同等なので、上記の場合は http://localhost:8080 を処理するのが FortuneController.index メソッドであるということになる。

クラスを追加したり、ファイルを保存したりすると、アプリケーションが自動的に再起動する様子がログから分かる。これは spring-boot-devtools によるもの。アプリケーションのクラスローダーと依存ライブラリのクラスローダーが分かれていて、アプリケーションのソースが更新されると、自動的にアプリケーションのクラスローダーだけが破棄、再読み込みされる。アプリケーションのクラスだけが再読み込みされるので、再起動にかかる時間は短く済む。なお、devtools 内の再起動処理では、依存ライブラリ内の static なキャッシュをリフレクション API を使って削除するしたり、一時的にヒープを圧迫させて Soft/WeakReference を解放させるといった、涙ぐましい努力がなされている。

さて、コントローラにアクセスしてみよう。

$ curl http://localhost:8080/
2017-03-11T16:05:26.669
$ curl http://localhost:8080/
2017-03-11T16:05:46.557

現在日時が返ってきている。ちゃんと機能しているようだ。

fortune の出力を返すようにする

現在日時を返すだけではつまらないし、せっかくの Spring なので依存性注入も使いたい。そこで fortune (/usr/games/fortune) の出力を応答として返すようにする。このような業務処理 (fortune の出力を得ることを、業務と呼ぶべきかどうかはさておき) は、コントローラのようなプレゼンテーション層ではなくサービスなどのモデル層に書くべき処理である。今回はサービスクラスとして作ることにした。

@Service
public class FortuneService {
    public String say() {
        ProcessBuilder pb = new ProcessBuilder("/usr/games/fortune");
        Process p = null;
        try {
            p = pb.start();
            String text;
            try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
                text = br.lines().collect(Collectors.joining("\n"));
            }
            p.waitFor();
            return text;
        }
        catch (IOException | InterruptedException e) {
            return "MOU DAMEPO";
        }
        finally {
            if (p != null) {
                closeQuietly(p.getErrorStream());
                closeQuietly(p.getOutputStream());
                closeQuietly(p.getInputStream());
                p.destroy();
            }
        }
    }
}

/usr/games/fortune のプロセスを起動し、その出力を得るだけのプログラムである。Java でのプロセスの扱いは面倒で、かつ間違いが起きやすい (close, destroy 周り) ので、なるべくなら避けたいものである。closeQuietly は例外を握りつぶしつつ Closeable.close を呼ぶメソッドで、そういうユーティリティがあるものと思ってほしい。

これを使うよう、コントローラを修正する。層をまたいだ依存関係の設定は、外部 (ここでは Spring) にまかせてしまう (これが依存性注入と呼ばれるもの)。

@RestController
public class FortuneController {
    private FortuneService fortune;

    public FortuneController(FortuneService fortune) {
        this.fortune = fortune;
    }

    @GetMapping
    public String index() {
        return fortune.say();
    }
}

改めて curl で叩く。

$ curl http://localhost:8080/
You are not dead yet.  But watch for further reports.
$ curl http://localhost:8080/
Wagner's music is better than it sounds.
                -- Mark Twain

ありがたいお言葉が返ってくるようになった。

なお、最初に FortuneApplication を起動してから、fortune の結果を得られるようになるまで、Eclipse を使ったアプリケーションの再起動は一度も行わなかった。spring-boot-devtools は素晴らしい。

パッケージング、デプロイ

パッケージングは Maven で package goal を叩けばよい。

$ ./mvnw package
$ file target/fortune-0.0.1-SNAPSHOT.jar
target/fortune-0.0.1-SNAPSHOT.jar: Java Jar file data (zip)

この jar ファイルは Maven の spring-boot プラグインによって後処理 (repackage) されたもので、特殊なレイアウトになっている。

$ unzip -l target/fortune-0.0.1-SNAPSHOT.jar
Archive:  target/fortune-0.0.1-SNAPSHOT.jar
  Length      Date    Time    Name
---------  ---------- -----   ----
        0  2017-03-24 16:10   META-INF/
      567  2017-03-24 16:10   META-INF/MANIFEST.MF
        0  2017-03-24 16:10   BOOT-INF/
        0  2017-03-24 16:10   BOOT-INF/classes/
        0  2017-03-24 16:10   BOOT-INF/classes/org/
        0  2017-03-24 16:10   BOOT-INF/classes/org/creasys/
        0  2017-03-24 16:10   BOOT-INF/classes/application.properties
      725  2017-03-24 16:10   BOOT-INF/classes/org/creasys/FortuneController.class
     2464  2017-03-24 16:10   BOOT-INF/classes/org/creasys/FortuneService.class
      703  2017-03-24 16:10   BOOT-INF/classes/org/creasys/FortuneApplication.class
        0  2017-03-24 16:10   META-INF/maven/
        0  2017-03-24 16:10   META-INF/maven/org.creasys/
        0  2017-03-24 16:10   META-INF/maven/org.creasys/fortune/
     1573  2017-03-21 09:12   META-INF/maven/org.creasys/fortune/pom.xml
      119  2017-03-24 16:10   META-INF/maven/org.creasys/fortune/pom.properties
        0  2017-03-24 16:10   BOOT-INF/lib/
     2346  2017-03-03 15:47   BOOT-INF/lib/spring-boot-starter-web-1.5.2.RELEASE.jar
     2289  2017-03-03 15:46   BOOT-INF/lib/spring-boot-starter-1.5.2.RELEASE.jar
     2310  2017-03-03 15:46   BOOT-INF/lib/spring-boot-starter-logging-1.5.2.RELEASE.jar
   309130  2017-03-01 20:40   BOOT-INF/lib/logback-classic-1.1.11.jar
   475477  2017-03-01 20:39   BOOT-INF/lib/logback-core-1.1.11.jar
    16516  2017-02-24 12:09   BOOT-INF/lib/jcl-over-slf4j-1.7.24.jar
     4597  2017-02-24 12:09   BOOT-INF/lib/jul-to-slf4j-1.7.24.jar
    23647  2017-02-24 12:09   BOOT-INF/lib/log4j-over-slf4j-1.7.24.jar
   273599  2016-02-19 13:13   BOOT-INF/lib/snakeyaml-1.17.jar
     2294  2017-03-03 15:47   BOOT-INF/lib/spring-boot-starter-tomcat-1.5.2.RELEASE.jar
  3015953  2017-01-10 21:03   BOOT-INF/lib/tomcat-embed-core-8.5.11.jar
   239791  2017-01-10 21:03   BOOT-INF/lib/tomcat-embed-el-8.5.11.jar
   241640  2017-01-10 21:03   BOOT-INF/lib/tomcat-embed-websocket-8.5.11.jar
   725129  2016-12-08 10:48   BOOT-INF/lib/hibernate-validator-5.3.4.Final.jar
    63777  2013-04-10 15:02   BOOT-INF/lib/validation-api-1.1.0.Final.jar
    66802  2015-05-28 09:49   BOOT-INF/lib/jboss-logging-3.3.0.Final.jar
    64982  2016-09-27 22:24   BOOT-INF/lib/classmate-1.3.3.jar
  1237433  2017-02-21 01:07   BOOT-INF/lib/jackson-databind-2.8.7.jar
    55784  2016-07-03 22:20   BOOT-INF/lib/jackson-annotations-2.8.0.jar
   282314  2017-02-20 17:01   BOOT-INF/lib/jackson-core-2.8.7.jar
   817936  2017-03-01 08:34   BOOT-INF/lib/spring-web-4.3.7.RELEASE.jar
   380004  2017-03-01 08:32   BOOT-INF/lib/spring-aop-4.3.7.RELEASE.jar
   762747  2017-03-01 08:31   BOOT-INF/lib/spring-beans-4.3.7.RELEASE.jar
  1139269  2017-03-01 08:32   BOOT-INF/lib/spring-context-4.3.7.RELEASE.jar
   915615  2017-03-01 08:34   BOOT-INF/lib/spring-webmvc-4.3.7.RELEASE.jar
   263286  2017-03-01 08:32   BOOT-INF/lib/spring-expression-4.3.7.RELEASE.jar
   667710  2017-03-03 15:34   BOOT-INF/lib/spring-boot-1.5.2.RELEASE.jar
  1045757  2017-03-03 15:41   BOOT-INF/lib/spring-boot-autoconfigure-1.5.2.RELEASE.jar
    41205  2017-02-24 12:08   BOOT-INF/lib/slf4j-api-1.7.24.jar
  1118609  2017-03-01 08:31   BOOT-INF/lib/spring-core-4.3.7.RELEASE.jar
        0  2017-03-24 16:10   org/
        0  2017-03-24 16:10   org/springframework/
        0  2017-03-24 16:10   org/springframework/boot/
        0  2017-03-24 16:10   org/springframework/boot/loader/
     2415  2017-03-03 15:28   org/springframework/boot/loader/LaunchedURLClassLoader$1.class
     1454  2017-03-03 15:28   org/springframework/boot/loader/PropertiesLauncher$ArchiveEntryFilter.class
     1807  2017-03-03 15:28   org/springframework/boot/loader/PropertiesLauncher$PrefixMatchingArchiveFilter.class
     4599  2017-03-03 15:28   org/springframework/boot/loader/Launcher.class
     1165  2017-03-03 15:28   org/springframework/boot/loader/ExecutableArchiveLauncher$1.class
        0  2017-03-24 16:10   org/springframework/boot/loader/jar/
     2002  2017-03-03 15:28   org/springframework/boot/loader/jar/JarFile$1.class
     9657  2017-03-03 15:28   org/springframework/boot/loader/jar/Handler.class
     3350  2017-03-03 15:28   org/springframework/boot/loader/jar/JarEntry.class
     1427  2017-03-03 15:28   org/springframework/boot/loader/jar/JarFile$3.class
     2943  2017-03-03 15:28   org/springframework/boot/loader/jar/CentralDirectoryEndRecord.class
      430  2017-03-03 15:28   org/springframework/boot/loader/jar/CentralDirectoryVisitor.class
     1300  2017-03-03 15:28   org/springframework/boot/loader/jar/JarFile$JarFileType.class
    10924  2017-03-03 15:28   org/springframework/boot/loader/jar/JarFileEntries.class
    12697  2017-03-03 15:28   org/springframework/boot/loader/jar/JarFile.class
     1540  2017-03-03 15:28   org/springframework/boot/loader/jar/JarFileEntries$1.class
      672  2017-03-03 15:28   org/springframework/boot/loader/jar/JarURLConnection$1.class
     1199  2017-03-03 15:28   org/springframework/boot/loader/jar/JarFile$2.class
      262  2017-03-03 15:28   org/springframework/boot/loader/jar/JarEntryFilter.class
     4457  2017-03-03 15:28   org/springframework/boot/loader/jar/AsciiBytes.class
     4602  2017-03-03 15:28   org/springframework/boot/loader/jar/CentralDirectoryParser.class
     2169  2017-03-03 15:28   org/springframework/boot/loader/jar/Bytes.class
     1629  2017-03-03 15:28   org/springframework/boot/loader/jar/ZipInflaterInputStream.class
     1967  2017-03-03 15:28   org/springframework/boot/loader/jar/JarFileEntries$EntryIterator.class
      306  2017-03-03 15:28   org/springframework/boot/loader/jar/FileHeader.class
     3641  2017-03-03 15:28   org/springframework/boot/loader/jar/JarURLConnection$JarEntryName.class
     9111  2017-03-03 15:28   org/springframework/boot/loader/jar/JarURLConnection.class
     5449  2017-03-03 15:28   org/springframework/boot/loader/jar/CentralDirectoryFileHeader.class
        0  2017-03-24 16:10   org/springframework/boot/loader/data/
     1531  2017-03-03 15:28   org/springframework/boot/loader/data/ByteArrayRandomAccessData.class
     3534  2017-03-03 15:28   org/springframework/boot/loader/data/RandomAccessDataFile$DataInputStream.class
     2051  2017-03-03 15:28   org/springframework/boot/loader/data/RandomAccessDataFile$FilePool.class
     1341  2017-03-03 15:28   org/springframework/boot/loader/data/RandomAccessData$ResourceAccess.class
     3390  2017-03-03 15:28   org/springframework/boot/loader/data/RandomAccessDataFile.class
      551  2017-03-03 15:28   org/springframework/boot/loader/data/RandomAccessData.class
     4698  2017-03-03 15:28   org/springframework/boot/loader/LaunchedURLClassLoader.class
     1533  2017-03-03 15:28   org/springframework/boot/loader/JarLauncher.class
     1468  2017-03-03 15:28   org/springframework/boot/loader/MainMethodRunner.class
     1382  2017-03-03 15:28   org/springframework/boot/loader/PropertiesLauncher$1.class
     3128  2017-03-03 15:28   org/springframework/boot/loader/ExecutableArchiveLauncher.class
     1669  2017-03-03 15:28   org/springframework/boot/loader/WarLauncher.class
        0  2017-03-24 16:10   org/springframework/boot/loader/archive/
     1749  2017-03-03 15:28   org/springframework/boot/loader/archive/JarFileArchive$EntryIterator.class
     3792  2017-03-03 15:28   org/springframework/boot/loader/archive/ExplodedArchive$FileEntryIterator.class
     1068  2017-03-03 15:28   org/springframework/boot/loader/archive/ExplodedArchive$FileEntry.class
     1051  2017-03-03 15:28   org/springframework/boot/loader/archive/JarFileArchive$JarFileEntry.class
      302  2017-03-03 15:28   org/springframework/boot/loader/archive/Archive$Entry.class
     7016  2017-03-03 15:28   org/springframework/boot/loader/archive/JarFileArchive.class
     4974  2017-03-03 15:28   org/springframework/boot/loader/archive/ExplodedArchive.class
      906  2017-03-03 15:28   org/springframework/boot/loader/archive/Archive.class
     1438  2017-03-03 15:28   org/springframework/boot/loader/archive/ExplodedArchive$FileEntryIterator$EntryComparator.class
      399  2017-03-03 15:28   org/springframework/boot/loader/archive/Archive$EntryFilter.class
      273  2017-03-03 15:28   org/springframework/boot/loader/archive/ExplodedArchive$1.class
    17141  2017-03-03 15:28   org/springframework/boot/loader/PropertiesLauncher.class
        0  2017-03-24 16:10   org/springframework/boot/loader/util/
     4887  2017-03-03 15:28   org/springframework/boot/loader/util/SystemPropertyUtils.class
---------                     -------
 14428545                     106 files

依存ライブラリが jar ファイルのまま内包されていることが分かる。内包する jar ファイルをクラスパスに含めつつアプリケーションの main メソッド (ここでは FortuneApplication.main) を叩くため、spring-boot-loader のクラス群があらかじめ jar ファイル内に展開されている。

この jar ファイルを使ってアプリケーションを起動するには、java コマンドを叩けばいい。

$ java -jar target/fortune-0.0.1-SNAPSHOT.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.2.RELEASE)

2017-03-11 17:08:05.071  INFO 12061 --- [           main] org.creasys.FortuneApplication             : Starting FortuneApplication v0.0.1-SNAPSHOT on carrot with PID 12061 (/home/kazuki/sources/writing/spring/fortune/target/fortune-0.0.1-SNAPSHOT.jar started by kazuki in /home/kazuki/sources/writing/spring/fortune)

(snip)

2017-03-11 17:08:07.998  INFO 12061 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2017-03-11 17:08:08.002  INFO 12061 --- [           main] org.creasys.FortuneApplication             : Started FortuneApplication in 3.364 seconds (JVM running for 3.902)

Eclipse から起動したときと異なり、メインスレッドの名前が main となっていることが分かる。spring-boot:repackage により、spring-boot-devtools が除外されているため。特に難しいことは考えることなく、リリースする jar ファイルでは spring-boot-devtools による余計な制御が行われないようになっている。

デプロイするには、この jar ファイルを適当な場所におき、systemd なりから叩くようにすればよい。詳しくは Spring Boot のリファレンス参照。

ここでは紹介しないが、war ファイルとしてパッケージングし、任意の Servlet コンテナにデプロイすることもできる。詳しくは (やはり) リファレンスを参照。

Spring IoC コンテナ

広大な Spring Framework の世界において、その中心 (というより底という表現がしっくりくるかもしれない) にあるのが IoC コンテナです。Spring Boot を使ってお気楽ご気楽にプログラミングできるのも、AOP で宣言的ほげほげできるのも、全部 IoC コンテナの下支えがあってこそ。

ここでは縁の下の力持ちであるところの IoC コンテナに焦点を当てる。IoC コンテナが地味で泥臭いことを請け負っていることもあり、話の内容も地味で泥臭い感じになってしまうが、Spring Framework を理解するには IoC コンテナの理解が必要不可欠なので、ご容赦いただきたい。

用語

まずは Spring の IoC コンテナを語る上で把握しておくべき用語について触れる。

IoC とは

IoC = Inversion of Cotnrol = 制御の反転

A から B に依存関係があるとき、A が B を呼び出すのが通常の制御、B が A を呼び出すのが反転した制御。

ライブラリ vs フレームワークみたいな話だと理解している。ライブラリもフレームワークも、いずれもアプリケーションから依存されるもので、依存の方向は同じである。しかし制御の方向は、ライブラリはアプリケーションから制御されるが、フレームワークはアプリケーションを制御する。つまり依存関係と制御の関係が、フレームワークは逆転している。(一般的に) フレームワークは IoC に則っていると言えるのではないか。

例えば GUI フレームワークなんかは反転した制御の典型。アプリは GUI フレームワークに依存しているが、すべての制御は GUI フレームワーク側が行う。例えば Java の Swing なら EDT でループを回すのは GUI フレームワークの仕事。

制御の反転 - Wikipedia

ソフトウェア工学において、制御の反転(Inversion of Control、IoC)とは、コンピュータ・プログラムの中で、個別の目的のために書かれたコード部分が、一般的で再利用可能なライブラリによるフロー制御を受ける形の設計を指す。この設計を採用した ソフトウェアアーキテクチャは、伝統的な手続き型プログラミングと比べると制御の方向が反転している。すなわち、従来の手続き型プログラミングでは、個別に開発するコードが、そのプログラムの目的を表現しており、汎用的なタスクを行う場合に再利用可能なライブラリを呼び出す形で作られる。一方、制御を反転させたプログラミングでは、再利用可能なコードの側が、個別目的に特化したコードを制御する。

IoC コンテナとは

アプリケーションを構成するオブジェクトの組み立てを行う人。オブジェクト同士の依存関係は、オブジェクト自身が解決するのではなく、IoC コンテナが解決する。

Inversion of Control コンテナと Dependency Injection パターン - マーチン・ファウラー

ここで疑問なのは、軽量コンテナは制御のどういった側面を反転させているのか、ということだ。 私がはじめて制御の反転というものに遭遇したとき、それはユーザインタフェースのメインループのことだった。 初期のユーザインターフェースは、アプリケーションプログラムで制御されていた。 「名前の入力」「住所の入力」みたいな一連のコマンドを取り扱いたいとなれば、 プログラムでプロンプトの表示と、それぞれの入力を制御する。 これがグラフィカルなUI(コンソールベースでもいいけど)になると、UIフレームワークにはメインループがあり、フレームワークからスクリーンの様ざまなフィールドの代わりとしてイベントハンドラが提供されている。プログラムではこのイベントハンドラを取り扱う。ここではプログラムの中心となる制御が反転されている。制御は個々のプログラムからフレームワークへと移されているのだ。

新種のコンテナにおいて反転されているのは、プラグイン実装のルックアップ方法である。 私の素朴なサンプルでいえば、MovieLister は MovieFinder の実装を直接インスタンス化することでルックアップしている。 これだと、ファインダはプラグインではなくなっている。 新種のコンテナが採用しているアプローチには、プラグインを利用するにあたって必ず従わなければならない取り決めが存在する。 この規約があることで、コンテナはMovieFinder 実装を MovieLister オブジェクトにインジェクト(inject: 注入)することが可能になる。

結論をいえば、このパターンにはもっと明確な名前が必要なように思う。 「制御の反転」という用語では包括的すぎる。これでは混乱する人が出てくるのも無理はない。 様ざまな IoC 支持者と多くの議論を重ねた末、その名前は Dependency Injection (依存オブジェクト注入)に落ち着いた。

DI (Dependency Injection) は IoC の一種である。Spring の IoC コンテナが提供しているのは DI なので、Spring においては IoC コンテナ = DI コンテナと言ってよさそう。

ここでいう コンテナ とは、「雑多なオブジェクトを自身の管理下におき、それらを協調させるオブジェクトのこと」と理解している。2000 年代の Java 界隈では何かとコンテナと呼ばれるものがあった (今もある)。Servlet コンテナ、EJB コンテナ、軽量コンテナ etc... Spring の IoC コンテナは、最後の軽量コンテナに属する。今どきコンテナと言えば Docker に代表されるそれだが、Spring の文脈では別の意味となる。

Bean とは

Spring の IoC コンテナによって管理されるオブジェクトを Bean と呼ぶ。JavaBeans とは違う。

Spring の設定とは、すなわち Bean の設定のことを指す。

  • どのような Bean を定義するか
  • Bean にどのようなプロパティを与えるか
  • Bean をどのように初期化するか
  • Bean をどのように破棄するか
  • どの Bean と Bean をつなげるか

といった設定を IoC コンテナに食わせると、IoC コンテナは Bean のオブジェクトツリーを構築し、適切に生成/破棄を行う。

IoC コンテナが扱うオブジェクト (= Bean) は、基本的には POJO である。特定のインタフェースの実装することや、アノテーションをつけることが求められる場合もある。POJO なので、ユニットテストではふつうに new でき、テストしやすい。

コンテナの表現

Spring の IoC コンテナは ApplicationContext インタフェースで表現されている。実装のバリエーションはいくつもある

Spring Boot では SpringApplication クラスが ApplicationContext の実装を選択している。デフォルトでは AnnotationConfigApplicationContextAnnotationConfigEmbeddedWebApplicationContext使うようになっている

設定の読み込み、Bean の管理、依存関係の解決などは ApplicationContext によって提供される。Spring の中心には常に ApplicationContext がある。

厳密には、Bean の管理と依存関係の解決は BeanFactory インタフェースによって提供される。ApplicationContextBeanFactory のスーパーセットである。ApplicationContextBeanFactory の比較はリファレンスが詳しい。アプリケーション開発においては、よほど特別な理由がない限り BeanFacotry を直接使うことはなさそう。

以下は伝統的な XML による Bean 定義を読み込むタイプの ApplicationContext を使ったプログラムである。

public class HelloXmlAppContext {
    public static void main(String[] args) {
        try (ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml")) {
            GreetingService g = ctx.getBean("greetingService", GreetingService.class);
            System.err.println(g.hello("Bob"));
        }
    }
}

クラスパス直下の beans.xml を読み込むよう指定している。コンテナの構築後は、greetingService と名前のついた Bean をコンテナから取得し、hello メソッドを呼び出している。

XML は以下のような感じ:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="greetingService" class="org.creasys.hellospring.xml.XmlGreetingService">
    </bean>

</beans>

なお、これ以後で XML による設定は使わない。先の Spring Boot の例を見たとおり、最近の Spring Framework では XML ではなくアノテーションなどを使った Java Config が主流であるため。

Java Config

かつての Spring は XML による設定だけを提供していたが、最近のバージョンでは Java Config と呼ばれるアノテーションベースの設定方法が用意されている。Spring Boot は、デフォルトで Java Config を使うようになっているので、ここでも Java Config による設定についてのみ触れることにする。

基本となるのは @Configuration@Bean である。

@Configuration

@Configuration はクラスにつけるアノテーションである。そのクラスが設定を主としたクラスであることを、IoC コンテナに知らせるためのマーカーである。

@Configuration
public class Config {
    // ...
}

このクラスの中に、設定を書いていく。

@Bean

@Bean はメソッドにつけるアノテーションである。メソッドが Bean 定義であることを IoC コンテナに知らせるためのマーカーである。メソッドの戻り値は IoC コンテナによって管理される Bean となる。

@Configuration
public class Config {
    @Bean
    public MyApiGateway myApiGateway() {
        return new MyApiGateway();
    }
}

Bean に依存関係を設定するには、メソッドの引数を使う。

@Configuration
public class Config {
    @Bean
    public HttpClient httpClient() {
        return new HttpClient();
    }

    @Bean
    public MyApiGateway myApiGateway(HttpClient httpClient) {
        return new MyApiGateway(httpClient);
    }
}

MyApiGateway は HttpClient に依存する。依存する HttpClient もまた Bean として定義されている。MyApiGateway が依存する HttpClient のインスタンスは、IoC コンテナによって与えられる。

@Scope

Bean のライフサイクルは IoC コンテナによって管理される。デフォルトでは、コンテナの起動時にインスタンス化され、シングルトンとして扱われる。すなわちひとつのオブジェクトが全ての場所で共有される。そしてコンテナの終了とともに、インスタンスも破棄される。

Bean のライフサイクルを変えたい (= シングルトンにしたくない) 場合には @Scope を使う。Spring では、Bean がいつ生成され、いつ破棄されるかを、Bean の「スコープ」と表現している。

@Configuration
public class Config {
    @Bean
    public HttpClient httpClient() {
        return new HttpClient();
    }

    @Bean
    @Scope("prototype")
    public MyApiGateway myApiGateway(HttpClient httpClient) {
        return new MyApiGateway(httpClient);
    }
}

デフォルトで用意されているスコープは以下の通り:

  • singleton
    • シングルトン。デフォルト
  • prototype
    • ApplicationContext.getBean するたびに new する。

基本的にはこの 2 つ。Web 向けとして:

  • request
  • session
  • globalSession
  • application
  • websocket

が用意されている。リファレンス が詳しい。

リファレンスによれば、かんたんなガイドラインとして、

  • コンポーネントがステートレスなら singleton
  • ステートフルなら prototype

にすべし、とある。

As a rule, use the prototype scope for all stateful beans and the singleton scope for stateless beans.

Web アプリケーションの文脈ならば、状態は DB に持つのであり、主要なコンポーネント (コントローラ、サービス、リポジトリ) には状態を持たないので、基本的には singleton で十分である。

@Component

@Component はクラスにつけるアノテーションである。このアノテーションをつけたクラスが、IoC コンテナの管理対象であることを知らせるためのマーカーである。@Component がついたクラスは @Bean による Bean 定義を書かずとも、IoC コンテナの管理対象になる。同じような役割の Bean が大量にある場合には、いちいち @Bean で Bean を定義していくよりも手軽で使いでがある。

@Component はクラスだけでなくアノテーションにもつけることができる (= メタアノテーション)。@Component がついたアノテーションをつけたクラスもまた、IoC コンテナの管理対象として扱われる。@Component がついたアノテーションはステレオタイプと呼ばれたりもする。

Spring Framework 本体では、典型的なコンポーネントとして 3 つのステレオタイプが用意されている:

  • @Controller
    • Web アプリケーションのコントローラクラスであることを表す
  • @Service
    • サービス層のクラスであることを表す
  • @Repository
    • 永続層のクラスであることを表す

例えば以下のクラスは @Bean による Bean 定義を書かずとも、IoC コンテナによって、適切に DI が行われ、オブジェクトが管理される。

@Service
public class AuthService {
    private UsersRepository users;

    public AuthService(UsersRepository users) {
        this.users = users;
    }

    // ...
}

@Configuration
public class Config {
    // 以下は必要ない
    //@Bean
    //public AuthService authService(UsersRepository users) {
    //  return new AuthService(users);
    //}
}

先の Spring Boot の例もこの仕組みを利用している。FortuneControllerFortuneService もステレオタイプのアノテーションがついているので、@Bean による設定を行わなくても IoC コンテナに登録されるようになっている。Web アプリケーションの開発において、コントローラ、サービス、リポジトリといった主要なコンポーネントはたくさん作ることになる。これらをすべて @Bean で Bean 定義するのは面倒である。

@Autowired

かつての XML 設定では Bean の依存関係の解決は、設定ファイルでひとつひとつ明示的に指定するのが基本だった。イメージ的には @Bean メソッド内で、setter をひとつひとつ呼ぶような感じ。後に Autowire と呼ばれる依存関係の自動解決の仕組みが入った。Java Config では @Autowired アノテーションを使って依存関係の自動解決を行う。

依存性を注入する方法として、Spring では 3 つの方法が提供されている:

  • コンストラクタインジェクション
  • フィールドインジェクション
  • メソッドインジェクション

Spring 4.3 からは、コンストラクタに @Autowired をつけなくても Autowire されるようになった

@Component
public class Spring42 {
    @Autowired
    public Spring42(MyBean obj) { }
}

@Component
public class Spring43 {
    public Spring43(MyBean obj) { }
}

基本的にはコンストラクタインジェクションを使うのが好ましい。テストの際、依存コンポーネントをモックにすり替えるのがかんたんであるため。また、大量の依存関係を持つことはコンストラクタ引数が増えることに繋がるので、依存関係を増やしすぎてしまうことへの抑止力にもなる (良識のあるプログラマに対しては)。

フィールドインジェクション、メソッドインジェクションを使う場合には @Autowired を使う。

@Component
public class MyBean1 {
    @Autowired
    private MyBean2 obj;

    @Autowired
    public void setup(MyBean3 a, MyBean4 b) { }
}

IoC コンテナが MyBean1 を構築する際、@Autowired がついたメンバを探す@Autowired がついたメンバの型 (フィールドならその型、メソッドならパラメータの型) を持つオブジェクトに依存するものと判断する。IoC コンテナはこれらの型を自身から探してきて、フィールドやメソッドのパラメータに指定することで依存関係を解決する。

@Value

@Value はフィールドやメソッドのパラメータにつけるアノテーションである。構造を持ったオブジェクトではなく、整数や文字列といった単純な値をコンテナから与えてもらうときに使う。

@Configuration
public class Config {
    @Bean
    public MyApiGateway myApiGateway(@Value("${myapi.baseurl}") String baseUrl) {
        return new MyApiGateway(baseUrl);
    }
}

@Value の引数には SpEL を書く。上の例では ${myapi.baseurl} としており、これはプロパティ myapi.baseurlbaseUrl に設定することを表現している。IoC コンテナが @Value に指定した式を評価し、メソッドの引数に指定する。

@PostConstruct, @PreDestroy

Bean の構築後、破棄前にフックをかけることができる。@PostConstruct は構築後、@PreDestroy は破棄前に呼ばれるメソッドにつけるアノテーションである。これらのアノテーションは Spring 独自ではなく JSR-250 で標準化されているアノテーションである。

public class MyApiGateway {
    private HttpClient client;

    public MyApiGateway(HttpClient httpClient) {
        this.httpClient = httpClient;
    }

    @PostConstruct
    public void init() {
        httpClient.post("http://mygreatapi.example.com/hello");
    }

    @PreDestroy
    public void destroy() {
        httpClient.post("http://mygreatapi.example.com/bye");
    }
}

@PostConstruct は DI が完了した状態で呼ばれる。

なお、Java Config では Bean 定義を Java プログラムのメソッド内に書くので、Bean を new したあとに初期化メソッドを呼んでしまえば済む話である。

@Bean
public MyApiGateway myApiGateway(HttpClient httpClient) {
    MyApiGateway gw = new MyApiGateway(httpClient);
    gw.init();
    return gw;
}

@Component がついたクラスではこのようなことができないので、@PostConstruct を使って初期化メソッドを書く。

Environment

Spring では実行環境を Environment インタフェースによって抽象化している。ここで言うところの実行環境には、

  • プロファイル
  • プロパティ

を含む。

プロファイルは モード みたいなもの。よくあるのは、開発中は develop プロファイル、製品版は production プロファイルを指定する、みたいな感じである。プロファイルごとに有効な設定を切り替えることができる。よくあるのは DB の接続先を開発中と製品版とで切り替えるといったもの。Bean 定義をプロファイルごとに変えることもできる。

@Configuration
public class AppConfig {
    @Bean
    @Profile("develop")
    public GreetingService developGreetingService() {
        return new DevelopGreetingService();
    }

    @Bean
    @Profile("production")
    public GreetingService productionGreetingService() {
        return new ProductionGreetingService();
    }
}

以下のプログラムでは production プロファイルを有効にしている。GreetingService の実装として ProductionGreetingService が使われる。

@ComponentScan
public class HelloProfile {
    public static void main(String[] args) {
        try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext()) {
            ctx.register(HelloProfile.class);
            ctx.getEnvironment().setActiveProfiles("production");
            ctx.refresh();

            GreetingService g = ctx.getBean(GreetingService.class);
            System.err.println(g.hello("Alice"));
        }
    }
}

プロパティはアプリケーションに指定できるオプションのようなものを指す。プロパティの元ネタは PropertySource クラスによって抽象化されている。コマンドライン引数、Servlet 設定、JNDI などの実装がある。

BeanPostProcessor

IoC コンテナを拡張するためのインタフェースとして BeanPostProcessor がある。このインタフェースは 2 つのメソッドを提供する:

  • Object postProcessBeforeInitialization(Object bean, String beanName)
  • Object postProcessAfterInitialization(Object bean, String beanName)

いずれも Bean のインスタンスと名前を受け取り、オブジェクトを返す。戻り値のオブジェクトが、与えられた名前の Bean のインスタンスとして使われる。

それぞれ Bean の初期化前と初期化後に呼ばれるメソッドである。ここでいう初期化とは、InitializingBeanafterPropertiesSet の呼び出しや @BeaninitMethod に指定したメソッドの呼び出しのことを指す。つまり、IoC コンテナが Bean のインスタンスを生成し、依存関係を解決し、初期化メソッドを呼び出す前後で呼び出されるフックであると言える。

この仕組みは Spring の中で広く使われている。AOP を始め、Bean の Validation、定期実行など。どのような実装があるかは BeanPostProcessor の Javadoc が詳しい。

アプリケーション開発でこの仕組みを直接使うことはないかもしれないが、このような仕組みの存在を覚えておくと、Spring がどのようにして魔法のような機能を実現しているか、なんとなく想像できる。例えば AOP なら、メソッドの引数で受け取ったオブジェクトの Proxy を返すように実装すれば、実現できそうである (実際そうなっているかはさておき)。

かんたんな実装例として org.springframework.validation.beanvalidation.BeanValidationPostProcessor がある。Bean の構築後に、Bean についている javax.validation (JSR-303) のアノテーションに基づいて Validation を行っている。Validation に失敗した場合には Bean の生成/初期化に失敗したものとして例外を投げる。

Spring Boot のソースを読む

少し Spring Boot の中を覗いてみることにする。今回は組み込み Tomcat の生成から起動に至るプロセスを追ってみることにする。

組み込み Tomcat の起動プロセスを追う

Spring Boot は、Auto Configuration とそこから駆動される便利クラス群から成り立っている。まずは組み込み Tomcat が使われることを決定付ける Auto Configuration を見てみる。

Auto Configuration とは Spring Boot によって提供される機能である。Spring のコア機能である @Configuration の有効/無効を、実行環境や依存ライブラリの構成にあわせて自動的に切り替えるというものである。Spring Boot の autoconfigure プロジェクトには大量の Auto Configuration が用意されており、これによってアプリケーション側でちまちまと設定を行わなくていいようになっている。

Eclipse で org.apache.catalina.startup.Tomcat クラスの参照を探すと、 org.springframework.boot.autoconfigure.web.EmbeddedTomcat というクラスが見つかる。パッケージ名に autoconfigure とあることから、これが Tomcat を使うよう決定づける Auto Configuration であると推測できる。

/**
 * Nested configuration if Tomcat is being used.
 */
@Configuration
@ConditionalOnClass({ Servlet.class, Tomcat.class })
@ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
public static class EmbeddedTomcat {

    @Bean
    public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() {
        return new TomcatEmbeddedServletContainerFactory();
    }

}

EmbeddedServletContainerFactory の実装として、 TomcatEmbeddedServletContainerFactory を Bean 定義している。こいつが Tomcat のインスタンスを作っているものと推測できる。なお、この設定 (EmbeddedTomcat) が有効になるのは、 javax.servlet.Servletorg.apache.catalina.startup.Tomcat がクラスパス内に存在し (@ConditionalOnClass)、かつ EmbeddedServletContainerFactory の Bean 定義が他にない (@ConditionalOnMissingBean) 場合に限る。独自に EmbeddedServletContainerFactory の Bean を定義したり、Web 環境でない場合には、この設定は有効とならない。このように、実行時の環境、条件に応じて設定の有効/無効が自動的に切り替わることから、Auto Configuration と呼ばれている。

次に TomcatEmbeddedServletContainerFactory を追う。

@Override
public EmbeddedServletContainer getEmbeddedServletContainer(
        ServletContextInitializer... initializers) {
    Tomcat tomcat = new Tomcat();
    // (snip)
    return getTomcatEmbeddedServletContainer(tomcat);
}

Tomcat のインスタンスを作り、TomcatEmbeddedServletContainer でラップして返している。

このメソッドの参照を Eclipse で探すと、EmbeddedWebApplicationContext が見つかる。ソースを読むと、onRefresh メソッド経由で getEmbeddedServletContainer を呼び出していることが分かる。

@Override
protected void onRefresh() {
    super.onRefresh();
    try {
        createEmbeddedServletContainer();
    }
    catch (Throwable ex) {
        throw new ApplicationContextException("Unable to start embedded container",
                ex);
    }
}

onRefresh は、Spring の IoC コンテナを refresh したときに呼び出されるフックである。ApplicationContext の実装ごとの、特別な Bean の準備するために呼び出される。アプリケーションの起動時はもちろん、spring-boot-devtools でアプリケーションのクラスローダーを再読み込みするときにも呼び出される。以上が Servlet コンテナの構築の流れとなる。

続けて、起動の流れを追う。TomcatEmbeddedServletContainer.start の参照を探すと、先ほどと同様に EmbeddedWebApplicationContext が見つかる。さらに追っていくと、finishRefresh 経由で呼び出していることが分かる。

@Override
protected void finishRefresh() {
    super.finishRefresh();
    EmbeddedServletContainer localContainer = startEmbeddedServletContainer();
    if (localContainer != null) {
        publishEvent(
                new EmbeddedServletContainerInitializedEvent(this, localContainer));
    }
}

finishRefresh は IoC コンテナの refresh が完了するタイミングで呼び出されるフックである。すなわち Bean 定義の読み込みや設定がひと通り終わったタイミングで Servlet コンテナが動き始める。

FortuneApplication の main メソッドで呼び出している SpringApplication.run では、実際に利用する ApplicationContext の決定が行われる。Web 環境で使われるデフォルトの ApplicationContext の実装は、org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext となっている。AnnotationConfigEmbeddedWebApplicationContextEmbeddedWebApplicationContext のサブクラスである。

public static final String DEFAULT_WEB_CONTEXT_CLASS = "org.springframework."
        + "boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext";

(snip)

protected ConfigurableApplicationContext createApplicationContext() {
    Class<?> contextClass = this.applicationContextClass;
    if (contextClass == null) {
        try {
            contextClass = Class.forName(this.webEnvironment
                    ? DEFAULT_WEB_CONTEXT_CLASS : DEFAULT_CONTEXT_CLASS);
        }
        catch (ClassNotFoundException ex) {
            throw new IllegalStateException(
                    "Unable create a default ApplicationContext, "
                            + "please specify an ApplicationContextClass",
                    ex);
        }
    }
    return (ConfigurableApplicationContext) BeanUtils.instantiate(contextClass);
}

これにより、

  • IoC コンテナの実装として EmbeddedWebApplicationContext のサブクラスである AnnotationConfigEmbeddedWebApplicationContext が使われ、
  • EmbeddedWebApplicationContext.onRefresh で Tomcat のインスタンスが作られ、
  • EmbeddedWebApplicationContext.finishRefresh で Tomcat が動き出す

ということが分かった。

組み込み Tomcat の起動プロセスがわかったところで「だからなんだ」という話なのだが、アプリケーションのコードを読むだけでは「実際にはどう動いているのか」は、Spring Boot の黒魔術によって隠蔽されており、さっぱり分からない。こうして少しでも中身を理解することで、動作イメージが浮かびやすくなる。動作イメージが浮かぶようになれば、トラブルが発生したときにも落ち着いて対処できる (かもしれない)。