從 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)
}
}
|
使用空字符串調用該方法
發(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)
}
|
驗證者驗證人
驗證約束違規(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)
}
|
調用經(jīng)過驗證的方法
驗證約束違規(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)
)
|
注釋應使用 javax.validation.Constraint 進行注釋
可以如上所述以硬編碼方式提供消息模板。如果未指定,Micronaut 會嘗試使用 MessageSource 接口(可選)使用 ClassName.message 查找消息
要支持重復注釋,您可以定義一個內部注釋(可選)
您可以使用 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())
}
}
}
|
使用內聯(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)
}
|
調用經(jīng)過驗證的方法
驗證了約束違規(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())
}
}
|
此外
更多建議: