LoginSignup
19
13

More than 3 years have passed since last update.

Spring MVCのRestControllerのRequestParamで任意のEnumをコードなどの別の値で受け取る方法

Posted at

はじめに

EnumをRESTful APIなどで他アプリと連携する場合、Enumの名前を変更する可能性を考慮するとコードで連携したいと思うときがあると思います。
その場合、Spring MVCを使ってクライアントからのリクエストはコードで受け取りつつも、コントローラの引数で受け取ったときにはEnumに変換し終わっててほしいと思うと思います。

今回の記事はそれを実現する一つの方法のご紹介です。

達成したいこと

  • @RestControllerのメソッドで@RequestParamでEnumを受け取りとり、ファクトリメソッドを使って取得できるようにする

想定していること

  • クライアントとのやり取りをコード値でしている場合などを想定。

事前調査・ヒント

  • StringToEnumConverterFactory. StringToEnumConverterFactoryの実装を真似すればファクトリメソッド経由でやりたいことは実現できる。
    • ただ、単純に真似するとEnumの数だけFactoryまたはConverterを用意することになる
    • 何度も書きたくないので処理を共通化しておきたい。

いきなり結論〜実現した方法〜

  1. コード値を用いるEnum用のinterfaceを用意する
  2. コード値を用いるEnumで(1)を実装する
  3. そのEnumに対してStringToEnumを参考にしたConverterとFactoryを実装する
  4. RestControllerにEnumを使う

何はともあれサンプルコードと解説

コード解説

コード内でコメントする形で解説していきます

Java版実装

1. コード値を用いるEnum用のinterfaceを用意する
import java.util.Arrays;

public interface EnumBase<E extends Enum<E>> { // 1. GenericsでEnumを指定することで、Enumにしか継承させないようにする
                                               // 同僚に教えてもらったが、再起的ジェネリクスという名前がついているらしいです。
                                               // よくわかってないので間違っていれば指摘お願いします🙇‍♂️

    // 2. コード値を使うので、そのgetterを生やしておく
    String getCode();

    // 3. `<E extends EnumBase> E`のGenericsでinput/outputの方が同じだと宣言する
    public static <E extends EnumBase> E of(Class<E> enumClass, String code) {
        return Arrays.stream(enumClass.getEnumConstants()) // 4. (3)によって`enumClass`はEnumであると仮定できるはずなので、`getEnumConstants()`で定義されているものをすべて取得してstreamに変換
                     .filter(it -> it.getCode().equals(code)) // 6. 引数の`code`と一致するものを検索
                     .findFirst() // 7. 1件のみ該当するはずなので最初に一致したものを選択
                     // 8. 該当するものがなかった場合、コーディングバグなのでExceptionを飛ばしてFWまで貫通させてシステムエラーにする
                     // もし、このハンドリングでは問題がある場合は`null`を返して、クライアントコードにハンドリングさせる
                     .orElseThrow(() -> new IllegalArgumentException(String.format("%s does not have such code => [%s]",
                                                                                   enumClass.getSimpleName(),
                                                                                   code)));
    }
}
2. コード値を用いるEnumで(1)を実装する
public enum JvmLanguage implements EnumBase<JvmLanguage> { // 1. Genericsに自分自身を指定してEnumであることをEnumBaseに教える
    JAVA("10", "Java"), // 2. コード値とともにEnumを宣言する
    KOTLIN("20", "Kotlin"),
    SCALA("30", "Scala"),
    GROOVY("40", "Groovy");
    // Jvm言語は他にもあるが、よく見聞きするものだけを取り上げている。ここでマサカリは欲しくない…。

    private String code;
    private String genericName;

    @Override
    public String getCode() {
        return this.code;
    }

    public String getGenericName() {
        return this.genericName;
    }

    JvmLanguage(String code, String genericName) {
        this.code = code;
        this.genericName = genericName;
    }
}
3. そのEnumに対してStringToEnumを参考にしたConverterとFactoryを実装する
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;

class CustomStringToEnumConverterFactory implements ConverterFactory<String, EnumBase> {
    @Override // 1. `StringToEnumConverterFactory`では`<T extends Enum>`となっているが、`EnumBase`を継承したクラスをGenericsに指定する
    public <T extends EnumBase> Converter<String, T> getConverter(Class<T> targetType) {
        return new CustomStringToEnumConverter(targetType);
    }

    // 2. `StringToEnumConverter`では`<T extends Enum>`となっているが、ここも`EnumBase`を継承したクラスをGenericsに指定する
    private static class CustomStringToEnumConverter<T extends EnumBase> implements Converter<String, T> {
        private Class<T> enumType;

        CustomStringToEnumConverter(Class<T> enumType) {
            this.enumType = enumType;
        }

        @Override
        public T convert(String source) {
            if (source.isEmpty()) {
                return null;
            }
            return EnumBase.of(enumType, source.trim());
        }
    }
}

呼び出しているSpring側のコードも読んでGenericsがどう作用しているのかもっと詳細な解説をしようかと思ったのですが、正直読み解くのが大変そうで断念してしまいましたorz

不勉強で申し訳ないです

興味がある方はご自身でGenericConversionServiceを読んでみてください。

4. RestControllerにEnumを使う
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@RequestMapping("/jvm-languages")
@RestController
public class JvmLanguageResource {

    @GetMapping
    public List<String> list() {
        return Arrays.stream(JvmLanguage.values())
                     .map(JvmLanguage::getGenericName)
                     .collect(Collectors.toList());
    }

    @GetMapping("/genericName") // 1. `@RequestParam`に`EnumBase`を継承したEnumを指定する
    public String answer(@RequestParam(name = "code") JvmLanguage jvmLanguage) {
        return jvmLanguage.getGenericName();
    }
}

Kotlin版実装

Javaと違いはないので解説はしません。
コードを載せていきます。

1. コード値を用いるEnum用のinterfaceを用意する
interface EnumBase<E : Enum<E>> {
    val code: String

    companion object {
        fun <E : EnumBase<*>> of(enumClass: Class<E>, code: String): E {
            return enumClass.enumConstants
                .firstOrNull { it.code == code }
                ?: throw IllegalArgumentException(String.format("%s does not have such code => [%s]", enumClass.simpleName, code))
        }
    }
}
2. コード値を用いるEnumで(1)を実装する
enum class JvmLanguage(override val code: String, val genericName: String) : EnumBase<JvmLanguage> {
    JAVA("10", "Java"),
    KOTLIN("20", "Kotlin"),
    SCALA("30", "Scala"),
    GROOVY("40", "Groovy");
}
3. そのEnumに対してStringToEnumを参考にしたConverterとFactoryを実装する
import org.springframework.core.convert.converter.Converter
import org.springframework.core.convert.converter.ConverterFactory

class CustomStringToEnumConverterFactory : ConverterFactory<String, EnumBase<*>> {
    override fun <T : EnumBase<*>> getConverter(targetType: Class<T>): Converter<String, T> {
        return CustomStringToEnumConverter(targetType)
    }

    private class CustomStringToEnumConverter<T : EnumBase<*>>(private val enumType: Class<T>) : Converter<String, T> {
        override fun convert(source: String): T? {
            return if (source.isEmpty()) {
                null
            } else EnumBase.of(enumType, source.trim { it <= ' ' })
        }
    }
}
4. RestControllerにEnumを使う
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController

@RequestMapping("/jvm-languages")
@RestController
class JvmLanguageResource {
    @GetMapping
    fun list(): List<String> {
        return JvmLanguage.values().map { it.genericName }
    }

    @GetMapping("/genericName")
    fun answer(@RequestParam(name = "code") jvmLanguage: JvmLanguage): String? {
        return jvmLanguage.genericName
    }
}
デモ

これで、どちらの実装でもコード値をリクエストしてEnumで受け取ることができるようになりました。
実際にcurlを実行してみると以下のようになります。

$ echo `curl -sS http://localhost:8080/jvm-languages/genericName?code=10`
Java

参考にさせていただいた記事

  • 以下の記事を参考にさせていただきました。ありがとうございました🙇‍♂️
  • Enumの逆引きが冗長なので共通化する
    • interface(EnumBase)に生やすstaticメソッドの処理はこちらの記事から着想を得ました。とうかコピペさせて頂きました🙇‍♂️
19
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
13