LoginSignup
1
0

More than 1 year has passed since last update.

@NotBlank のバリデーションがどう実装されているのか調査してみた

Posted at

@NotBlank のバリデーションの実装や仕様を調査してまとめようと考えていたのですが、一つ疑問が残ってしまいました。もしご存知の方がいれば教えていただきたいです。

はじめに

Springでバリデーションをする時にBeanValidationを用いることがあると思います。javax.validation.constraints.NotBlank@NotBlankを使ったときに、文字列が全角スペースのみだった場合にチェックエラーとならず、自分の予想と異なりました。@NotBlankがどのような実装や仕様であるか調べたので、備忘録としてまとめます。

TL;DR

  • Bean Validationは Java オブジェクトの検証を行うための仕様であり、実装はHibernate Validatorでされています。そのため、javaxのパッケージ配下でバリデーションの実装を探しても見つかりません。
  • @NotBlankの実装は文字列の先頭と末尾のスペースをtrim()で除去したのち、文字列の長さが 0 より大きいときは OK、そうでないときはチェックエラーとなる実装となっていました。
  • 【疑問点】@NotBlankの仕様は「null でないかつ、ホワイトスペースではない文字が少なくとも 1 つ以上含む」でした。また、ホワイトスペースの定義には全角スペースも含まれているようでした。しかし、実装は全角スペースも除去するstrip()ではなくtrim()を使っており、trim()を使っている理由が不明のまま疑問点として残りました。

@NotBlankのバリデーションの実装について

NotBlankValidatorに実装があり、下記のようになっています。

	public boolean isValid(CharSequence charSequence, ConstraintValidatorContext constraintValidatorContext) {
		if ( charSequence == null ) {
			return false;
		}

		return charSequence.toString().trim().length() > 0;
	}

引数のCharSequence型のcharSequencenullの時はfalseをリターンします。charSequencenullでない時は、前後のスペースを削除した後の文字列の長さが 0 より大きいかどうかをリターンします。CharSequenceは文字列を操作するためのいくつかのメソッドを提供するインタフェースであり、StringStringBuilderなどがCharSequenceを実装しています。

@NotBlankのバリデーションの仕様について

NotBlankValidatorの JavaDoc について

isValid()メソッドには下記のように記載されており、「nullでないこと」と「前後のホワイトスペースを除去した後に空でないこと」をチェックすると書かれています。

Checks that the character sequence is not null nor empty after removing any leading or trailing whitespace.

BeanValidationに記載されている仕様について

Jakarta Bean Validation specification@NotBlankのところをみると、下記のように記載されています。@NotBlankのところでコードジャンプしても同様の記載が見られます。

The annotated element must not be null and must contain at least one non-whitespace character. Accepts CharSequence.

ホワイトスペースの定義を確認すれば、全角スペースだけの時@NotBlankのバリデーション結果が OK で良いのか明らかにできそうです。

ホワイトスペースの定義

JavaのドキュメントのCharacterにホワイトスペースの定義がありました。全角スペース(U+3000)がこの定義に含まれているかどうか確認すればよさそうです。SPACE_SEPARATORの中に全角スペースがあるかどうか確認します。

Determines if the specified character is white space according to Java. A character is a Java whitespace character if and only if it satisfies one of the following criteria:
It is a Unicode space character (SPACE_SEPARATOR, LINE_SEPARATOR, or PARAGRAPH_SEPARATOR) but is not also a non-breaking space ('\u00A0', '\u2007', '\u202F').
It is '\t', U+0009 HORIZONTAL TABULATION.
It is '\n', U+000A LINE FEED.
It is '\u000B', U+000B VERTICAL TABULATION.
It is '\f', U+000C FORM FEED.
It is '\r', U+000D CARRIAGE RETURN.
It is '\u001C', U+001C FILE SEPARATOR.
It is '\u001D', U+001D GROUP SEPARATOR.
It is '\u001E', U+001E RECORD SEPARATOR.
It is '\u001F', U+001F UNIT SEPARATOR.

SPACE_SEPARATORの定義をみると、General category "Zs" in the Unicode specification.とありZsに属するユニコード文字の一覧を見れば良さそうです。Zsに所属するユニコード文字の一覧をみると、全角スペース(U+3000)がありました。

仕様と実装が異なるように見え、混乱してきたので、Stringtrim()メソッドの中身を見ていきます。

Stringtrim()メソッドについて

trim()は文字列の先頭と末尾の文字が空白スペース(U+0020)以下であれば、取り除いて新しい文字列を返すようになっていました。つまり、全角スペース(U+3000)は空白スペース(U+0020)以下でないので、trim()では先頭と末尾に全角スペースがあっても取り除かれないことが分かりました。

trim()メソッドの詳細

StringLatin1.trim()StringUTF16.trim()を呼んでいます。

String.java
    public String trim() {
        String ret = isLatin1() ? StringLatin1.trim(value)
                                : StringUTF16.trim(value);
        return ret == null ? this : ret;
    }

それぞれの文字のアスキーコードが空白のアスキーコード(32)以下であれば、文字列の先頭と末尾の空白を削除して返す実装になっています。

StringLatin1.java
    public static String trim(byte[] value) {
        int len = value.length;
        int st = 0;
        while ((st < len) && ((value[st] & 0xff) <= ' ')) {
            st++;
        }
        while ((st < len) && ((value[len - 1] & 0xff) <= ' ')) {
            len--;
        }
        return ((st > 0) || (len < value.length)) ?
            newString(value, st, len - st) : null;
    }

それぞれの文字をUnicode文字に変換して、その文字のユニコードが空白のユニコード(U+0020)以下であれば、文字列の先頭と末尾の空白を削除して返す実装になっています。

StringUTF16.java
    public static String trim(byte[] value) {
        int length = value.length >> 1;
        int len = length;
        int st = 0;
        while (st < len && getChar(value, st) <= ' ') {
            st++;
        }
        while (st < len && getChar(value, len - 1) <= ' ') {
            len--;
        }
        return ((st > 0) || (len < length )) ?
            new String(Arrays.copyOfRange(value, st << 1, len << 1), UTF16) :
            null;
    }

まとめ

  • @NotBlankのバリデーションの実装は文字列の先頭と末尾のスペースをtrim()で除去したのち、文字列の長さが 0 より大きいときはtrue、そうでないときはfalseとなることが分かりました。trim()を使っているため、文字列が全角スペースのみの時は、trueとなることが分かりました。
  • Bean Validationの仕様に従うなら、trim()ではなくstrip()を使うべきなのかなと思いました。実装がtrim()となっている理由や歴史的な背景があれば、教えていただきたいです。

参考情報

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0