はじめに
SpringBoot(に含まれるSpringMVC:以下SpringMVCと略す)で受け取る入力パラメータの変換と設定する方法と、入力チェックの両方を行う方法を紹介します。
サンプルコードの動作確認バージョン
- SpringBoot 3.0.2
- JDK 17
入力項目を受け取るには
SpringMVC にてWebからパラメータを受け取る方法は、以下の手順で行います。
-
@Controller
アノテーションないしは@RestController
を付与したクラスを用意する -
@RequestMapping
にURLのパスを設定 - HTTPメソッドに対応した
@GetMapping
や@PostMapping
を付与したメソッドを作成する - メソッドの引数にパラメータ名と同じ名前の変数 ないしは パラメータ名と同じ名前の変数を持たせた任意のクラスを配置する
例:リクエストするURLを /shop
に設定し、受け取るパラメータに 店舗番号 shopNumber と、店舗名 shopName を設定した Controller
package com.github.apz.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/shop")
@Slf4j
public class ShopController {
@GetMapping
public String display(Model model, Integer shopNumber, String shopName) {
log.info("shopNumber: {}, shopName {}", shopNumber, shopName);
return "success";
}
}
このControllerへリクエストします
http://localhost:8080/shop?shopNumber=120&shopName=Alpha
出力したログ(一部を省略)
c.g.a.controller.ShopController : shopNumber: 120, shopName Alpha
SpringMVCを含む JavaのWebアプリケーションは、すべてのリクエストパラメータはStringで受け取ります。SpringMVCではプリミティブ型とそのラッパークラス(Integerなど)、他にもSpringMVCがデフォルトで自動的に型変換の候補にいれているクラスは、SpringMVCが自動的に判断して変数の値を型変換1 します。
なお、入力項目をControllerの引数にすべて並べる以外にも、別のクラスに定義する方法も選べます。これによりContollerメソッドの引数を簡素にできます。
package com.github.apz.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/shop")
@Slf4j
public class ShopController {
@GetMapping
public String display(Model model, ShopForm shopForm) {
log.info("shopForm: {}", shopForm);
return "success";
}
}
先程と同じパラメータを受け取る ShopForm クラスを用意し、Controllerの引数に記述します。
package com.github.apz.controller;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@NoArgsConstructor @Getter @Setter @ToString
public class ShopForm {
private ShopNumber shopNumber;
private String shopName;
}
入力項目は ShopForm へ格納されます。特殊な設定は必要ありません。なお、このControllerでパラメータを受け取るクラスは複数に分割もできます。
入力項目を入力チェック(Validation)するには
リクエストを受け取るControllerメソッドの引数に @Validated
を付与します。入力チェックの結果は、@Validated
を付与した次の引数に BindingResult
クラスを配置して、入力チェックの結果を受け取れます。
Validationのパッケージは、SpringBootのバージョン3.0.2に対応している spring-boot-starter-validation 3.0.2が hibernate-valiator 8.0.0-final を指定しますので、 jakarta.validation になっています。
package com.github.apz.controller;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/shop")
@AllArgsConstructor
@Slf4j
public class ShopController {
@GetMapping
public String display(Model model, @Validated ShopForm shopForm, BindingResult bindingResult) {
log.info("shopForm: {}", shopForm);
if (bindingResult.hasErrors()) {
log.info("error: {}" , bindingResult.getFieldError());
return "field error: " + bindingResult.getFieldError();
}
return "success";
}
}
package com.github.apz.controller;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@NoArgsConstructor @Getter @Setter @ToString
public class ShopForm {
@NotNull
@Min(1) @Max(200)
private Integer shopNumber;
@NotEmpty
@Size(min = 2, max = 8)
private String shopName;
}
入力項目を型変換(Type Convertion)する
Webからのパラメータを受け取る変数にプリミティブ型やそのラッパー型以外のクラス、またはSpringMVCがデフォルトで変換を提供しているクラス以外のものを変換したい場合は、独自で型変換を定義できます。
例えば以下のように、shopNumberの値を扱う変数を Integer ではなく、ShopNumber クラスの子要素であるvalue に格納させたい場合です。
package com.github.apz.controller;
import com.github.apz.model.shop.ShopNumber;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@NoArgsConstructor @Getter @Setter @ToString
public class ShopForm {
private ShopNumber shopNumber;
private String shopName;
}
package com.github.apz.model.shop;
import lombok.Value;
@Value(staticConstructor = "of")
public class ShopNumber {
private Integer value;
}
Webからのパラメータ shopNumber と shopName を扱う ShopForm クラスでは、shopNumber はShopNumberクラスに対応させます。shopNumberの値は、ShopNumber内の value 変数に格納させているとします。
この Webからの shopNumber の値を ShopNumber へ変換してインスタンスを生成する術を、SpringMVCへ設定 します。
設定は以下の2つどちらかで行います。
- 個々のControllerにて、
@InitBinder
を付与したメソッドにて、PropertyEditor
を実装した変換用のクラスを定義する - アプリケーション全体の設定である
@Configulation
を付与しているWebMvcConfigurer
を実装するクラスにて、変換用のクラスを定義する
個々のControllerに設定する @InitBinder を利用した例
手順:
-
java.beans.PropertyEditor
を実装 ないしはjava.beans.PropertyEditorSupport
を継承した 変換用のクラスを作成する -
@InitBinder
を付与したメソッドにて、上記の変換クラスを設定する
SpringMVCは、PropertyEditor(Support)をString(文字列)と変換対象のクラスを相互変換させるために利用しています。
package com.github.apz.controller;
import com.github.apz.config.ShopNumberPropertyEditor;
import com.github.apz.model.shop.ShopNumber;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ui.Model;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/shop")
@AllArgsConstructor
@Slf4j
public class ShopController {
@InitBinder
void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(ShopNumber.class, new ShopNumberPropertyEditor());
}
@GetMapping
public String display(Model model, ShopForm shopForm) {
log.info("shopForm: {}", shopForm);
return "success";
}
}
package com.github.apz.config;
import com.github.apz.model.shop.ShopNumber;
import java.beans.PropertyEditorSupport;
import java.util.Objects;
public class ShopNumberPropertyEditor extends PropertyEditorSupport {
@Override
public void setAsText(String text) throws IllegalArgumentException {
try {
setValue(ShopNumber.of(Integer.valueOf(text)));
} catch (NumberFormatException exception) {
throw new IllegalArgumentException(text);
}
}
public String getAsText() {
ShopNumber shopNumber = (ShopNumber) getValue();
return Objects.nonNull(shopNumber) ? shopNumber.toString() : null;
}
}
アプリケーション全体設定として WebMvcConfigurerの実装クラスで定義する例
設定手順はこちらの方が簡単です。すべてのControllerに適用されます。
package com.github.apz.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new ShopNumberConverter());
}
}
変換クラスはSpringMVCで用意されている org.springframework.core.convert.converter.Converter
を実装します。
package com.github.apz.config;
import com.github.apz.model.shop.ShopNumber;
import org.springframework.core.convert.converter.Converter;
public class ShopNumberConverter implements Converter<String, ShopNumber> {
@Override
public ShopNumber convert(String source) {
return ShopNumber.of(Integer.valueOf(source));
}
}
型変換させると@Validatedだけでは空振りしてしまう
さて、型変換を実装できたところで入力チェックも同じように設定します。
public class ShopController {
@GetMapping
public String display(Model model, @Validated ShopForm shopForm, BindingResult bindingResult) {
....
}
}
@NoArgsConstructor @Getter @Setter @ToString
public class ShopForm {
private ShopNumber shopNumber;
@NotEmpty
@Size(min = 2, max = 8)
private String shopName;
}
@Value(staticConstructor = "of")
public class ShopNumber {
@NotNull
@Min(1) @Max(200)
private Integer value;
}
何と、このままではShopNumberの入力チェックは機能しません。それは入力チェックのアノテーション @Validated
は、付与した変数とその子要素までを対象とするからです。
先程挙げたクラスを例にすると、@Validated
を付与している ShopForm が対象になるので、このクラスの変数が入力チェック対象になります。だからといって、
@NoArgsConstructor @Getter @Setter @ToString
public class ShopForm {
@NotNull
@Min(1) @Max(200)
private ShopNumber shopNumber;
@NotEmpty
@Size(min = 2, max = 8)
private String shopName;
}
このようにShopNumberへ直接に @NotNull
や @Min(1)
などの制約をつけても、
「貴殿はShopNumberクラスなのにIntegerクラスの制約をつけているぞ?」
と当然のように弾かれてしまい、これはうまく動いていません。
自動型変換しつつ、従来どおり入力チェックを行うには
入力チェックのアノテーション @Validated
は、付与した変数とその子要素までを対象としますので、さらに入力チェックを掘り下げるよう、ShopNumberクラスそのものもValidationを適用させるため、@Valid
を ShopNumberへ付与します。
package com.github.apz.controller;
import com.github.apz.model.shop.ShopNumber;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@NoArgsConstructor @Getter @Setter @ToString
public class ShopForm {
@Valid // Validation対象を対象のクラス内にも広げる
private ShopNumber shopNumber;
@NotEmpty
@Size(min = 2, max = 8)
private String shopName;
}
package com.github.apz.model.shop;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Value;
@Value(staticConstructor = "of")
public class ShopNumber {
@NotNull
@Min(1) @Max(200)
private Integer value;
}
package com.github.apz.controller;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/shop")
@AllArgsConstructor
@Slf4j
public class ShopController {
@GetMapping
public String display(Model model, @Validated ShopForm shopForm, BindingResult bindingResult) {
log.info("shopForm: {}", shopForm);
if (bindingResult.hasErrors()) {
log.info("error: {}" , bindingResult.getFieldError());
return "field error: " + bindingResult.getFieldError();
}
return "success";
}
}
これで想定どおりの動作をしてくれます。
利点
入力項目に対して型変換とチェック方法を統一でき、これがWebアプリケーションの制約としても働くので、複数Controller間で変数の扱いや入力チェックのルールを揃えることが可能です。
まとめ
- 入力項目を格納するクラスへ自動的に型変換する方法を示しました
- 型変換したクラスにも入力チェックを実行する方法を示しました
参考記事
サンプルプロジェクト
-
JSR-354 Money & Currency(https://github.com/JavaMoney/jsr354-api) の通貨関連クラスと, JSR-310 Date-Time|Joda-Time 2.xなど日付時刻関連クラスへの自動変換とフォーマット、などなど。 ↩