spring-boot-starterを作成する

はじめに

spring-bootstarterとは、依存関係や設定を自動化するモジュールを意味します。
たとえば、spring-boot-starter-jpaに依存性を追加した際、以下のことを行います。

  • spring-aop,spring-jdbcなどの依存性をかける
  • classpathを検索して、どのDBを使用するか把握し、自動でentityManagerを構成する
  • 当該モジュール設定に必要なproperties設定を提供する(Configuration Processorを使用すると効果UP)

プロジェクトを進める上で、一般的に使用されるspring設定をモジュールに据え置き、使用することができます。
また、必要に応じて上位プロジェクトで何回も設定を上書きすることができます。

今回は、spring-boot-starterを作成して動作する方法を共有します。
spring-bootバージョンは2.0.0.RELEASEを使用します。

実装内容

以下の要件を満たしている場合に、reaquest parameterをloggingするspring-boot-starterを作成します。

  • application.ymlspring.mvc.custom-uri-logging-filter.enabled: trueであること
  • application.ymlspring.mvc.custom-uri-logging-filter.level: info等、指定されたレベルでとること

実装

説明

sample-boot-starter 内部に3つのモジュールを生成します。

  • sample-spring-boot-autoconfigure : @Configurationで、特定の条件に合わせて設定を実行
  • sample-spring-boot-starter-request-parameter-logging-filter : autoconfigureと必要な依存関係を持つ
  • sample-spring-boot-starter-web : starterを注入してもらう

autoconfigurestarterを分けず、starter内にautoconfigureを定義して配布する場合もあります。

Module Naming

starterを作成する際、設定を担当するautoconfigureと、依存関係を担うstarterモジュールを作成する必要があります。
spring referenceでは、次のようにモジュールの命名規則を定義しています。

  • spring-bootで始まらないこと
  • acmeに対するstarterを作成する場合、
    • autoconfigure : acme-spring-boot-autoconfigure
    • starter : acme-spring-boot-starter

springの場合、spring-boot-autoconfigureで、すべてのspring-boot-starter-XXXの自動設定事項を保持しています。
これを基盤としてspring-boot-starter-XXX(例: spring-boot-starter-jpa)モジュールでは、依存性だけを管理しています。

このようなルールを応用して{project}-spring-boot-configure, {project}-spring-boot-starter-{module}と命名しても問題ないと思われます。

application property key

referenceでは可能な限り、固有のkeyを使うことを推奨しています。server,management,springなど、springがすでに定義したproperty keyを使用した場合、今後、springの修正内容がどのような影響を及ぼすか分からないためです。

autoconfigureモジュール

autoconfigureモジュールは、自動設定に必要なすべての要素(@ConfigurationPropertiesなど)とlibraryを有しています。autoconfigureで参照した依存性にはoptionalをかけた方が良いでしょう。この場合、autoconfigureを参照するモジュールで必要な依存性がないとき、Spring Bootは自動設定をしません。

実装

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         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>com.parfait.study</groupId>
    <artifactId>sample-spring-boot-autoconfigure</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>sample-spring-boot-autoconfigure</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <spring-boot.version>2.0.0.RELEASE</spring-boot.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies> <!-- The parent should provide all that -->
            <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>
</project>
  • slf4j-api : logを使用するために依存
  • javax.servlet-api : Filterを使用するために依存
  • spring-boot-configuration-processor :  IDEがapplication.ymlの内容をガイドできるようにする。追って説明する。

application.yml

default設定を定義できます。

spring.mvc.request-parameter-logging-filter:
  enabled: true
  level: DEBUG
logging.level.com.parfait.study.autoconfigure.logging.filter.RequestParameterLoggingFilter: ${spring.mvc.request-parameter-logging-filter.level}

src/main/resoucres/META-INF/additional-spring-configuration-metadata.json

application.ymlで設定したkeyの情報を定義できます。
これをConfiguration Metadataと呼び、このファイルを定義した場合、IDEから対象keyのガイドを表示できます。
Configuration Metadataについて

{
  "properties": [
    {
      "name": "spring.mvc.request-parameter-logging-filter.enabled",
      "type": "org.slf4j.event.Level",
      "description": "true 또는 false"
    },
    {
      "name": "spring.mvc.request-parameter-logging-filter.level",
      "type": "java.lang.String",
      "description": "ロギングのレベル設定"
    }
  ]
}

 

RequestParameterLoggingFilterProperties

application.ymlで作成したkeyのJava classを定義できます。

@ConfigurationProperties(prefix = "spring.mvc.request-parameter-logging-filter")
public class RequestParameterLoggingFilterProperties {
    private boolean enabled;
    private Level level;
    // getters and setters
}

RequestParameterLoggingFilter

実際のロジックを担当するフィルタを定義します。

@Slf4j
public class RequestParameterLoggingFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String params = request.getParameterMap()
                            .entrySet()
                            .stream()
                            .map(entry -> entry.getKey() + "=" + String.join(",", entry.getValue()))
                            .flatMap(Stream::of)
                            .collect(Collectors.joining("&"));
        log(params);
        chain.doFilter(request, response);
    }

    // private void log(String params) { ... }
}

SampleAutoConfiguration

材料(property,logic)がすべて集まったので、これを自動設定できるようにしてみよう。

@Configuration
@AutoConfigureAfter(WebMvcAutoConfiguration.class) // 1
@EnableConfigurationProperties(RequestParameterLoggingFilterProperties.class) // 2
public class SampleAutoConfiguration {

    @Autowired
    private RequestParameterLoggingFilterProperties requestParameterLoggingFilterProperties; 

    @Bean
    @ConditionalOnProperty(name = "spring.mvc.request-parameter-logging-filter.enabled", havingValue = "true") // 3
    public Filter customUriLoggingFilter() {
        return new RequestParameterLoggingFilter(requestParameterLoggingFilterProperties.getLevel());
    }
}
  1. Filter設定(webmvc-specific)をするもので、webmvc設定が完了した後、設定が動作する。
  2. RequestParameterLoggingFilterPropertiesをbeanに生成して@Autowiredできるようにする。
  3. @ConditionalOnXXXを使って@Bean作成条件、@Configuration動作条件を作成できる。
    @Conditional(Condition condition)Conditionインターフェイスを実装して、詳細な条件を指定することもできる。

src/main/resource/META-INF/spring.factories

.jarファイルに含まれ、当該bootモジュールが設定すべき情報を持っています。
org.springframework.boot.autoconfigure.EnableAutoConfigurationkeyに作成した@Configurationclassをカンマ(,)区切りにしておきます。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.parfait.study.autoconfigure.SampleAutoConfiguration

spring-boot-autoconfigureで使用されるspring.factories

starterモジュール

必要な設定情報は、autoconfigureですべて完了しました。starterでは依存性だけをかけておけばよいでしょう。autoconfigureoptionalを削除するだけです。

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>com.parfait.study</groupId>
    <artifactId>sample-spring-boot-starter-request-parameter-logging-filter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>sample-spring-boot-starter-request-parameter-logging-filter</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <spring-boot.version>2.0.0.RELEASE</spring-boot.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.parfait.study</groupId>
            <artifactId>sample-spring-boot-autoconfigure</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies> <!-- The parent should provide all that -->
            <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>
</project>

webモジュール

これから設定したstarterを使ってみよう。
sample-spring-boot-starter-webという名前ですが、単なるWeb API Applicationです。

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>com.parfait.study</groupId>
    <artifactId>sample-spring-boot-starter-web</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>sample-spring-boot-starter-web</name>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- starter 依存性設定 -->
        <dependency>
            <groupId>com.parfait.study</groupId>
            <artifactId>sample-spring-boot-starter-request-parameter-logging-filter</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

UserController

@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping("/{id}")
    public String get(@PathVariable Long id) {
        return new RestTemplate().getForObject("https://jsonplaceholder.typicode.com/users/{id}", String.class, id);
    }
}

application.yml

src/main/resoucres/META-INF/additional-spring-configuration-metadata.jsonで述べたIDEのガイドとは、このようなものです。



値型を解析して候補まで表示してくれます。

spring.mvc.request-parameter-logging-filter.enabled=true
spring.mvc.request-parameter-logging-filter.level=info

autoconfigureで作成したappliaction.yml値がdefaultです。
autoconfigureにおいてもdefault値を指定せずに、Configuration Metaだけ渡すこともできます。

実行結果

サーバーに接続してAPIを実行してみよう。
使用するソースでは、application.yml以外は何も設定していませんが、フィルタが動作することを確認できます。

GET /users/1?name=hello

2018-03-15 21:24:20.656  INFO 16048 --- [nio-8080-exec-1] .p.s.a.l.f.RequestParameterLoggingFilter : uri : name=hello

このほかにできること

spring-boot-starterは自動設定に対応できるという点で、強く推奨します。bootを使用するチーム間のサポートを非常に簡単に行えるようにしてくれます。
たとえば、ビッグデータ分析のために情報収集が必要なサービスでは、bigdata-spring-boot-starter-logを提供して、下記のような設定だけでログ収集ロジックを動作させたり、

bigdata.log:
  enable: true
  server: http://bigdata.server.com
  service.name: ABC Portal

会員の暗号化トークンをCookieやヘッダで受け取った後、会員サーバーと通信し、その結果をrequestのattributeに注入する場合、以下のような設定だけで行えるようになります。

member-server:
  enabled: true
  request:
    token:
      # or HEADER
      type: COOKIE 
      name: MEMBER_TOKEN
    read-timeout: 2000
    connect-timeout: 1000
  result.attribute-name: RESOLVER_MEMBER_INFO

フィルタ以外に他の例を挙げてみよう。
springのCache Abstractionを使って@CacheableNear Cacheとして構成することもできます。

chained-cache-namager:
  bean-name: cacheManager
  local:
    type: EHCACHE
    config: classpath:/ehcache.xml
  global:
    type: REDIS
    cluster.hosts: 
      - 10.0.0.1:6379
      - 10.0.0.2:6379

まとめ

spring-bootによって、設定が簡潔化され、開発者はさらに重要なロジックに注力できるようになりました。かつては上位pom.xmlから依存性を管理するか、開発プロジェクトでそれぞれ設定するか、プロジェクトを作成するたびに、同じような検討を繰り返すことが多かったですが、現在は非常に簡単に設定を簡素化できています。bootを使用されている方は、ぜひstarterを試してみてほしいです。

TOAST Meetup 編集部

TOASTの技術ナレッジやお得なイベント情報を発信していきます
pagetop