- 投稿日:2020-10-21T22:31:48+09:00
DateAndTime APIを用いた日時取得方法
概要
Date and Time APIで以下のパターンをJavaとKotlin(ほぼ同じ)で実装する。
- 日時の変更
- 独自フォーマット
- ISOフォーマット
- 月初日時
- 月末日時
実装例
JavaのUtilsクラスだとこちら。
LocalDateUtils.javaimport java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.time.temporal.TemporalAdjusters; public class LocalDateTimeUtils { private static final DateTimeFormatter YYYYMM_FORMAT = DateTimeFormatter.ofPattern("yyyyMM"); // 日時の変更 public static LocalDateTime getDateTimeAfterOneYear(LocalDateTime dateTime) { return dateTime.plusYears(1); } // 独自フォーマット public static String getYearAndMonth(LocalDateTime dateTime) { return dateTime.format(YYYYMM_FORMAT); } // ISOフォーマット 例)2011-12-03 public static String getIsoLocalDate(LocalDateTime dateTime) { return dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE); } // 月初日時 public static LocalDateTime getFirstDayDateTime(LocalDateTime dateTime) { return LocalDateTime.of(dateTime.with(TemporalAdjusters.firstDayOfMonth()).toLocalDate(), LocalTime.MIN); } // 月末日時 public static LocalDateTime getLastDayDateTime(LocalDateTime dateTime) { return LocalDateTime.of(dateTime.with(TemporalAdjusters.lastDayOfMonth()).toLocalDate(), LocalTime.MAX); } }Kotlinの拡張クラスだとこちら。
LocalDateTime.ktimport java.time.LocalDateTime import java.time.LocalTime import java.time.format.DateTimeFormatter import java.time.temporal.TemporalAdjusters private val YYYYMM_FORMAT = DateTimeFormatter.ofPattern("yyyyMM") // 日時の変更 fun LocalDateTime.getDateTimeAfterOneYear(): LocalDateTime = plusYears(1) // 独自フォーマット fun LocalDateTime.getYearAndMonth(): String = format(YYYYMM_FORMAT) // ISOフォーマット 例)2011-12-03 fun LocalDateTime.getIsoLocalDate(): String = format(DateTimeFormatter.ISO_LOCAL_DATE) // 月初日時 fun LocalDateTime.getFirstDayDateTime(): LocalDateTime = LocalDateTime.of(with(TemporalAdjusters.firstDayOfMonth()).toLocalDate(), LocalTime.MIN) // 月末日時 fun LocalDateTime.getLastDayDateTime(): LocalDateTime = LocalDateTime.of(with(TemporalAdjusters.lastDayOfMonth()).toLocalDate(), LocalTime.MAX)
- 投稿日:2020-10-21T22:01:46+09:00
Javaで昇順の並べ替え(バブルソート:単純交換法のアルゴリズム)
Javaで昇順の並べ替え(バブルソート:単純交換法のアルゴリズム)
Javaの基礎を学ぶ中でアルゴリズムに関する問題にあたったので、思考整理のため記載しました。
昇順に並べ変える(例:2,5,3,4,1→1,2,3,4,5)
ちなみに降順(5,4,3,2,1)イメージは左から1つずつ順番にfor文で比較し、並べ替える。
(比較例:2と5、2と3、2と4、2と1…。)書き方はいろいろありますが、下記コード例
1 int[] date = {2,5,3,4,1}; 2 for (int i = 0; i < date.length - 1; i++) { 3 for (int j = i + 1; j < date.length ; j++) { 4 if (date[i] > date[j]) { 5 int a = date[i]; 6 date[i] = date[j]; 7 date[j] = a; 8 } 9 } 10} 11for (int i = 0; i < date.length; i++) { 12 System.out.print(date[i] + " "); 13}2行目のfor文で比較元となる1つ目の要素を取得するために条件式を指定します。
初期値i = 0;条件式(i < date.length - 1);次の一歩 i++一つの要素が比較する値の数を
date.length - 1
で取得します。
並べ替える数が増えても問題ありません。比較する値の数とは、例:2,5,3,4,1のうち 2と5,2と3,2と4,2と1の4回3行目のfor文で比較対象となる2つ目の要素を取得するため初期値
j = i +i
の形で条件式を記述します。初期値(j = i +i);条件式(j < date.length);次の一歩 j++4〜8行目のif文で条件に当てはまる値の比較を行い、並び替えを行います。
if (date[i] > date[j]) { int a = date[i]; date[i] = date[j]; date[j] = a; }並べ変え後のdateの表示
for (int i = 0; i < date.length; i++) { System.out.print(date[i] + " "); }コンソール表示 1 2 3 4 5参照
- 投稿日:2020-10-21T21:10:55+09:00
Javaの strictfp が実際に意味を持つ環境を用意する
Javaには
strictfp
という修飾子があります。strictfp
によって実際に動作が変わるコードと実行環境を用意してみました。マシン
最近のマシンでJavaを実行する際には
strictfp
は意味を持ちません。strictfp
があってもなくてもIEEE準拠の挙動をするはずです。(なので、
strictfp
の有無にかかわらずIEEE準拠の挙動をさせようというJEPが出ています: JEP 306: Restore Always-Strict Floating-Point Semantics)
strictfp
が意味を持つのはx86系の、SSE2が実装されていないCPUです。Intelの場合は2000年ごろのPentium 4以降でSSE2が実装されているので、SSE2がないマシンを用意しようとするとそれよりも前のマシンが必要になります。ですが、20年以上前のマシンを用意するのは難しいですよね(筆者はレトロPCマニアではありません)。そこで、エミュレーションを利用します。ここではIntel SDEとQEMUの2つのエミュレーターを考えます。
Intel SDEで指定できるCPUの種類の中で、SSE2が実装されていないものとしては
-quark
があるようです。その次に機能が少なそうなのが-p4
つまりPentium 4なので、Intel SDEでSSE2のない環境を用意しようと思ったら-quark
一択です。QEMUでは
-cpu pentium
,-cpu pentium2
,-cpu pentium3
などが利用できます。Linux上ではuser space emulationと言って仮想マシンを用意しなくてもプログラムを動かせるので便利です。この記事でもLinuxを使います。JVMは、Oracleが配布しているJava 8 Update 261を利用します。こいつはPentium II以降に対応しているようです。新しめのJREがどうなっているかは確認していません。
プログラム
strictfp
が意味を持つのは、浮動小数点演算の途中でオーバーフローまたはアンダーフローが起こるような場合、だそうです。strictfp
が指定された場合はIEEE準拠の動作をし、strictfp
が指定されなかった場合はオーバーフローまたはアンダーフローが回避されたり、異なる値を返す可能性があります。ここでは、
double
同士の乗算double
3個の乗算float
3個の乗算について、
strictfp
ありの版となしの版を用意します。そして、特定の値に対するそれらの値を計算させます。3個の乗算に関しては、JITコンパイルが行われることを期待して10万回実行させ、その前後での値を表示させます。class StrictfpTest { static double multiplyDefault(double x, double y) { return x * y; } static strictfp double multiplyStrict(double x, double y) { return x * y; } static double multiplyThreeDoublesDefault(double x, double y, double z) { return x * y * z; } static strictfp double multiplyThreeDoublesStrict(double x, double y, double z) { return x * y * z; } static float multiplyThreeFloatsDefault(float x, float y, float z) { return x * y * z; } static strictfp float multiplyThreeFloatsStrict(float x, float y, float z) { return x * y * z; } public static void main(String[] args) { { double x = 0x1.00002fff0p0, y = 0x1.000000008p0; System.out.printf("multiplyDefault(%a, %a) = %a\n", x, y, multiplyDefault(x, y)); System.out.printf("multiplyStrict(%a, %a) = %a\n", x, y, multiplyStrict(x, y)); } { double x = 0x1.fffe0effffffep0, y = 0x1.0000000000001p0; System.out.printf("multiplyDefault(%a, %a) = %a\n", x, y, multiplyDefault(x, y)); System.out.printf("multiplyStrict(%a, %a) = %a\n", x, y, multiplyStrict(x, y)); } { double x = 0x1.fffe0effffffep-51, y = 0x1.0000000000001p-1000; System.out.printf("multiplyDefault(%a, %a) = %a\n", x, y, multiplyDefault(x, y)); System.out.printf("multiplyStrict(%a, %a) = %a\n", x, y, multiplyStrict(x, y)); } { double x = 0x1p-1000, y = 0x1p-1000, z = 0x1p1000; System.out.printf("multiplyThreeDoublesDefault(%a, %a, %a) = %a\n", x, y, z, multiplyThreeDoublesDefault(x, y, z)); System.out.printf("multiplyThreeDoublesStrict(%a, %a, %a) = %a\n", x, y, z, multiplyThreeDoublesStrict(x, y, z)); for (int i = 0; i < 100000; ++i) { multiplyThreeDoublesDefault(x, z, y); multiplyThreeDoublesStrict(x, z, y); } System.out.printf("multiplyThreeDoublesDefault(%a, %a, %a) = %a\n", x, y, z, multiplyThreeDoublesDefault(x, y, z)); System.out.printf("multiplyThreeDoublesStrict(%a, %a, %a) = %a\n", x, y, z, multiplyThreeDoublesStrict(x, y, z)); } { float x = 0x1p-100f, y = 0x1p-100f, z = 0x1p100f; System.out.printf("multiplyThreeFloatsDefault(%a, %a, %a) = %a\n", x, y, z, multiplyThreeFloatsDefault(x, y, z)); System.out.printf("multiplyThreeFloatsStrict(%a, %a, %a) = %a\n", x, y, z, multiplyThreeFloatsStrict(x, y, z)); for (int i = 0; i < 1000000; ++i) { multiplyThreeFloatsDefault(x, z, y); multiplyThreeFloatsStrict(x, z, y); } System.out.printf("multiplyThreeFloatsDefault(%a, %a, %a) = %a\n", x, y, z, multiplyThreeFloatsDefault(x, y, z)); System.out.printf("multiplyThreeFloatsStrict(%a, %a, %a) = %a\n", x, y, z, multiplyThreeFloatsStrict(x, y, z)); } } }まず、モダンな環境での実行結果は次の通りです。筆者はx86_64で実行しましたが、SSE2を持つx86系プロセッサーや、AArch64などのCPUであれば同じ結果となるはずです。
$ java StrictfpTest multiplyDefault(0x1.00002fffp0, 0x1.000000008p0) = 0x1.00002fff80001p0 multiplyStrict(0x1.00002fffp0, 0x1.000000008p0) = 0x1.00002fff80001p0 multiplyDefault(0x1.fffe0effffffep0, 0x1.0000000000001p0) = 0x1.fffe0fp0 multiplyStrict(0x1.fffe0effffffep0, 0x1.0000000000001p0) = 0x1.fffe0fp0 multiplyDefault(0x1.fffe0effffffep-51, 0x1.0000000000001p-1000) = 0x0.0000000ffff07p-1022 multiplyStrict(0x1.fffe0effffffep-51, 0x1.0000000000001p-1000) = 0x0.0000000ffff07p-1022 multiplyThreeDoublesDefault(0x1.0p-1000, 0x1.0p-1000, 0x1.0p1000) = 0x0.0p0 multiplyThreeDoublesStrict(0x1.0p-1000, 0x1.0p-1000, 0x1.0p1000) = 0x0.0p0 multiplyThreeDoublesDefault(0x1.0p-1000, 0x1.0p-1000, 0x1.0p1000) = 0x0.0p0 multiplyThreeDoublesStrict(0x1.0p-1000, 0x1.0p-1000, 0x1.0p1000) = 0x0.0p0 multiplyThreeFloatsDefault(0x1.0p-100, 0x1.0p-100, 0x1.0p100) = 0x0.0p0 multiplyThreeFloatsStrict(0x1.0p-100, 0x1.0p-100, 0x1.0p100) = 0x0.0p0 multiplyThreeFloatsDefault(0x1.0p-100, 0x1.0p-100, 0x1.0p100) = 0x0.0p0 multiplyThreeFloatsStrict(0x1.0p-100, 0x1.0p-100, 0x1.0p100) = 0x0.0p0モダンな環境では
strictfp
ありなしで結果は変わらないことが見て取れます。JITコンパイルの前後で値が変わるようなこともありません。次に、QEMUを使ってPentium IIで実行させます。
~/jre1.8.0_261/bin/java
に32ビット版のJavaコマンドを置きました。$ qemu-i386 -cpu pentium2 ~/jre1.8.0_261/bin/java StrictfpTest multiplyDefault(0x1.00002fffp0, 0x1.000000008p0) = 0x1.00002fff80001p0 multiplyStrict(0x1.00002fffp0, 0x1.000000008p0) = 0x1.00002fff80001p0 multiplyDefault(0x1.fffe0effffffep0, 0x1.0000000000001p0) = 0x1.fffe0fp0 multiplyStrict(0x1.fffe0effffffep0, 0x1.0000000000001p0) = 0x1.fffe0fp0 multiplyDefault(0x1.fffe0effffffep-51, 0x1.0000000000001p-1000) = 0x0.0000000ffff08p-1022 multiplyStrict(0x1.fffe0effffffep-51, 0x1.0000000000001p-1000) = 0x0.0000000ffff07p-1022 multiplyThreeDoublesDefault(0x1.0p-1000, 0x1.0p-1000, 0x1.0p1000) = 0x0.0p0 multiplyThreeDoublesStrict(0x1.0p-1000, 0x1.0p-1000, 0x1.0p1000) = 0x0.0p0 multiplyThreeDoublesDefault(0x1.0p-1000, 0x1.0p-1000, 0x1.0p1000) = 0x1.0p-1000 multiplyThreeDoublesStrict(0x1.0p-1000, 0x1.0p-1000, 0x1.0p1000) = 0x0.0p0 multiplyThreeFloatsDefault(0x1.0p-100, 0x1.0p-100, 0x1.0p100) = 0x0.0p0 multiplyThreeFloatsStrict(0x1.0p-100, 0x1.0p-100, 0x1.0p100) = 0x0.0p0 multiplyThreeFloatsDefault(0x1.0p-100, 0x1.0p-100, 0x1.0p100) = 0x0.0p0 multiplyThreeFloatsStrict(0x1.0p-100, 0x1.0p-100, 0x1.0p100) = 0x0.0p0まず、最初の例
0x1.00002fffp0 * 0x1.000000008p0
ではオーバーフローもアンダーフローも起こらないため、strictfp
の有無で結果は変わりません。次の例0x1.fffe0effffffep0 * 0x1.0000000000001p0
でも同様です。一方、第3の例
0x1.fffe0effffffep-51 * 0x1.0000000000001p-1000
ではアンダーフローが起こり、結果が非正規化数となります。そして、strictfp
の有無で最後の桁が1ずれています。IEEE 754準拠なのはもちろんstrictfp
をつけた方で、この場合は真の値に近いのもstrictfp
をつけた方です。第4の例では
double
で0x1p-1000 * 0x1p-1000 * 0x1p1000
($2^{-1000} \times 2^{-1000} \times 2^{1000}$)を計算しています。途中結果の0x1p-2000
($2^{-2000}$)はdouble
では指数部が小さ過ぎて表現できないので0
となり、最終結果も0
となるはずです。実際、strictfp
をつけた方や、JITコンパイルする前の方は0x0.0p0
を返しています。しかし、
strictfp
をつけなかった方は、JITコンパイル後の結果が0x1p-1000
となっています。これは、指数部が本来のdouble
よりも広い範囲で途中計算が行われたことを意味します。第5の例では
float
で0x1p-100 * 0x1p-100 * 0x1p100
を計算させて見ました。途中結果の0x1p-200
はfloat
では表現できないので、最終結果は0
となるはずです。実際そうなっています。こちらは、JITコンパイルさせても結果が変わるようなことはありませんでした。ちなみに、Intel SDEに
-quark
を指定して実行した場合は無印のPentium相当となるようで、Javaが対応しておらずExecuted instruction not valid for specified chip (PENTIUM): 0xf7f61dd0: nop ebx, edi
で落ちました。ここでは深い解説はしません
ここでは深い解説はしません。
そのうち「x87 FPUの呪い」だとか「Javaの
strictfp
は何のために導入され、いかにして不要となったか」みたいな記事を書くかもしれません。
- 投稿日:2020-10-21T14:40:27+09:00
小規模改修のついでに既存のコードの問題点を修正する
そもそもの経緯
担当となったシステムのコーディングがヤバかった。
しかし、大規模な改修案件は降りてこない。
ならば、小規模案件のついでに少しづつ直してしまおうという流れ。現状のコーディング
例えば、検索のビジネスクラスは以下の構成になってます。
1.バリデーション
2.検索ロジックの実行
3.画面表示項目の編集これだけ見ると、(多分)普通の構成だが、実際のコーディングは以下のような感じ。
// バリデーション // nullチェック if(joken1 == null || joken2 == null || joken3 == null || ... ) { error.add("エラー"); return false; } // 空文字チェック if(joken1 != null && joken2 != null && joken3 != null && ... ) { if(joken1 == "" || joken2 == "" || ... ) { error.add("エラー"); return false; } } // オブジェクトの生成 ObInfo obInfo = new ObInfo(); ObResult obResult = new ObResult(); int kensakuResult = 0; // 検索条件の判定 if (joken == CONST.JOKEN1 || joken == CONST.JOKEN2 || joken == CONST.JOKEN3 || ... ){ // 検索実行 kensakuResult = obInfo.getKensakuResult(joken1, joken2, ... , obResult); if(kensakuResult == 0) { error.add("エラー"); return false; } } // 画面表示項目の編集 ObKensakuInfoBean obKensakuInfoBean = new ObKensakuInfoBean(); editKensakuInfo(obKensakuInfoBean, kensakuResult); // ワーニング if (kensakuResult == 2){ error.add("ワーニング"); } else if (kensakuResult == 3){ error.add("ワーニング"); } else if (kensakuResult == 4){ ... } return true;以上が検索実行時に最初に呼び出されるビジネスロジックの概要です。
実際にはより多くのバリデーションがあり、項目編集があり、編集コメントがあります。
この編集コメントが厄介で、ただでさえ見づらいコードをより見づらくしています。/* 20XX ○○案件 START */ // ObInfo obInfo = new ObInfo(); /* 20XX ○○案件 END */ /* 20XX ××案件 START */ // ObInfo obInfo /* 20XX ××案件 END */ obInfo /* 20XX △△案件 START */ // = obGetInfo.getInfo(obInfo, obAuthInfo, obCode); /* 20XX △△案件 END */ /* 20XX □□案件 START */ = obGetInfo.getInfo(obINfo); /* 20XX □□案件 END */みたいなかんじです。
ここ一か所なら判別できますが、これがコード全体にあるので、全体のコードの把握が難しくなっています。
現在ではSVNでソース管理を行っているため、コメントは必要ないのですが、慣習としてコメントが残されている状態です。問題点
このコードでも動いてはいます。おそらく、最初は単純な条件で十分な要件だったんだと思います。しかし、徐々に複雑な条件が必要とされてしまった。
そして、このコードは(なぜか)条件が追加されることを考慮されていません。
インプットや、if条件、バリデーションが全てビジネスロジック内部で記載されています。
そのため、改修が入るたびにビジネスロジック内部の条件・バリデーションを修正する必要がありました。
当然、改修箇所の全画面分です。
しかも、ビジネスロジックによって別クラスに条件を記載していたり、ビジネスロジックに直接かかれていたり、メソッドで記載していたりと仕様がバラバラでした。
つまり、改修箇所ごとに修正が入るクラスがバラバラということです。対応
本来なら、バリデーションクラスを作成してそこでバリデーションを実装するのがベストかと思います。
しかし、小規模案件での作成かつ、修正ステップ数によって工数が決まるため、根本からの修正は難しいです。
なので、今回は主に検索条件の修正を行いました。対応方法
検索条件ですが、あらかじめ決まった定数を条件として設定しているため、それらを配列にして配列内に条件と一致するかを新たに条件として追加しました。
if(CONST.JOKENS.contain(joken)){ kensakuResult = ... }といった感じです。
本来は一律でまとめたかったのですが、そうすると膨大な退行テストが必要になってしまいます。(今までの条件で正常に動作することのテストが必要になる)
単純な量の問題なら、結合テストは難しくとも、単体テストだけならJUnitなどで出来るでしょう。(既存の条件に対応するJUnitのテストケースがあれば、ですが)残念ながら、JUnitもなければローカル環境すらないので、退行テストを実施するのはあきらめました。
なので、今回は新規で追加した条件のみリスト化して対応しています。if(joken == CONST.JOKEN || ... CONST.JOKENS.contain(joken)){ ... }という具合です。
根本原因
そもそもの原因は、立ち上げ時に詳細設計を怠ったことが始まりかと思います。
基本設計しか明確にドキュメント化されておらず、画面表示が仕様通りになるように各自が実装してしまった。
それによってクラスの動作が指定されず、クラスごとの仕様がバラバラになったのかと。
改修時のために、実装を元に詳細設計を作成するのも有りだと思いますが、そんな工数は降りないです。
それで、こういった細かい修正で対処するしかなかったわけです。総括
設計・コーディング規約はちゃんとしよう。
運用・保守フェーズに、クラス追加するような改修の工数は降りないから。
- 投稿日:2020-10-21T13:39:20+09:00
入力エラー時にテキストボックスの枠を赤くしたい
はじめに
入力エラー時にテキストボックスの枠を赤くしようとしたのですが、実装に苦労したので投稿します。
環境
OS: macOS Catalina 10.15.6
JDK:14.0.1
Spring Boot 2.3.3
jquery 3.3.1-1
bootstrap 4.2.1①入力チェックにあたって
入力チェック(バリデーション)を行うにあたって、作者は下記のようにクラスを作成しました。
■コントローラークラス
SignupController.javapackage com.example.demo.login.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; 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.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import com.example.demo.login.domain.model.GroupOrder; import com.example.demo.login.domain.model.SignupForm; import com.example.demo.login.domain.model.User; import com.example.demo.login.domain.service.UserService; //ユーザー新規登録用コントローラー @Controller public class SignupController { @Autowired private UserService userService; //ユーザー登録画面へ遷移 @PostMapping("/signup") public String postSignUp(@ModelAttribute SignupForm form, Model model) { return "signup/signup"; } //ユーザー新規登録実行用メソッド @PostMapping("/signupUser") //「GroupOrder」で設定した順序でバリデーションを実行 public String postSignUp(@ModelAttribute @Validated(GroupOrder.class) SignupForm form, BindingResult bindingResult, Model model) { //バインディングでエラー発生(バリデーションエラー含む)した場合、ユーザー登録画面へ遷移 if (bindingResult.hasErrors()) { return postSignUp(form, model); } //新規登録した内容をコンソールへ表示 System.out.println(form); //Userインスタンスの作成 User user = new User(); //登録フォームに入力した内容をUserクラスにセット user.setUserId(form.getUserId()); user.setMailAddress(form.getMailAddress()); user.setPassword(form.getPassword()); //フォーム入力内容をセットした「User」を引数にサービスクラスへ処理を投げる boolean result = userService.insertOne(user); //登録作業の実行結果をコンソールに表示 if (result == true) { System.out.println("insert成功"); } else { System.out.println("insert失敗"); } //ログイン画面へ遷移 return "redirect:/login"; } }■バリデーショングループ
GroupOrder.javapackage com.example.demo.login.domain.model; import javax.validation.GroupSequence; //ユーザー新規登録とユーザー登録情報更新の際に「ValidGroup1」「ValidGroup2」の順番にバリデーションを実行 @GroupSequence({ValidGroup1.class, ValidGroup2.class}) public interface GroupOrder { }ValidGroup1.javapackage com.example.demo.login.domain.model; //ValidGroup1のインターフェース public interface ValidGroup1 { }ValidGroup2.javapackage com.example.demo.login.domain.model; //ValidGroup2のインターフェース public interface ValidGroup2 { }■入力フォームのクラス
SignupForm.javapackage com.example.demo.login.domain.model; import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Pattern; import org.hibernate.validator.constraints.Length; import lombok.Data; //ユーザー新規登録フォームで使用するSignupForm @Data public class SignupForm { /* * null 半角スペース 空文字の場合エラー発生(ValidGroup1に分類) * 3字以上20字以内(ValidGroup2に分類) */ @NotBlank(groups = ValidGroup1.class) @Length(min = 3, max = 20, groups = ValidGroup2.class) private String userId; /* * null 半角スペース 空文字の場合エラー発生(ValidGroup1に分類) * メールアドレスの形式でないとエラー発生(ValidGroup2に分類) */ @NotBlank(groups = ValidGroup1.class) @Email(groups = ValidGroup2.class) private String mailAddress; /* * null 半角スペース 空文字の場合エラー発生(ValidGroup1に分類) * 3字以上20字以内(ValidGroup2に分類) * 英数字のみ(ValidGroup2に分類) */ @NotBlank(groups = ValidGroup1.class) @Length(min = 3, max = 20, groups = ValidGroup2.class) @Pattern(regexp = "^[a-zA-Z0-9]+$", groups = ValidGroup2.class) private String password; }■エラーメッセージ
messages.properties#バリデーションエラーメッセージ signupForm.userId=ユーザーID NotBlank.signupForm.userId={0}を入力してください Length.signupForm.userId={0}は、{2}字以上{1}字以下で入力してください signupForm.mailAddress=メールアドレス NotBlank.signupForm.mailAddress={0}を入力してください Email.signupForm.mailAddress=メールアドレス形式で入力してください signupForm.password=パスワード NotBlank.signupForm.password={0}を入力してください Length.signupForm.password={0}は、{2}字以上{1}字以下で入力してください Pattern.signupForm.password={0}は半角英数字で入力してください searchForm.keyword=キーワード NotBlank.searchForm.keyword={0}を入力してください #ログインエラーメッセージをカスタマイズ AbstractUserDetailsAuthenticationProvider.badCredentials=ログインIDまたはパスワードが間違っています。②入力フォームの作成(修正前)
当初、下記のように入力フォームのhtmlを作成しました。
signup.html<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/template}"> <head> <meta charset="UTF-8"></meta> <title>SignUp</title> </head> <!-- ユーザー登録画面 --> <body> <div layout:fragment="content"> <div class="col-sm-4 offset-sm-4"> <div class="page-header"> <h1>ユーザー登録画面</h1> </div> <form method="post" th:action="@{/signupUser}" th:object="${signupForm}"> <table class="table table-bordered table-hover"> <tr> <td> <!-- modelオブジェクト「signupForm」の「userId」フィールドでエラーが発生したら、class属性に「has-error」を追加し、テキストボックスの周りを赤くする --> <div class="form-group" th:classappend="${#fields.hasErrors('userId')}?'has-error'"> <input type="text" class="form-control" placeholder="ログインID" th:field="*{userId}"/> <span class="text-danger" th:if="${#fields.hasErrors('userId')}" th:errors="*{userId}"> userId error </span> </div> </td> <tr> <tr> <td> <!-- modelオブジェクト「signupForm」の「mailAddress」フィールドでエラーが発生したら、class属性に「has-error」を追加し、テキストボックスの周りを赤くする --> <div class="form-group" th:classappend="${#fields.hasErrors('mailAddress')}?'has-error'"> <input type="text" class="form-control" placeholder="メールアドレス" th:field="*{mailAddress}"/> <span class="text-danger" th:if="${#fields.hasErrors('mailAddress')}" th:errors="*{mailAddress}"> mailAddress error </span> </div> </td> <tr> <tr> <td> <!-- modelオブジェクト「signupForm」の「password」フィールドでエラーが発生したら、class属性に「has-error」を追加し、テキストボックスの周りを赤くする --> <div class="form-group" th:classappend="${#fields.hasErrors('password')}?'has-errors'"> <input type="text" class="form-control" placeholder="パスワード" th:field="*{password}"/> <span class="text-danger" th:if="${#fields.hasErrors('password')}" th:errors="*{password}"> password error </span> </div> </td> <tr> </table> <button class="btn btn-primary col-sm-6" type="submit">新規登録</button> </form> </div> </div> </body> </html>html内のコメントでも記述しているように、各フィールドでエラーが発生したら、テキストボックスの枠が赤くなるよう記述したのですが、結果は下の通り枠は赤くはなりませんでした。
ちなみに「has-error」はBootstrapのclassでテキストボックスの枠を赤くしてくれるものです。
「Bootstrap4移行ガイド」を確認すると下記のような記述がありました。
【Bootstrap3.xとの変更箇所】
各検証状態の表示スタイルからHTML5フォーム検証機能を使用したスタイルに変更
.has-warning, .has-error, .has-success, .has-feedback, .form-control-feedback は廃止
フォールバックとして、.is-invalid クラスと .is-valid クラスをサーバー側の検証用の疑似クラスの代わりに使用可能。.was-validated 親クラスは必要ない。URL:https://bootstrap-guide.com/components/forms?
Bootstrap4以降「has-error」は廃止、代わりに「is-invalid」が機能として提供されているようです。
③入力フォームの修正
divタグ内ではなく、inputタグの中に記述します。
signup.html<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/template}"> <head> <meta charset="UTF-8"></meta> <title>SignUp</title> </head> <!-- ユーザー登録画面 --> <body> <div layout:fragment="content"> <div class="col-sm-4 offset-sm-4"> <div class="page-header"> <h1>ユーザー登録画面</h1> </div> <form method="post" th:action="@{/signupUser}" th:object="${signupForm}"> <table class="table table-bordered table-hover"> <tr> <td> <div class="form-group"> <!-- modelオブジェクト「signupForm」の「userId」フィールドでエラーが発生したら、class属性を追加し「is-invalid」でテキストボックスの周りを赤くする--> <input type="text" class="form-control" placeholder="ログインID" th:field="*{userId}" th:classappend="${#fields.hasErrors('userId')}?'is-invalid'"/> <span class="text-danger" th:if="${#fields.hasErrors('userId')}" th:errors="*{userId}"> userId error </span> </div> </td> <tr> <tr> <td> <div class="form-group"> <!-- modelオブジェクト「signupForm」の「mailAddress」フィールドでエラーが発生したら、class属性を追加し「is-invalid」でテキストボックスの周りを赤くする--> <input type="text" class="form-control" placeholder="メールアドレス" th:field="*{mailAddress}" th:classappend="${#fields.hasErrors('mailAddress')}?'is-invalid'"/> <span class="text-danger" th:if="${#fields.hasErrors('mailAddress')}" th:errors="*{mailAddress}"> mailAddress error </span> </div> </td> <tr> <tr> <td> <div class="form-group"> <!-- modelオブジェクト「signupForm」の「password」フィールドでエラーが発生したら、class属性を追加し「is-invalid」でテキストボックスの周りを赤くする--> <input type="text" class="form-control" placeholder="パスワード" th:field="*{password}" th:classappend="${#fields.hasErrors('password')}?'is-invalid'"/> <span class="text-danger" th:if="${#fields.hasErrors('password')}" th:errors="*{password}"> password error </span> </div> </td> <tr> </table> <button class="btn btn-primary col-sm-6" type="submit">新規登録</button> </form> </div> </div> </body> </html>実行すると下のように、入力エラー時テキストボックスの枠を赤くすることができました。
参考:
https://www.e-pokke.com/blog/bootstrap4-invalid-feedback.html
https://learning-collection.com/thymeleaf%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9/
- 投稿日:2020-10-21T06:52:02+09:00
JUnit 5 + Apache Maven による Java の自動テスト導入
概要
- JUnit 5 + Apache Maven による Java の自動テストについて基本的なサンプルを書く
今回の環境
- Java 11 (AdoptOpenJDK 11.0.8+10)
- JUnit Jupiter 5.7.0
- Apache Maven 3.6.3
- Maven Surefire Plugin 3.0.0-M5
JUnit 5 + Apache Maven の基本的なサンプル
ソースコード一覧
├── pom.xml └── src ├── main │ └── java │ └── myapp │ └── Calc.java └── test └── java └── myapp └── CalcTest.javapom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>mygroup</groupId> <artifactId>myapp</artifactId> <packaging>jar</packaging> <version>1.0.0</version> <name>myapp</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> </properties> <build> <plugins> <!-- JUnit 5 でのテスト実行に必要なプラグインを導入 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.0.0-M5</version> </plugin> </plugins> </build> <dependencies> <!-- テストコードの記述に必要なライブラリを導入 --> <!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.7.0</version> <scope>test</scope> </dependency> </dependencies> </project>参考:
- JUnit 5 User Guide - 4.2.2. Maven
- Maven Surefire Plugin – Introduction
- Maven Repository: org.junit.jupiter » junit-jupiter-api
Calc.java
package myapp; public class Calc { private int base; // 基準となる値を設定 public Calc(int base) { this.base = base; } // 足す public int plus(int num) { return base + num; } // 引く public int minus(int num) { return base - num; } }CalcTest.java
package myapp; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; class CalcTest { // テスト開始前に1回だけ実行される @BeforeAll static void beforeAll() { System.out.println("CalcTest 開始"); } // テスト開始後に1回だけ実行される @AfterAll static void afterAll() { System.out.println("CalcTest 終了"); } // 各テストメソッド開始前に実行される @BeforeEach void beforeEach() { System.out.println("CalcTest のテストメソッドをひとつ開始"); } // 各テストメソッド開始後に実行される @AfterEach void afterEach() { System.out.println("CalcTest のテストメソッドをひとつ終了"); } // テストメソッドは private や static メソッドにしてはいけない // 値を返してもいけないので戻り値は void にする @Test void testPlus() { System.out.println("testPlus を実行: 2 + 3 = 5"); Calc calc = new Calc(2); // 第1引数: expected 想定される結果 // 第2引数: actual 実行結果 // 第3引数: message 失敗時に出力するメッセージ assertEquals(5, calc.plus(3), "2 + 3 = 5 の検証"); } @Test void testMinus() { System.out.println("testMinus を実行: 5 - 2 = 3"); Calc calc = new Calc(5); assertEquals(3, calc.minus(2), "5 - 2 = 3 の検証"); } }参考:
テスト成功時の例
$ mvn test [INFO] Scanning for projects... [INFO] [INFO] ---------------------------< mygroup:myapp >---------------------------- [INFO] Building myapp 1.0.0 [INFO] --------------------------------[ jar ]--------------------------------- (中略) [INFO] --- maven-surefire-plugin:3.0.0-M5:test (default-test) @ myapp --- [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running myapp.CalcTest CalcTest 開始 CalcTest のテストメソッドをひとつ開始 testMinus を実行: 5 - 2 = 3 CalcTest のテストメソッドをひとつ終了 CalcTest のテストメソッドをひとつ開始 testPlus を実行: 2 + 3 = 5 CalcTest のテストメソッドをひとつ終了 CalcTest 終了 [INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.087 s - in myapp.CalcTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------テスト失敗時の例
$ mvn test [INFO] Scanning for projects... [INFO] [INFO] ---------------------------< mygroup:myapp >---------------------------- [INFO] Building myapp 1.0.0 [INFO] --------------------------------[ jar ]--------------------------------- (中略) [INFO] --- maven-surefire-plugin:3.0.0-M5:test (default-test) @ myapp --- [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running myapp.CalcTest CalcTest 開始 CalcTest のテストメソッドをひとつ開始 testMinus を実行: 5 - 2 = 3 CalcTest のテストメソッドをひとつ終了 CalcTest のテストメソッドをひとつ開始 testPlus を実行: 2 + 3 = 5 CalcTest のテストメソッドをひとつ終了 CalcTest 終了 [ERROR] Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.063 s <<< FAILURE! - in myapp.CalcTest [ERROR] myapp.CalcTest.testMinus Time elapsed: 0.026 s <<< FAILURE! org.opentest4j.AssertionFailedError: 5 - 2 = 3 の検証 ==> expected: <3> but was: <7> at myapp.CalcTest.testMinus(CalcTest.java:53) [INFO] [INFO] Results: [INFO] [ERROR] Failures: [ERROR] CalcTest.testMinus:53 5 - 2 = 3 の検証 ==> expected: <3> but was: <7> [INFO] [ERROR] Tests run: 2, Failures: 1, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------assertAll でまとめてテストするサンプル
サンプルコード
package myapp; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; class CalcTest { @Test void testPlus() { Calc calc = new Calc(10); // まとめて検証 // 途中で失敗しても停止せずにすべて検証する assertAll( () -> assertEquals(30, calc.plus(20)), () -> assertEquals(99, calc.plus(90)), () -> assertEquals(11, calc.plus(50)), () -> assertEquals(40, calc.plus(30)) ); } }参考:
テスト失敗時の例
$ mvn test [INFO] Scanning for projects... [INFO] [INFO] ---------------------------< mygroup:myapp >---------------------------- [INFO] Building myapp 1.0.0 [INFO] --------------------------------[ jar ]--------------------------------- (中略) [INFO] --- maven-surefire-plugin:3.0.0-M5:test (default-test) @ myapp --- [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running myapp.CalcTest [ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.067 s <<< FAILURE! - in myapp.CalcTest [ERROR] myapp.CalcTest.testPlus Time elapsed: 0.044 s <<< FAILURE! org.opentest4j.MultipleFailuresError: Multiple Failures (2 failures) org.opentest4j.AssertionFailedError: expected: <99> but was: <100> org.opentest4j.AssertionFailedError: expected: <11> but was: <60> at myapp.CalcTest.testPlus(CalcTest.java:15) [INFO] [INFO] Results: [INFO] [ERROR] Failures: [ERROR] CalcTest.testPlus:15 Multiple Failures (2 failures) org.opentest4j.AssertionFailedError: expected: <99> but was: <100> org.opentest4j.AssertionFailedError: expected: <11> but was: <60> [INFO] [ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------例外発生テストのサンプル
package myapp; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; class CalcTest { @Test void testPlus() { Calc calc = new Calc(100); // 0 で割ったら ArithmeticException が発生することを想定 ArithmeticException e = assertThrows(ArithmeticException.class, () -> calc.divide(0)); assertTrue(e instanceof ArithmeticException); } }参考資料