かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot 2.0.x の Web アプリを 2.1.x へバージョンアップする ( その4 )( テストが大量に失敗する原因を解消する )

概要

記事一覧はこちらです。

Spring Boot 2.0.x の Web アプリを 2.1.x へバージョンアップする ( その3 )( build.gradle を変更する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • テストが大量に失敗するようになったので、原因を調査し解消します。

参照したサイト・書籍

  1. IntelliJ IDEA 2017.3 EAP: Configurable command line shortener and more
    https://blog.jetbrains.com/idea/2017/10/intellij-idea-2017-3-eap-configurable-command-line-shortener-and-more/

目次

  1. 現状を確認する
  2. エラーの詳細を調べるために Run ‘All Tests’ を実行する。。。が Command line is too long. のメッセージが表示されてテストが実行できないので解消する
  3. ApplicationConfig クラスに定義している mvcValidator bean を単純に削除してよいか確認する
  4. mvcValidator の BeanDefinitionOverrideException が出ないようにし、かつ日本語のメッセージを messages_ja_JP.properties から取得できるようにするには?
  5. unittest の時には devtools の AutoConfiguration が実行されないようにする

手順

現状を確認する

まずは現在の状況をまとめます。build タスクを実行すると以下のように出力されており、

f:id:ksby:20190210080649p:plain (.....途中省略.....) f:id:ksby:20190210080800p:plain

  • 141 tests completed, 119 failed, 1 skipped と出力されている。
  • 失敗しているテストはほとんど java.lang.IllegalStateException Caused by: org.springframework.beans.factory.support.BeanDefinitionOverrideException というエラーが出力されている。
ksbysample.webapp.lending.helper.library.LibraryHelperTest > testGetSelectedLibrary_図書館が選択されていない場合 FAILED
    java.lang.IllegalStateException
        Caused by: org.springframework.beans.factory.support.BeanDefinitionOverrideException

エラーの詳細を調べるために Run ‘All Tests’ を実行する。。。が Command line is too long. のメッセージが表示されてテストが実行できないので解消する

エラーの詳細を出力するために Project Tool Window で src/test を選択した後、コンテキストメニューを表示して「Run ‘All Tests’」を選択します。。。が、Error running 'All in ksbysample-webapp-lending_test': Command line is too long. Shorten command line for All in ksbysample-webapp-lending_test or also for JUnit default configuration. というメッセージが表示されて実行できませんでした。

f:id:ksby:20190210081757p:plain

2.0.x までは問題なかったのですが、2.1.x にすると Command line is too long. になるようです。実際に実行しようとしている Command line の文字列を調べる方法がないか調べてみましたが分かりませんでした。ただし Edit Configuration で JUnit の設定を変更すれば解決できるようなので、設定を変更して解消します。

メインメニューから「Run」-「Edit Configurations...」を選択します。

「Run/Debug Configurations」画面が表示されます。画面左側のリストから「Templates」-「JUnit」を選択した後、画面右側の「Shorten command line」にデフォルトでは「user-local default: none」が選択されているので「JAR manifest」か「classpath file」を選択します。

f:id:ksby:20190210120253p:plain

選択肢の説明は IntelliJ IDEA 2017.3 EAP: Configurable command line shortener and more に書かれてあります。個人的には「classpath file」の設定が好みなので、こちらを選択します。

設定後に「Run ‘All Tests’」を実行すると、今度はテストが実行されました。見事にほとんどのテストが失敗していますが。。。

f:id:ksby:20190210122032p:plain

とりあえずこれでエラーの詳細が確認できるようになりました。エラー時に出力されているメッセージを確認すると以下のものでした。

Caused by: org.springframework.beans.factory.support.BeanDefinitionOverrideException: Invalid bean definition with name 'mvcValidator' defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]: Cannot register bean definition [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration; factoryMethodName=mvcValidator; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]] for bean 'mvcValidator': There is already [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=applicationConfig; factoryMethodName=mvcValidator; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [ksbysample/webapp/lending/config/ApplicationConfig.class]] bound.

これは Spring Boot 2.1 Release Notes - Bean Overriding に記述されているもので、src/main/java/ksbysample/webapp/lending/config/ApplicationConfig.java で mvcValidator bean を定義しているのですが、既に定義されている bean を上書きしようとしてエラーになっているようです。

既に定義されている mvcValidator bean は org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.EnableWebMvcConfiguration#mvcValidator でした。

f:id:ksby:20190211094327p:plain

IntelliJ IDEA で EnableWebMvcConfiguration クラス関連の Diagram を作成してみると以下のようになります。

f:id:ksby:20190211095833p:plain

src/main/java/ksbysample/webapp/lending/config/ApplicationConfig.java の mvcValidator bean は以下のように実装しています。以前はこの実装がないと ValidationMessages.properties を削除して messages.properties だけにできなかったのですが削除してよいか確認します。

    /**
     * Controller クラスで直接 {@link Validator} を呼び出すために Bean として定義している
     * また Hibernate Validator のメッセージを ValidationMessages.properties ではなく
     * messages.properties に記述できるようにするためにも使用している
     *
     * @return new {@link LocalValidatorFactoryBean}
     */
    @Bean
    public Validator mvcValidator() {
        LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean();
        localValidatorFactoryBean.setValidationMessageSource(this.messageSource);
        return localValidatorFactoryBean;
    }

ApplicationConfig クラスに定義している mvcValidator bean を単純に削除してよいか確認する

ksbysample-webapp-lending Web アプリケーションには Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( 番外編 )( Spring MVC メモ書き ) で実装した Bean Validation 検証用の画面が存在するので、これを利用して確認します。

最初に src/main/java/ksbysample/webapp/lending/config/ApplicationConfig.java から mvcValidator bean の定義を削除します。

削除後 Tomcat を起動してから http://localhost:8080/springMvcMemo/beanValidationGroup にアクセスします。

f:id:ksby:20190210193434p:plain

「データ更新」ボタンを押すと ID が必須になっているのでエラーメッセージが出るはず。。。なのですが、HV000030: No validator could be found for constraint 'javax.validation.constraints.NotBlank' validating type 'java.lang.Long'. Check configuration for 'id' というエラーメッセージが表示されました。

f:id:ksby:20190210193634p:plain

原因を調べたところ Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その3 )( build.gradle を変更する ) の時に src/main/java/ksbysample/webapp/lending/web/springmvcmemo/EditFormChecker.javaprivate Long id; に付与するアノテーションを org.hibernate.validator.constraints.NotBlank → javax.validation.constraints.NotBlank に変更したのですが、前者は Long 型でも問題なかったのですが後者は CharSequence 型でないと動作しないためでした。

以下2つのソースの private Long id; に付与するアノテーション@NotBlank@NotNull(javax.validation.constraints.NotNull) に変更します。

  • src/main/java/ksbysample/webapp/lending/web/springmvcmemo/EditFormChecker.java
  • src/main/java/ksbysample/webapp/lending/web/springmvcmemo/SendmailFormChecker.java

また src/main/resources/messages_ja_JP.properties のエラーメッセージの定義も変更が必要なので以下のように変更します。

# Bean Validation 用メッセージ
error.size.max = {max}文字以内で入力して下さい。
javax.validation.constraints.NotBlank.message=必須の入力項目です。
javax.validation.constraints.NotNull.message=必須の入力項目です。
org.hibernate.validator.constraints.Email.message=メールアドレスを入力して下さい。
typeMismatch.java.lang.Long=数値を入力して下さい。
  • javax.validation.constraints.NotBlank.message=必須の入力項目です。 を追加します。
  • org.hibernate.validator.constraints.NotBlank.message=必須の入力項目です。 を削除します。

変更後に Tomcat を再起動して画面を表示し直してから「データ更新」ボタンを押すと今度はエラーメッセージが出ましたが、日本語ではなく Bean Validation のアノテーションのデフォルトメッセージ(英語)が表示されました。

f:id:ksby:20190210195703p:plain

どうしたら日本語のメッセージを表示できるかいろいろ試してみたところ、

  • messages.properties、messages_ja_JP.properties は全く参照されない。
  • ValidationMessages.properties(ValidationMessages_ja_JP.properties でも可)に javax.validation.constraints.NotBlank.message/javax.validation.constraints.NotNull.message という項目名でメッセージを定義すれば日本語のメッセージが表示される。ただし native2ascii で変換した後の文字列を記述する必要がある。変換しない UTF-8 の文字列だと文字化けする。
javax.validation.constraints.NotBlank.message=\u5FC5\u9808\u306E\u5165\u529B\u9805\u76EE\u3067\u3059\u3002
javax.validation.constraints.NotNull.message=\u5FC5\u9808\u306E\u5165\u529B\u9805\u76EE\u3067\u3059\u3002

上のように定義しておけば日本語でエラーメッセージが表示されます。

f:id:ksby:20190210204555p:plain

ValidationMessages.properties を止めて messages.properties にまとめたのに今更 ValidationMessages.properties を復活させたくはないので、mvcValidator bean を単純に削除するのは止めます。

mvcValidator の BeanDefinitionOverrideException が出ないようにし、かつ日本語のメッセージを messages_ja_JP.properties から取得できるようにするには?

結論を言うと以下のように変更します。

  • mvcValidator が bean 名で重複していて BeanDefinitionOverrideException が発生しているので、mvcValidator → validator に変更する。
  • 戻り値の型を org.springframework.validation.Validator にしているが、javax.validation.Validator インターフェースあるいはその実装クラスを返すようにしておくと org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.EnableWebMvcConfiguration#mvcValidator 内で validator bean を使用するように処理してくれる。LocalValidatorFactoryBean クラスは javax.validation.Validator インターフェースの実装クラスなので、戻り値の型を org.springframework.validation.Validator → LocalValidatorFactoryBean に変更する。

    f:id:ksby:20190211210332p:plain

変更後の src/main/java/ksbysample/webapp/lending/config/ApplicationConfig.java に定義している validator bean は以下のようになります。戻り値の型とメソッド名を変更しただけです。

    /**
     * javax.validation や Hibernate Validator のメッセージを ValidationMessages.properties ではなく
     * messages.properties に記述できるようにするために定義している
     *
     * @return new {@link LocalValidatorFactoryBean}
     */
    @Bean
    public LocalValidatorFactoryBean validator() {
        LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean();
        localValidatorFactoryBean.setValidationMessageSource(this.messageSource);
        return localValidatorFactoryBean;
    }

また src/main/java/ksbysample/webapp/lending/web/springmvcmemo/BeanValidationGroupController.java の以下の点を変更します。

@Controller
@RequestMapping("/springMvcMemo/beanValidationGroup")
public class BeanValidationGroupController {

    private static final String THYMELEAF_TEMPLATE = "springmvcmemo/beanValidationGroup";

    private final Validator validator;

    /**
     * @param validator ???
     */
    public BeanValidationGroupController(Validator validator) {
        this.validator = validator;
    }
  • フィールドに定義している private final Validator mvcValidator;private final Validator validator; に変更します。

src/main/test/ksbysample/webapp/lending/values/validation/ValuesEnumValidatorTest.java も同じ変更を行います。

@RunWith(SpringRunner.class)
@SpringBootTest
public class ValuesEnumValidatorTest {

    ..........

    private Validator validator;

    @Before
    public void setup() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
    }
  • フィールドに定義している private Validator mvcValidator;private Validator validator; に変更します。

Tomcat を再起動して http://localhost:8080/springMvcMemo/beanValidationGroup に再度アクセスして画面を表示した後「データ更新」ボタンを押すと、今度はエラーメッセージが日本語で表示されました。

f:id:ksby:20190211212821p:plain

ここまでの修正で build タスクは "BUILD SUCCESSFUL" のメッセージが出るようになりましたが、

f:id:ksby:20190211224310p:plain

Project Tool Window の src/test から「Run ‘All Tests’」を実行すると1件だけテストが失敗します。java.lang.IllegalStateException: Error processing condition on org.springframework.boot.devtools.autoconfigure.DevToolsDataSourceAutoConfiguration というエラーメッセージが出ています。

f:id:ksby:20190211224753p:plain

unittest の時には devtools の AutoConfiguration が実行されないようにする

java.lang.IllegalStateException: Error processing condition on org.springframework.boot.devtools.autoconfigure.DevToolsDataSourceAutoConfiguration のエラーは以下のようなエラーが出ているのですが、

java.lang.IllegalStateException: Failed to load ApplicationContext
    ..........
Caused by: java.lang.IllegalStateException: Error processing condition on org.springframework.boot.devtools.autoconfigure.DevToolsDataSourceAutoConfiguration
    at org.springframework.boot.autoconfigure.condition.SpringBootCondition.matches(SpringBootCondition.java:64)
    at org.springframework.context.annotation.ConditionEvaluator.shouldSkip(ConditionEvaluator.java:108)
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader$TrackedConditionEvaluator.shouldSkip(ConfigurationClassBeanDefinitionReader.java:447)
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader$TrackedConditionEvaluator.shouldSkip(ConfigurationClassBeanDefinitionReader.java:436)
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass(ConfigurationClassBeanDefinitionReader.java:128)
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(ConfigurationClassBeanDefinitionReader.java:117)
    at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:327)
    at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:232)
    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:275)
    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:95)
    at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:691)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:528)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:775)
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:316)
    at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:127)
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:99)
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:117)
    ... 52 more
Caused by: java.lang.NullPointerException
    at org.springframework.boot.devtools.autoconfigure.DevToolsDataSourceAutoConfiguration$DevToolsDataSourceCondition.getMatchOutcome(DevToolsDataSourceAutoConfiguration.java:182)
    at org.springframework.boot.autoconfigure.condition.SpringBootCondition.matches(SpringBootCondition.java:47)
    ... 69 more

unittest の時に devtools は不要なので、無効になるよう設定を変更してそれでエラーが出なくなるなら良しとします。AutoConfiguration で設定しているようなので devtools の AutoConfiguration を調べて無効にします。

DevToolsDataSourceAutoConfiguration クラスを見ると package は org.springframework.boot.devtools.autoconfigure でした。org.springframework.boot.devtools.autoconfigure package 内に AutoConfiguration のクラスは LocalDevToolsAutoConfiguration、RemoteDevToolsAutoConfiguration、DevToolsDataSourceAutoConfiguration の3つがありましたので、

f:id:ksby:20190211232959p:plain

src/main/resources/application-unittest.properties の spring.autoconfigure.exclude に , org.springframework.boot.devtools.autoconfigure.LocalDevToolsAutoConfiguration, org.springframework.boot.devtools.autoconfigure.RemoteDevToolsAutoConfiguration, org.springframework.boot.devtools.autoconfigure.DevToolsDataSourceAutoConfiguration を追加して無効にします。

spring.autoconfigure.exclude=com.integralblue.log4jdbc.spring.Log4jdbcAutoConfiguration, org.springframework.boot.devtools.autoconfigure.LocalDevToolsAutoConfiguration, org.springframework.boot.devtools.autoconfigure.RemoteDevToolsAutoConfiguration, org.springframework.boot.devtools.autoconfigure.DevToolsDataSourceAutoConfiguration

logging.level.root=OFF

再度「Run ‘All Tests’」を実行すると先程と同じテストでエラーが出ていましたが、

f:id:ksby:20190211234703p:plain

今度はエラーの内容が以下のものに変わっていました。

java.lang.IllegalStateException: Failed to load ApplicationContext
    ..........
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'webMvcConfig': Unsatisfied dependency expressed through method 'configureThymeleafSpringTemplateEngine' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'templateEngine' defined in class path resource [org/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfiguration$ThymeleafDefaultConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.thymeleaf.spring5.SpringTemplateEngine]: Factory method 'templateEngine' threw exception; nested exception is java.lang.ExceptionInInitializerError
    ..........
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'templateEngine' defined in class path resource [org/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfiguration$ThymeleafDefaultConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.thymeleaf.spring5.SpringTemplateEngine]: Factory method 'templateEngine' threw exception; nested exception is java.lang.ExceptionInInitializerError
    ..........
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.thymeleaf.spring5.SpringTemplateEngine]: Factory method 'templateEngine' threw exception; nested exception is java.lang.ExceptionInInitializerError
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:185)
    at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:622)
    ... 83 more
Caused by: java.lang.ExceptionInInitializerError
    at org.thymeleaf.spring5.dialect.SpringStandardDialect.<clinit>(SpringStandardDialect.java:143)
    at org.thymeleaf.spring5.SpringTemplateEngine.<clinit>(SpringTemplateEngine.java:59)
    at org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration$ThymeleafDefaultConfiguration.templateEngine(ThymeleafAutoConfiguration.java:156)
    at org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration$ThymeleafDefaultConfiguration$$EnhancerBySpringCGLIB$$b3eec7c4.CGLIB$templateEngine$0(<generated>)
    at org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration$ThymeleafDefaultConfiguration$$EnhancerBySpringCGLIB$$b3eec7c4$$FastClassBySpringCGLIB$$9a6ac718.invoke(<generated>)
    at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:244)
    at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:363)
    at org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration$ThymeleafDefaultConfiguration$$EnhancerBySpringCGLIB$$b3eec7c4.templateEngine(<generated>)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154)
    ... 84 more
Caused by: java.lang.NullPointerException
    at org.thymeleaf.spring5.util.SpringVersionUtils.<clinit>(SpringVersionUtils.java:52)
    ... 97 more

失敗しているテストは PowerMock を使って static メソッドをテストしているものなのですが、gradle の build タスクからだとテストが成功するが IntelliJ IDEA から実行すると失敗するようです。

長くなったので一旦ここで止めて、次回へ続きます。

履歴

2019/02/12
初版発行。