Micronaut Bean 驗證

2023-02-28 16:42 更新

從 Micronaut 1.2 開始,Micronaut 內置了對驗證用 javax.validation 注釋注釋的 beans 的支持。至少將 micronaut-validation 模塊作為編譯依賴包含在內:

 Gradle Maven 
implementation("io.micronaut:micronaut-validation")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-validation</artifactId>
</dependency>

請注意,Micronaut 的實現(xiàn)目前并不完全符合 Bean Validator 規(guī)范,因為該規(guī)范嚴重依賴于基于反射的 API。

目前不支持以下功能:

  • 通用參數(shù)類型的注釋,因為只有 Java 語言支持此功能。

  • 與約束元數(shù)據(jù) API 的任何交互,因為 Micronaut 使用編譯時生成的元數(shù)據(jù)。

  • 基于 XML 的配置

  • 不使用 javax.validation.ConstraintValidator,而是使用 ConstraintValidator (io.micronaut.validation.validator.constraints.ConstraintValidator) 來定義自定義約束,它支持在編譯時驗證注解。

Micronaut 的實施包括以下好處:

  • 反射和運行時代理免費驗證,從而減少內存消耗

  • 更小的 JAR 大小,因為 Hibernate Validator 又增加了 1.4MB

  • 啟動速度更快,因為 Hibernate Validator 增加了 200 毫秒以上的啟動開銷

  • 通過注解元數(shù)據(jù)的可配置性

  • 支持反應式 Bean 驗證

  • 支持在編譯時驗證源 AST

  • 無需額外配置即可自動兼容 GraalVM native

如果您需要完全符合 Bean Validator 2.0,請將 micronaut-hibernate-validator 模塊添加到您的構建中,它會取代 Micronaut 的實現(xiàn)。

 Gradle Maven 
implementation("io.micronaut.beanvalidation:micronaut-hibernate-validator")
<dependency>
    <groupId>io.micronaut.beanvalidation</groupId>
    <artifactId>micronaut-hibernate-validator</artifactId>
</dependency>

驗證 Bean 方法

您可以通過對參數(shù)應用 javax.validation 注釋來驗證聲明為 Micronaut bean 的任何類的方法:

驗證方法

 Java Groovy  Kotlin 
import jakarta.inject.Singleton;
import javax.validation.constraints.NotBlank;

@Singleton
public class PersonService {
    public void sayHello(@NotBlank String name) {
        System.out.println("Hello " + name);
    }
}
import jakarta.inject.Singleton
import javax.validation.constraints.NotBlank

@Singleton
class PersonService {
    void sayHello(@NotBlank String name) {
        println "Hello $name"
    }
}
import jakarta.inject.Singleton
import javax.validation.constraints.NotBlank

@Singleton
open class PersonService {
    open fun sayHello(@NotBlank name: String) {
        println("Hello $name")
    }
}

上面的示例聲明 @NotBlank 注釋將在調用 sayHello 方法時進行驗證。

如果您使用 Kotlin,則必須將類和方法聲明為開放的,這樣 Micronaut 才能創(chuàng)建編譯時子類。或者,您可以使用 @Validated 注釋該類,并配置 Kotlin 全開放插件以打開使用該類型注釋的類。請參閱編譯器插件部分。

如果發(fā)生驗證錯誤,則拋出 javax.validation.ConstraintViolationException。例如:

ConstraintViolationException 示例

 Java Groovy  Kotlin 
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;

import jakarta.inject.Inject;
import javax.validation.ConstraintViolationException;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

@MicronautTest
class PersonServiceSpec {

    @Inject PersonService personService;

    @Test
    void testThatNameIsValidated() {
        final ConstraintViolationException exception =
                assertThrows(ConstraintViolationException.class, () ->
                personService.sayHello("") // (1)
        );

        assertEquals("sayHello.name: must not be blank", exception.getMessage()); // (2)
    }
}
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import spock.lang.Specification

import jakarta.inject.Inject
import javax.validation.ConstraintViolationException

@MicronautTest
class PersonServiceSpec extends Specification {

    @Inject PersonService personService

    void "test person name is validated"() {
        when:"The sayHello method is called with a blank string"
        personService.sayHello("") // (1)

        then:"A validation error occurs"
        def e = thrown(ConstraintViolationException)
        e.message == "sayHello.name: must not be blank" //  (2)
    }
}
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test
import jakarta.inject.Inject
import javax.validation.ConstraintViolationException

@MicronautTest
class PersonServiceSpec {

    @Inject
    lateinit var personService: PersonService

    @Test
    fun testThatNameIsValidated() {
        val exception = assertThrows(ConstraintViolationException::class.java) {
            personService.sayHello("") // (1)
        }

        assertEquals("sayHello.name: must not be blank", exception.message) // (2)
    }
}
  1. 使用空字符串調用該方法

  2. 發(fā)生異常

驗證數(shù)據(jù)類

驗證數(shù)據(jù)類,例如POJO(通常用于 JSON 交換),該類必須使用 @Introspected 注釋(請參閱前面關于 Bean Introspection 的部分),或者,如果該類是外部的,則由 @Introspected 注釋導入。

POJO 驗證示例

 Java  Groovy Kotlin 
import io.micronaut.core.annotation.Introspected;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;

@Introspected
public class Person {

    private String name;

    @Min(18)
    private int age;

    @NotBlank
    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
import io.micronaut.core.annotation.Introspected

import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank

@Introspected
class Person {

    @NotBlank
    String name

    @Min(18L)
    int age
}
import io.micronaut.core.annotation.Introspected
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank

@Introspected
data class Person(
    @field:NotBlank var name: String,
    @field:Min(18) var age: Int
)

@Introspected 注釋可以用作元注釋; @javax.persistence.Entity 等常見注釋被視為@Introspected

上面的示例定義了一個 Person 類,該類具有兩個應用了約束的屬性(姓名和年齡)。請注意,在 Java 中,注解可以在字段或 getter 上,而對于 Kotlin 數(shù)據(jù)類,注解應該以字段為目標。

要手動驗證類,請注入一個 Validator 實例:

手動驗證示例

 Java Groovy  Kotlin 
@Inject
Validator validator;

@Test
void testThatPersonIsValidWithValidator() {
    Person person = new Person();
    person.setName("");
    person.setAge(10);

    final Set<ConstraintViolation<Person>> constraintViolations = validator.validate(person);  // (1)

    assertEquals(2, constraintViolations.size()); // (2)
}
@Inject Validator validator

void "test person is validated with validator"() {
    when:"The person is validated"
    def constraintViolations = validator.validate(new Person(name: "", age: 10)) // (1)

    then:"A validation error occurs"
    constraintViolations.size() == 2 //  (2)
}
@Inject
lateinit var validator: Validator

@Test
fun testThatPersonIsValidWithValidator() {
    val person = Person("", 10)
    val constraintViolations = validator.validate(person) // (1)

    assertEquals(2, constraintViolations.size) // (2)
}
  1. 驗證者驗證人

  2. 驗證約束違規(guī)

或者,在 Bean 方法上,您可以使用 javax.validation.Valid 來觸發(fā)級聯(lián)驗證:

ConstraintViolationException 示例

 Java Groovy  Kotlin 
@Singleton
public class PersonService {
    public void sayHello(@Valid Person person) {
        System.out.println("Hello " + person.getName());
    }
}
@Singleton
class PersonService {
    void sayHello(@Valid Person person) {
        println "Hello $person.name"
    }
}
@Singleton
open class PersonService {
    open fun sayHello(@Valid person: Person) {
        println("Hello ${person.name}")
    }
}

PersonService 現(xiàn)在在調用時驗證 Person 類:

手動驗證示例

 Java Groovy  Kotlin 
@Inject
PersonService personService;

@Test
void testThatPersonIsValid() {
    Person person = new Person();
    person.setName("");
    person.setAge(10);

    final ConstraintViolationException exception =
        assertThrows(ConstraintViolationException.class, () ->
            personService.sayHello(person) // (1)
        );

    assertEquals(2, exception.getConstraintViolations().size()); // (2)
}
@Inject PersonService personService

void "test person name is validated"() {
    when:"The sayHello method is called with an invalid person"
    personService.sayHello(new Person(name: "", age: 10)) // (1)

    then:"A validation error occurs"
    def e = thrown(ConstraintViolationException)
    e.constraintViolations.size() == 2 //  (2)
}
@Inject
lateinit var personService: PersonService

@Test
fun testThatPersonIsValid() {
    val person = Person("", 10)
    val exception = assertThrows(ConstraintViolationException::class.java) {
        personService.sayHello(person) // (1)
    }

    assertEquals(2, exception.constraintViolations.size) // (2)
}
  1. 調用經(jīng)過驗證的方法

  2. 驗證約束違規(guī)

驗證配置屬性

您還可以驗證使用 @ConfigurationProperties 注釋的類的屬性,以確保配置正確。

建議您使用@Context 注釋具有驗證功能的@ConfigurationProperties,以確保在啟動時進行驗證。

定義附加約束

要定義其他約束,請創(chuàng)建一個新注釋,例如:

示例約束注釋

 Java Groovy  Kotlin 
import javax.validation.Constraint;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Retention(RUNTIME)
@Constraint(validatedBy = { }) // (1)
public @interface DurationPattern {

    String message() default "invalid duration ({validatedValue})"; // (2)

    /**
     * Defines several constraints on the same element.
     */
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    @interface List {
        DurationPattern[] value(); // (3)
    }
}
import javax.validation.Constraint
import java.lang.annotation.Retention

import static java.lang.annotation.RetentionPolicy.RUNTIME

@Retention(RUNTIME)
@Constraint(validatedBy = []) // (1)
@interface DurationPattern {
    String message() default "invalid duration ({validatedValue})" // (2)
}
import javax.validation.Constraint
import kotlin.annotation.AnnotationRetention.RUNTIME

@Retention(RUNTIME)
@Constraint(validatedBy = []) // (1)
annotation class DurationPattern(
    val message: String = "invalid duration ({validatedValue})" // (2)
)
  1. 注釋應使用 javax.validation.Constraint 進行注釋

  2. 可以如上所述以硬編碼方式提供消息模板。如果未指定,Micronaut 會嘗試使用 MessageSource 接口(可選)使用 ClassName.message 查找消息

  3. 要支持重復注釋,您可以定義一個內部注釋(可選)

您可以使用 MessageSource 和 ResourceBundleMessageSource 類添加消息和消息包。

定義注解后,實現(xiàn)一個 ConstraintValidator 來驗證注解。您可以創(chuàng)建一個直接實現(xiàn)接口的 bean 類,也可以定義一個返回一個或多個驗證器的工廠。

如果您計劃定義多個驗證器,建議使用后一種方法:

示例約束驗證器

 Java Groovy  Kotlin 
import io.micronaut.context.annotation.Factory;
import io.micronaut.validation.validator.constraints.ConstraintValidator;

import jakarta.inject.Singleton;

@Factory
public class MyValidatorFactory {

    @Singleton
    ConstraintValidator<DurationPattern, CharSequence> durationPatternValidator() {
        return (value, annotationMetadata, context) -> {
            context.messageTemplate("invalid duration ({validatedValue}), additional custom message"); // (1)
            return value == null || value.toString().matches("^PT?[\\d]+[SMHD]{1}$");
        };
    }
}
import io.micronaut.context.annotation.Factory
import io.micronaut.core.annotation.AnnotationValue
import io.micronaut.validation.validator.constraints.ConstraintValidator
import io.micronaut.validation.validator.constraints.ConstraintValidatorContext

import jakarta.inject.Singleton

@Factory
class MyValidatorFactory {

    @Singleton
    ConstraintValidator<DurationPattern, CharSequence> durationPatternValidator() {
        return { CharSequence value,
                 AnnotationValue<DurationPattern> annotation,
                 ConstraintValidatorContext context ->
            context.messageTemplate("invalid duration ({validatedValue}), additional custom message") // (1)
            return value == null || value.toString() ==~ /^PT?[\d]+[SMHD]{1}$/
        } as ConstraintValidator<DurationPattern, CharSequence>
    }
}
import io.micronaut.context.annotation.Factory
import io.micronaut.validation.validator.constraints.ConstraintValidator
import jakarta.inject.Singleton

@Factory
class MyValidatorFactory {

    @Singleton
    fun durationPatternValidator() : ConstraintValidator<DurationPattern, CharSequence> {
        return ConstraintValidator { value, annotation, context ->
            context.messageTemplate("invalid duration ({validatedValue}), additional custom message") // (1)
            value == null || value.toString().matches("^PT?[\\d]+[SMHD]{1}$".toRegex())
        }
    }
}
  1. 使用內聯(lián)調用覆蓋默認消息模板,以更好地控制驗證錯誤消息。 (自 2.5.0 起)

上面的示例實現(xiàn)了一個驗證器,它驗證用 DurationPattern 注釋的任何字段、參數(shù)等,確保可以用 java.time.Duration.parse 解析字符串。

一般認為 null 有效,@NotNull 用于約束一個值不為 null。上面的示例將 null 視為有效值。

例如:

示例自定義約束用法

 Java Groovy  Kotlin 
@Singleton
public class HolidayService {

    public String startHoliday(@NotBlank String person,
                               @DurationPattern String duration) {
        final Duration d = Duration.parse(duration);
        return "Person " + person + " is off on holiday for " + d.toMinutes() + " minutes";
    }

    public String startHoliday(@DurationPattern String fromDuration, @DurationPattern String toDuration, @NotBlank String person
    ) {
        final Duration d = Duration.parse(fromDuration);
        final Duration e = Duration.parse(toDuration);
        return "Person " + person + " is off on holiday from " + d + " to " + e;
    }
}
@Singleton
class HolidayService {

    String startHoliday(@NotBlank String person,
                        @DurationPattern String duration) {
        final Duration d = Duration.parse(duration)
        return "Person $person is off on holiday for ${d.toMinutes()} minutes"
    }
}
@Singleton
open class HolidayService {

    open fun startHoliday(@NotBlank person: String,
                          @DurationPattern duration: String): String {
        val d = Duration.parse(duration)
        return "Person $person is off on holiday for ${d.toMinutes()} minutes"
    }
}

要驗證上述示例是否驗證了持續(xù)時間參數(shù),請定義一個測試:

測試示例自定義約束用法

 Java Groovy  Kotlin 
@Inject HolidayService holidayService;

@Test
void testCustomValidator() {
    final ConstraintViolationException exception =
        assertThrows(ConstraintViolationException.class, () ->
            holidayService.startHoliday("Fred", "junk") // (1)
        );

    assertEquals("startHoliday.duration: invalid duration (junk), additional custom message", exception.getMessage()); // (2)
}

// Issue:: micronaut-core/issues/6519
@Test
void testCustomAndDefaultValidator() {
    final ConstraintViolationException exception =
            assertThrows(ConstraintViolationException.class, () ->
                    holidayService.startHoliday( "fromDurationJunk", "toDurationJunk", "")
            );

    String notBlankValidated = exception.getConstraintViolations().stream().filter(constraintViolation -> Objects.equals(constraintViolation.getPropertyPath().toString(), "startHoliday.person")).map(ConstraintViolation::getMessage).findFirst().get();
    String fromDurationPatternValidated = exception.getConstraintViolations().stream().filter(constraintViolation -> Objects.equals(constraintViolation.getPropertyPath().toString(), "startHoliday.fromDuration")).map(ConstraintViolation::getMessage).findFirst().get();
    String toDurationPatternValidated = exception.getConstraintViolations().stream().filter(constraintViolation -> Objects.equals(constraintViolation.getPropertyPath().toString(), "startHoliday.toDuration")).map(ConstraintViolation::getMessage).findFirst().get();
    assertEquals("must not be blank", notBlankValidated);
    assertEquals("invalid duration (fromDurationJunk), additional custom message", fromDurationPatternValidated);
    assertEquals("invalid duration (toDurationJunk), additional custom message", toDurationPatternValidated);
}
void "test test custom validator"() {
    when:"A custom validator is used"
    holidayService.startHoliday("Fred", "junk") // (1)

    then:"A validation error occurs"
    def e = thrown(ConstraintViolationException)
    e.message == "startHoliday.duration: invalid duration (junk), additional custom message" //  (2)
}
@Inject
lateinit var holidayService: HolidayService

@Test
fun testCustomValidator() {
    val exception = assertThrows(ConstraintViolationException::class.java) {
        holidayService.startHoliday("Fred", "junk") // (1)
    }

    assertEquals("startHoliday.duration: invalid duration (junk), additional custom message", exception.message) // (2)
}
  1. 調用經(jīng)過驗證的方法

  2. 驗證了約束違規(guī)

在編譯時驗證注解

您可以使用 Micronaut 的驗證器在編譯時通過在注釋處理器類路徑中包含 micronaut-validation 來驗證注釋元素:

 Gradle Maven 
annotationProcessor("io.micronaut:micronaut-validation")
<annotationProcessorPaths>
    <path>
        <groupId>io.micronaut</groupId>
        <artifactId>micronaut-validation</artifactId>
    </path>
</annotationProcessorPaths>

然后 Micronaut 將在編譯時驗證注釋值,這些注釋值本身用 javax.validation 注釋。例如考慮以下注解:

注釋驗證

 Java Groovy  Kotlin 
import java.lang.annotation.Retention;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Retention(RUNTIME)
public @interface TimeOff {
    @DurationPattern
    String duration();
}
import java.lang.annotation.Retention

import static java.lang.annotation.RetentionPolicy.RUNTIME

@Retention(RUNTIME)
@interface TimeOff {
    @DurationPattern
    String duration()
}
import kotlin.annotation.AnnotationRetention.RUNTIME

@Retention(RUNTIME)
annotation class TimeOff(
    @DurationPattern val duration: String
)

如果您嘗試在源代碼中使用 @TimeOff(duration="junk"),Micronaut 將因持續(xù)時間值違反 DurationPattern 約束而編譯失敗。

如果持續(xù)時間是一個屬性占位符,例如 @TimeOff(duration="${my.value}"),驗證將延遲到運行時。

請注意,要在編譯時使用自定義 ConstraintValidator,您必須將驗證器定義為一個類:

示例約束驗證器

 Java Groovy  Kotlin 
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.validation.validator.constraints.ConstraintValidator;
import io.micronaut.validation.validator.constraints.ConstraintValidatorContext;

public class DurationPatternValidator implements ConstraintValidator<DurationPattern, CharSequence> {
    @Override
    public boolean isValid(
            @Nullable CharSequence value,
            @NonNull AnnotationValue<DurationPattern> annotationMetadata,
            @NonNull ConstraintValidatorContext context) {
        return value == null || value.toString().matches("^PT?[\\d]+[SMHD]{1}$");
    }
}
import io.micronaut.core.annotation.NonNull
import io.micronaut.core.annotation.Nullable
import io.micronaut.core.annotation.AnnotationValue
import io.micronaut.validation.validator.constraints.ConstraintValidator
import io.micronaut.validation.validator.constraints.ConstraintValidatorContext

class DurationPatternValidator implements ConstraintValidator<DurationPattern, CharSequence> {
    @Override
    boolean isValid(
            @Nullable CharSequence value,
            @NonNull AnnotationValue<DurationPattern> annotationMetadata,
            @NonNull ConstraintValidatorContext context) {
        return value == null || value.toString() ==~ /^PT?[\d]+[SMHD]{1}$/
    }
}
import io.micronaut.core.annotation.AnnotationValue
import io.micronaut.validation.validator.constraints.ConstraintValidator
import io.micronaut.validation.validator.constraints.ConstraintValidatorContext

class DurationPatternValidator : ConstraintValidator<DurationPattern, CharSequence> {
    override fun isValid(
            value: CharSequence?,
            annotationMetadata: AnnotationValue<DurationPattern>,
            context: ConstraintValidatorContext): Boolean {
        return value == null || value.toString().matches("^PT?[\\d]+[SMHD]{1}$".toRegex())
    }
}

此外

  • 定義引用該類的 META-INF/services/io.micronaut.validation.validator.constraints.ConstraintValidator 文件。

  • 該類必須是公共的,并且有一個公共的無參數(shù)構造函數(shù)

  • 該類必須位于要驗證的項目的注釋處理器類路徑中。


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號