はじめてのSpring Bootという書籍を読んでSpring Boot DIアプリ開発について勉強しました。
プロジェクトの雛形を作成。
$ mvn -B archetype:generate -DgroupId=com.example -DartifactId=hello_spring_di -Dversion=1.0.0-SNAPSHOT -DarchetypeArtifactId=maven-archetype-quickstart
pom.xmlを編集します。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>HelloSpringDI</artifactId> <packaging>jar</packaging> <version>1.0.0-SNAPSHOT</version> <name>HelloSpringDI</name> <url>http://maven.apache.org</url> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.1.8.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> <properties> <java.version>1.8</java.version> </properties> </project>
Calculatorインターフェイスの作成
appフォルダを用意してCalculator.javaを作成します。
package com.example.app; public interface Calculator { int calc(int a, int b); }
AddCalculator.javaを用意します。
package com.example.app; public interface Calculator { int calc(int a, int b); }
Bean定義ファイルの作成
Spring Frameworkの定義ファイルはXML形式とJavaクラス(JavaConfig)の二種類があります。今回はJavaクラスを使用します。
AppConfig.javaを作成。
package com.example; import com.example.app.AddCalculator; import com.example.app.Calculator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class AppConfig { @Bean Calculator calculator() { return new AddCalculator(); } }
ここまでのファイル構成はこんな感じです。
エントリ・ポイントの作成
Appクラスを編集してCalculatorを実行するエントリポイントを作成します。
package com.example; import com.example.app.Calculator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Import; import java.util.Scanner; @EnableAutoConfiguration @Import(AppConfig.class) public class App { public static void main(String[] args) { try (ConfigurableApplicationContext context = SpringApplication.run(App.class, args)) { Scanner scanner = new Scanner(System.in); System.out.print("Enter 2 numbers like 'a b' : "); int a = scanner.nextInt(); int b = scanner.nextInt(); Calculator calculator = context.getBean(Calculator.class); int result = calculator.calc(a, b); System.out.println("result = " + result); } } }
実行
次のコマンドでクラスを実行します。
$ mvn spring-boot:run
Enter 2 numbers like 'a b' :と表示されるので『100 200』と入力してEnterキーを押下します。result = 300と表示されます。
アプリケーションの抽象化
『Calcylator』の引数をつくるために『ArgumentResolver』インターフェイスを作ります。
package com.example.app; import java.io.InputStream; public interface ArgumentResolver { Argument resolve(InputStream stream); }
引数オブジェクトとしてArgumentクラスをつくります。
package com.example.app; import lombok.Data; import lombok.RequiredArgsConstructor; @Data @RequiredArgsConstructor public class Argument { private final int a; private final int b; }
ArgumentResolverの実装としてjava.util.Scannerを使って引数を取得するクラスを作ります。
package com.example.app; import java.io.InputStream; import java.util.Scanner; public class ScannerArgumentResolver implements ArgumentResolver { @Override public Argument resolve(InputStream stream) { Scanner scanner = new Scanner(stream); int a = scanner.nextInt(); int b = scanner.nextInt(); return new Argument(a, b); } }
Bean定義ファイルにArgumentResolverの実装クラスを定義します。
package com.example; import com.example.app.AddCalculator; import com.example.app.ArgumentResolver; import com.example.app.Calculator; import com.example.app.ScannerArgumentResolver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class AppConfig { @Bean Calculator calculator() { return new AddCalculator(); } @Bean ArgumentResolver argumentResolver() { return new ScannerArgumentResolver(); } }
Appクラス内でArgumentResolverをDIコンテナから取得するように修正します。
package com.example; import com.example.app.Argument; import com.example.app.ArgumentResolver; import com.example.app.Calculator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Import; @EnableAutoConfiguration @Import(AppConfig.class) public class App { public static void main(String[] args) { try (ConfigurableApplicationContext context = SpringApplication.run(App.class, args)) { System.out.print("Enter 2 numbers like 'a b' : "); ArgumentResolver argumentResolver = context.getBean(ArgumentResolver.class); Argument argument = argumentResolver.resolve(System.in); Calculator calculator = context.getBean(Calculator.class); int result = calculator.calc(argument.getA(), argument.getB()); System.out.println("result = " + result); } } }
オート・ワイヤリング対応
Appクラス内でcontext.getBeanの呼び出しが多くなってきたので次はこれを改善します。Appクラスで実装した処理を集約するFrontendクラスを用意します。@AutowiredアノテーションをつけるとDIコンテナとして管理されます。
package com.example.app; import org.springframework.beans.factory.annotation.Autowired; public class Frontend { @Autowired ArgumentResolver argumentResolver; @Autowired Calculator calculator; public void run() { System.out.print("Enter 2 numbers like 'a b' : "); Argument argument = argumentResolver.resolve(System.in); int result = calculator.calc(argument.getA(), argument.getB()); System.out.println("result = " + result); } }
FrontedクラスもBean定義ファイルに定義します。importの宣言部分もスッキリしました。
package com.example; import com.example.app.*; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class AppConfig { @Bean Calculator calculator() { return new AddCalculator(); } @Bean ArgumentResolver argumentResolver() { return new ScannerArgumentResolver(); } @Bean Frontend frontend() { return new Frontend(); } }
これでAppクラスはFrontedを実行するだけになります。
package com.example; import com.example.app.Frontend; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Import; @EnableAutoConfiguration @Import(AppConfig.class) public class App { public static void main(String[] args) { try (ConfigurableApplicationContext context = SpringApplication.run(App.class, args)) { Frontend frontend = context.getBean(Frontend.class); frontend.run(); } } }
コンポーネント・スキャンと自動Bean登録
この時点ではDIコンテナに登録したいBeanを1つずつ定義することになっています。これを改善していきます。
@ComponentScanアノーテーションを使います。
package com.example; import com.example.app.Frontend; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Import; @EnableAutoConfiguration @ComponentScan public class App { public static void main(String[] args) { try (ConfigurableApplicationContext context = SpringApplication.run(App.class, args)) { Frontend frontend = context.getBean(Frontend.class); frontend.run(); } } }
DIコンテナに登録したい以下のクラスに@Componentとimportの部分を追記します。
AddCalcylatorクラス
package com.example.app; import org.springframework.stereotype.Component; @Component public class AddCalculator implements Calculator { @Override public int calc(int a, int b) { return a + b; } }
ScannerArgumentResolverクラス
package com.example.app; import org.springframework.stereotype.Component; import java.io.InputStream; import java.util.Scanner; @Component public class ScannerArgumentResolver implements ArgumentResolver { @Override public Argument resolve(InputStream stream) { Scanner scanner = new Scanner(stream); int a = scanner.nextInt(); int b = scanner.nextInt(); return new Argument(a, b); } }
Frontedクラス
package com.example.app; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class Frontend { @Autowired ArgumentResolver argumentResolver; @Autowired Calculator calculator; public void run() { System.out.print("Enter 2 numbers like 'a b' : "); Argument argument = argumentResolver.resolve(System.in); int result = calculator.calc(argument.getA(), argument.getB()); System.out.println("result = " + result); } }
これでAppConfigクラスが不要になりました。削除しても今まで通り動作します。
$ mvn spring-boot:run
・・ですが自分の環境だと次のエラーが発生しました。
パッケージlombokは存在しません
Getter, Setter の自動生成で使用するlombokの準備を忘れていました。pom.xmlにlombokを追加すればいいみたいです。
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.14.0</version> <scope>provided</scope> </dependency> </dependencies>
これで正常に実行されるようになります。
CommandLineRunnerの利用
CommandLineRunnerを利用することで上記で作成したFrontedクラスの機能をAppクラス側で実装できるようになります。
package com.example; import com.example.app.Argument; import com.example.app.ArgumentResolver; import com.example.app.Calculator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.ComponentScan; @EnableAutoConfiguration @ComponentScan public class App implements CommandLineRunner { @Autowired ArgumentResolver argumentResolver; @Autowired Calculator calculator; @Override public void run(String... strings) throws Exception { System.out.print("Enter 2 numbers like 'a b' : "); Argument argument = argumentResolver.resolve(System.in); int result = calculator.calc(argument.getA(), argument.getB()); System.out.println("result = " + result); } public static void main(String[] args) { SpringApplication.run(App.class, args); } }
これでFrontedクラスを削除しても正常に処理を実行できます。
$ mvn spring-boot:run
JARファイルの作成
最後にJARファイルを生成します。
$ mvn package
jarファイルは次のコマンドで実行できます。
$ java -jar target/HelloSpringDI-1.0.0-SNAPSHOT.jar
以上がSpring BootのDIアプリを開発するときの流れになります。