Hibernate ValidatorでBean Validationしてみる

f:id:ts0818:20210403160837j:plain

www.atmarkit.co.jp

 Cockroach Labsは主要クラウドサービスの処理性能を比較した年次レポートの最新版「2021 Cloud Report」を公開した。

「AWS」「Azure」「GCP」の処理性能を比較、Cockroach Labsが2021年版のレポートを公開:三大クラウドの処理性能はどう違う? - @IT

⇧ いやはや「仁義なき戦い(監督:深作 欣二)」といった様相を呈しているんですかね、どうもボクです。

というわけで、 今回はJavaについてです。

レッツトライ~。

 

Bean Validationって何?

Wikipediaさん、よろしくお願いいたします。

Bean Validationビーン・バリデーション)は、JavaBeansのバリデーション(値の検証)のためのメタデータモデルとAPIを定めたJavaソフトウェアフレームワークである。

Bean Validation - Wikipedia

Bean Validationは、2009年11月16日にJavaの仕様を定めるJCPによりJSR 303として採用され、Java EE 6においてバージョン1.0が仕様の一つとして取り込まれている。2013年5月に公開された1.1は、Java EE 7に含まれる。

Bean Validation - Wikipedia

⇧ 実装なのか、仕様なのか、どっちなんだい~!? 

O/Rマッピングフレームワークを開発するHibernateプロジェクトは、Bean Validationの仕様を実装したHibernate Validatorを提供しており、リファレンス実装にもなっている。

Bean Validation - Wikipedia

⇧ 仕様なのかね?

で、「Hibernate Validator」ってのが「リファレンス実装」 になっているということらしいんですが、

リファレンス実装(リファレンスじっそう、reference implementation)は、なんらかの機能を実現するハードウェアまたはソフトウェアであり、他者がそれを参考にして独自に実装することを助ける目的で作られたものを言う。参考実装(さんこうじっそう)とも呼ばれる。

リファレンス実装 - Wikipedia

⇧ ということらしいですと。

  

Bean Validationのライブラリって何があるの?

う~ん、 ネットの情報を調べた感じでは、

penguinlabo.hatenablog.com

blog.katty.in

⇧ 上記サイト様が参考になりそうでしょうか。

で、

って4つが有名どころ?みたいなんですが、 2021年4月2日(金)現在だと、「Hibernate Validator」以外の3つは、いずれも「Hibernate Validator」に依存してるっぽいらしい。 

ちなみに、「Hibernate Validator」のコンセプトとしては、

docs.jboss.org

⇧ って感じでレイヤー毎にバリデーションするのは手間がかかるので、

⇧ ってな感じで、バリデーションを集約しましょうよ、ってことみたいね。

 

Hibernate Validator」のパッケージ変更問題

どういうことかと言うと、

Java Platform, Enterprise Edition (Java EE) は、Javaで実装されたアプリケーションサーバの標準規格及びそのAPIを定めたもの。Java Platform, Standard Edition (Java SE) の拡張機能の形で提供される。

Java Platform, Enterprise Edition - Wikipedia

Java EEの権利はサン・マイクロシステムズを買収したオラクル保有してきたが、同社は2017年にJava EEEclipse Foundationに寄贈してオープンソース化をすることを発表。Java EEの商標については引き続きオラクルが保有するため、Java EE 9以後はJakarta EEの名で開発が進められる事が発表された。

Java Platform, Enterprise Edition - Wikipedia

⇧ とあるように、「Java EE」から「Jakarta EE」に変わることで、パッケージ名が変わっちゃうんですって。

どこまで変わっちゃうのかは分からんけども、

って感じの変更があるみたいなんですと。

というか、「Hibernate Validator」については、既に変わってる模様。

hibernate.org

⇧ 見た感じ「7.0.0.Final」から、パッケージ名が「jakarta」に変わったらしい。 

ちなみに、「Java EE(これからは、Jakarta EEってことになってくのかな)」と「Java SE(Java Platform, Standard Edition)」の関係は、

⇧ 上図のような感じになるらしい。

「Bean Validation 1.0」からは「Java EE 6」の仕様の1つとして、「Java EE 6」に同梱されてるってことらしいんだけど、「Java SE」には含まれてませんと。

つまり、「Java EE」とか「Java EE」を基にしてるフレームワーク以外で「Bean Validation」を使いたいってなった場合は、「Bean Validation」をインストールする必要があるみたいね。

 

実際に使ってみる

とりあえずは、パッケージが「jakarta」の場合で試してみますか。(というのも何かまだ「jakarta」パッケージでやってみました、に関する情報がネットを探してみても出回ってなかったので。まぁ、実際、開発現場なんかでは、新規案件でもない限り暫くは「javax」パッケージのままって感じになるんでしょうね...)

検証する方法としては、JUnitを使ってテストします。

今回は、Eclipseで「Gradleプロジェクト」を作成していきます。

Eclipseを起動して、「ファイル(F)」>「新規(N)」>「Gradle プロジェクト」を選択。

f:id:ts0818:20210403090545p:plain

適当に「プロジェクト名」を入力して「次へ(N)>」を選択しました。

f:id:ts0818:20210403090803p:plain

デフォルトの状態で「次へ(N)>」を選択しました。

f:id:ts0818:20210403090844p:plain

「完了(F)」を押下すると「Gradle プロジェクト」が作成されます。

f:id:ts0818:20210403090919p:plain

「Gradle プロジェクト」が作成されたら、「build.gradle」に「Bean Validation」に必要なライブラリの依存関係を追加していきます。

f:id:ts0818:20210403091811p:plain

追加しなければいけないライブラリについては、

github.com

⇧ 上記サイト様を参考。 

「build.gradle」を以下のように編集。(ライブラリについては「Maven Repository」ってサイトで検索する各ビルドツール毎のコードスニペットが掲載されてるので、コピペしてます。)

なんか、Gradleって、バージョンによってなのか、人によるのか分からんけど、「build.gradle」の書き方がどれをお手本にして良いか分からんくなってくるのが辛い...

/*
 * This file was generated by the Gradle 'init' task.
 *
 * This generated file contains a sample Java Library project to get you started.
 * For more details take a look at the Java Libraries chapter in the Gradle
 * User Manual available at https://docs.gradle.org/6.3/userguide/java_library_plugin.html
 */

plugins {
    // Apply the java-library plugin to add support for Java Library
    id 'java-library'
    id 'application'
}

repositories {
    // Use jcenter for resolving dependencies.
    // You can declare any Maven/Ivy/file repository here.
    jcenter()
    //mavenCentral()
}

dependencies {
    // This dependency is exported to consumers, that is to say found on their compile classpath.
    api 'org.apache.commons:commons-math3:3.6.1'

    // This dependency is used internally, and not exposed to consumers on their own compile classpath.
    implementation 'com.google.guava:guava:28.2-jre'

    // https://mvnrepository.com/artifact/commons-beanutils/commons-beanutils
    implementation group: 'commons-beanutils', name: 'commons-beanutils', version: '1.9.4'

    // https://mvnrepository.com/artifact/jakarta.validation/jakarta.validation-api
    // implementation group: 'jakarta.validation', name: 'jakarta.validation-api', version: '3.0.0'

    // https://mvnrepository.com/artifact/org.hibernate/hibernate-validator
    runtimeOnly 'org.hibernate.validator:hibernate-validator:7.0.1.Final'

    // https://mvnrepository.com/artifact/org.glassfish/jakarta.el
    runtimeOnly group: 'org.glassfish', name: 'jakarta.el', version: '4.0.1'

    // https://mvnrepository.com/artifact/org.hibernate/hibernate-validator-cdi
    //runtimeOnly group: 'org.hibernate', name: 'hibernate-validator-cdi', version: '7.0.1.Final'

    // https://mvnrepository.com/artifact/org.hibernate/hibernate-validator-annotation-processor
    //runtimeOnly group: 'org.hibernate', name: 'hibernate-validator-annotation-processor', version: '7.0.1.Final'

    // Use JUnit test framework
    testImplementation 'junit:junit:4.12'

}

application {
    mainClassName = 'Java_one_hundred_nock_comp.Library';
}

⇧ ってな感じで、保存すると、「プロジェクトと外部の依存関係」にライブラリが表示されます。(自分の場合ですと、「C:\Users\[ユーザー名]\.gradle\caches\modules-2\files-2.1」配下に各種ライブラリのjarとかがインストールされてました。)

f:id:ts0818:20210403150552p:plain 

続きまして、「Bean Validation」を実施するクラスを作る前に、今回は、独自のバリデーションも追加したいので、独自バリデーションのために、

  1. 独自バリデーションのための注釈(アノテーションのことかと)
  2. 1をimplemantsしたクラス

を作成します。

「新規(W)」>「注釈」を選択で。

f:id:ts0818:20210403092725p:plain

適当に「名前(M):」を入力し「完了(F)」を押下。(パッケージ名で警告出てますが、今回はそのままにしました。)

f:id:ts0818:20210403093303p:plain

続いて、「新規(W)」>「クラス」を選択。

f:id:ts0818:20210403095716p:plain

適当に「名前(M):」を入力後、「インタフェース(I):」の「追加(A)...」を押下。

f:id:ts0818:20210403100004p:plain

「ConstraintValidator」ってインターフェースを選択で。

f:id:ts0818:20210403100231p:plain

「完了(F)」を押下。

f:id:ts0818:20210403100310p:plain

とりあえず、エラーは出るけど、この後、コーディングしてくのでエラーは消えます。

ちなみに、エラーが出てるのは、implementsした「インタフェース(ここでは、「jakarta.validation.ConstrainValidator<A, T>」のこと)」の引数に何も指定してないからですね。

f:id:ts0818:20210403100457p:plain

というわけで、ファイルを編集。 

■FieldMatch.java

package Java_one_hundred_nock_comp_01;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;

@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = FieldMatchValidator.class)
@Documented
public @interface FieldMatch
{
    String message() default "{constraints.fieldmatch}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    /**
     * @return The first field
     */
    String first();

    /**
     * @return The second field
     */
    String second();

    /**
     * Defines several @FieldMatch annotations on the same element * * @see FieldMatch */ @Target({TYPE, ANNOTATION_TYPE}) @Retention(RUNTIME) @Documented @interface List { FieldMatch[] value(); } }

■FieldMatchValidator.java

package Java_one_hundred_nock_comp_01;

import org.apache.commons.beanutils.BeanUtils;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object>
{
    private String firstFieldName;
    private String secondFieldName;

    @Override
    public void initialize(final FieldMatch constraintAnnotation)
    {
        firstFieldName = constraintAnnotation.first();
        secondFieldName = constraintAnnotation.second();
    }

    @Override
    public boolean isValid(final Object value, final ConstraintValidatorContext context)
    {
        try
        {
            final Object firstObj = BeanUtils.getProperty(value, firstFieldName);
            final Object secondObj = BeanUtils.getProperty(value, secondFieldName);

            return firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
        }
        catch (final Exception ignore)
        {
            // ignore
        }
        return true;
    }
}

そしたらば、「Bean Validation」の実施対象のクラスを作成なんですが、今回は、「Gradle プロジェクト」作成でデフォルトで作成されたクラスを編集していく感じで行きたいと思います。(自分の場合ですと、「Library.java」ってクラス)

f:id:ts0818:20210403101906p:plain

そんでは、編集。 

/*
 * This Java source file was generated by the Gradle 'init' task.
 */
package Java_one_hundred_nock_comp_01;

import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;

@FieldMatch(first = "password", second = "confirmPass")
public class Library {

	@NotEmpty
	@Pattern(regexp = "^[0-9a-zA-Z]+$")
	@Size(max = 15)
	private String loginName;

	@Size(max = 15)
	private String nickName;

	@NotEmpty
	@Pattern(regexp = "^[a-zA-Z0-9_.+-]+@([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\\.)+[a-zA-Z]{2,}$")
	private String mail;

	@NotEmpty
	@Pattern(regexp = "^[0-9a-zA-Z]+$")
	@Size(min = 8)
	private String password;

	@NotEmpty
	private String confirmPass;

	private String birth;
	private String gender;

    public boolean someLibraryMethod() {
        return true;
    }

	public String getLoginName() {
		return loginName;
	}

	public void setLoginName(String loginName) {
		this.loginName = loginName;
	}

	public String getNickName() {
		return nickName;
	}

	public void setNickName(String nickName) {
		this.nickName = nickName;
	}

	public String getMail() {
		return mail;
	}

	public void setMail(String mail) {
		this.mail = mail;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public String getConfirmPass() {
		return confirmPass;
	}

	public void setConfirmPass(String confirmPass) {
		this.confirmPass = confirmPass;
	}

	public String getBirth() {
		return birth;
	}

	public void setBirth(String birth) {
		this.birth = birth;
	}

	public String getGender() {
		return gender;
	}

	public void setGender(String gender) {
		this.gender = gender;
	}
}   

⇧「Lombok」とかのライブラリを使えば、GetterとかSetterのコーディングしなくてスッキリ書けたかもですが、今回はべた書きで。

そしたら、今回は、「JUnit」で「Bean Validation」の動作を検証してみます。

「Gradle プロジェクト」の場合、テストクラスについても、デフォルトで用意されてるので、そちらを編集していく感じで。(ここでは「LibraryTest.java」ってのがデフォルトのテストクラス)

ちなみに、「Maven プロジェクト」とか「Spring Boot」とかでもテストクラスが用意されてるんじゃなかったっけな...たぶん。

f:id:ts0818:20210403102608p:plain

というわけで、テストクラスを編集。

qiita.com

⇧ 上記サイト様を参考にさせていただきました。

/*
 * This Java source file was generated by the Gradle 'init' task.
 */
package Java_one_hundred_nock_comp_01;

import static org.junit.Assert.*;

import java.util.Set;

import org.junit.Before;
import org.junit.Test;

import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;

public class LibraryTest {

	private static Validator validator;

	@Before
	public void init() {
		ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
		validator = factory.getValidator();
	}

    @Test
    public void testSomeLibraryMethod() {
        Library libraryUnderTest = new Library();
        libraryUnderTest.setLoginName("");
        libraryUnderTest.setNickName("HUBERT BLAINE WOLFE­SCHLEGEL­STEIN­HAUSEN­BERGER­DORFF SR");
        libraryUnderTest.setMail("");
        libraryUnderTest.setPassword(" ");
        libraryUnderTest.setConfirmPass(" ");

        Set<ConstraintViolation<Library>> constraintViolations =validator.validate(libraryUnderTest);

        assertFalse(constraintViolations.isEmpty());
        constraintViolations.forEach(constraintViolation->{
            String msg =
                    "message = " + constraintViolation.getMessage() + "\n" +
                    "messageTemplate = " + constraintViolation.getMessageTemplate() + "\n" +
                    "rootBean = " + constraintViolation.getRootBean() + "\n" +
                    "rootBeanClass = " + constraintViolation.getRootBeanClass() + "\n" +
                    "invalidValue = " + constraintViolation.getInvalidValue() + "\n" +
                    "propertyPath = " + constraintViolation.getPropertyPath() + "\n" +
                    "leafBean = " + constraintViolation.getLeafBean() + "\n" +
                    "descriptor = " + constraintViolation.getConstraintDescriptor() + "\n" ;

                System.out.println(msg);
        });
    }

//     private static int checkFieldHasAnnotation(Object object) {
//    	Class<?> clazz = object.getClass();
//    	List<Field> allFields = new ArrayList<>(Arrays.asList(clazz.getDeclaredFields()));
//
//    	int fieldCountHasAnnotation = 0;
//    	for (Field field: allFields) {
//    		if (field.getDeclaredAnnotations().length > 0) {
//    			Annotation[] fieldAnnotations = field.getAnnotations();
//    			if (fieldAnnotations.length > 0) {
//    				fieldCountHasAnnotation++;
//    			}
//    		}
//    	}
//    	return fieldCountHasAnnotation;
//    }
}

⇧ ってな感じで保存したら、「JUnit」でテストしてみますか。

今回は、どこまで実行されたかが分かりやすいように「カバレッジ(V)」>「Junit テスト」を選択で。

f:id:ts0818:20210403103330p:plain

実行が上手くいくと「カバレッジ」タブにどれだけ網羅されてるかが表示されます。

f:id:ts0818:20210403144928p:plain

「コンソール」タブの方には、「System.out.println」の結果が表示されましたね。

f:id:ts0818:20210403161703p:plain

まぁ、何て言うか、「Hibernate Validator」のみの影響なのかは分からんのだけど、「JUnit」のテストの上手い方法が分からん...

結局、結果をコンソールに出力して確認する感じになっちゃってるし...

何か、クラスのフィールド毎に「JUnit 」で検証したかったんだけどね...

あれかな、「Spring Framework」とかのバリデーションとかだったら、もっと良しなにテストする方法があるんかな?

というわけで、毎回モヤモヤが募るばかりですが...

今回はこのへんで。