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に対応していません。
@PropertySource
のfactory
属性のようにファイルの読み込みをカスタマイズできるポイントがなく、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.*
を受け取るための@ConfigurationProperties
Beanを定義します。
(2) messageSource
Beanを定義します。
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で独自に拡張することを避けているのかもしれませんが。