从 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)
}
}
|
使用空字符串调用该方法
发生异常
验证数据类
验证数据类,例如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)
}
|
验证者验证人
验证约束违规
或者,在 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)
}
|
调用经过验证的方法
验证约束违规
验证配置属性
您还可以验证使用 @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)
)
|
注释应使用 javax.validation.Constraint 进行注释
可以如上所述以硬编码方式提供消息模板。如果未指定,Micronaut 会尝试使用 MessageSource 接口(可选)使用 ClassName.message 查找消息
要支持重复注释,您可以定义一个内部注释(可选)
您可以使用 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())
}
}
}
|
使用内联调用覆盖默认消息模板,以更好地控制验证错误消息。 (自 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)
}
|
调用经过验证的方法
验证了约束违规
在编译时验证注解
您可以使用 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())
}
}
|
此外
更多建议: