Spring Bootで、Webアクセスのパラメータをクラスで受け取るときの注意点

こんにちは。 エキサイト株式会社の三浦です。

Spring BootでAPIなどを作る時は、Webからのアクセスを受け取ることになります。 そのアクセスにクエリパラメータ等でパラメータが付いている場合、そのパラメータを何かしらの方法で受け取る必要があります。

今回は、クラスを使ってパラメータを受け取るときに、予期しないデータを受け取ってしまう場合がある点について説明していきます。

クラスでパラメータを受け取る方法

例えば、以下のアクセスを受け付けるとします。

http://localhost/sample?test1=aaa&test2=bbb

その場合、以下のコードで受け取ることができます。

import lombok.Value;

import java.beans.ConstructorProperties;

@Value
public class SampleModel {
    String test1;
    String test2;

    @ConstructorProperties({"test1", "test2"})
    public SampleModel(String test1, String test2) {
        this.test1 = test1;
        this.test2 = test2;
    }
}
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping
public class SampleController {
    @GetMapping("sample")
    public String sample(
            @ModelAttribute SampleModel sampleModel
    ) {
        return "OK";
    }
}

実際にデバッグモードで実行してみると、SampleModelに以下のデータが入っていることがわかります。

SampleModel(test1=aaa, test2=bbb)

上記のコードであれば問題はありませんが、実は特定のパターンだと想定外のデータが入ってしまうことがあります。

想定外のデータが入ってしまうパターン

アクセスは同じく

http://localhost/sample?test1=aaa&test2=bbb

とし、受け取りクラスのコードを少し変えてみます。

import lombok.Data;

import java.beans.ConstructorProperties;

// ValueではなくDataで受け取る(各プロパティにSetterが付く)
@Data
public class SampleModel {
    String test1;
    String test2;

    @ConstructorProperties({"test1", "test2"})
    public SampleModel(String test1, String test2) {
        this.test1 = test1;

        // test2は、一部文字列を変更してからプロパティに代入する
        this.test2 = test2 + "ccc";
    }
}

こちらは同じコードです。

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping
public class SampleController {
    @GetMapping("sample")
    public String sample(
            @ModelAttribute SampleModel sampleModel
    ) {
        return "OK";
    }
}

本来は

SampleModel(test1=aaa, test2=bbbccc)

こうなってほしいはずですが、実際は

SampleModel(test1=aaa, test2=bbb)

こうなってしまいます。

どうやら、「 Setter が存在し、かつアクセスのパラメータと同じ名前のプロパティ」があると、コンストラクタでプロパティに値を代入した後に、直接アクセスのパラメータの値をプロパティに代入してしまい、結果として上書きされてしまうようです。

このような場合、 Setter をつけないようにすることで解決できます。

import lombok.Value;

import java.beans.ConstructorProperties;

// SetterがつかないValueを使用する
@Value
public class SampleModel {
    String test1;
    String test2;

    @ConstructorProperties({"test1", "test2"})
    public SampleModel(String test1, String test2) {
        this.test1 = test1;
        this.test2 = test2 + "ccc";
    }
}

このコードであれば、想定通り以下のデータとなります。

SampleModel(test1=aaa, test2=bbbccc)

最後に

今回はシンプルな例でしたが、複雑になってくると見つけるのが大変になっていく恐れがあります。 コンストラクタでデータを入れる場合は Setter は不要なはずなので、可能な限り外していくことをおすすめします。