カジュアルな技術ノート

小難しい技術のお話をできるだけわかりやすく...

Spring・コントローラーのメソッドで引数を受け取る仕組み

HTTP リクエストのパスに応じて、どの view を返すのか。。
ここの制御は Spring では、コントローラーを実装することで実現されています。

Spring を使用するなら必要不可欠の実装です。
でもコントローラーのメソッド実装において、いろんなものを引数で受け取ることができるってご存知ですか?(ここで思いっきり「はい!!」と答えられると、これから先辛いんだけれども。。)

今回は、コントローラーの実装メソッドによって何を受け取ることができるのか、それはどのように実現しているかを説明していきます。

どうやって受け取っているの?

コントローラーの実装メソッドで受け取る引数は HandlerMethodArgumentResolver というインターフェースを実装して定義されています。 実装クラスを探してみるとデフォルトで多数存在していることがわかります。

例えば、こんなコントローラーメソッドを実装したとします。

@PostMapping
public String paramTest(@RequestParam("curryType") String curryType, Model model) {
    model.addAttribute("curryType", curryType);
    return "pages/curry_love";
}

ここでは、@RequestParam を使用して curryType というリクエストパラメータと、model を引数として受け取っています。
よくありそうな実装ですが、ここでは次の HandlerMethodArgumentResolver たちにお世話になっています。

HandlerMethodArgumentResolver 提供機能
RequestParamMapMethodArgumentResolver @RequestParam をもった引数をサポートし、割り当て
ModelMethodProcessor Model クラスの引数をサポートし、割り当て

具体的には、どう実装していくのでしょうか。
このインターフェースが持っている2つの抽象メソッドを実装することで実現します。
順に見ていきましょう。

public interface HandlerMethodArgumentResolver {

    // 実装メソッドが持つ引数をサポートする条件をここで定義
    boolean supportsParameter(MethodParameter parameter);

    // メソッドの引数に、どう割り当てるのかを定義
    @Nullable
    Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;

}

イメージとしては、まず実装メソッドが持つ引数それぞれに対して、どの HandlerMethodArgumentResolver を割り当てるのかを supportsParameter メソッドにより決定します。
その後、選択された HandlerMethodArgumentResolverresolveArgument メソッドによって実際に引数に値を割り当てます。 f:id:shin-kinoshita:20181108094528p:plain

かんたんな実装例

ここまで来てイメージ湧いて来たでしょうか?
流れはわかったとしても、しっかりとイメージするには実際に実装してみるのが一番です。

まず OriginalObject というクラスを独自に定義します。

public class OriginalObject {
    private String name;
    private String favoriteCurry;

    public OriginalObject(String name, String favoriteCurry) {
        this.name = name;
        this.favoriteCurry = favoriteCurry;
    }

    public String getName() {
        return name;
    }

    public String getFavoriteCurry() {
        return favoriteCurry;
    }
}

名前と好きなカレーを格納する OriginalObject。 このクラスのインスタンスをコントローラーから受け取ることを考えます。

// OriginalObject のインスタンスを引数で受け取るコントローラーメソッド
@GetMapping
public String show(Model model, OriginalObject originalObject) {
    model.addAttribute("originalObject", originalObject);
    return "argument/original/show";
}

ここで指定した view では、 originalObject の格納している値を表示するものにして見ましょう。

<!-- originalObject を表示する view -->
<h1>Show Page</h1>
<p>I often know you, <b th:text="${originalObject.name}" style="font-size: 20px"></b></p>
<p>Your favorite curry is <b th:text="${originalObject.favoriteCurry}" style="font-size: 20px"></b>, isn't it??</p>

f:id:shin-kinoshita:20181107224542p:plain これは、originalObject の各フィールドが

  • name: "subaru"
  • favoriteCurry: "goa curry"

と設定されていた時の表示例です。

さあ、これを実現するための HandlerMethodArgumentResolver の実装サンプルを見ていきましょう。

// HandlerMethodArgumentResolver の実装サンプル
public class OriginalObjectArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return OriginalObject.class.isAssignableFrom(parameter.getParameterType());
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        return new OriginalObject("subaru", "goa curry");
    }
}

超ミニマムの実装です。

supportsParameter メソッドの parameter という引数は、コントローラーのメソッド引数の情報を持っています。
これの型情報が OriginalObject に適応可能であれば true を返し、OriginalObjectArgumentResolver の選択が決定されるというわけです。

resolveArgument メソッドは name に "subaru" 、favoriteCurry に "goa curry" を指定した OriginalObject のインスタンスを返すようにしています。

最後に定義した OriginalObjectArgumentResolver クラスを使用するさせるために WebMvcConfigurer の実装クラスから登録してあげる必要があります。

@Configuration
public class OriginalMvcConfigurer implements WebMvcConfigurer {

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

ちょっとだけ工夫した実装例

ここまで超ミニマム実装を見て来ましたが、結局どんなリクエストが来ても subaru が goa curry 大好きなことしかわからない実装になってしまっているので、あまりメソッドの引数で OriginalObject のインスタンスを受け取る必要性がないですね。。

なので、リクエスト情報から誰がどんなカレーが好きなのか決めるような仕様に変更してみましょう。

変更としては、name=yogesh&favoriteCurry=veg curry というパラメータを追可したリクエストを送ったとします。
ここで表示される画面は指定された名前とカレーにして見ましょう。
f:id:shin-kinoshita:20181108100445p:plain これを実装するには、resolveArgument メソッドで OriginalObject を初期化する際に、リクエストパラメータを取得できれば良いです。
以下実装例です。

// OriginalObjectArgumentResolver の resolveArgument を修正!
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
    HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
    String name = request.getParameter("name");
    String favoriteCurry = request.getParameter("favoriteCurry");

    if (name != null || favoriteCurry != null) {
        return new OriginalObject(name, favoriteCurry);
    }

    return new OriginalObject("subaru", "goa curry");
}

resolveArgument メソッドで受け取るたくさんの引数の中から webRequest を利用することでリクエストパラメータを取得します。

他にもいろんな情報を取得できますが、リクエストの情報取得は一番よく使用するところでしょう。

まとめ

今回は HandlerMethodArgumentResolver を使用して簡単な実装をすることで、コントローラーのメソッドで引数を受け取る仕組みを解説してみました。
ミニマム実装なので大したことはしてないですが、イメージ湧いたでしょうか?
より複雑な実装も、またの機会に掘り下げていきたいですね〜。
ではまた。。