38
51

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Spring Boot キャンプ : Spring Boot + YAML編

Last updated at Posted at 2019-09-06

Spring Bootキャンプシリーズ、Spring Boot + YAML編です。

今回の目的

Spring BootアプリのプロパティファイルをできるかぎりYAML化してみます。

今回使用するライブラリ

  • spring-boot-starter:2.2.0.M4
  • yaml-resource-bundle:1.1
  • spring-boot-starter-test:2.2.0.M4
  • lombok:1.18.8

プロパティファイル vs YAML

プロパティの管理

プロパティファイル(.properties)ではプレーンなKey Valueとしてプロパティを定義します。
このため、プロパティの意味的なまとまりを表現しづらく、管理が大変です。

server.port=8080
logging.file.name=logfile
server.servlet.context-path=/context

YAMLは構造的なKey Valueとしてプロパティを定義します。
プロパティファイルと比べると、整理されていることが分かります。

server:
  port: 8080
  servlet:
    context-path: /context
logging:
  file:
    name: logfile

日本語のサポート

古いJavaの慣習に則り、プロパティファイルはネイティブコードで書かれることが多く、IDEのプロパティファイルエディタ等では、native2asciiして人間が読める形で表示しています。このため日本語を使用する場合、読むためにIDEが必要となるケースが多くなります。
YAMLならこのようなことはありません。

プロパティファイルもUTF-8で書けばnative2ascii不要ですが、多くのIDEは古い前提に立っています。

YAMLのサポート

YAMLを使用する場合、SnakeYAML等のライブラリが別途必要になります。
Spring Bootではデフォで依存関係に入るため、意識する必要はありません。

Spring Bootで扱うプロパティファイル

Spring Bootでは、アプリケーションの設定値をプロパティファイルで管理します。

  • Spring Bootの設定ファイル
    • application.properties
    • @PropertySource
    • @TestPropertySource
  • メッセージ定義ファイル
    • messages.properties
  • 入力チェックエラーメッセージ定義ファイル
    • ValidationMessages.properties

アプリケーションの実装によっては、もう少し扱うプロパティファイルが増えますが、今回は上記について触れます。

Spring Bootの設定ファイル

Spring Bootアプリの設定を行うファイルには、以下の種類があります。

name description
application.properties Spring Bootの設定ファイル(標準)
@PropertySource Spring Frameworkの設定ファイル(追加)
@TestPropertySource Spring Testの設定ファイル(テスト用)

プロパティの読み込みには2種類の方法があり、機能に違いがあります。

  • @ConfigurationPropertiesを付与したBeanで読み込み(Type-Safe)
  • @Valueで読み込み(Not Type-Safe)

基本的には、タイプセーフにプロパティを扱える@ConfigurationPropertiesを利用した読み込みをお勧めします。

src/main/resources/application.properties

app.config.app-name=Sample

src/main/java/*/properties/AppConfigProperties.java

@Getter
@Setter
@ConfigurationProperties(prefix = "app.config")
public class AppConfigProperties {
    private String appName;
}

src/main/java/*/service/AppService.java

@Service
public class AppService {
    @AutoWired
    private AppConfigProperties properties;

    public void test() {
        properties.getAppName(); // -> "Sample"
    }
}

application.propertiesのYAML対応

application.propertiesは標準でYAMLに対応しています。
application.propertiesをapplication.ymlに変更するだけでOKです。

@PropertySourceのYAML対応

@PropertySourceで読み込むプロパティファイルは標準でYAMLに対応していません。Spring JIRA#SPR-13912で議論され、対応しないこととなったようです。

拡張ポイントがあり、@PropertySource(factory)属性でファイルの読み込みをカスタマイズ可能です。
独自にPropertySourceFactoryインターフェイスを実装して、YAML形式のプロパティファイルを読み込んでみます。

src/main/java/*/YamlPropertySourceFactory.java

public class YamlPropertySourceFactory implements PropertySourceFactory {

    // (1)
    @Override
    public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
        return name == null ? new YamlResourcePropertySource(resource) : new YamlResourcePropertySource(name, resource);
    }
    
    // (2)
    public static class YamlResourcePropertySource extends PropertiesPropertySource {

        public YamlResourcePropertySource(EncodedResource resource) throws IOException {
            this(getNameForResource(resource.getResource()), resource);
        }

        public YamlResourcePropertySource(String name, EncodedResource resource) throws IOException {
            super(name, loadYamlProperties(resource));
        }

        // (3)
        private static String getNameForResource(Resource resource) {

            String name = resource.getDescription();
            if (!StringUtils.hasText(name)) {
                name = resource.getClass().getSimpleName() + "@" + System.identityHashCode(resource);
            }
            return name;
        }

        // (4)
        private static Properties loadYamlProperties(EncodedResource resource) throws FileNotFoundException {

            YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
            factory.setResources(resource.getResource());

            try {
                return factory.getObject();
            } catch (IllegalStateException e) {
                // (5)
                Throwable cause = e.getCause();
                throw cause instanceof FileNotFoundException ? (FileNotFoundException) cause : e;
            }
        }
    }
}

(1) PropertySourceFactoryインターフェイスを実装して、createPropertySourceメソッドでYAML形式のプロパティファイルを読み込みます。

(2) PropertiesPropertySourceインターフェイスを実装したYamlResourcePropertySourceクラスで、実際にプロパティファイルを読み込みます。

(3) PropertySourceの名前を決定するgetNameForResourceメソッドは、properties形式で読み込むResourcePropertySourceクラスからそのまま持ってきています。特にこだわりのない部分です。

(4) SpringのYamlPropertiesFactoryBeanクラスを利用して、YAML形式のプロパティファイルを読み込みます。

(5) @PropertySource(ignoreResourceNotFound)属性がtrueの場合、指定したファイルが見つからない場合に無視します。
「指定したファイルが見つからない」ことはFileNotFoundExceptionがスローされたかどうかで判断されますが、YamlPropertiesFactoryBeanクラスではYAMLファイルが見つからない場合にIllegalStateExceptionがスローされるため、中からFileNotFoundExceptionを取り出してあげる必要があります。

src/main/java/*/YamlPropertySource.java

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@PropertySource(value = "", factory = YamlPropertySourceFactory.class) // (1)
public @interface YamlPropertySource {

    @AliasFor(annotation = PropertySource.class, attribute = "name")
    String name() default "";

    @AliasFor(annotation = PropertySource.class, attribute = "value")
    String[] value();

    @AliasFor(annotation = PropertySource.class, attribute = "ignoreResourceNotFound")
    boolean ignoreResourceNotFound() default false;

    @AliasFor(annotation = PropertySource.class, attribute = "encoding")
    String encoding() default "";
}

YAML形式のプロパティファイルを読み込む@PropertySource拡張アノテーションを作成しておきます。
アノテーション作成は必須ではありませんが、作っておくと楽です。

(1) @PropertySource(factory)属性に、先ほど実装したYamlPropertySourceFactoryクラスを指定します。
それ以外の属性は@PropertySourceにそのまま伝播されるようにしています。

src/main/java/*/Application.java

@SpringBootApplication
@YamlPropertySource("classpath:/settings.yml")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

メインクラスにさきほど作成した@YamlPropertySourceを付与します。

src/main/resources/settings.yml

app:
  config:
    app-name: Sample

@PropertySourceでYAML形式のプロパティファイルを読み込むことができるようになりました!

@TestPropertySourceのYAML対応

@TestPropertySourceで読み込むプロパティファイルは標準でYAMLに対応していません。

@PropertySourcefactory属性のようにファイルの読み込みをカスタマイズできるポイントがなく、YAMLに対応させることはできませんでした。

ちなみに、properties形式以外にXML形式にも対応していますw

メッセージ定義ファイル

メッセージ定義ファイルでは、画面等に表示するメッセージの定義、およびメッセージの国際化が可能です。

name description
messages.properties 標準で利用するメッセージ定義ファイル

メッセージの読み込みには、2種類の方法があります。

  • MessageSourceのBeanで読み込み(Controller、Service等で利用)
  • テンプレートエンジンの機能で読み込み(Thymeleaf等のViewで利用)

src/main/resources/messages.properties

message.user-not-found=User {0} Not Found!

src/main/java/*/service/AppService.java

@Service
public class AppService {
    @AutoWired
    private MessageSource messageSource;

    public void test(String userName) { // <- "Tom"
        messageSource.getMessage("message.user-not-found", userName, LocaleContextHolder.getLocale()); // -> "User Tom Not Found!"
    }
}

messages.properties

messages.propertiesは標準でYAMLに対応していません。Spring JIRA#SPR-15353では対応されておらず、十分な議論もなされていないようです。

Spring Bootとしての拡張ポイントはありませんが、MessageSourceのBeanを上書きすることでファイルの読み込みをカスタマイズ可能です。
MessageSourceでは、国際化対応のためResourceBundleによりファイルを読み込む必要がありますが、ここではOSS公開されているライブラリyaml-resource-bundleを利用してYAMLを読み込んでみます。

pom.xml

<dependency>
    <groupId>net.rakugakibox.util</groupId>
    <artifactId>yaml-resource-bundle</artifactId>
    <version>1.1</version>
</dependency>

yaml-resource-bundleを依存ライブラリに追加します。

src/main/java/*/MessageSourceConfig.java

@Configuration
public class MessageSourceConfig {

    // (1)
    @Bean
    @ConfigurationProperties(prefix = "spring.messages")
    public MessageSourceProperties messageSourceProperties() {
        return new MessageSourceProperties();
    }

    // (2)
    @Bean("messageSource")
    public MessageSource messageSource(MessageSourceProperties properties) {

        // (3)
        YamlMessageSource messageSource = new YamlMessageSource();

        // (4)
        if (StringUtils.hasText(properties.getBasename())) {
            messageSource.setBasenames(StringUtils
                    .commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));
        }
        if (properties.getEncoding() != null) {
            messageSource.setDefaultEncoding(properties.getEncoding().name());
        }
        messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
        Duration cacheDuration = properties.getCacheDuration();
        if (cacheDuration != null) {
            messageSource.setCacheMillis(cacheDuration.toMillis());
        }
        messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
        messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
        return messageSource;
    }

    // (3')
    private static class YamlMessageSource extends ResourceBundleMessageSource {
        @Override
        protected ResourceBundle doGetBundle(String basename, Locale locale) throws MissingResourceException {
            return ResourceBundle.getBundle(basename, locale, YamlResourceBundle.Control.INSTANCE);
        }
    }
}

(1) Spring Boot設定ファイルのプロパティspring.messages.*を受け取るための@ConfigurationPropertiesBeanを定義します。

(2) messageSourceBeanを定義します。

messageSourceという名前のBeanを定義することで、Spring BootのMessageSourceAutoConfigurationが無効になり、Auto-ConfigではMessageSourceがセットアップされなくなります。Auto-Configを理解しないと適切にカスタマイズできないポイントですね。

(3) YamlResourceBundleクラスを利用してYAML形式のメッセージ定義ファイルを読み込みます。

(4) 生成したYamlMessageSourceにMessageSourcePropertiesのプロパティをすべてセットします。
これにより、Spring Boot標準のResourceBundleMessageSourceと完全に同じように扱うことができます。

src/main/resources/messages.yml

message:
  user-not-found: User {0} Not Found!

messages.ymlを読み込むことができるようになりました!

入力チェックエラーメッセージ定義ファイル

入力チェックエラーメッセージ定義ファイルでは、Bean Validationによる入力チェックエラー時に表示するメッセージの定義、およびメッセージの国際化が可能です。

name description Spring MVC Validation Method Validation
messages.properties Spring MVC Validationで利用する入力チェックエラーメッセージ定義ファイル ×
ValidationMessages.properties Bean Validationで利用する入力チェックエラーメッセージ定義ファイル

messages.propertiesはSpring MVCのValidation(リクエストパラメータ)には適用されますが、Method Validation(Serviceの引数・戻り値等)には適用されません。このため、基本的に入力チェックエラーメッセージはValidationMessages.propertiesに定義することになります。

ValidationMessages.propertiesはBean Validation(Hibernate Validator)により自動的に読み込まれます。

src/main/resources/ValidationMessages.properties

javax.validation.constraints.NotNull.message=Must not be null!

src/main/java/*/service/AppService.java

@Service
@Validated
public class AppService {
    public void test(@NotNull String userName) { // <- null
        // -> throw ConstraintViolationException!
    }
}

ValidationMessages.properties

Hibernate Validatorの標準では、ValidationMessages.propertiesはYAMLに対応していません。
メッセージ定義ファイルの読み込みに拡張ポイントがあり、ResourceBundleLocatorインターフェイスを拡張することで変更することが可能なようです。

ただ、Spring側でもう少し簡単に対策できるため、今回はそちらを紹介します。

messages.propertiesをMethod Validationに適用する

Spring MVCおよびMethod ValidationからBean Validationを呼び出すLocalValidatorFactoryBeanでは、ValidationMessages.propertiesの代わりにMessageSourceをBean Validationのメッセージ定義ファイルとして利用することができます。

これを利用して、入力チェックエラーメッセージはすべてmessages.propertiesに定義することができます。

src/main/java/*/ValidationConfig.java

@Configuration
public class ValidationConfig {
    @Bean
    public static LocalValidatorFactoryBean validator(MessageSource messageSource) {
        LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
        factoryBean.setValidationMessageSource(messageSource);
        return factoryBean;
    }
}

LocalValidatorFactoryBeanのBean定義を上書きし、setValidationMessageSourceメソッドでMessageSourceをセットします。

src/main/resources/messages.yml

# Size: "size must be between {2} and {1}." ## (1)
javax:
  validation:
    constraints:
      Size.message: "size must be between {min} and {max}." ## (2)

(1)ではSpring MVC Validationの入力チェックエラーメッセージを定義しています。(デフォ)
(2)ではBean Validationの入力チェックエラーメッセージを定義しています。(今回の拡張)

(1)はSpring MVC Validationにしか適用されないのに対して、(2)はMethod Validationにも適用されます。
(1)ではパラメータの解決に{数字}を指定するのに対して、(2)では{属性名}を指定します。

ValidationMessages.propertiesの代わりにmessages.ymlでメッセージを定義することができました!

まとめ

YAMLを使うには何とも中途半端なサポート状況ですね。
もっともapplication.properties以外はSpring Frameworkの機能なので、Spring Bootで独自に拡張することを避けているのかもしれませんが。

38
51
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
38
51

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?