Spring Bootの勉強として、自分でAutoConfigurationを作ってみようかなと思いまして。
AutoConfigurationの内容を読んだりはしますが、自分で作ってみた方がより理解が深まりますよね。
というわけで、お題を以下のように設定してみました。
- Spring Boot 1.5.1.RELEASEを使用
- Ehcache3のAutoConfigurationを作る(現Spring Bootにはない)
- SpringのCache Abstractionには絡めない
- 単純に、CacheManagerをAutoConfigurationするだけ
- 作成したstarterを、別のプロジェクトで読み込んで動かしてみる
参考にしたのは、まずは以下のドキュメント。
44. Creating your own auto-configuration
そして、Spring BootのAutoConfiguration自体も参考にしています。
https://github.com/spring-projects/spring-boot/tree/v1.5.1.RELEASE/spring-boot-autoconfigure
というか、これが1番の教材ではないでしょうか…。
それでは、始めてみます。
Mavenプロジェクトを作る
まずは、Mavenプロジェクトを作成します。
以下に習うと、「autoconfigure」と「starter」という2つのモジュールを作るのがよいそうです。
Namingについては、「xxxxx-spring-boot-autoconfigure」とか「xxxxx--spring-boot-starter」みたいな名前に
するべきだそうです。サードパーティ製のものが、「spring-boot」で始めるのは避けるようにしましょう、と。
「autoconfigure」には起動に必要な設定、Bean定義などを実装し、「starger」は依存関係を管理するだけの
モジュールとします。
というわけで、Mavenプロジェクトは以下のようなマルチプロジェクト構成となります。
$ find ehcache3-spring-boot -maxdepth 2 | grep pom.xml ehcache3-spring-boot/pom.xml ehcache3-spring-boot/ehcache3-spring-boot-autoconfigure/pom.xml ehcache3-spring-boot/ehcache3-spring-boot-starter/pom.xml
以降は、「ehcache3-spring-boot」ディレクトリ内で作業をします。
$ cd ehcache3-spring-boot
親となる「ehcache3-spring-boot」プロジェクトのpom.xmlは、こんな感じ。
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <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/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.littlewings</groupId> <artifactId>ehcache3-spring-boot</artifactId> <packaging>pom</packaging> <version>0.0.1</version> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <spring.boot.version>1.5.1.RELEASE</spring.boot.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring.boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.ehcache</groupId> <artifactId>ehcache</artifactId> <version>3.2.0</version> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring.boot.version}</version> </plugin> </plugins> </build> <modules> <module>ehcache3-spring-boot-autoconfigure</module> <module>ehcache3-spring-boot-starter</module> </modules> </project>
今回は、dependencyManagementに必要な依存関係を定義しておきます。
以降、「autoconfigure」と「starter」を見ていきましょう。
autoconfigureの作成
まずはautoconfigureを作成します。
pom.xmlの定義
pom.xmlの定義は、こんな感じにしました。
ehcache3-spring-boot-autoconfigure/pom.xml
<?xml version="1.0" encoding="UTF-8"?> <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/xsd/maven-4.0.0.xsd"> <parent> <artifactId>ehcache3-spring-boot</artifactId> <groupId>org.littlewings</groupId> <version>0.0.1</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>ehcache3-spring-boot-autoconfigure</artifactId> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.ehcache</groupId> <artifactId>ehcache</artifactId> <optional>true</optional> </dependency> </dependencies> </project>
依存関係にはSpring Bootを使うのに最低限必要な「spring-boot-starter」を含め、Ehcache3はoptionalに設定。
あと、「spring-boot-configuration-processor」を足していますが、これは@ConfigurationPropertiesに対して
コード補完のためのメタデータを生成することができます。
※@makingさんにツッコミいただきました。ありがとうございます。
Appendix B. Configuration meta-data
参考)
Spring Bootの@ ConfigurationPropertiesで型安全なプロパティ設定
@ConfigurationPropertiesを使ったサンプルは後で出てきますが、こちらを使用するとPluggable Annotation Processing APIで
アノテーションからメタデータを生成し(META-INF/spring-configuration-metadata.jsonというファイルができます)、
IDEでコード補完を行うことができるようになります。
AutoConfigurationを作成する
以下の2つの内容に習い、AutoConfigurationを作成します。
Understanding auto-configured beans
各種@Conditinalなアノテーションを付与した@Configurationなクラスを作成し、Beanの定義をしていきます。
@Conditionalアノテーション自体は、各種@Conditionalなアノテーションに付与するアノテーションみたいですね。
以下のような各種条件で、設定が可能なようです。
- Class conditions(@ConditionalOnClass、@ConditionalOnMissingClass) … クラスパス上に指定のクラスの有無
- Bean conditions(@ConditionalOnBean、@ConditionalOnMissingBean) … ApplicationContextへのBean定義の有無
- Property conditions(@ConditionalOnProperty) … プロパティ定義の有無
- Resource conditions(@ConditionalOnResource) … リソースファイルの有無
- Web application conditions(@ConditionalOnWebApplication、@ConditionalOnNotWebApplication) … Webアプリケーションかどうか
- SpEL expression conditions(@ConditionalOnExpression) … SpELによる条件
今回は、シンプルにいってみます。こんなクラスを作成。
ehcache3-spring-boot-autoconfigure/src/main/java/org/littlewings/spring/boot/ehcache3/autoconfigure/Ehcache3AutoConfiguration.java
package org.littlewings.spring.boot.ehcache3.autoconfigure; import java.io.IOException; import java.net.URL; import java.util.Set; import java.util.concurrent.TimeUnit; import org.ehcache.CacheManager; import org.ehcache.config.builders.CacheConfigurationBuilder; import org.ehcache.config.builders.CacheManagerBuilder; import org.ehcache.config.builders.ResourcePoolsBuilder; import org.ehcache.expiry.Duration; import org.ehcache.expiry.Expirations; import org.ehcache.xml.XmlConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @ConditionalOnClass(CacheManager.class) @ConditionalOnMissingBean(CacheManager.class) @EnableConfigurationProperties(Ehcache3Properties.class) public class Ehcache3AutoConfiguration { @Bean(destroyMethod = "close") public CacheManager ehcache3CacheManager(Ehcache3Properties ehcache3Properties) throws IOException { if (ehcache3Properties.getConfig() != null) { // Ehcache3の設定ファイルがある場合 URL configUrl = ehcache3Properties.getConfig().getURL(); org.ehcache.config.Configuration configuration = new XmlConfiguration(configUrl); CacheManager cacheManager = CacheManagerBuilder.newCacheManager(configuration); cacheManager.init(); return cacheManager; } else { // 設定からCacheManager作成 Set<String> cacheNames = ehcache3Properties.getCacheNames(); Long heapEntries = ehcache3Properties.getHeapEntries(); Integer timeToLiveExpirationSeconds = ehcache3Properties.getTimeToLiveExpirationSeconds(); Integer timeToIdleExpirationSeconds = ehcache3Properties.getTimeToIdleExpirationSeconds(); CacheManagerBuilder<CacheManager> cacheManagerBuilder = CacheManagerBuilder .newCacheManagerBuilder(); if (cacheNames != null) { for (String cacheName : cacheNames) { CacheConfigurationBuilder<Object, Object> cacheConfigurationBuilder = CacheConfigurationBuilder .newCacheConfigurationBuilder(Object.class, Object.class, ResourcePoolsBuilder.heap(heapEntries)); // ttl if (timeToLiveExpirationSeconds != null) { cacheConfigurationBuilder = cacheConfigurationBuilder.withExpiry(Expirations.timeToLiveExpiration(Duration.of(timeToLiveExpirationSeconds, TimeUnit.SECONDS))); } // idle timeout if (timeToIdleExpirationSeconds != null) { cacheConfigurationBuilder = cacheConfigurationBuilder.withExpiry(Expirations.timeToIdleExpiration(Duration.of(timeToIdleExpirationSeconds, TimeUnit.SECONDS))); } cacheManagerBuilder = cacheManagerBuilder.withCache(cacheName, cacheConfigurationBuilder.build()); } } CacheManager cacheManager = cacheManagerBuilder.build(); cacheManager.init(); return cacheManager; } } }
@Configurationを付与したクラスを作成して、
@Configuration @ConditionalOnClass(CacheManager.class) @ConditionalOnMissingBean(CacheManager.class) @EnableConfigurationProperties(Ehcache3Properties.class) public class Ehcache3AutoConfiguration {
CacheManagerがクラスパス上にあり
@ConditionalOnClass(CacheManager.class)
CacheManagerがBean定義になければ有効ですと。
@ConditionalOnMissingBean(CacheManager.class)
あと、設定情報としてEhcache3Propertiesというクラスを使用するようにします。
@EnableConfigurationProperties(Ehcache3Properties.class)
Ehcache3Propertiesクラスの定義は、こんな感じです。
ehcache3-spring-boot-autoconfigure/src/main/java/org/littlewings/spring/boot/ehcache3/autoconfigure/Ehcache3Properties.java
package org.littlewings.spring.boot.ehcache3.autoconfigure; import java.util.Set; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.core.io.Resource; import static org.littlewings.spring.boot.ehcache3.autoconfigure.Ehcache3Properties.EHCACHE3_PREFIX; @ConfigurationProperties(prefix = EHCACHE3_PREFIX) public class Ehcache3Properties { public static final String EHCACHE3_PREFIX = "ehcache3"; private Set<String> cacheNames; private Integer timeToLiveExpirationSeconds; private Integer timeToIdleExpirationSeconds; private Long heapEntries; private Resource config; // getter/setterは省略 }
これで、application.propertiesにこんな感じとか(これは設定ファイルの指定)
ehcache3.config=ehcache3.xml
こんな感じに書けるようになります。
ehcache3.cache-names=fooCache,barCache ehcache3.heap-entries=10 ehcache3.time-to-live-expiration-seconds=3
これを使って、Ehcache3の設定ファイルが指定されていればそれを使い、そうでなければその他の
プロパティからCacheManagerとCacheを定義するようにしています。
@Bean(destroyMethod = "close") public CacheManager ehcache3CacheManager(Ehcache3Properties ehcache3Properties) throws IOException { if (ehcache3Properties.getConfig() != null) { // Ehcache3の設定ファイルがある場合 URL configUrl = ehcache3Properties.getConfig().getURL(); org.ehcache.config.Configuration configuration = new XmlConfiguration(configUrl); CacheManager cacheManager = CacheManagerBuilder.newCacheManager(configuration); cacheManager.init(); return cacheManager; } else { // 設定からCacheManager作成 Set<String> cacheNames = ehcache3Properties.getCacheNames(); Long heapEntries = ehcache3Properties.getHeapEntries(); Integer timeToLiveExpirationSeconds = ehcache3Properties.getTimeToLiveExpirationSeconds(); Integer timeToIdleExpirationSeconds = ehcache3Properties.getTimeToIdleExpirationSeconds(); CacheManagerBuilder<CacheManager> cacheManagerBuilder = CacheManagerBuilder .newCacheManagerBuilder(); if (cacheNames != null) { for (String cacheName : cacheNames) { CacheConfigurationBuilder<Object, Object> cacheConfigurationBuilder = CacheConfigurationBuilder .newCacheConfigurationBuilder(Object.class, Object.class, ResourcePoolsBuilder.heap(heapEntries)); // ttl if (timeToLiveExpirationSeconds != null) { cacheConfigurationBuilder = cacheConfigurationBuilder.withExpiry(Expirations.timeToLiveExpiration(Duration.of(timeToLiveExpirationSeconds, TimeUnit.SECONDS))); } // idle timeout if (timeToIdleExpirationSeconds != null) { cacheConfigurationBuilder = cacheConfigurationBuilder.withExpiry(Expirations.timeToIdleExpiration(Duration.of(timeToIdleExpirationSeconds, TimeUnit.SECONDS))); } cacheManagerBuilder = cacheManagerBuilder.withCache(cacheName, cacheConfigurationBuilder.build()); } } CacheManager cacheManager = cacheManagerBuilder.build(); cacheManager.init(); return cacheManager; } }
@ConditionalOnMissingBeanなどは今回クラス定義に付与していますが、もうちょっと別のBean定義も制御したいなどと
いった場合は、メソッドにも付与することができます。Spring Boot自身のAutoConfigurationでも多用されているので、
確認してみるとよいでしょう。
※これでもCacheManagerがBean定義されていなかったら、という意味になります
@Bean(destroyMethod = "close") @ConditionalOnMissingBean public CacheManager ehcache3CacheManager(Ehcache3Properties ehcache3Properties) throws IOException {
spring.factoriesの作成
META-INFディレクトリに、spring.factoriesというファイルを作成します。EnableAutoConfigurationに対する指定として、
先ほど作成した@Configurationなクラスを指定すればOKです。
ehcache3-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.littlewings.spring.boot.ehcache3.autoconfigure.Ehcache3AutoConfiguration
ドキュメントに記載のあるように、複数ある場合は「,」区切りで記述すればよさそうです。
starterを作成する
starterはpom.xmlのみで、依存関係を定義したものだけを作成します。
ehcache3-spring-boot-starter/pom.xml
<?xml version="1.0" encoding="UTF-8"?> <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/xsd/maven-4.0.0.xsd"> <parent> <artifactId>ehcache3-spring-boot</artifactId> <groupId>org.littlewings</groupId> <version>0.0.1</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>ehcache3-spring-boot-starter</artifactId> <dependencies> <dependency> <groupId>org.littlewings</groupId> <artifactId>ehcache3-spring-boot-autoconfigure</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>org.ehcache</groupId> <artifactId>ehcache</artifactId> </dependency> </dependencies> </project>
とりあえず、「mvn install」
このできあがったAutoConfigurationを別のプロジェクトから使うために、「mvn install」しておきます。
$ mvn install
使ってみる
それでは、この作成したAutoConfigurationを使ったプロジェクトを作成してみます。
<?xml version="1.0" encoding="UTF-8"?> <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/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.littlewings</groupId> <artifactId>ehcache3-spring-boot-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <spring.boot.version>1.5.1.RELEASE</spring.boot.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring.boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.littlewings</groupId> <artifactId>ehcache3-spring-boot-starter</artifactId> <version>0.0.1</version> </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> <version>${spring.boot.version}</version> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
先ほど作成したAutoConfigurationを指定します。
<dependency> <groupId>org.littlewings</groupId> <artifactId>ehcache3-spring-boot-starter</artifactId> <version>0.0.1</version> </dependency>
これで、作成したAutoConfigurationとEhcache3が依存関係に引き込まれます。
起動用のクラス作成。
src/main/java/demo/App.java
package demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class App { public static void main(String... args) { SpringApplication.run(App.class, args); } }
設定も、簡単にしてみます。
src/main/resources/application.properties
ehcache3.cache-names=fooCache,barCache ehcache3.heap-entries=10 ehcache3.time-to-live-expiration-seconds=3
確認は、テストコードで行ってみましょう。こんな感じで作成。
src/test/java/demo/DemoAutoConfigurationTest.java
package demo; import java.util.concurrent.TimeUnit; import java.util.stream.IntStream; import java.util.stream.StreamSupport; import org.ehcache.Cache; import org.ehcache.CacheManager; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringRunner.class) @SpringBootTest public class DemoAutoConfigurationTest { @Autowired CacheManager cacheManager; @Test public void gettingStarted() { assertThat(cacheManager.getCache("fooCache", Object.class, Object.class)) .isNotNull(); assertThat(cacheManager.getCache("barCache", Object.class, Object.class)) .isNotNull(); assertThat(cacheManager.getCache("hogeCache", Object.class, Object.class)) .isNull(); } @Test public void expire() throws InterruptedException { Cache<Object, Object> cache = cacheManager.getCache("fooCache", Object.class, Object.class); cache.put("key", "value"); assertThat(cache.get("key")) .isEqualTo("value"); TimeUnit.SECONDS.sleep(5L); assertThat(cache.get("key")) .isNull(); IntStream.rangeClosed(1, 20).forEach(i -> cache.put("key" + i, "value" + i)); long size = StreamSupport .stream(cache.spliterator(), false) .count(); assertThat(size) .isEqualTo(10L); } }
これで動作します。AutoConfigurationで作成されたCacheManagerがインジェクション可能になっています。
@Autowired
CacheManager cacheManager;
次に、Ehcache3の設定ファイルベースに切り替えてみましょう。
src/main/resources/application.properties
ehcache3.config=ehcache3.xml
Ehcache3の設定は、こんな感じ。先ほどとはCacheの名前を変えておきます。
src/main/resources/ehcache3.xml
<?xml version="1.0" encoding="UTF-8"?> <config xmlns='http://www.ehcache.org/v3'> <cache-template name="configureCacheTemplate"> <key-type>java.lang.Object</key-type> <value-type>java.lang.Object</value-type> <expiry> <tti unit="seconds">5</tti> </expiry> <resources> <heap unit="entries">10</heap> </resources> </cache-template> <cache alias="configureCache" uses-template="configureCacheTemplate"/> </config>
テストコードは、こんな感じのものに変更。
@Test public void configureCache() throws InterruptedException { Cache<Object, Object> cache = cacheManager.getCache("configureCache", Object.class, Object.class); cache.put("key", "value"); assertThat(cache.get("key")) .isEqualTo("value"); TimeUnit.SECONDS.sleep(5L); assertThat(cache.get("key")) .isNull(); IntStream.rangeClosed(1, 20).forEach(i -> cache.put("key" + i, "value" + i)); long size = StreamSupport .stream(cache.spliterator(), false) .count(); assertThat(size) .isEqualTo(10L); }
これでも動作します。
最後は、自分でCacheManagerを自分でBean定義してAutoConfigurationの内容を使わないようにしてみます。
起動クラスに、Bean定義を含めてみましょう。
src/main/java/demo/App.java
package demo; import java.util.concurrent.TimeUnit; import org.ehcache.CacheManager; import org.ehcache.config.builders.CacheConfigurationBuilder; import org.ehcache.config.builders.CacheManagerBuilder; import org.ehcache.config.builders.ResourcePoolsBuilder; import org.ehcache.expiry.Duration; import org.ehcache.expiry.Expirations; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class App { public static void main(String... args) { SpringApplication.run(App.class, args); } @Bean(destroyMethod = "close") public CacheManager cacheManager() { CacheManagerBuilder<CacheManager> cacheManagerBuilder = CacheManagerBuilder .newCacheManagerBuilder(); CacheConfigurationBuilder<Object, Object> cacheConfigurationBuilder = CacheConfigurationBuilder .newCacheConfigurationBuilder(Object.class, Object.class, ResourcePoolsBuilder.heap(10)); // ttl cacheConfigurationBuilder = cacheConfigurationBuilder.withExpiry(Expirations.timeToLiveExpiration(Duration.of(3, TimeUnit.SECONDS))); cacheManagerBuilder = cacheManagerBuilder.withCache("myCache", cacheConfigurationBuilder.build()); CacheManager cacheManager = cacheManagerBuilder.build(); cacheManager.init(); return cacheManager; } }
今度は、「myCache」にしてみました。
cacheManagerBuilder = cacheManagerBuilder.withCache("myCache", cacheConfigurationBuilder.build());
テストコードはこんな感じで。
@Test public void myCache() throws InterruptedException { Cache<Object, Object> cache = cacheManager.getCache("myCache", Object.class, Object.class); cache.put("key", "value"); assertThat(cache.get("key")) .isEqualTo("value"); TimeUnit.SECONDS.sleep(5L); assertThat(cache.get("key")) .isNull(); IntStream.rangeClosed(1, 20).forEach(i -> cache.put("key" + i, "value" + i)); long size = StreamSupport .stream(cache.spliterator(), false) .count(); assertThat(size) .isEqualTo(10L); }
AutoConfigurationで定義されたBeanではなく、アプリケーション側でBean定義されたCacheManagerが使用されたことが
確認できました。
とりあえず、こんな感じで動かすことができましたよ、と。
まとめ
簡単にではありますが、Spring BootのAutoConfigurationを自分でも作ってみました。
極めて初歩的な内容だと思いますが、あとはSpring BootのAutoConfigurationのソースコードを見たりしてパターンを覚えて
いけばいいのかな?と思います。
依存関係とかをもうちょっとまとめたい場合は、BOMを作った方がいいのかもしれません。
Spring Bootがそもそもこのスタイルですね。
https://github.com/spring-projects/spring-boot/tree/v1.5.1.RELEASE/spring-boot-dependencies