これまでは、Spring BootのWEB画面上での相関チェックをサービスクラスで実装してきたが、特定のFormオブジェクトのチェック処理を集約したバリデータを利用すると、サービスクラスからチェック処理を切り離すことができることがわかったので、そのサンプルプログラムを共有する。
前提条件
下記記事の実装が完了していること。
サンプルプログラムの作成
作成したサンプルプログラムの構成は以下の通り。
なお、上図の赤枠のうち、「DemoFormCheckValidator.java」「SearchFormCheckValidator.java」が新規で作成したプログラムで、他は変更したプログラムとなる。
「DemoFormCheckValidator.java」「SearchFormCheckValidator.java」の内容はそれぞれ以下の通りで、Validatorインタフェースを継承し、supportsメソッドでチェック対象となるクラスかどうかをチェックし、validateメソッドでチェック処理を実装している。
package com.example.demo; import org.springframework.stereotype.Component; import org.springframework.validation.Errors; import org.springframework.validation.Validator; @Component public class DemoFormCheckValidator implements Validator { @Override public boolean supports(Class<?> clazz) { //チェック対象となるクラスがDemoFormクラスかどうかをチェックする return DemoForm.class.isAssignableFrom(clazz); } @Override public void validate(Object form, Errors errors) { DemoForm demoForm = (DemoForm) form; //DemoForm内の単項目チェックでエラーだった場合は何もしない if(errors.hasErrors()){ return; } //生年月日の日付チェック処理を行う //エラーがある場合は、エラーメッセージ・エラーフィールドの設定を行う int checkDate = DateCheckUtil.checkDate(demoForm.getBirthYear() , demoForm.getBirthMonth(), demoForm.getBirthDay()); switch(checkDate){ case 1: //生年月日_年が空文字の場合のエラー処理 errors.rejectValue("birthYear", "validation.date-empty" , new String[]{"生年月日_年"}, ""); break; case 2: //生年月日_月が空文字の場合のエラー処理 errors.rejectValue("birthMonth", "validation.date-empty" , new String[]{"生年月日_月"}, ""); break; case 3: //生年月日_日が空文字の場合のエラー処理 errors.rejectValue("birthDay", "validation.date-empty" , new String[]{"生年月日_日"}, ""); break; case 4: //生年月日の日付が不正な場合のエラー処理 errors.rejectValue("birthYear", "validation.date-invalidate"); //生年月日_月・生年月日_日は、エラーフィールドの設定を行い、 //メッセージを空文字に設定している errors.rejectValue("birthMonth", "validation.empty-msg"); errors.rejectValue("birthDay", "validation.empty-msg"); break; case 5: //生年月日の日付が未来日の場合のエラー処理 errors.rejectValue("birthYear", "validation.date-future"); //生年月日_月・生年月日_日は、エラーフィールドの設定を行い、 //メッセージを空文字に設定している errors.rejectValue("birthMonth", "validation.empty-msg"); errors.rejectValue("birthDay", "validation.empty-msg"); break; default: //性別が不正に書き換えられていないかチェックする //正常な場合は何もしない if(!demoForm.getSexItems().keySet().contains(demoForm.getSex())) { errors.rejectValue("sex", "validation.sex-invalidate"); } break; } } }
package com.example.demo; import org.springframework.stereotype.Component; import org.springframework.validation.Errors; import org.springframework.validation.Validator; @Component public class SearchFormCheckValidator implements Validator { @Override public boolean supports(Class<?> clazz) { //チェック対象となるクラスがSearchFormクラスかどうかをチェックする return SearchForm.class.isAssignableFrom(clazz); } @Override public void validate(Object form, Errors errors) { SearchForm searchForm = (SearchForm) form; int checkDate = DateCheckUtil.checkSearchForm(searchForm); switch (checkDate){ case 1: //生年月日_fromが不正な場合のエラー処理 errors.rejectValue("fromBirthYear", "validation.date-invalidate-from"); errors.rejectValue("fromBirthMonth", "validation.empty-msg"); errors.rejectValue("fromBirthDay", "validation.empty-msg"); break; case 2: //生年月日_toが不正な場合のエラー処理 errors.rejectValue("toBirthYear", "validation.date-invalidate-to"); errors.rejectValue("toBirthMonth", "validation.empty-msg"); errors.rejectValue("toBirthDay", "validation.empty-msg"); break; case 3: //生年月日_from>生年月日_toの場合のエラー処理 errors.rejectValue("fromBirthYear", "validation.date-invalidate-from-to"); errors.rejectValue("fromBirthMonth", "validation.empty-msg"); errors.rejectValue("fromBirthDay", "validation.empty-msg"); errors.rejectValue("toBirthYear", "validation.empty-msg"); errors.rejectValue("toBirthMonth", "validation.empty-msg"); errors.rejectValue("toBirthDay", "validation.empty-msg"); break; default: //正常の場合は何もしない break; } } }
また、「DemoController」クラスの内容は以下の通り。
package com.example.demo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.SessionAttributes; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.validation.BindingResult; import org.springframework.web.bind.support.SessionStatus; import org.springframework.data.domain.Pageable; import java.util.ArrayList; import java.util.List; @Controller @SessionAttributes(types = {DemoForm.class, SearchForm.class}) public class DemoController { /** * Demoサービスクラスへのアクセス */ @Autowired private DemoService demoService; /** * SearchFormオブジェクトのチェック処理オブジェクト */ @Autowired private SearchFormCheckValidator searchFormCheckValidator; /** * DemoFormオブジェクトのチェック処理オブジェクト */ @Autowired private DemoFormCheckValidator demoFormCheckValidator; /** * SearchFormオブジェクトのチェック処理オブジェクトを設定 * @param binder WebDataバインダ */ @InitBinder("searchForm") public void initBinderSearchForm(WebDataBinder binder) { binder.addValidators(searchFormCheckValidator); } /** * DemoFormオブジェクトのチェック処理オブジェクトを設定 * @param binder WebDataバインダ */ @InitBinder("demoForm") public void initBinderDemoForm(WebDataBinder binder) { binder.addValidators(demoFormCheckValidator); } /** * ユーザーデータテーブル(user_data)のデータを取得して返却する * @return ユーザーデータリスト */ @ModelAttribute("demoFormList") public List<DemoForm> userDataList(){ List<DemoForm> demoFormList = new ArrayList<>(); return demoFormList; } /** * 追加・更新用Formオブジェクトを初期化して返却する * @return 追加・更新用Formオブジェクト */ @ModelAttribute("demoForm") public DemoForm createDemoForm(){ DemoForm demoForm = new DemoForm(); return demoForm; } /** * 検索用Formオブジェクトを初期化して返却する * @return 検索用Formオブジェクト */ @ModelAttribute("searchForm") public SearchForm createSearchForm(){ SearchForm searchForm = new SearchForm(); return searchForm; } /** * 初期表示(検索)画面に遷移する * @return 検索画面へのパス */ @RequestMapping("/") public String index(){ return "search"; } /** * 検索処理を行い、一覧画面に遷移する * @param searchForm 検索用Formオブジェクト * @param result バインド結果 * @param model Modelオブジェクト * @return 一覧画面へのパス */ @PostMapping("/search") public String search(@Validated SearchForm searchForm, BindingResult result, Model model){ //検索用Formオブジェクトのチェック処理でエラーがある場合は、 //検索画面のままとする if(result.hasErrors()){ return "search"; } //現在ページ数を1ページ目に設定し、一覧画面に遷移する searchForm.setCurrentPageNum(1); return movePageInList(model, searchForm); } /** * 一覧画面で「先頭へ」リンク押下時に次ページを表示する * @param searchForm 検索用Formオブジェクト * @param model Modelオブジェクト * @return 一覧画面へのパス */ @GetMapping("/firstPage") public String firstPage(SearchForm searchForm, Model model){ //現在ページ数を先頭ページに設定する searchForm.setCurrentPageNum(1); return movePageInList(model, searchForm); } /** * 一覧画面で「前へ」リンク押下時に次ページを表示する * @param searchForm 検索用Formオブジェクト * @param model Modelオブジェクト * @return 一覧画面へのパス */ @GetMapping("/backPage") public String backPage(SearchForm searchForm, Model model){ //現在ページ数を前ページに設定する searchForm.setCurrentPageNum(searchForm.getCurrentPageNum() - 1); return movePageInList(model, searchForm); } /** * 一覧画面で「次へ」リンク押下時に次ページを表示する * @param searchForm 検索用Formオブジェクト * @param model Modelオブジェクト * @return 一覧画面へのパス */ @GetMapping("/nextPage") public String nextPage(SearchForm searchForm, Model model){ //現在ページ数を次ページに設定する searchForm.setCurrentPageNum(searchForm.getCurrentPageNum() + 1); return movePageInList(model, searchForm); } /** * 一覧画面で「最後へ」リンク押下時に次ページを表示する * @param searchForm 検索用Formオブジェクト * @param model Modelオブジェクト * @return 一覧画面へのパス */ @GetMapping("/lastPage") public String lastPage(SearchForm searchForm, Model model){ //現在ページ数を最終ページに設定する searchForm.setCurrentPageNum(demoService.getAllPageNum(searchForm)); return movePageInList(model, searchForm); } /** * 更新処理を行う画面に遷移する * @param id 更新対象のID * @param model Modelオブジェクト * @return 入力・更新画面へのパス */ @GetMapping("/update") public String update(@RequestParam("id") String id, Model model){ //更新対象のユーザーデータを取得 DemoForm demoForm = demoService.findById(id); //ユーザーデータを更新 model.addAttribute("demoForm", demoForm); return "input"; } /** * 削除確認画面に遷移する * @param id 更新対象のID * @param model Modelオブジェクト * @return 削除確認画面へのパス */ @GetMapping("/delete_confirm") public String delete_confirm(@RequestParam("id") String id, Model model){ //削除対象のユーザーデータを取得 DemoForm demoForm = demoService.findById(id); //ユーザーデータを更新 model.addAttribute("demoForm", demoForm); return "confirm_delete"; } /** * 削除処理を行う * @param demoForm 追加・更新用Formオブジェクト * @return 一覧画面の表示処理 */ @PostMapping(value = "/delete", params = "next") public String delete(DemoForm demoForm){ //指定したユーザーデータを削除 demoService.deleteById(demoForm.getId()); //一覧画面に遷移 return "redirect:/to_index"; } /** * 削除完了後に一覧画面に戻る * @param searchForm 検索用Formオブジェクト * @param model Modelオブジェクト * @return 一覧画面 */ @GetMapping("/to_index") public String toIndex(SearchForm searchForm, Model model){ //一覧画面に戻り、1ページ目のリストを表示する searchForm.setCurrentPageNum(1); return movePageInList(model, searchForm); } /** * 削除確認画面から一覧画面に戻る * @param model Modelオブジェクト * @param searchForm 検索用Formオブジェクト * @return 一覧画面 */ @PostMapping(value = "/delete", params = "back") public String confirmDeleteBack(Model model, SearchForm searchForm){ //一覧画面に戻る return movePageInList(model, searchForm); } /** * 追加処理を行う画面に遷移する * @param model Modelオブジェクト * @return 入力・更新画面へのパス */ @PostMapping(value = "/add", params = "next") public String add(Model model){ model.addAttribute("demoForm", new DemoForm()); return "input"; } /** * 追加処理を行う画面から検索画面に戻る * @return 検索画面へのパス */ @PostMapping(value = "/add", params = "back") public String addBack(){ return "search"; } /** * エラーチェックを行い、エラーが無ければ確認画面に遷移し、 * エラーがあれば入力画面のままとする * @param demoForm 追加・更新用Formオブジェクト * @param result バインド結果 * @return 確認画面または入力画面へのパス */ @PostMapping(value = "/confirm", params = "next") public String confirm(@Validated DemoForm demoForm, BindingResult result){ //追加・更新用Formオブジェクトのチェック処理でエラーがある場合は、 //入力画面のままとする if(result.hasErrors()){ return "input"; } //エラーが無ければ確認画面に遷移する return "confirm"; } /** * 一覧画面に戻る * @param model Modelオブジェクト * @param searchForm 検索用Formオブジェクト * @return 一覧画面の表示処理 */ @PostMapping(value = "/confirm", params = "back") public String confirmBack(Model model, SearchForm searchForm){ //一覧画面に戻る return movePageInList(model, searchForm); } /** * 完了画面に遷移する * @param demoForm 追加・更新用Formオブジェクト * @param result バインド結果 * @return 完了画面 */ @PostMapping(value = "/send", params = "next") public String send(@Validated DemoForm demoForm, BindingResult result){ //チェック処理を行い、エラーがなければ、更新・追加処理を行う if(result.hasErrors()){ return "input"; } demoService.createOrUpdate(demoForm); return "redirect:/complete"; } /** * 完了画面に遷移する * @param sessionStatus セッションステータス * @return 完了画面 */ @GetMapping("/complete") public String complete(SessionStatus sessionStatus){ //セッションオブジェクトを破棄 sessionStatus.setComplete(); return "complete"; } /** * 入力画面に戻る * @return 入力画面 */ @PostMapping(value = "/send", params = "back") public String sendBack(){ return "input"; } /** * 一覧画面に戻り、指定した現在ページのリストを表示する * @param model Modelオブジェクト * @param searchForm 検索用Formオブジェクト * @return 一覧画面の表示処理 */ private String movePageInList(Model model, SearchForm searchForm){ //現在ページ数, 総ページ数を設定 model.addAttribute("currentPageNum", searchForm.getCurrentPageNum()); model.addAttribute("allPageNum", demoService.getAllPageNum(searchForm)); //ページング用オブジェクトを生成し、現在ページのユーザーデータリストを取得 Pageable pageable = demoService.getPageable(searchForm.getCurrentPageNum()); List<DemoForm> demoFormList = demoService.demoFormList(searchForm, pageable); //ユーザーデータリストを更新 model.addAttribute("demoFormList", demoFormList); return "list"; } }
@InitBinderアノテーションを利用してチェック処理オブジェクト「DemoFormCheckValidator.java」「SearchFormCheckValidator.java」を「WebDataBinder」オブジェクトに設定することで、それぞれのFormオブジェクトの入力チェックのタイミングで、それぞれのチェック処理が呼ばれるようにしている。
さらに、searchメソッド、confirmメソッドが呼ばれたタイミングで、それぞれのFormオブジェクトのチェック処理が呼ばれるよう、@ValidatedアノテーションをFormオブジェクトの引数に付与している。
なお、引数のBindingResultは、@Validatedアノテーションを付与した引数の直後に記載する必要がある。詳細は以下を参照のこと。
https://qiita.com/orangeeeee/items/334cf4b7042efbce9265
さらに、「DemoService.java」「DemoServiceImpl.java」は以下の通りで、それぞれのFormオブジェクトのチェック処理を削除する対応を行っている。
package com.example.demo; import org.springframework.data.domain.Pageable; import java.util.List; public interface DemoService { /** * ユーザーデータリストを取得 * @param searchForm 検索用Formオブジェクト * @param pageable ページネーションオブジェクト * @return ユーザーデータリスト */ List<DemoForm> demoFormList(SearchForm searchForm, Pageable pageable); /** * 引数のIDに対応するユーザーデータを取得 * @param id ID * @return ユーザーデータ */ DemoForm findById(String id); /** * 引数のIDに対応するユーザーデータを削除 * @param id ID */ void deleteById(String id); /** * 引数のユーザーデータがあれば更新し、無ければ追加 * @param demoForm 追加・更新用Formオブジェクト */ void createOrUpdate(DemoForm demoForm); /** * ユーザー検索時に利用するページング用オブジェクトを生成する * @param pageNumber ページ番号 * @return ページング用オブジェクト */ Pageable getPageable(int pageNumber); /** * 一覧画面の全ページ数を取得する * @param searchForm 検索用Formオブジェクト * @return 全ページ数 */ int getAllPageNum(SearchForm searchForm); }
package com.example.demo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.data.domain.Pageable; import java.util.ArrayList; import java.util.Collection; import java.util.List; @Service public class DemoServiceImpl implements DemoService{ /** * ユーザーデータテーブル(user_data)へアクセスするマッパー */ @Autowired private UserDataMapper mapper; /** * 1ページに表示する行数(application.propertiesから取得) */ @Value("${demo.list.pageSize}") private String listPageSize; /** * {@inheritDoc} */ @Override public List<DemoForm> demoFormList(SearchForm searchForm, Pageable pageable) { List<DemoForm> demoFormList = new ArrayList<>(); //ユーザーデータテーブル(user_data)から検索条件に合うデータを取得する Collection<UserData> userDataList = mapper.findBySearchForm(searchForm, pageable); for (UserData userData : userDataList) { demoFormList.add(getDemoForm(userData)); } return demoFormList; } /** * {@inheritDoc} */ @Override public DemoForm findById(String id) { Long longId = stringToLong(id); UserData userData = mapper.findById(longId); return getDemoForm(userData); } /** * {@inheritDoc} */ @Override @Transactional(readOnly = false) public void deleteById(String id){ Long longId = stringToLong(id); mapper.deleteById(longId); } /** * {@inheritDoc} */ @Override @Transactional(readOnly = false) public void createOrUpdate(DemoForm demoForm){ //更新・追加処理を行うエンティティを生成 UserData userData = getUserData(demoForm); //追加・更新処理 if(demoForm.getId() == null){ userData.setId(mapper.findMaxId() + 1); mapper.create(userData); }else{ mapper.update(userData); } } /** * {@inheritDoc} */ @Override public Pageable getPageable(int pageNumber){ Pageable pageable = new Pageable() { @Override public int getPageNumber() { //現在ページ数を返却 return pageNumber; } @Override public int getPageSize() { //1ページに表示する行数を返却 //listPageSizeは、本プログラムの先頭に定義している return Integer.parseInt(listPageSize); } @Override public int getOffset() { //表示開始位置を返却 //例えば、1ページに2行表示する場合の、2ページ目の表示開始位置は //(2-1)*2+1=3 で計算される return ((pageNumber - 1) * Integer.parseInt(listPageSize) + 1); } @Override public Sort getSort() { //ソートは使わないのでnullを返却 return null; } }; return pageable; } /** * {@inheritDoc} */ @Override public int getAllPageNum(SearchForm searchForm) { //1ページに表示する行数を取得 int listPageSizeNum = Integer.parseInt(listPageSize); if(listPageSizeNum == 0){ return 1; } //一覧画面に表示する全データを取得 //第二引数のpageableにnullを設定することで、一覧画面に表示する //全データが取得できる Collection<UserData> userDataList = mapper.findBySearchForm(searchForm, null); //全ページ数を計算 //例えば、1ページに2行表示する場合で、全データ件数が5の場合、 //(5+2-1)/2=3 と計算される int allPageNum = (userDataList.size() + listPageSizeNum - 1) / listPageSizeNum; return allPageNum == 0 ? 1 : allPageNum; } /** * DemoFormオブジェクトに引数のユーザーデータの各値を設定する * @param userData ユーザーデータ * @return DemoFormオブジェクト */ private DemoForm getDemoForm(UserData userData){ if(userData == null){ return null; } DemoForm demoForm = new DemoForm(); demoForm.setId(String.valueOf(userData.getId())); demoForm.setName(userData.getName()); demoForm.setBirthYear(String.valueOf(userData.getBirthY())); demoForm.setBirthMonth(String.valueOf(userData.getBirthM())); demoForm.setBirthDay(String.valueOf(userData.getBirthD())); demoForm.setSex(userData.getSex()); demoForm.setMemo(userData.getMemo()); demoForm.setSex_value(userData.getSex_value()); return demoForm; } /** * UserDataオブジェクトに引数のフォームの各値を設定する * @param demoForm DemoFormオブジェクト * @return ユーザーデータ */ private UserData getUserData(DemoForm demoForm){ UserData userData = new UserData(); if(!DateCheckUtil.isEmpty(demoForm.getId())){ userData.setId(Long.valueOf(demoForm.getId())); } userData.setName(demoForm.getName()); userData.setBirthY(Integer.valueOf(demoForm.getBirthYear())); userData.setBirthM(Integer.valueOf(demoForm.getBirthMonth())); userData.setBirthD(Integer.valueOf(demoForm.getBirthDay())); userData.setSex(demoForm.getSex()); userData.setMemo(demoForm.getMemo()); userData.setSex_value(demoForm.getSex_value()); return userData; } /** * 引数の文字列をLong型に変換する * @param id ID * @return Long型のID */ private Long stringToLong(String id){ try{ return Long.parseLong(id); }catch(NumberFormatException ex){ return null; } } }
その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/java/tree/master/spring-boot-web-check-validator/demo
サンプルプログラムの実行結果
入力画面のチェック処理については、下記記事の「完成した画面イメージの共有」と同等の実行結果となる。
さらに、検索ボタンでエラーが発生した場合は、例えば以下のような画面表示となる。
要点まとめ
- 特定のFormオブジェクトのチェック処理を集約したバリデータを作成するには、Validatorインタフェースを継承し、supportsメソッドでチェック対象となるクラスかどうかをチェックし、validateメソッドでチェック処理を実装すればよい。
- 特定のFormオブジェクトの入力チェックのタイミングで作成したバリデータを呼び出すには、@InitBinderアノテーションを利用してWebDataBinderにバリデータを登録する。