阅读(4630) (0)

Micronaut Bean 验证

2023-02-23 10:58:52 更新

从 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 的实现目前并不完全符合 Bean Validator 规范,因为该规范严重依赖于基于反射的 API。

目前不支持以下功能:

  • 通用参数类型的注释,因为只有 Java 语言支持此功能。

  • 与约束元数据 API 的任何交互,因为 Micronaut 使用编译时生成的元数据。

  • 基于 XML 的配置

  • 不使用 javax.validation.ConstraintValidator,而是使用 ConstraintValidator (io.micronaut.validation.validator.constraints.ConstraintValidator) 来定义自定义约束,它支持在编译时验证注解。

Micronaut 的实施包括以下好处:

  • 反射和运行时代理免费验证,从而减少内存消耗

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

  • 启动速度更快,因为 Hibernate Validator 增加了 200 毫秒以上的启动开销

  • 通过注解元数据的可配置性

  • 支持反应式 Bean 验证

  • 支持在编译时验证源 AST

  • 无需额外配置即可自动兼容 GraalVM native

如果您需要完全符合 Bean Validator 2.0,请将 micronaut-hibernate-validator 模块添加到您的构建中,它会取代 Micronaut 的实现。

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

验证 Bean 方法

您可以通过对参数应用 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 才能创建编译时子类。或者,您可以使用 @Validated 注释该类,并配置 Kotlin 全开放插件以打开使用该类型注释的类。请参阅编译器插件部分。

如果发生验证错误,则抛出 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. 发生异常

验证数据类

验证数据类,例如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 数据类,注解应该以字段为目标。

要手动验证类,请注入一个 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. 验证约束违规

或者,在 Bean 方法上,您可以使用 javax.validation.Valid 来触发级联验证:

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 现在在调用时验证 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. 调用经过验证的方法

  2. 验证约束违规

验证配置属性

您还可以验证使用 @ConfigurationProperties 注释的类的属性,以确保配置正确。

建议您使用@Context 注释具有验证功能的@ConfigurationProperties,以确保在启动时进行验证。

定义附加约束

要定义其他约束,请创建一个新注释,例如:

示例约束注释

 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 类添加消息和消息包。

定义注解后,实现一个 ConstraintValidator 来验证注解。您可以创建一个直接实现接口的 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. 使用内联调用覆盖默认消息模板,以更好地控制验证错误消息。 (自 2.5.0 起)

上面的示例实现了一个验证器,它验证用 DurationPattern 注释的任何字段、参数等,确保可以用 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"
    }
}

要验证上述示例是否验证了持续时间参数,请定义一个测试:

测试示例自定义约束用法

 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. 调用经过验证的方法

  2. 验证了约束违规

在编译时验证注解

您可以使用 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 将因持续时间值违反 DurationPattern 约束而编译失败。

如果持续时间是一个属性占位符,例如 @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 文件。

  • 该类必须是公共的,并且有一个公共的无参数构造函数

  • 该类必须位于要验证的项目的注释处理器类路径中。