Java
spring
spring-boot
spring-mvc

【Spring MVC】Optionalを使わずにURLに含まれる動的なパラメータを実装する(~Java7)

※(07/14) タイトルを最適化

私は今、Java7をベースにWebシステムの構築をしています。
あえてJava8を使わない理由はいろいろとあるのですが、
その理由と引き換えにとにかく困ったのがOptionalが使えないことでした。

Optionalを使用して実装する(Java8以降の場合)

Optionalというのは
「引数に値が入ってくるのかわからんけどいったん受け取ってみる!」
というときに使われますが、Spring MVCにおいてはREST形式で表現されたURLパラメータを取得するときに使われます。

たとえば以下のようにコントローラを定義したとします。

SampleController.java
@Controller
@RequestMapping("page")
public class SampleController {

  @RequestMapping({"", "{num}"})
  public ModelAndView getMessages(@PathVariable("num") Optional<String> num) {
    if (num.ifPresent()) {
      // パラメータが存在する場合
    } else {
      // パラメータが存在しない場合
    }
    return new ModelAndView("index");
  }

}

※(07/14) アノテーションがRestControllerになっていたので修正しました。

パラメータ値があろうがなかろうが同じアクションメソッドの処理を行いたいという実装です。
今回は http://localhost/page/1http://localhost/page というURLを想定しています。

前者がリクエストされた場合、2番目のスラッシュ以降の文字列(1)がパラメータとして判定されるため、
引数 numに1が入力され、入力があったかを判定する ifPresent() の結果はtrueとなります。
ここでは型変数が<String>となっているため文字列型として num.get()での値取得が可能です。

後者のURLがリクエストされた場合、パラメータ値が存在しないと判断され、引数 numに値が入力されません
故に ifPresent() の判定結果はfalseとなります。

Java8以降では上記の実装で十分対応が可能です。
ここまでは、外部サイトやQiita内でも紹介されているよくある方法です。

Optionalを使用しないで実装する

しかし、OptionalがJava8で新たに実装されたクラスであるため、Java7まででは上記のプログラムをそのままでは使用できません。
「引数の型をStringにしてnullで取れないか」とも考えたのですが、NullPointerExceptionを投げてしまいます。

アクションメソッドを分割すれば実装することはできますが、それはそれで負けた気がするのであまりやりたくはありません。
(同じクラス内に同じソースが少なくとも2本できたり、前者を回避するためにprivateメソッドを実装して共通化するのも嫌でした。
 やるなら徹頭徹尾を同じソースにしたいです。)

実装方法

じゃあどうするのかといいますと、Optionalっぽく使えるクラス(以下、パラメータクラス)を作って代替とします。

SampleParameter.java
public class SampleParameter {
  private String num;

  // lombok を使用しない場合は以下が必要
  // pubric String getNum() { return num; }
  // pubric String setNum(String num) { this.num = num; }

  boolean ifPresent() {
    return num != null;
  }
}
SampleController.java
@Controller
@RequestMapping("page")
public class SampleController {

  @RequestMapping({"", "{num}"})
  public ModelAndView getMessages(@PathVariable("num") SampleParameter num) {
    if (num.ifPresent()) {
      // パラメータが存在する場合
    } else {
      // パラメータが存在しない場合
    }
    return new ModelAndView("index");
  }

}

※(07/14) アノテーションがRestControllerになっていたので修正しました。

はい、こんな感じです。@ModelAttribute のときなどと同様、フォームクラス的なものを作ってるようにも見えます。
実際、パラメータクラスは@RequestMappingで指定しているパラメータ名称がない場合、
NullPointerExceptionを投げるので、注意が必要です。

メリットと実装の注意点

メリット

一見、車輪の再発明をしているように見えますが、結構都合のいいクラスでもあります。

  1. ゲッター/セッターメソッドに加工を加えられる。
    最大級のメリットです。実際私がパラメータクラスを定義した際は、lombokを使用せず
    AES-128+Base64で暗号化したパラメータの復号処理をゲッターに入れていました。
  2. 好きなメソッドを追加できる/好きなメソッドを追加できる。
    好き勝手し放題です。(ただし、パラメータ名に対応したゲッターは必ず用意する必要があります)
  3. 継承(extends)/実装(implements)が自在にできる。
    Optionalができないという意味ではありませんが、オリジナルのクラスなので、
    継承/実装が自由にできます。

実装の注意点

反対に、実装を検討するうえで以下の点に注意が必要です。

  1. @RequestMappingのパラメータ名に一致する、セッターメソッドをパラメータクラスに用意する必要がある。
    たとえば@RequestMapping({"", "{num}"})に対してsetNum(Object)メソッドが必要です。
    lombok によってセッターメソッドを作る場合はあまり意識しないことかもしれません。
  2. アクションメソッドの引数名称も合わせる必要がある。
    きちんとルール付けしていればさほどな問題ですが、そうでないときは厄介です。
    (こっちはもしかしたら回避方法があるのかもしれません)