🍀

Spring Webの@PathVariableキャプチャ値をValueObjectにマッピングする

2022/11/24に公開

Spring Webで同じPathVariableを受け取るエンドポイントがいくつか出てきたので、Validationを共通化したいなーみたいな気持ちになった。てのと、ValueObjectへの変換メソッドもそこに持たせちゃいたいなみたいな気持ちになった。

なので取りあえずDTO作ってそこにValidation定義してみた。

public record PetIdDto(
    @Min(value = PetId.MIN_VALUE, message = "petIdには{value}以上の整数を指定してください。")
    @Max(value = PetId.MAX_VALUE, message = "petIdには{value}以下の整数を指定してください。")
    @Digits(integer = PetId.MAX_DIGIT, fraction = petId.FRACTION_DIGIT, message = "petIdには{integer}桁以下の整数を指定してください。")
    String value
) {
    public PetId convertToDomain() {
        return PetId.of(Long.parseLong(value()));
    }
}

で、これどうやって@PathVariableに指定できるんだとか思って、適当に指定してみたら動いちゃってなんでこれ動いてんだみたいな気持ちになった。

@GetMapping("/pets/{petId}")
public Pet getPet(@PathVariable("petId") @Valid petId) {
    // ...
}

ドキュメントで以下の通り、型変換?コンバーター?によって変換されるみたいな記載はあるけれど、独自に作ったDTOなのになんで変換できるんだろみたいな気持ちになった。

https://spring.pleiades.io/spring-framework/docs/5.3.23-SNAPSHOT/reference/html/web.html#mvc-ann-typeconversion

String ベースのリクエスト入力を表す一部のアノテーション付きコントローラーメソッド引数(@RequestParam、@RequestHeader、@PathVariable、@MatrixVariable、@CookieValue など)は、引数が String 以外のものとして宣言されている場合、型変換を必要とする場合があります。

このような場合、構成されたコンバーターに基づいて型変換が自動的に適用されます。デフォルトでは、単純型(int、long、Date など)がサポートされています。WebDataBinder (DataBinder を参照)を介して、または Formatters を FormattingConversionService に登録することにより、型変換をカスタマイズできます。Spring フィールドのフォーマットを参照してください。

なので、DTOを一時的にclassにしてコンストラクタにブレークポイント置いてみたら、以下な感じで呼び出された。

at com.example.application.dto.PetIdDto.<init>(PetIdDto.java:19)
at com.example.application.dto.PetIdDto.valueOf(PetIdDto.java:50)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:568)
at org.springframework.core.convert.support.ObjectToObjectConverter.convert(ObjectToObjectConverter.java:109)
at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:41)
at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:192)
at org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:129)
at org.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:73)
at org.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:53)
at org.springframework.validation.DataBinder.convertIfNecessary(DataBinder.java:729)
at org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver.resolveArgument(AbstractNamedValueMethodArgumentResolver.java:125)
at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:122)
at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:179)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:146)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1071)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:964)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)

で、たしかにTypeConverterとか出てきてるなーとか思って、その辺の処理追ったら determineFactoryConstructor とかってのでコンストラクタを取得してるっぽかった。

https://github.com/spring-projects/spring-framework/blob/v5.3.23/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java#L138-L158

ただ、その手前の determineFactoryMethod ての見たら staticメソッドで名前が valueOfofまたはfromがあればそれ使ってくれるっぽい。

https://github.com/spring-projects/spring-framework/blob/v5.3.23/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java#L188-L204

おもしろーとか思ってDTOにvalueOf作ることにしてみた。

public record PetIdDto(
    @Min(value = PetId.MIN_VALUE, message = "petIdには{value}以上の整数を指定してください。")
    @Max(value = PetId.MAX_VALUE, message = "petIdには{value}以下の整数を指定してください。")
    @Digits(integer = PetId.MAX_DIGIT, fraction = petId.FRACTION_DIGIT, message = "petIdには{integer}桁以下の整数を指定してください。")
    String value
) {
    public PetId convertToDomain() {
        return PetId.of(Long.parseLong(value()));
    }

    public static PetIdDto valueOf(String value) {
        return new PetIdDto(value);
    }
}

で、ちゃんとそれをコンバーターとして使ってくれてるっぽい。

at org.springframework.core.convert.support.ObjectToObjectConverter.determineFactoryMethod(ObjectToObjectConverter.java:195)
at org.springframework.core.convert.support.ObjectToObjectConverter.getValidatedExecutable(ObjectToObjectConverter.java:147)
at org.springframework.core.convert.support.ObjectToObjectConverter.hasConversionMethodOrConstructor(ObjectToObjectConverter.java:135)
at org.springframework.core.convert.support.ObjectToObjectConverter.matches(ObjectToObjectConverter.java:88)
at org.springframework.core.convert.support.GenericConversionService$ConvertersForPair.getConverter(GenericConversionService.java:664)
at org.springframework.core.convert.support.GenericConversionService$Converters.getRegisteredConverter(GenericConversionService.java:561)
at org.springframework.core.convert.support.GenericConversionService$Converters.find(GenericConversionService.java:545)
at org.springframework.core.convert.support.GenericConversionService.getConverter(GenericConversionService.java:261)
at org.springframework.core.convert.support.GenericConversionService.canConvert(GenericConversionService.java:146)
at org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:127)
at org.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:73)

ひとまずこれでValidation共通化してみる。

Discussion