1. 概要

この記事では、Springのデータバインディングメカニズムを使用して、オブジェクト変換に自動プリミティブを適用することにより、コードをより明確で読みやすくする方法を示します。

デフォルトでは、Springは単純型を変換する方法しか知りません。 つまり、コントローラー Int String 、または Boolean タイプのデータにデータを送信すると、適切なJavaタイプに自動的にバインドされます。

しかし、実際のプロジェクトでは、より複雑なタイプのオブジェクトをバインドする必要があるかもしれないので、それだけでは十分ではありません。

2. 個々のオブジェクトをリクエストパラメータにバインドする

単純なものから始めて、最初に単純な型をバインドしましょう。 のカスタム実装を提供する必要がありますコンバータインターフェース S 変換元のタイプであり、 T 変換するタイプは次のとおりです。

@Component
public class StringToLocalDateTimeConverter
  implements Converter<String, LocalDateTime> {

    @Override
    public LocalDateTime convert(String source) {
        return LocalDateTime.parse(
          source, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
    }
}

これで、コントローラーで次の構文を使用できます。

@GetMapping("/findbydate/{date}")
public GenericEntity findByDate(@PathVariable("date") LocalDateTime date) {
    return ...;
}

2.1. リクエストパラメータとしてのEnumの使用

次に、列挙型をRequestParameterとして使用する方法を説明します。

ここに、単純な列挙型モードがあります。

public enum Modes {
    ALPHA, BETA;
}

次のように、StringからenumConverterを作成します。

public class StringToEnumConverter implements Converter<String, Modes> {

    @Override
    public Modes convert(String from) {
        return Modes.valueOf(from);
    }
}

次に、コンバーターを登録する必要があります。

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToEnumConverter());
    }
}

これで、EnumRequestParameterとして使用できます。

@GetMapping
public ResponseEntity<Object> getStringToMode(@RequestParam("mode") Modes mode) {
    // ...
}

またはPathVariableとして:

@GetMapping("/entity/findbymode/{mode}")
public GenericEntity findByEnum(@PathVariable("mode") Modes mode) {
    // ...
}

3. オブジェクトの階層をバインドする

オブジェクト階層のツリー全体を変換する必要がある場合があります。個々のコンバーターのセットではなく、より集中化されたバインディングを使用する方が理にかなっています。

この例では、基本クラスAbstractEntityがあります。

public abstract class AbstractEntity {
    long id;
    public AbstractEntity(long id){
        this.id = id;
    }
}

そして、サブクラスFooBar

public class Foo extends AbstractEntity {
    private String name;
    
    // standard constructors, getters, setters
}
public class Bar extends AbstractEntity {
    private int value;
    
    // standard constructors, getters, setters
}

この場合、 ConverterFactoryを実装できますここで、Sは変換元のタイプであり、Rは基本タイプです。 変換できるクラスの範囲を定義します。

public class StringToAbstractEntityConverterFactory 
  implements ConverterFactory<String, AbstractEntity>{

    @Override
    public <T extends AbstractEntity> Converter<String, T> getConverter(Class<T> targetClass) {
        return new StringToAbstractEntityConverter<>(targetClass);
    }

    private static class StringToAbstractEntityConverter<T extends AbstractEntity>
      implements Converter<String, T> {

        private Class<T> targetClass;

        public StringToAbstractEntityConverter(Class<T> targetClass) {
            this.targetClass = targetClass;
        }

        @Override
        public T convert(String source) {
            long id = Long.parseLong(source);
            if(this.targetClass == Foo.class) {
                return (T) new Foo(id);
            }
            else if(this.targetClass == Bar.class) {
                return (T) new Bar(id);
            } else {
                return null;
            }
        }
    }
}

ご覧のとおり、実装する必要がある唯一のメソッドは、必要なタイプのコンバーターを返す getConverter()です。 次に、変換プロセスがこのコンバーターに委任されます。

次に、ConverterFactoryを登録する必要があります。

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverterFactory(new StringToAbstractEntityConverterFactory());
    }
}

最後に、コントローラーで好きなように使用できます。

@RestController
@RequestMapping("/string-to-abstract")
public class AbstractEntityController {

    @GetMapping("/foo/{foo}")
    public ResponseEntity<Object> getStringToFoo(@PathVariable Foo foo) {
        return ResponseEntity.ok(foo);
    }
    
    @GetMapping("/bar/{bar}")
    public ResponseEntity<Object> getStringToBar(@PathVariable Bar bar) {
        return ResponseEntity.ok(bar);
    }
}

4. ドメインオブジェクトのバインド

データをオブジェクトにバインドしたい場合がありますが、それは直接的な方法ではありません(たとえば、 Session Header 、または Cookie 変数)またはデータソースに保存されます。 そのような場合は、別のソリューションを使用する必要があります。

4.1. カスタム引数リゾルバー

まず、そのようなパラメータの注釈を定義します。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Version {
}

次に、カスタム HandlerMethodArgumentResolver:を実装します

public class HeaderVersionArgumentResolver
  implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        return methodParameter.getParameterAnnotation(Version.class) != null;
    }

    @Override
    public Object resolveArgument(
      MethodParameter methodParameter, 
      ModelAndViewContainer modelAndViewContainer, 
      NativeWebRequest nativeWebRequest, 
      WebDataBinderFactory webDataBinderFactory) throws Exception {
 
        HttpServletRequest request 
          = (HttpServletRequest) nativeWebRequest.getNativeRequest();

        return request.getHeader("Version");
    }
}

最後に、Springにそれらを検索する場所を知らせます。

@Configuration
public class WebConfig implements WebMvcConfigurer {

    //...

    @Override
    public void addArgumentResolvers(
      List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(new HeaderVersionArgumentResolver());
    }
}

それでおしまい。 これで、コントローラーで使用できます。

@GetMapping("/entity/{id}")
public ResponseEntity findByVersion(
  @PathVariable Long id, @Version String version) {
    return ...;
}

ご覧のとおり、 HandlerMethodArgumentResolver resolveArgument() メソッドは物体。 つまり、オブジェクトだけでなく、任意のオブジェクトを返すことができます。

5. 結論

その結果、私たちは多くのルーチン変換を取り除き、Springにほとんどのことをさせてくれました。 最後に、次のように結論付けましょう。

  • 個々の単純なタイプからオブジェクトへの変換には、Converterの実装を使用する必要があります
  • さまざまなオブジェクトの変換ロジックをカプセル化するために、ConverterFactoryの実装を試すことができます
  • データが間接的に送信される場合、または関連データを取得するために追加のロジックを適用する必要がある場合は、HandlerMethodArgumentResolverを使用することをお勧めします。

いつものように、すべての例は常にGitHubリポジトリにあります。