Spring Boot でログイン画面 + 一覧画面 + 登録画面の Webアプリケーションを作る ( その14 )( 気になった点を修正 )
概要
Spring Boot でログイン画面 + 一覧画面 + 登録画面の Webアプリケーションを作る ( その13 )( 登録画面作成3 ) の続きです。今回は気になっていた点を修正します。
- 今回の手順で確認できるのは以下の内容です。
- PagenationHelper クラスのコンストラクタの引数を Page<?> page だけにする。
- Constant クラスのフィールドを public final にして、getter メソッドを削除する。
- application-develop.properties、application-product.properties の共通設定は application.properties に記述する。
- Controller クラスのメソッド内から直接 response.sendError を呼び出さずに、例外を throw して @ControllerAdvice を付加したクラス内で response.sendError を呼び出すようにする。
ソフトウェア一覧
参考にしたサイト
Spring-Bootの設定プロパティと環境変数
http://qiita.com/NewGyu/items/d51f527c7199b746c6b6- 設定の反映順序が書いてあり、参考にしました。
Spring Boot Reference Guide - 23. Externalized Configuration
http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-configSpring Boot Error Responses
http://www.jayway.com/2014/10/19/spring-boot-error-responses/Spring Framework Reference Documentation - 17. Web MVC framework - Advising controllers with the @ControllerAdvice annotation
http://docs.spring.io/spring-framework/docs/4.1.x/spring-framework-reference/html/mvc.html#mvc-ann-controller-adviceJava言語の例外(Exception)とカスタム例外の定義方法
http://www.syboos.jp/java/doc/exception-and-custom-exception.htmlIntelliJ Idea generating serialVersionUID
http://stackoverflow.com/questions/12912287/intellij-idea-generating-serialversionuid多言語対応化メッセージリソースをロケールを使って読込む方法
http://javatechnology.net/spring/read-message-properties/Spring Resource Bundle With ResourceBundleMessageSource Example
http://www.mkyong.com/spring/spring-resource-bundle-with-resourcebundlemessagesource-example/@CONTROLLERADVICE IMPROVEMENTS IN SPRING 4
http://blog.codeleak.pl/2013/11/controlleradvice-improvements-in-spring.html
手順
PagenationHelper のコンストラクタの引数を Page<?> page だけにする
メソッドの引数にジェネリクスの変数を渡す方法が分かったので、変更します。
IntelliJ IDEA 上で 1.0.x-argsgenerics ブランチを作成します。
src/main/java/ksbysample/webapp/basic/helper の下の PagenationHelper.java を リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/basic/web の下の CountryListController.java を リンク先の内容 に変更します。
build タスクを実行し、BUILD SUCCESSFUL が表示されることを確認します。
bootRun タスクを実行して Tomcat を起動後、検索/一覧画面でページネーションが正常に動作することを確認します。
commit、GitHub へ Push、1.0.x-argsgenerics -> 1.0.x へ Pull Request、1.0.x でマージ、1.0.x-argsgenerics ブランチを削除、をします。
Constant クラスのフィールドを public final にして、getter メソッドを削除する
Constant クラスのフィールドは外部からの変更を禁止するために本当は public final にしたかったのですが ( public final で宣言すれば constant.CONTINENT_LIST の形式でアクセスできるため )、final が付いたフィールドには @Value アノテーションで値をセットできないため getter メソッドのみ提供する形で実現していました。
final が付いたフィールドに @Value アノテーションで取得した値をセットする方法が分かりましたので変更します。
IntelliJ IDEA 上で 1.0.x-constantfinal ブランチを作成します。
final を付ける件とは直接関係はありませんが、設定ファイル内の定数名をフィールドに合わせたいので修正します。src/main/resources の下の constant.properties を リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/basic/config の下の Constant.java を リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/basic/web の下の CountryController.java を リンク先のその1の内容 に変更します。
build タスクを実行し、BUILD SUCCESSFUL が表示されることを確認します。
bootRun タスクを実行して Tomcat を起動後、登録画面で Continent の項目に constant.properties の CONTINENT_LIST に設定した値が表示されることを確認します。
commit、GitHub へ Push、1.0.x-constantfinal -> 1.0.x へ Pull Request、1.0.x でマージ、1.0.x-constantfinal ブランチを削除、をします。
application-develop.properties、application-product.properties の共通設定は application.properties に記述する
spring.profiles.active で指定した文字列が付いた設定ファイル ( -Dspring.profiles.active=develop ならば application-develop.properties ) だけが読み込まれるものと思っていたのですが、最初に application.properties が読み込まれて、その後 application-develop.properties の設定が読み込まれて上書きされるようです。
application.properties に spring.messages.cache-seconds = -1
を、application-develop.properties に spring.messages.cache-seconds = 0
を設定したら application-develop.properties の spring.messages.cache-seconds = 0
の設定の方が有効でした。
共通の設定は application.properties に記述するようにします。ついでに設定項目がアルファベット順で並ぶよう変更します。
IntelliJ IDEA 上で 1.0.x-applicationproperties ブランチを作成します。
src/main/resources の下に application.properties を新規作成します。作成後、リンク先の内容 に変更します。
src/main/resources の下の application-develop.properties を リンク先の内容 に変更します。
src/main/resources の下の application-product.properties を リンク先の内容 に変更します。
build タスクを実行し、BUILD SUCCESSFUL が表示されることを確認します。
bootRun タスクを実行して Tomcat を起動後、検索/一覧画面、登録画面 ( 入力→確認→完了 ) でエラーが出ずに正常に動作することを確認します。
commit、GitHub へ Push、1.0.x-applicationproperties -> 1.0.x へ Pull Request、1.0.x でマージ、1.0.x-applicationproperties ブランチを削除、をします。
Controller クラスのメソッド内から直接 response.sendError を呼び出さずに、例外を throw して @ControllerAdvice を付加したクラス内で response.sendError を呼び出すようにする
Spring Framework では Controller クラスの例外処理を @ControllerAdvice アノテーションを付加したクラスに集約できるので、Controller クラスは例外を throw して、@ControllerAdvice アノテーションを付加したクラス内で response.sendError をするよう変更します。
IntelliJ IDEA 上で 1.0.x-invalidrequest ブランチを作成します。
src/main/java/ksbysample/webapp/basic の下に exception パッケージを作成します。
独自 Exception クラスを作成します。まずは Exception クラスに付加する serialVersionUID フィールドを IntelliJ IDEA に自動生成してもらうために設定を変更します。
IntelliJ IDEA のメインメニューから「File」-「Settings...」を選択します。
「Settings」ダイアログが表示されたら、画面左側で「Editor」-「Inspections」を選択し、画面中央に表示されるリストから「Serialization issues」-「Serializable class without 'serialVersionUID'」をチェックします。チェック後、「OK」ボタンをクリックしてダイアログを閉じます。
src/main/java/ksbysample/webapp/basic/exception の下に InvalidRequestException.java を新規作成します。作成後、リンク先の内容 に変更します。
例外発生時のエラーメッセージを作成します。src/main/resources の下の messages_ja_JP.properties を リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/basic/web の下の CountryController.java を リンク先のその2の内容 に変更します。
src/main/java/ksbysample/webapp/basic/web の下に ExceptionHandlerAdvice.java を新規作成します。作成後、リンク先の内容 に変更します。
build タスクを実行し、BUILD SUCCESSFUL が表示されることを確認します。
bootRun タスクを実行して Tomcat を起動します。
ブラウザで http://localhost:8080/country/update にアクセスします。ログイン画面が表示されたら test / ptest を入力後「ログイン」ボタンをクリックします。
Whitelabel Error Page が表示され、HTTPステータスコードが 400(BAD REQUEST) になっていることが確認できます。尚、この時ログを見てみたのですが、例外が発生したソースと行数が出るログは出力されていませんでした。必要な場合にはログ出力する処理を追加する必要があります。
Ctrl+F2 を押して Tomcat を停止します。
commit、GitHub へ Push、1.0.x-invalidrequest -> 1.0.x へ Pull Request、1.0.x でマージ、1.0.x-invalidrequest ブランチを削除、をします。
- commit 時に「Code Analysis」ダイアログが表示されますが、
Class 'ExceptionHandlerAdvice' is never used
はSuppress for classes annotated by ...
を選択して無視されるようにし、それ以外の '... is never used' の Warning は何もしません。
- commit 時に「Code Analysis」ダイアログが表示されますが、
ソースコード
PagenationHelper.java
public PagenationHelper(Page<?> page) { int number = page.getNumber(); int totalPages = page.getTotalPages();
- コンストランクタの引数を
int number, int size, int totalPages
→Page<?> page
へ変更します。以前Page<T> page
で渡そうとしてエラーになりどうすればよいか分からなかったのですが、Page<T>
ではなくPage<?>
の形で渡せばよいことが分かりました。 - number, totalPages の変数に展開する処理を追加します。コンストラクタ内で毎回 page.getNumber() 等と書くのは面倒なので、最初に変数に展開します。size はメソッド内で使用していなかったので変数に展開しませんでした。
CountryListController.java
@RequestMapping public String index(@Validated CountryListForm countryListForm , BindingResult bindingResult , @PageableDefault(size = DEFAULT_PAGEABLE_SIZE, page = 0) Pageable pageable , Model model) { if (bindingResult.hasErrors()) { return "countryList"; } countryListForm.setSize(pageable.getPageSize()); countryListForm.setPage(pageable.getPageNumber()); Page<Country> page = countryService.findCountry(countryListForm, pageable); PagenationHelper ph = new PagenationHelper(page); model.addAttribute("page", page); model.addAttribute("ph", ph); return "countryList"; }
- index メソッド内の PagenationHelper のインスタンスを生成する時の引数を
page.getNumber(), page.getSize(), page.getTotalPages()
→page
へ変更します。
constant.properties
CONTINENT_LIST=Asia,Europe,North America,Africa,Oceania,Antarctica,South America
- 定数名を
continent.list
→CONTINENT_LIST
へ変更します。
Constant.java
package ksbysample.webapp.basic.config; import lombok.Getter; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.PropertySource; import org.springframework.stereotype.Component; import java.util.List; @Component @PropertySource("classpath:constant.properties") public class Constant { public final List<String> CONTINENT_LIST; @Autowired public Constant( @Value("#{'${CONTINENT_LIST}'.split(',')}") List<String> CONTINENT_LIST ) { this.CONTINENT_LIST = CONTINENT_LIST; } }
- クラスに付加していた @Getter アノテーションを削除します。
- フィールド CONTINENT_LIST を
private List<String>
→public final List<String>
へ修正します。また @Value アノテーションを削除します。 - コンストラクタを追加します。コンストラクタには @Autowired アノテーションを付加します。またコンストラクタの引数に
@Value("#{'${CONTINENT_LIST}'.split(',')}") List<String> CONTINENT_LIST
を追加し、コンストラクタ内で final のフィールド CONTINENT_LIST に値をセットするようにします。
CountryController.java
■その1
package ksbysample.webapp.basic.web; import ksbysample.webapp.basic.config.Constant; import ksbysample.webapp.basic.service.CountryService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Controller @RequestMapping("/country") public class CountryController { @Autowired private Constant constant; @Autowired private CountryFormValidator countryFormValidator; @Autowired private CountryService countryService; @InitBinder public void initBinder(WebDataBinder binder) { binder.addValidators(countryFormValidator); } @RequestMapping("/input") public String input(CountryForm countryForm , Model model) { model.addAttribute("continentList", constant.CONTINENT_LIST); return "country/input"; } @RequestMapping("/input/back") public String inputBack(CountryForm countryForm , RedirectAttributes redirectAttributes) { redirectAttributes.addFlashAttribute("countryForm", countryForm); return "redirect:/country/input"; } @RequestMapping("/confirm") public String confirm(@Validated CountryForm countryForm , BindingResult bindingResult , Model model) { if (bindingResult.hasErrors()) { model.addAttribute("continentList", constant.CONTINENT_LIST); return "country/input"; } return "country/confirm"; } @RequestMapping("/update") public String update(@Validated CountryForm countryForm , BindingResult bindingResult , Model model , HttpServletResponse response) throws IOException { if (bindingResult.hasErrors()) { response.sendError(HttpStatus.BAD_REQUEST.value()); return null; } countryService.save(countryForm); return "redirect:/country/complete"; } @RequestMapping("/complete") public String complete() { return "country/complete"; } }
constant.getCONTINENT_LIST()
→constant.CONTINENT_LIST
へ変更します。
■その2
package ksbysample.webapp.basic.web; import ksbysample.webapp.basic.config.Constant; import ksbysample.webapp.basic.exception.InvalidRequestException; import ksbysample.webapp.basic.service.CountryService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Locale; @Controller @RequestMapping("/country") public class CountryController { @Autowired private Constant constant; @Autowired private CountryFormValidator countryFormValidator; @Autowired private CountryService countryService; @Autowired private MessageSource messageSource; @InitBinder public void initBinder(WebDataBinder binder) { binder.addValidators(countryFormValidator); } @RequestMapping("/input") public String input(CountryForm countryForm , Model model) { model.addAttribute("continentList", constant.CONTINENT_LIST); return "country/input"; } @RequestMapping("/input/back") public String inputBack(CountryForm countryForm , RedirectAttributes redirectAttributes) { redirectAttributes.addFlashAttribute("countryForm", countryForm); return "redirect:/country/input"; } @RequestMapping("/confirm") public String confirm(@Validated CountryForm countryForm , BindingResult bindingResult , Model model) { if (bindingResult.hasErrors()) { model.addAttribute("continentList", constant.CONTINENT_LIST); return "country/input"; } return "country/confirm"; } @RequestMapping("/update") public String update(@Validated CountryForm countryForm , BindingResult bindingResult , Locale locale , Model model , HttpServletResponse response) throws IOException, InvalidRequestException { if (bindingResult.hasErrors()) { throw new InvalidRequestException(messageSource.getMessage("common.invalidRequestException.message", new Object[]{bindingResult.toString()}, locale)); } countryService.save(countryForm); return "redirect:/country/complete"; } @RequestMapping("/complete") public String complete() { return "country/complete"; } }
private MessageSource messageSource;
を追加します。- update メソッドの引数に
Locale locale
を追加します。また throws宣言 にInvalidRequestException
を追加します。 if (bindingResult.hasErrors()) { ... }
内の処理を InvalidRequestException を throw するよう変更します。
application.properties
hibernate.dialect = org.hibernate.dialect.MySQLDialect spring.jpa.hibernate.ddl-auto = none spring.jpa.hibernate.naming_strategy = org.hibernate.cfg.EJB3NamingStrategy
application-develop.properties
spring.datasource.url = jdbc:log4jdbc:mysql://localhost/world spring.datasource.username = root spring.datasource.password = xxxxxxxx spring.datasource.driverClassName = net.sf.log4jdbc.sql.jdbcapi.DriverSpy spring.messages.cache-seconds = 0 spring.thymeleaf.cache = false
- application.properties に記述した設定を削除します。
- 設定項目をアルファベット順に並び替えます。
application-product.properties
spring.datasource.url = jdbc:mysql://localhost/world spring.datasource.username = root spring.datasource.password = xxxxxxxx spring.datasource.driverClassName = com.mysql.jdbc.Driver spring.thymeleaf.cache = true
- application.properties に記述した設定を削除します。
- 設定項目をアルファベット順に並び替えます。
InvalidRequestException.java
package ksbysample.webapp.basic.exception; public class InvalidRequestException extends Exception { private static final long serialVersionUID = -2148809788806855935L; public InvalidRequestException() { super(); } public InvalidRequestException(String message) { super(message); } public InvalidRequestException(String message, Throwable cause) { super(message, cause); } public InvalidRequestException(Throwable cause) { super(cause); } }
serialVersionUID フィールドは以下の手順で IntelliJ IDEA に自動生成してもらいます。
クラス名の InvalidRequestException のところにカーソルを移動します。
電球アイコンが表示されるので、クリックしてメニューを表示した後「Add 'serialVersionUID' field」を選択します。
InvalidRequestException は IOException のソースを参考に作成しました。
messages_ja_JP.properties
AbstractUserDetailsAuthenticationProvider.locked=入力された ID はロックされています AbstractUserDetailsAuthenticationProvider.disabled=入力された ID は使用できません AbstractUserDetailsAuthenticationProvider.expired=入力された ID の有効期限が切れています AbstractUserDetailsAuthenticationProvider.credentialsExpired=入力された ID のパスワードの有効期限が切れています AbstractUserDetailsAuthenticationProvider.badCredentials=入力された ID あるいはパスワードが正しくありません typeMismatch.java.math.BigDecimal=数値を入力して下さい。 typeMismatch.java.lang.Long=数値を入力して下さい。 common.invalidRequestException.message = 本来発生し得ない Validation エラーが発生しました。不正にアクセスされた可能性があります。{0} countryForm.code2.equalCode = Code2 には Code と異なる文字列を入力して下さい。 countryForm.continent.notAsia = Name に "Japan" あるいは "日本" を入力している場合、Continent は "Asia" を選択して下さい。 countryForm.region.notAsiaPattern = Continent に "Asia" を選択している場合、Region には "Eastern Asia", "Middle East", "Southeast Asia", "Southern and Central Asia" のいずれかの文字列を入力して下さい。
- common.invalidRequestException.message を追加します。
ExceptionHandlerAdvice.java
package ksbysample.webapp.basic.web; import ksbysample.webapp.basic.exception.InvalidRequestException; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @ControllerAdvice public class ExceptionHandlerAdvice { @ExceptionHandler public void handleInvalidRequestException(InvalidRequestException e, HttpServletResponse response) throws IOException { response.sendError(HttpStatus.BAD_REQUEST.value()); } }
履歴
2015/02/25
初版発行。