@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
型のcharSequence
がnull
の時はfalse
をリターンします。charSequence
がnull
でない時は、前後のスペースを削除した後の文字列の長さが 0 より大きいかどうかをリターンします。CharSequence
は文字列を操作するためのいくつかのメソッドを提供するインタフェースであり、String
やStringBuilder
などが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
)がありました。
仕様と実装が異なるように見え、混乱してきたので、String
のtrim()
メソッドの中身を見ていきます。
String
のtrim()
メソッドについて
trim()
は文字列の先頭と末尾の文字が空白スペース(U+0020
)以下であれば、取り除いて新しい文字列を返すようになっていました。つまり、全角スペース(U+3000
)は空白スペース(U+0020
)以下でないので、trim()
では先頭と末尾に全角スペースがあっても取り除かれないことが分かりました。
trim()メソッドの詳細
StringLatin1.trim()
とStringUTF16.trim()
を呼んでいます。
public String trim() {
String ret = isLatin1() ? StringLatin1.trim(value)
: StringUTF16.trim(value);
return ret == null ? this : ret;
}
それぞれの文字のアスキーコードが空白のアスキーコード(32)以下であれば、文字列の先頭と末尾の空白を削除して返す実装になっています。
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
)以下であれば、文字列の先頭と末尾の空白を削除して返す実装になっています。
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()
となっている理由や歴史的な背景があれば、教えていただきたいです。
参考情報