Spring Data 2.2.8はDomainClassConverterが死んでいるので注意


更新情報

2020/07/24に2.2.9がリリースされ無事に以下で説明する不具合は起こらなくなりました。

はじめに

Spring Dataにはリクエストパラメータ(クエリストリングもしくはフォーム)や@PathVariableとしてidを渡すとドメインオブジェクトを検索して設定してくれるDomainClassConverterという超便利な機能があります。
https://docs.spring.io/spring-data/jpa/docs/2.2.8.RELEASE/reference/html/#core.web.basic

@PathVariableを変換してくれる例
    @GetMapping("/users/{id}")
    public User show(@PathVariable("id") User user) {
        return user;
    }

が、バージョン2.2.8ではこの機能が死んでいます。
上で例に示した/users/1にアクセスすると以下のように500エラーになります。

There was an unexpected error (type=Internal Server Error, status=500).
Failed to convert value of type 'java.lang.String' to required type 'com.example.demo.User'; 
nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'com.example.demo.User': no matching editors or conversion strategy found

なお私が報告を受けたのはSpring Data JPAを使っているプロジェクトでフォーム送信(POST)が死ぬという状況だったのですが、この場合、フォームに埋めてある関連オブジェクトを検索しようとして死亡、ステータスとしては400が返されてました。

解決方法

ありません。
不具合報告されており、修正もされています。2.2.9ではこの不具合は発生しません。

2.2.9がリリースされるまではpom.xmlをいじるなどして2.2.7に戻しましょう(個人的に致命的なので早めにリリースされてほしい)

何が問題なのか

2.2.8に含まれるDomainClassConverterには以下の修正が行われています。
Defer initialization of Repositories in DomainClassConverter.

この結果、2.2.7まではアプリ起動時に行われていたConverterの登録処理が

2.2.7のDomainClassConverter
    public void setApplicationContext(ApplicationContext context) {

        this.repositories = new Repositories(context);

        this.toEntityConverter = Optional.of(new ToEntityConverter(this.repositories, this.conversionService));
        this.toEntityConverter.ifPresent(it -> this.conversionService.addConverter(it));

        this.toIdConverter = Optional.of(new ToIdConverter());
        this.toIdConverter.ifPresent(it -> this.conversionService.addConverter(it));
    }

2.2.8では遅延実行されることになり、

2.2.8のDomainClassConverter
    public void setApplicationContext(ApplicationContext context) {

        this.repositories = Lazy.of(() -> {

            Repositories repositories = new Repositories(context);

            this.toEntityConverter = Optional.of(new ToEntityConverter(repositories, conversionService));
            this.toEntityConverter.ifPresent(it -> conversionService.addConverter(it));

            this.toIdConverter = Optional.of(new ToIdConverter(repositories, conversionService));
            this.toIdConverter.ifPresent(it -> conversionService.addConverter(it));

            return repositories;
        });
    }

this.repositoriessetApplicationContextメソッドの上にあるgetConverterメソッドが呼び出されたとき1に遅延初期化されるものの、
もうおわかりだろう・・・
誰も!!getConverterメソッドを呼んでいないのである!!!

どうにか呼び出す方法はないものかと小一時間悩んでも解決できなかったので最新だと修正されてるか確認したら初めに挙げたコミットを見つけたというオチです。

あとがき

というわけで今回はSpring Dataの修正によりはまったというお話でした。一つバグを直したら別のところが壊れたという状況ですね。
ただ遅延初期化を導入したコミット自体はとなっており、「テストしてない部分は壊れててもわからない」ということはSpringのように規模が大きくなってくると起こりえるのだなと自戒も含めて思いました。


  1. getConverterメソッドはconvertメソッド(このメソッドはSpringのコア機能)が呼ばれたときに使われる。