20201021のJavaに関する記事は6件です。

DateAndTime APIを用いた日時取得方法

概要

Date and Time APIで以下のパターンをJavaとKotlin(ほぼ同じ)で実装する。

  • 日時の変更
  • 独自フォーマット
  • ISOフォーマット
  • 月初日時
  • 月末日時

実装例

JavaのUtilsクラスだとこちら。

LocalDateUtils.java
import 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.kt
import 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)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

参照


【Java】配列を使って並び替えをする(バブルソート編)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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の例では double0x1p-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の例では float0x1p-100 * 0x1p-100 * 0x1p100 を計算させて見ました。途中結果の 0x1p-200float では表現できないので、最終結果は 0 となるはずです。実際そうなっています。こちらは、JITコンパイルさせても結果が変わるようなことはありませんでした。

ちなみに、Intel SDEに -quark を指定して実行した場合は無印のPentium相当となるようで、Javaが対応しておらず Executed instruction not valid for specified chip (PENTIUM): 0xf7f61dd0: nop ebx, edi で落ちました。

ここでは深い解説はしません

ここでは深い解説はしません。

そのうち「x87 FPUの呪い」だとか「Javaの strictfp は何のために導入され、いかにして不要となったか」みたいな記事を書くかもしれません。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

小規模改修のついでに既存のコードの問題点を修正する

そもそもの経緯

担当となったシステムのコーディングがヤバかった。
しかし、大規模な改修案件は降りてこない。
ならば、小規模案件のついでに少しづつ直してしまおうという流れ。

現状のコーディング

例えば、検索のビジネスクラスは以下の構成になってます。
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)){
  ...
}

という具合です。

根本原因

そもそもの原因は、立ち上げ時に詳細設計を怠ったことが始まりかと思います。
基本設計しか明確にドキュメント化されておらず、画面表示が仕様通りになるように各自が実装してしまった。
それによってクラスの動作が指定されず、クラスごとの仕様がバラバラになったのかと。
改修時のために、実装を元に詳細設計を作成するのも有りだと思いますが、そんな工数は降りないです。
それで、こういった細かい修正で対処するしかなかったわけです。

総括

設計・コーディング規約はちゃんとしよう。
運用・保守フェーズに、クラス追加するような改修の工数は降りないから。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

入力エラー時にテキストボックスの枠を赤くしたい

はじめに

入力エラー時にテキストボックスの枠を赤くしようとしたのですが、実装に苦労したので投稿します。

環境

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.java
package 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.java
package com.example.demo.login.domain.model;

import javax.validation.GroupSequence;

//ユーザー新規登録とユーザー登録情報更新の際に「ValidGroup1」「ValidGroup2」の順番にバリデーションを実行
@GroupSequence({ValidGroup1.class, ValidGroup2.class})
public interface GroupOrder {


}
ValidGroup1.java
package com.example.demo.login.domain.model;

//ValidGroup1のインターフェース
public interface ValidGroup1 {

}
ValidGroup2.java
package com.example.demo.login.domain.model;

//ValidGroup2のインターフェース
public interface ValidGroup2 {

}

■入力フォームのクラス

SignupForm.java
package 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でテキストボックスの枠を赤くしてくれるものです。

スクリーンショット 2020-10-21 13.00.49.png

「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>

実行すると下のように、入力エラー時テキストボックスの枠を赤くすることができました。

スクリーンショット 2020-10-21 13.28.10.png

参考:
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/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.java

pom.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>

参考:

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);
  }
}

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む