参考资料:
https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/
Bean Validation 为 JavaBean 和方法验证定义了一组元数据模型和 API 规范,常用于后端数据的声明式校验。
Bean Validation 规范最早在 Oracle Java EE 下维护。
2017 年 11 月,Oracle 将 Java EE 移交给 Eclipse 基金会。 2018 年 3 月 5 日,Eclipse 基金会宣布 Java EE (Enterprise Edition) 被更名为 Jakarta EE。因此 Bean Validation 规范经历了下面两个阶段:
- 2009.10.12 Bean Validation 1.0(JSR-303) - Java EE 6
- 2013.04.10 Bean Validation 1.1(JSR-349) - Java EE 7
- 2017.06.21 Bean Validation 2.0.0.CR1(JSR-380)- Java EE 8
- 2019.08 Jakarta Bean Validation 2.0 - Jakarta EE 8
- 2020.10.7 akarta Bean Validation 3.0 - Jakarta EE 9
Jakarta Bean Validation 2.0和Bean Validation 2.0之间没有变化,除了 GAV:现在是jakarta.validation:jakarta.validation-api.
1、Bean Validation1.1的新功能
依赖注入:
Bean验证使用了一些组件MessageInterpolator,TraversableResolver,ParameterNameProvider,ConstraintValidatorFactory和ConstraintValidator。
Bean Validation 1.1 标准化了容器如何管理这些对象以及这些对象如何从容器服务中受益。
方法验证:
Bean Validation 1.1 允许对任意方法和构造函数的参数和返回值设置约束:
- 调用者在调用方法或构造方法之前必须满足的先决条件。
- 调用者在方法或构造方法调用返回后所保证的后置条件。
分组转换:(ConvertGroup)
执行级联验证时,可以通过使用组转换功能转换到另外一个组。组转换是通过使用@ConvertGroup注释声明的。
通过统一表达语言进行消息插值
约束冲突消息现在可以使用EL表达式来实现更灵活的呈现和字符串格式化。特别是在EL上下文中注入格式化器对象,将数字、日期等转换为特定于语言环境的字符串表示形式。同样,经过验证的值在EL上下文中也是可用的。
Bean Validation 2.0 的主要贡献是利用 Java 8 的新语言特性和 API 添加来进行验证。使用 Bean Validation 2.0 需要 Java 8 或更高版本。
变化包括:
-
支持通过注释参数化类型的类型参数来验证容器元素
- 更灵活的集合类型级联验证;例如,现在可以验证映射的Key和Value:Map<@Valid CustomerType, @Valid Customer> customersByType
- 支持 java.util.Optional
- 通过插入附加值提取器来支持自定义容器类型
-
全新的内置约束限制:@Email,@NotEmpty,@NotBlank,@Positive,@PositiveOrZero,@Negative,@NegativeOrZero,@PastOrPresent和@FutureOrPresent(内置约束定义)
-
所有内置约束现在都标记为可重复
-
使用反射检索参数名称(参阅命名参数)
-
ConstraintValidator#initialize()是默认方法(参阅约束验证实现)
-
Bean Validation XML 描述符的命名空间与约束映射文件已更改为http://xmlns.jcp.org/xml/ns/validation/configurationformeta-INF/validation.xml和http://xmlns.jcp.org/xml/ns/validation/mapping
Hibernate Validator为Bean Validation的具体实现,目前有三个稳定版本:7.0、 6.2、6.0 。请查看:http://hibernate.org/validator/releases/
| Hibernate Validator | 7.0 | 6.2 | 6.0 |
|---|---|---|---|
| Java | 8, 11 or 17 | 8, 11 or 17 | 8, 11 or 17 |
| Bean Validation | N/A | N/A | 2.0 |
| Jakarta Bean Validation | 3.0 | 2.0 | N/A |
数据校验 - 小试牛刀
验证数据是发生在所有应用程序层(从表示层到持久层)的常见任务。通常在每一层中实现相同的验证逻辑,这既耗时又容易出错。为了避免重复这些验证,开发人员经常将验证逻辑直接捆绑到域模型中,将域类与验证代码混淆,验证代码实际上是关于类本身的元数据。
Bean Validation 规范定义了用于 JavaBean 验证的元数据模型和 API。默认的元数据源是注解,能够通过使用 XML 验证描述符来覆盖和扩展元数据。API 不依赖于特定的应用程序层或编程模型。它特别不依赖于 Web 或持久层,并且可用于服务器端应用程序编程以及富客户端 Swing 应用程序开发人员。
public class Address {
//
@NotNull @Size(max = 50)
private String street1;
@NotNull @ZipCode
private String zipCode;
@NotNull @Size(max = 30)
private String city;
[。。。]
}
Constraints(约束)
Constraints 约束是 Bean Validation 规范的核心。约束是通过约束注解和一系列约束验证实现的组合来定义的。约束注解可应用于类型、字段、方法、构造函数、参数、容器元素或其它约束注解。以下介绍内建的以及第三方的Constraints。
在Bean Validation 2.0中定义了22个内建的约束校验注解,如下:https://beanvalidation.org/2.0/spec/#builtinconstraints
| 注解 | 支持Java类型 | 说明 |
|---|---|---|
| @Null | Object | 为null |
| @NotNull | Object | 不为null |
| @NotBlank | CharSequence | 不为null,且必须有一个非空格字符 |
| @NotEmpty | CharSequence、Collection、Map、Array | 不为null,且不为空(length/size>0) |
Boolean值检查
| 注解 | 支持Java类型 | 说明 | 备注 |
|---|---|---|---|
| @AssertTrue | boolean、Boolean | 为true | 为null有效 |
| @AssertFalse | boolean、Boolean | 为false | 为null有效 |
日期检查
| 注解 | 支持Java类型 | 说明 | 备注 |
|---|---|---|---|
| @Future | Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate | 验证日期为当前时间之后 | 为null有效 |
| @FutureOrPresent | Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate | 验证日期为当前时间或之后 | 为null有效 |
| @Past | Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate | 验证日期为当前时间之前 | 为null有效 |
| @PastOrPresent | Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate | 验证日期为当前时间或之前 | 为null有效 |
数值检查
| 注解 | 支持Java类型 | 说明 | 备注 |
|---|---|---|---|
| @Max | BigDecimal、BigInteger,byte、short、int、long以及包装类 | 小于或等于 | 为null有效 |
| @Min | BigDecimal、BigInteger,byte、short、int、long以及包装类 | 大于或等于 | 为null有效 |
| @DecimalMax | BigDecimal、BigInteger、CharSequence,byte、short、int、long以及包装类 | 小于或等于 | 为null有效 |
| @DecimalMin | BigDecimal、BigInteger、CharSequence,byte、short、int、long以及包装类 | 大于或等于 | 为null有效 |
| @Negative | BigDecimal、BigInteger,byte、short、int、long、float、double以及包装类 | 负数 | 为null有效,0无效 |
| @NegativeOrZero | BigDecimal、BigInteger,byte、short、int、long、float、double以及包装类 | 负数或零 | 为null有效 |
| @Positive | BigDecimal、BigInteger,byte、short、int、long、float、double以及包装类 | 正数 | 为null有效,0无效 |
| @PositiveOrZero | BigDecimal、BigInteger,byte、short、int、long、float、double以及包装类 | 正数或零 | 为null有效 |
| @Digits(integer = 3, fraction = 2) | BigDecimal、BigInteger、CharSequence,byte、short、int、long以及包装类 | 整数位数和小数位数上限 | 为null有效 |
其他
| 注解 | 支持Java类型 | 说明 | 备注 |
|---|---|---|---|
| @Pattern | CharSequence | 匹配指定的正则表达式 | 为null有效 |
| CharSequence | 邮箱地址 | 为null有效,默认正则 '.*' | |
| @Size | CharSequence、Collection、Map、Array | 大小范围(length/size>0) | 为null有效 |
二、hibernate-validator 内建的Constraints
在hibernate-validator 扩展了Constraints,参考:Additional constraints
部分:
| 注解 | 支持Java类型 | 说明 |
|---|---|---|
| @Length | String | 字符串长度范围 |
| @Range | 数值类型和String | 指定范围 |
| @URL | URL地址验证 |
## 三、第三方的 Constraints
Java Bean Validation Extension
Collection Validators
约束是由约束注解和约束验证实现列表组合定义。在组合的情况下,约束注释应用于类型、字段、方法、构造函数、参数、容器元素或其他约束注释。
除非另有说明,否则 Jakarta Bean 验证 API 的默认包名称是javax.validation。
如果约束至少有一个约束校验器由(bean、字段、getter、方法返回值 或 方法/构造函数参数)注释的元素,那么该约束被称为泛型约束。
public class People{
@NotNull
private String name;
private Integer age;
public People(String name){
this.name = name;
}
@NotNull
public Integer getAge(){
return age;
}
}
如果约束有一个指向方法或构造函数参数数组的约束验证器(以验证多个方法/构造函数参数的一致性),则该约束被称为跨参数约束。
public class People{
private String name;
private Integer age;
@NotNull
public People(String name, Integer age){
this.name = name;
this.age = age;
}
}
Jakarta Bean Validation约束在大多数情况下要么是通用约束,要么是跨参数约束。在少数情况下,约束可能是两者兼有。
泛型约束注释可以针对以下任何一个ElementTypes:
- FIELD 对于受约束的属性
- METHOD 对于受约束的 getter 和受约束的方法返回值
- CONSTRUCTOR
- PARAMETER 对于受约束的方法和构造函数参数
- TYPE 对于受限bean
- ANNOTATION_TYPE 对于构成其他约束的约束
- TYPE_USE 对于容器元素约束
跨参数约束注释可以针对以下任何一个ElementTypes:
- METHOD
- CONSTRUCTOR
- ANNOTATION_TYPE 用于约束跨参数的,它构成了其他跨参数约束
JavaBean 上的约束通过一个或多个校验注解来表达。该校验注解保留策略包含RUNTIME并且使用javax.validation.Constraint进行修饰指定Bean Validation 校验器的实现。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@documented
@Constraint(validatedBy = {IsMobilevalidator.class})
public @interface IsMobile {
String message() default "电话错误";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
}
1、约束注解属性
约束定义包括约束注解属性的定义,其中message、groups、validationAppliesTo 和 payload为保留属性,其他约束注解属性可以自定义但不允许以valid开头。
每个约束注解都必须定义一个类型为String 的 message, message元素值用于创建错误消息。
String message() default "{com.acme.constraint.MyConstraint.message}";
resource bundle:
com.acme.constraint.MyConstraint.message=[自定义消息]
建议将消息值默认为资源束键,以启用国际化。还建议使用以下约定:Resource Bundle中设置的 key 连接到message的约束注释的完全限定类名。
消息器内插器负责将通过约束的消息属性指定的所谓消息描述符转换为完全扩展的、人类可读的错误消息。
每个符合 Jakarta Bean Validation 的实现都包含一个默认的消息内插器,它必须遵守定义的算法来内插消息描述符。作为消息插值的先决条件,以下适用:
- 每个约束通过其message属性定义一个消息描述符。
- 每个约束定义都为该约束定义了一个默认消息描述符。
- 通过在约束上设置message属性,可以在约束声明时覆盖消息。
消息描述符是一个字符串文字,可能包含一个或多个消息参数或表达式。消息参数和表达式是分别包含在{}或中的字符串文字${}。以下字符转义规则适用:
- {被视为文字{而不是被视为消息参数的开头
- }被视为文字}而不是被视为消息参数的结尾
- \被视为文字而不是被视为转义字符
- $被视为文字$而不是被视为消息表达式的开始
消息参数
Value must be between {min} and {max}
消息表达式
Must be greater than ${inclusive == true ? 'or equal to ' : ''}{value}
默认的消息插值算法:
默认消息内插器使用以下步骤:
- 消息参数是从消息字符串中提取的,并用作ResourceBundle``ValidationMessages``/ValidationMessages.properties使用定义的语言环境搜索命名(通常具体化为属性文件及其语言环境变体)的关键字。如果找到属性,则消息参数将替换为消息字符串中的属性值。递归地应用步骤 1,直到没有执行替换(即消息参数值本身可以包含消息参数)。
- 消息参数是从消息字符串中提取的,并用作ResourceBundle使用定义的语言环境搜索 Jakarta Bean 验证提供程序的内置的关键字。如果找到属性,则消息参数将替换为消息字符串中的属性值。 与第 1 步相反,第 2 步不是递归处理的。
- 如果第 2 步触发替换,则再次应用第 1 步。否则执行步骤4。
- 从消息字符串中提取消息参数。那些与约束属性名称匹配的内容将替换为约束声明中该属性的值。参数插值优先于消息表达式。例如,对于消息描述符${value},尝试评估{value}为消息参数优先于评估${value}为消息表达式。
- 消息表达式是从消息字符串中提取的,并使用 Jakarta 表达式语言进行评估。另请参阅使用 Jakarta 表达式语言的消息表达式
默认消息插值允许使用 Jakarta 表达式语言。 Jakarta 表达式语言要计算的表达式需要包含在${}消息描述符中。以下属性和 bean 必须在 Jakarta 表达式语言上下文中可用:
- 映射到其属性名称的约束声明的属性值
- 映射在 name 下的验证值validatedValue。
- 一个 bean 映射到formatter暴露 vararg 方法的名称format(String format, Object... args)。此方法必须表现得像java.util.Formatter.format(String format, Object... args). 用于格式化的语言环境由Locale定义用于默认消息插值。所述formatter豆允许格式的属性值,例如在验证值是98.12345678的情况下,${formatter.format('%1$.2f', validatedValue)}将其格式化为98.12(小数点后两位数字,其中Vs使用“”“”将是特定于语言环境)。
如果在消息插值期间发生异常,例如由于无效的表达式或对未知属性的引用,则消息表达式保持不变。
步骤:
1、定义国际化文件,且文件名称必须为ValidationMessages。
2、定义message属性为消息参数(即 "{IsMobile.message}"这样的格式),并在国际化文件中添加这个属性。
@Target({TYPE, FIELD, ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@documented
@Constraint(validatedBy = {IsMobilevalidator.class})
public @interface IsMobile {
String message() default "{IsMobile.message}";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
}
group
每个约束注解都必须定义一个groups元素,该元素指定与约束声明相关联的处理组,在分组校验中会用到。 参数的类型groups是Class>[]
Class>[] groups() default {};
默认值必须是一个空数组。
如果在对元素声明约束时未指定则认为该组为Default组。
groups通常用于控制计算约束的顺序,或者执行JavaBean部分状态的验证。
约束注释必须定义一个有效负载元素,该元素指定约束声明与之关联的有效负载(类似于log中的error、info等级别,用于表示其严重程度,框架可以利用这种严重性来调整约束失败的显示方式)。payload参数的类型为payload[]。
Class extends Payload>[] payload() default {};
默认值必须是一个空数组。
每个附加的有效载荷必须实现Payload接口。
public class Severity {
public static class Info implements Payload {};
public static class Error implements Payload {};
}
public class Address {
@NotNull(message="would be nice if we had one", payload=Severity.Info.class)
public String getZipCode() { [...] }
@NotNull(message="the city is mandatory", payload=Severity.Error.class)
String getCity() { [...] }
}
有效负载信息可以通过ConstraintDescriptor从错误报告中检索,可以通过ConstraintViolation对象(参见ConstraintViolation)或通过metadata API(参见 ConstraintDescriptor)访问。
在约束声明时使用validationAppliesTo来澄清约束的目标(例如,带注释的元素、方法返回值或方法参数),常见的情况是当一个约束注解标注在方法上时,由于该方法既有返回值又有参数,在这种情况下就需要澄清约束目标,是约束返回值呢? 还是参数呢?
ConstraintTarget validationAppliesTo() default ConstraintTarget.IMPLICIT;
元素validationAppliesTo只能出现在泛型和跨参数的约束中,在这种情况下是必须的。如果违反了这些规则,将引发ConstraintDefinitionException。
validationAppliesTo参数的类型是constraintarget。默认值必须是constraintarget . implicit。
public enum ConstraintTarget {
IMPLICIT,
RETURN_VALUE,
PARAMETERS
}
如果在非法情况下使用了constraintarget,则在验证时或请求元数据时将引发ConstraintDeclarationException。非法情况的例子有:
- 在无法推断的情况下使用IMPLICIT,
- 在没有参数的构造函数或方法上使用PARAMETERS,
- 在没有返回值的方法上使用RETURN_VALUE,
- 在类型(类或接口)或字段上使用PARAMETERS或RETURN_VALUE。
- 当使用同时支持方法或构造函数的约束时,鼓励约束用户显式设置constraintarget目标,因为它提高了可读性。
简单约束定义
// 将String标记为表示格式良好的订单号
@documented
@Constraint(validatedBy = OrderNumberValidator.class)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface OrderNumber {
String message() default "{com.acme.constraint.OrderNumber.message}";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
}
交叉参数约束定义
// 交叉参数约束,确保方法的两个日期参数按正确的顺序排列。
@documented
@Constraint(validatedBy = DateParametersConsistentValidator.class)
@Target({ METHOD, CONSTRUCTOR, ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface DateParametersConsistent {
String message() default "{com.acme.constraint.DateParametersConsistent.message}";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
}
即是泛型又是交叉参数的约束定义
// 使用el表达式进行验证,该约束接受任何类型,并且可以验证带注释的类型或跨参数应用限制。
@documented
@Constraint(validatedBy = ELAssertValidator.class)
@Target({ METHOD, FIELD, TYPE, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ELAssert {
String message() default "{com.acme.constraint.ELAssert.message}";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
ConstraintTarget validationAppliesTo() default ConstraintTarget.IMPLICIT;
String expression();
}
@ELAssert(
message="Please check that your passwords match and try again.",
expression="param[1]==param[2]",
validationAppliesTo=ConstraintType.PARAMETERS
)
public User createUser(String email, String password, String repeatPassword) { [...] }
泛型和跨参数的约束显示了既可以应用于带注释的元素又可以应用于方法或构造函数的跨参数的约束。注意,在本例中存在validationAppliesTo。
带默认参数的约束定义
@documented
@Constraint(validatedBy = Audiblevalidator.class)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface Audible {
Age age() default Age.YOUNG;
String message() default "{com.acme.constraint.Audible.message}";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
public enum Age {
YOUNG,
WONDERING,
OLD
}
}
确保给定的频率在人耳的范围内。约束定义包括在应用约束时可以指定的可选参数。
带强制参数的约束定义
@documented
@Constraint(validatedBy = DiscreteListOfIntegerValidator.class)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface Acceptable {
int[] value();
String message() default "{com.acme.constraint.Acceptable.message}";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
}
定义了一个表示为数组的可接受值列表:value应用约束时必须指定该属性。
对目标的同一属性多次使用相同的约束是很常见的,比如由于存在组的概念,因此某个属性上可能会存在多个@Pattern,用于判断是否与该正则表达式匹配。
为了支持这一需求,Jakarta Bean Validation提供程序以一种特殊的方式处理其值元素具有约束注释数组返回类型的常规注释(没有由@Constraint注释的注释)。值数组中的每个元素都由Jakarta Bean Validation作为常规约束注释来处理实现。这意味着value元素中指定的每个约束都应用于目标。注释必须具有保留RUNTIME,可以应用于type,field,property, executable parameter, executable return value, executable cross-parameter or another annotation(类型、字段、属性、可执行参数、可执行返回值、可执行跨参数或其他注释)。建议使用与初始约束相同的目标集。
约束设计人员注意:每个约束注释都应该与其相应的多值注释相耦合。该规范建议(虽然没有强制)定义名为List的内部注释。每个约束注释类型都应该用java.lang.annotation进行元注释。可重复,引用相应的List注释。这将约束注释类型标记为可重复的,并允许用户多次指定约束,而无需显式使用List注释。所有内置注释都遵循这个模式。
示例:
@documented
@Constraint(validatedBy = ZipCodevalidator.class)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
public @interface ZipCode {
String countryCode();
String message() default "{com.acme.constraint.ZipCode.message}";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@documented
@interface List {
ZipCode[] value();
}
}
public class Address {
@ZipCode(countryCode = "fr", groups = Default.class, message = "zip code is not valid")
@ZipCode(countryCode = "fr",groups = SuperUser.class,message = "zip code invalid. Requires overriding before saving.")
private String zipCode;
}
在此示例中,两个约束都适用于该zipCode字段,但具有不同的组和不同的错误消息。还可以通过显式使用@List注释多次指定约束(尽管简单地重复注释是 Jakarta Bean Validation 2.0 和 Java 8 的首选习惯用法)
public class Address {
@ZipCode.List( {
@ZipCode(countryCode="fr", groups=Default.class,message = "zip code is not valid"),
@ZipCode(countryCode="fr", groups=SuperUser.class,message = "zip code invalid. Requires overriding before saving.")
} )
private String zipCode;
}
三、组合约束注解定义
很多情况下对于某个属性可能包含多个约束注解,此时我们可以采取将这多个约束组合起来作为一个约束注解。
优点:
- 避免重复并促进对更基本的约束的重用。
- 将原语约束作为元数据API中组合约束的一部分公开,并增强工具意识。
组合是通过使用组合约束注解/注解/约束注解来完成的。
@Pattern(regexp = "[0-9]*")
@Size(min = 5, max = 5)
@Constraint(validatedBy = FrenchZipCodevalidator.class)
@documented
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface FrenchZipCode {
String message() default "Wrong zip code";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@documented
@interface List {
FrenchZipCode[] value();
}
}
说明:
- 用@FrenchZipCode(组合注释)注释一个元素相当于用@Pattern(regexp="[0-9]*")、@Size(min=5, max=5)(组合注释)和@FrenchZipCode注释它。
- 约束注释上的每个约束注释都应用于目标元素,这是递归完成的。
- 默认情况下,每个失败约束生成一个错误报告。
- 组合注解继承了主约束注解中的gorous,组合注解上的任何gorous定义都将被忽略。
- 组合注解继承了主约束注解的Payload。组合注解上的任何Payload定义都将被忽略。
- 组合注释继承主约束注解中validationAppliesTo(约束目标)。对组合注解的任何validationAppliesTo定义都会被忽略。
- 放置组合约束的类型必须与所有约束(组合和组合)兼容。约束设计人员应该确保这样的类型存在,并在JavaDoc中列出所有兼容的类型。
- 所有组合约束和组合约束都必须具有共同的约束类型。特别是,将纯泛型约束和纯交叉参数约束混合在一起是不合法的。
如果任何组合注解失败,托管此注解的约束注解将返回组合注解错误报告,每个单独的组合约束的错误报告将被忽略。
在此场景中,如果一个或多个组合注解无效,则会自动认为主要约束无效,并生成相应的错误报告。
如果想要任何一个组合约束失败,则会引发@FrenchZipCode对应的错误报告,而不会引发其他错误,请使用@ReportAsSingleViolation注释。
@Pattern(regexp = "[0-9]*")
@Size(min = 5, max = 5)
@ReportAsSingleViolation
@Constraint(validatedBy = FrenchZipCodevalidator.class)
@documented
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface FrenchZipCode {
String message() default "Wrong zip code";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@documented
@interface List {
FrenchZipCode[] value();
}
}
更具体地说,如果组合约束标记为@ReportAsSingleViolation,则组合约束的评估在第一个失败的约束处停止,并且生成并返回与组合约束相对应的错误报告。
2、@OverridesAttribute
观察上述组合注解发现,组合注解可以定义消息的值和自定义属性(不包括组、有效负载和validationAppliesTo),但对于像@Size的min、max、@Pattern的regexp等是写死的,对于每个使用@FrenchZipCode注解的属性等而言应该是可以变化的。
通过使用**@OverridesAttribute**,在主注解中定义的属性可以用于覆组合注解的一个或多个属性。需要注意的是覆盖属性与被覆盖属性的类型必须相同。
@Pattern(regexp = "[0-9]*")
@Size
@Constraint(validatedBy = FrenchZipCodevalidator.class)
@documented
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface FrenchZipCode {
String message() default "Wrong zip code";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
@OverridesAttribute(constraint = Size.class, name = "min")
@OverridesAttribute(constraint = Size.class, name = "max")
int size() default 5;
@OverridesAttribute(constraint = Size.class, name = "message")
String sizeMessage() default "{com.acme.constraint.FrenchZipCode.zipCode.size}";
@OverridesAttribute(constraint = Pattern.class, name = "message")
String numberMessage() default "{com.acme.constraint.FrenchZipCode.number.size}";
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@documented
@interface List {
FrenchZipCode[] value();
}
}
使用@OverridesAttribute (@FrenchZipCode.sizeMessage)注释的组合约束属性的值被应用到以@OverridesAttribute.name命名的组合约束属性,并驻留在类型为@OverridesAttribute的组合约束(@Size.message)上。同样,@FrenchZipCode。numberMessage值被映射到@Pattern.message。
如果未定义name属性,则@OverridesAttribute.name的默认值是包含@OverridesAttribute注释的组合约束属性的名称(比如:sizeMessage、numberMessage)。
Note:
组合约束本身可以是组合约束。在这种情况下,根据描述的规则递归地覆盖属性值。但是请注意,转发规则(由@OverridesAttribute定义)只应用于直接组合约束。
constraintIndex
对于组合约束可能会出现多个重复约束的情况,如果组合约束是直接在组合约束上给出的(即通过可重复注释特性),则constraintIndex指的是这种类型的约束从左到右的顺序,这些约束是在组合约束上给出的。如果使用对应的List注释指定组合约束,则constraintIndex指向值数组中的索引(比如:@ZipCode.List(@ZipCode(。。。),@ZipCode(。。。))。·
documented
@Constraint(validatedBy = {})
@Pattern(regexp = "[A-Z0-9._%+-][email protected][A-Z0-9.-]+\.[A-Z]{2,4}") // email
@Pattern(regexp = ".*?emmanuel.*?") // emmanuel
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface EmmanuelsEmail {
String message() default "Not emmanuel's email";
@OverridesAttribute(constraint = Pattern.class, name = "message", constraintIndex = 0)
String emailMessage() default "Not an email";
@OverridesAttribute(constraint = Pattern.class, name = "message", constraintIndex = 1)
String emmanuelMessage() default "Not Emmanuel";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@documented
@interface List {
EmmanuelsEmail[] value();
}
}
OverridesAttribute 总结
以下元素唯一标识一个被覆盖的约束属性:
- @OverridesAttribute.constraint
- @OverridesAttribute.name
- @OverridesAttribute.constraintIndex
如果组合无效,例如
- 无限递归组合
- 错误的属性覆盖
- 一个属性映射到多个源属性
- 标记为不同约束类型(即泛型和跨参数)的组合约束和组合约束
- 等等。
则会在验证时或在请求元数据时引发ConstraintDefinitionException。
鼓励约束设计者根据规范定义的内置约束使用组合(递归或不递归)。组合约束通过 Jakarta Bean Validation 元数据 API ( ConstraintDescriptor )公开。此元数据对于第三方元数据使用者特别有用,例如生成数据库模式的持久性框架(例如 Jakarta Persistence)或表示框架。
1、ConstraintValidator - 约束验证器
约束验证器主要用于对给定的约束注解与类型进行验证,约束验证器由约束注解定义的@Constraint注释的validatedBy元素指定,并且约束验证器必须实现ConstraintValidator接口
public interface ConstraintValidator { default void initialize(A constraintAnnotation) { } boolean isValid(T value, ConstraintValidatorContext context); }
如果在initialize()或isValid()方法中发生异常,则运行时异常被Jakarta Bean验证引擎包装到ValidationException中。
约束验证实现不允许更改传递给isValid()的值的状态。
注意:
对于null值的情况,是返回为true还是false需要进行考虑。
@SupportedValidationTarget
用于定义ConstraintValidator可以验证的目标。
ConstraintValidator可以针对由约束注释的(返回的)元素、方法或构造函数(又名交叉参数)的参数数组或两者。
如果@SupportedValidationTarget不存在,则ConstraintValidator以ConstraintValidator注释的(返回的)元素为目标。
以交叉参数为目标的ConstraintValidator必须接受Object[] (或Object )作为它验证的对象类型。
@documented
@Target({ TYPE })
@Retention(RUNTIME)
public @interface SupportedValidationTarget {
ValidationTarget[] value();
}
public enum ValidationTarget {
ANNOTATED_ELEMENT,
PARAMETERS
}
示例
@SupportedValidationTarget(ValidationTarget.PARAMETERS) public class scriptAssertValidator implements ConstraintValidator{ @Override public void initialize(scriptAssert constraintAnnotation) { [...] } @Override public boolean isValid(Object[] value, ConstraintValidatorContext context) { [...] } }
ConstraintValidator 示例
实现必须遵守以下限制:
-
T必须解析为非参数化类型(即因为该类型未使用泛型或因为使用原始类型而不是泛型版本)
-
或T泛型参数必须是无界通配符类型
有效的ConstraintValidator:
//String is not making use of generics public class SizevalidatorForString implements ConstraintValidator{ [...] } //Collection uses generics but the raw type is used public class SizevalidatorForCollection implements ConstraintValidator { [...] } //Collection uses generics and unbounded wildcard type public class SizevalidatorForCollection implements ConstraintValidator > { [...] } //Validator for cross-parameter constraint @SupportedValidationTarget(ValidationTarget.PARAMETERS) public class DateParametersConsistentValidator implements ConstraintValidator { [...] } //Validator for both annotated elements and executable parameters @SupportedValidationTarget({ValidationTarget.ANNOTATED_ELEMENT, ValidationTarget.PARAMETERS}) public class ELscriptValidator implements ConstraintValidator { [...] }
无效的ConstraintValidator:
//parameterized type (参数化类型) public class SizevalidatorForString implements ConstraintValidator> { [...] } //parameterized type using bounded wildcard (使用有界通配符的参数化类型) public class SizevalidatorForCollection implements ConstraintValidator > { [...] } //cross-parameter validator accepting the wrong type (跨参数的验证接受了错误的类型,应该是Object对象或Object数组而不是Number) @SupportedValidationTarget(ValidationTarget.PARAMETERS) public class NumberPositivevalidator implements ConstraintValidator { [...] }
传递给isValid()方法的ConstraintValidatorContext对象携带约束被验证到的上下文中可用的信息和操作。
public interface ConstraintValidatorContext {
void disableDefaultConstraintViolation();
String getDefaultConstraintMessageTemplate();
ClockProvider getClockProvider();
ConstraintViolationBuilder buildConstraintViolationWithTemplate(String messageTemplate);
T unwrap(Class type);
[。。。]
}
ConstraintValidatorContext接口提供了对特定约束验证有用的上下文信息的访问(例如,getlockprovider())。
它还允许重新定义当约束无效时生成的默认约束消息(即你定义约束注解时定义的mesage属性值)。默认情况下,每个无效的约束将导致生成一个错误对象,该错误对象由ConstraintViolation违例对象表示。
此对象由约束声明定义的默认约束消息模板和放置约束声明的上下文(bean、属性、可执行参数、跨参数、可执行返回值或容器元素)构建。
ConstraintValidatorContext方法让约束实现禁用默认的constraint违例生成(调用disableDefaultConstraintViolation),并创建一个或多个自定义生成。作为参数传递的非插值消息用于构建ConstraintViolation消息(消息插值操作应用于它)。
默认情况下,constraint违例中公开的Path表示bean、属性、参数、跨参数、返回值或承载约束的容器元素的路径(有关更多信息,请参阅constraint违例)。您可以使用约束违背构建器fluent API将其指向此默认路径的子路径。
示例:
1、ConstraintValidator 简单实现
public class BeginsWithValidator implements ConstraintValidator{ private Set allowedPrefixes; @Override public void initialize(BeginsWith constraint) { allowedPrefixes = Arrays.stream( constraint.value() ) .collect( collectingAndThen( toSet(), Collections::unmodifiableSet ) ); } @Override public boolean isValid(String value, ConstraintValidatorContext context) { if ( value == null ) return true; return allowedPrefixes.stream() .anyMatch( value::startsWith ); } }
2、交叉参数 ConstraintValidator 实现
@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class DateParametersConsistentValidator implements
ConstraintValidator {
@Override
public boolean isValid(Object[] value, ConstraintValidatorContext context) {
if ( value.length != 3 ) {
throw new IllegalArgumentException( "Unexpected method signature" );
}
// one or both limits are unbounded => always consistent
if ( value[1] == null || value[2] == null ) {
return true;
}
return ( (Date) value[1] ).before( (Date) value[2] );
}
}
3 、泛型和跨参数ConstraintValidator 实现
@SupportedValidationTarget({ValidationTarget.ANNOTATED_ELEMENT, ValidationTarget.PARAMETERS})
public class ELscriptValidator implements ConstraintValidator {
public void initialize(ELscript constraint) {
[...]
}
public boolean isValid(Object value, ConstraintValidatorContext context) {
[...]
}
}
重新定义约束消息
需要做的是,首先通过disableDefaultConstraintViolation()方法关闭默认ConstraintValidator,并将重新定义的约束消息作为buildConstraintViolationWithTemplate()的参数再调用addConstraintViolation方法即可重新定义约束消息。
更加复杂的操作可以参考:https://beanvalidation.org/2.0/spec/#example-constraintsdefinitionimplementation-validationimplementation-errorbuilder
public class SerialNumberValidator implements ConstraintValidator{ private int length; @Override public void initialize(SerialNumber constraint) { this.length = constraint.length(); } @Override public boolean isValid(String value, ConstraintValidatorContext context) { if ( value == null ) return true; context.disableDefaultConstraintViolation(); if ( !value.startsWith( "SN-" ) ) { String wrongPrefix = "{com.acme.constraint.SerialNumber.wrongprefix}"; context.buildConstraintViolationWithTemplate( wrongPrefix ) .addConstraintViolation(); return false; } if ( value.length() != length ) { String wrongLength = "{com.acme.constraint.SerialNumber.wronglength}"; context.buildConstraintViolationWithTemplate( wrongLength ) .addConstraintViolation(); return false; } return true; } }
时间约束校验器
时间约束的约束验证器(无论是内置的约束@Past, @PastOrPresent, @Future和@FutureOrPresent或自定义的时间约束)可以从ConstraintValidatorContext# getlockprovider()暴露的ClockProvider对象中获取当前瞬间,ClockProvider接口如下:
public interface ClockProvider {
Clock getClock();
}
示例:
// 是否是过去时间 public class PastValidatorForZonedDateTime implements ConstraintValidator{ @Override public boolean isValid(ZonedDateTime value, ConstraintValidatorContext context) { if ( value == null ) { return true; } ZonedDateTime now = ZonedDateTime.now( context.getClockProvider().getClock() ); return value.isBefore( now ); } }
约束验证
该节主要介绍约束验证的目标对象、要求、约束等。
一、约束验证对象的要求
Jakarta Bean进行验证时,对验证的Bean有以下要求:
-
要验证的属性必须遵循JavaBeans 规范中定义读取属性的方法签名约定即getter。
JavaBeans 规范指定 getter 是一种方法 getxxx - 具有返回类型但没有参数 isxxx - 返回boolean值但没有参数
-
静态字段和静态方法被排除在验证之外。
-
约束可以应用于接口(interface)和超类(SuperClass),但同样也存在限制接下来会说明。
二、约束验证的目标
约束验证的目标可以是以下:
- type
- field or property 字段或属性
- constructor or method return value 方法的返回值
- constructor or method parameter 构造器或方法的参数
- constructor or method cross-parameter 构造器或方法的交叉参数
- container element 容器元素
前提是:
- 约束定义支持指定的目标 ( java.lang.annotation.Target)
- 约束上声明的一个ConstraintValidator支持声明的目标类型,或者在跨参数的情况下,存在一个跨参数的ConstraintValidator(参阅ConstraintValidator解析算法以了解ConstraintValidator解析)
- 在容器元素约束的情况下,存在相应的值提取器(有关值提取器解析的详细信息,请参阅ValueExtractor解析)
1、field or property 字段或属性
约束声明可以应用于同一对象类型的字段和属性。 但是,不应在字段及其关联属性之间重复相同的约束(约束验证将应用两次),遵循单一状态策略。
作用于字段:
@NotNull private String name;
作用于属性:需要满足JavaBeans规范,即存在无参的getxx()方法(对于boolean也可以是isxxx()方法)
@NotNull
public String getName(){
return name;
}
2、container element 容器元素
约束可以应用于通用容器的元素,例如List,Map或Optional。这是通过将约束注解添加到此类容器的类型参数来完成的。
容器元素约束可以在以下声明中使用:
- 字段 field
- 属性 properties
- 方法或构造方法的参数
- 方法的返回值
private List<@Email String> emails;
public Optional<@Email String> getEmail() {
[...]
}
public Map<@NotNull String, @ValidAddress Address> getAddressesByType() {
[...]
}
public List<@NotBlank String> getMatchingRecords(List<@NotNull @Size(max=20) String> searchTerms) {
[...]
}
容器元素约束可以应用于嵌套容器类型:
private Map> addressesByType;
不支持在泛型类型或方法的类型参数上声明容器元素约束。 也不支持在类型定义的extendsorimplements子句中声明对类型参数的容器元素约束:
public class NonNullList<@NotNull T> {
[...]
}
public class ContainerFactory {
<@NotNull T> Container instantiateContainer(T wrapped) { [...] }
}
public class NonNullSet extends Set<@NotNull T> {
[...]
}
容器的隐式解包:
除了在类型参数上指定容器元素约束之外,还可以在非通用容器类型上声明容器元素约束。
这是通过隐式解包完成的,即约束不适用于带注释的容器本身,而是应用于其元素。比如:java.util.OptionalInt,OptionalLong和OptionalDouble以及JavaFX的的非通用的属性类型,例如StringProperty或IntegerProperty。
@Min(1) private OptionalInt optionalNumber; @Negative private LongProperty negativeLong; @Positive private IntegerProperty positiveInt; private final ListProperty<@NotBlank StringProperty> notBlankStrings;
以上注解都不是作用于对象本身,而是作用于被包装的数字和字符串值。
为此,必须定义一个明确可解析的值提取器(请参阅将容器级约束应用于容器元素的 ValueExtractor 解析算法),它带有@UnwrapByDefault注释(请参阅@UnwrapByDefault)
如果需要,可以通过Unwrap和Skip有效负载定义明确指定在容器上声明的约束的目标(容器或容器元素),两者不可同时定义,否则抛出ConstraintDeclarationException异常:
public interface Unwrapping {
public interface Unwrap extends Payload {
}
public interface Skip extends Payload {
}
}
例如:
@NotNull约束应用于StringProperty容器:
@NotNull(payload = Unwrapping.Skip.class) private StringProperty name;
@Email 作用于List容器中的元素
@Email(payload = Unwrapping.Unwrap.class) Listemails; 等价于 List<@Email String> emails; //直接标注在类型参数上更好
3、constructor or method参数与返回值
验证会忽略static 静态方法。对静态方法施加约束是不可移植的。对BeanValidation2.0来说,对方法不存在其他限制,但是与方法验证相结合的技术可能会对应用验证的方法施加进一步的限制。例如,某些集成技术可能要求要验证的方法必须具有public可见性和/或不能final。
为了对方法或构造函数参数使用约束注解,它们的元素类型必须是ElementType.PARAMETER。 为了在跨参数验证或方法或构造函数的返回值上使用约束注解(请参阅下面几节),它们的元素类型必须是ElementType.METHOD、ElementType.CONSTRUCTOR。 所有内置约束都支持这些元素类型,对于自定义约束,最好也这样做。
方法、构造器参数验证
参数约束是通过在方法或构造函数参数上放置约束注解(下例中的NotNull)来声明。
public class OrderService {
public OrderService(@NotNull CreditCardProcessor creditCardProcessor) {
[...]
}
public void placeOrder(
@NotNull @Size(min=3, max=20) String customerCode,
@NotNull Item item,
@Min(1) int quantity) {
[...]
}
}
交叉参数约束是通过在方法或构造函数上放置交叉参数约束注解(下例中的@ConsistentDateParameters)来声明。
public class CalendarService {
@ConsistentDateParameters
public void createEvent(
String title,
@NotNull Date startDate,
@NotNull Date endDate) {
[...]
}
}
注意:
一些约束注解可以针对可执行文件的返回值及其参数数组,此时如果存在歧义就需要约束注解上标注validationAppliesTo的属性值为ConstraintTarget.PARAMETERS。(上面的那个并不会发生歧义,因为该方法返回值为void,根据ConstraintTarget.IMPLICIT它选择的就是作用目标就是参数数组)
方法返回值验证
返回值约束是通过将约束注解直接放在方法或构造函数上来声明的,同样类似于方法参数验证需要在约束注解上标注validationAppliesTo为ConstraintTarget.RETURN_VALUE
public class OrderService {
private CreditCardProcessor creditCardProcessor;
@ValidonlineOrderService
public OrderService(onlineCreditCardProcessor creditCardProcessor) {
this.creditCardProcessor = creditCardProcessor;
}
@ValidBatchOrderService
public OrderService(BatchCreditCardProcessor creditCardProcessor) {
this.creditCardProcessor = creditCardProcessor;
}
@NotNull
@Size(min=1)
public Set getCreditCardProcessors() { [...] }
@NotNull
@Future
public Date getNextAvailableDeliveryDate() { [...] }
}
继承层次中的方法约束
在继承层次结构中定义方法约束(即通过扩展基类的类继承和通过实现或扩展接口的接口继承)时,必须遵守Liskov 替换原则,该原则要求:
- 不能在子类型中加强方法的前提条件(由参数约束表示)
- 不能在子类型中削弱方法的后置条件(由返回值约束表示)
仅适用于一般方法,验证构造函数约束时不适用,因为构造函数不会相互重写。
非法示例:
1、非法声明的参数约束:
public interface OrderService {
void placeOrder(String customerCode, Item item, int quantity);
}
public class SimpleOrderService implements OrderService {
@Override
public void placeOrder(
@NotNull @Size(min=3, max=20) String customerCode,
@NotNull Item item,
@Min(1) int quantity) {
[...]
}
}
约束SimpleOrderService是非法的,因为它们加强了placeOrder()由接口构成的方法的先决条件OrderService。
2、对并行类型非法声明的参数约束
public interface OrderService {
void placeOrder(String customerCode, Item item, int quantity);
}
public interface OrderPlacementService {
public void placeOrder(
@NotNull @Size(min=3, max=20) String customerCode,
@NotNull Item item,
@Min(1) int quantity);
}
public class SimpleOrderService implements OrderService, OrderPlacementService {
@Override
public void placeOrder(String customerCode, Item item, int quantity) {
[...]
}
}
在这里,类SimpleOrderService实现了两个接口OrderService和OrderPlacementService,这两个接口彼此不相关,但都定义了一个具有相同签名的方法placeOrder()。 这个层次结构对于参数约束是不合法的,因为SimpleOrderService的客户端必须满足在OrderPlacementService接口上定义的约束,即使客户端只期望OrderService。
正确示例:
1、正确声明子类的返回值约束
public class OrderService {
Order placeOrder(String customerCode, Item item, int quantity) {
[...]
}
}
public class SimpleOrderService extends OrderService {
@Override
@NotNull
@Valid
public Order placeOrder(String customerCode, Item item, int quantity) {
[...]
}
}
2、正确的子类参数约束
public interface OrderService {
void placeOrder(@NotNull @Size(min=3, max=20) String customerCode,
@NotNull Item item,
@Min(1) int quantity);
}
public class SimpleOrderService implements OrderService {
@Override
public void placeOrder(
String customerCode,
Item item,
int quantity) {
[...]
}
}
三、级联验证
Jakarta Bean Validation API 不仅允许验证单个类实例,还允许验证完整的对象图(级联验证)。
可以通过@Valid来标记,用于验证级联的属性、方法参数或方法返回类型。当验证属性、方法参数或方法返回类型时,将验证在对象及其属性上定义的约束。此行为以递归方式应用。
在级联验证期间会忽略null值。
1、作用目标 1、一般的字段与属性
public class Car {
@NotNull
@Valid
private Person driver;
}
public class Person {
@NotNull
private String name;
}
2、集合值、数组值以及通常的Iterable容器本身以及类型参数,包括:
- 对象数组
- java.util.Collection
- java.util.Set
- java.util.List
- java.util.Map
迭代器提供的每个对象都经过验证。 对于Map,每个的值(由 检索getValue)Map.Entry被验证( Key 未被验证)
Jakarta Bean Validation 2.0 开始 @Valid还允许验证嵌套的通用容器的元素。@Valid必须放入该嵌套容器类型的类型参数,以触发对所有嵌套容器的元素的验证。
@Valid注释应该放在容器本身或容器的类型参数上,但不能同时放在两者上(以防止容器元素被验证两次)。
不支持放入@Valid泛型类型或方法的类型参数。 也不支持@Valid在类型定义的extendsorimplements子句中放入类型参数。
public class NonNullList<@Valid T> {
[...]
}
public class ContainerFactory {
<@Valid T> Container instantiateContainer(T wrapped) { [...] }
}
public class NonNullSet extends Set<@Valid T> {
[...]
}
示例:
List
public class User {
// Jakarta Bean Validation 2.0 首选风格
private List<@Valid PhoneNumber> phoneNumbers;
// 传统风格
@Valid
private List phoneNumbers;
// 禁止,在容器本身与类型参数上放置@Valid注解
@Valid
private List<@Valid PhoneNumber> phoneNumbers;
}
map
public class User {
// Jakarta Bean Validation 2.0 首选风格
private Map addressesByType;
// 传统风格
@Valid
private Map addressesByType;
// 禁止,映射或映射值类型参数都应该用@Valid注释,但不能两者都用
@Valid
private Map addressesByType;
}
map的key与vaue进行级联验证
public class User {
private Map<@Valid AddressType, @Valid Address> addressesByType;
}
嵌套列表元素的map进行级联验证
public class User {
private Map> addressesByType;
}
嵌套map元素的map进行级联验证
public class User {
private Map> addressesByUserAndType;
}
3、方法的参数与返回值
@Valid注解也可用于方法/构造函数参数或返回值执行级联验证。标记时,参数或返回值被认为是要验证的bean对象。
public class OrderService {
@NotNull @Valid
private CreditCardProcessor creditCardProcessor;
@Valid
public OrderService(@NotNull @Valid CreditCardProcessor creditCardProcessor) {
this.creditCardProcessor = creditCardProcessor;
}
@NotNull @Valid
public Order getOrderByPk(@NotNull @Valid OrderPK orderPk) { [...] }
@NotNull
public Set<@Valid Order> getOrdersByCustomer(@NotNull @Valid CustomerPK customerPk) { [...] }
}
下面的递归验证将在验证OrderService类的方法时发生:
- 对传递给构造函数的creditCardProcessor参数的对象的约束进行验证
- 对构造函数返回的新创建的OrderService实例的约束进行验证,即字段creditCardProcessor上的@NotNull约束和引用的creditCardProcessor实例上的约束(因为字段是用@Valid注释的)。
- 对传递给orderPk参数的对象和getOrderByPk()方法返回的Order对象的约束进行验证
- 验证传递给customerPk参数的对象上的约束,以及getOrdersByCustomer()方法返回的Set中包含的每个对象上的约束
四、分组验证
有时我们需要在不同情况下才会对某个属性进行不同的验证,这是我们就可以采取分组验证。
如何分组?每一个约束注解都包含groups属性,可以通过指定groups属性来进行分组,如果没有明确声明组,则约束属于该Default组。
public interface Billable {
}
public interface BuyInOneClick {
}
public class User {
@NotNull
private String firstname;
@NotNull(groups = Default.class)
private String lastname;
@NotNull(groups = {Billable.class, BuyInOneClick.class})
private CreditCard defaultCreditCard;
}
在验证调用期间,将验证一个或多个组。在对象图上评估属于这组的所有约束。在将组分配给约束中,当Billable或BuyInOneClick组被验证时,将在defaultCreditCard上检查@NotNull。在验证Default组时,验证firstname和lastname上的@NotNull。提醒:父类和接口上的约束被考虑。
如何进行分组验证 ?
方式一:Validator的validate(T object, Class>… groups)方法。
public class Driver {
@Min(value = 18, groups = Minimal.class)
int age;
@AssertTrue
Boolean passedDrivingTest;
@Valid
Car car;
// setter/getters
}
public class Car {
@NotNull
String type;
@AssertTrue(groups = Later.class)
Boolean roadWorthy;
// setter/getters
}
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Driver driver = new Driver();
driver.setAge(16);
Car porsche = new Car();
driver.setCar(porsche);
Set> violations = validator.validate( driver, Minimal.class );
方式二:
使用Spring提供的@Validated注解,可以指定要验证的组
@Validated({IsMobileGroups.HUNAN.class, Default.class}
or
@GroupSequence(value = {IsMobileGroups.HUNAN.class,Default.class}) //有组序的概念,请查看下面解释
public interface PhoneSequence{
}
@Validated({PhoneSequence.class}
1、组继承
在某些情况下,一个组是一个或多个组的超集,一个组可以通过接口继承来继承一个或多个组。
public interface BuyInOneClick extends Default, Billable {
}
继承的组的验证规则:
public class User {
@NotNull
private String firstname;
@NotNull(groups = Default.class)
private String lastname;
@NotNull(groups = Billable.class)
private CreditCard defaultCreditCard;
}
在验证组BuyInOneClick将导致以下约束检查:
- @NotNull在firstname和lastname
- @NotNull 在 defaultCreditCard
因为Default和Billable是 的超接口BuyInOneClick,属于BuyInOneClick的一部分。
2、组序
默认情况下,无论约束属于哪个组,都不会按特定顺序评估约束(比如你指定了验证组序列为:Default、Billable,但并不一定会按照这个顺序)。然而,在某些情况下控制约束评估的顺序很有用,比如:
- 第二组依赖于稳定状态才能正常运行。这种稳定状态由第一组验证。
- 第二组是时间、CPU 或内存的大量消耗者,应尽可能避免对其进行评估。
Bean Validation使用@GroupSequence来定义组序列,从左往右排序(需要特别注意的是,当标注在class类上时有所不同)。
@Target({ TYPE })
@Retention(RUNTIME)
@documented
public @interface GroupSequence {
Class>[] value();
}
规则:
1、组序列中存在多个组(例如:@GroupSequence(value = {IsMobileGroups.HUNAN.class,Default.class})),会按照顺序一组一组的评估约束,只有当一组约束评估有效时才会评估下一组约束。
2、定义序列的组和组成序列的组不得参与循环依赖:
- 直接或间接
- 通过级联序列定义或组继承
如果对包含此类循环的组求值,则会引发GroupDefinitionException异常。
定义序列的组不应该直接继承其他组。换句话说,承载组序列的接口不应该有任何超级接口。
@GroupSequence(value = {IsMobileGroups.HUNAN.class,Default.class})
public interface PhoneSequence extends OttherInterface{
}
定义序列的组不应该在约束声明中直接使用。换句话说,承载组序列的接口不应该在约束声明中使用。
@GroupSequence(value = {IsMobileGroups.HUNAN.class,Default.class})
public interface PhoneSequence{
}
public class User {
@NotNull(group={PhoneSequence.class})
private String firstname;
。。。
}
示例:
@ZipCodeCoherenceChecker(groups = Address.HighLevelCoherence.class)
public class Address {
@NotNull @Size(max = 50)
private String street1;
@NotNull @ZipCode
private String zipCode;
@NotNull @Size(max = 30)
private String city;
public interface HighLevelCoherence {}
@GroupSequence({Default.class, HighLevelCoherence.class})
public interface Complete {}
}
当Address.Complete组被验证时,属于该Default组的所有约束都被验证。如果其中任何一个失败,验证将跳过该HighLevelCoherence组。如果所有Default约束都通过,HighLevelCoherence则会被评估约束。
3、重定义默认组
可以通过在类上标注@GroupSequence注解来重定义默认组,但这个默认组就是所标注类A的 A.class,且之前的Default.class不得在出现在该组序列中声明。
托管在类A并属于Default组(默认或显式)的约束注解的groups属性都隐式属于组A。
@GroupSequence({Address.class, HighLevelCoherence.class})
@ZipCodeCoherenceChecker(groups = Address.HighLevelCoherence.class)
public class Address {
@NotNull @Size(max = 50)
private String street1;
@NotNull @ZipCode
private String zipCode;
@NotNull @Size(max = 30)
private String city;
public interface HighLevelCoherence {}
}
在为Address重新定义默认组中,当为Default组验证地址对象时,将评估属于Default组并承载在Address上的所有约束。如果没有失败,将评估Address上的所有HighLevelCoherence约束。换句话说,当为Address验证Default组时,将使用在Address类上定义的组序列。
4、组转换
bean Validation 2.0新特性之一,在执行级联验证时,可以使用与最初使用组转换特性请求的组不同的组。组转换是通过使用@ConvertGroup注释声明的。
@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@documented
public @interface ConvertGroup {
Class> from() default Default.class;
Class> to();
@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@documented
public @interface List {
ConvertGroup[] value();
}
}
规则:
- @ConvertGroup需要搭配@Valid使用,则会引发一个ConstraintDeclarationException。
- 当一个元素被@Valid注释时,验证将被传播。除非使用@ConvertGroup注释,否则组将原样传递给嵌套的元素。
- 如果期望传递给嵌套元素验证的组被定义为@ConvertGroup注释的from属性,那么用于有效验证嵌套元素的组就是定义在to属性中的对应组。
- 如果未指定from属性的值,则Default.class将被用作转换的源组。
- 规则不会递归执行。如果找到匹配的规则,则不再计算后续规则。特别是,如果一组@ConvertGroup声明链A到B, B到C,则A组将被转换为B,而不是C。这将使规则更清晰。
- 有多个转换规则包含相同的from值是不合法的。在这种情况下,将引发一个ConstraintDeclarationException。
- 与常规约束声明一样,from属性不能引用组序列。在这种情况下会引发ConstraintDeclarationException。to属性可以。然后在验证关联对象之前展开组序列。
- 当存在继承层次的方法约束时,同样,如果子类重写了在两个不同接口或者一个类和一个接口中一摸一样的方法,则不能为该方法的返回值声明组转换规则。这也是为了避免向调用者保证的后置条件的意外更改。(请查看 示例三)
示例:
简单示例:
public interface Complete extends Default {}
public interface BasicPostal {}
public interface FullPostal extends BasicPostal {}
public class Address {
@NotNull(groups=BasicPostal.class)
String street1;
String street2;
@ZipCode(groups=BasicPostal.class)
String zipCode;
@CodeChecker(groups=FullPostal.class)
String doorCode;
}
public class User {
@Valid
@ConvertGroup(from=Default.class, to=BasicPostal.class)
@ConvertGroup(from=Complete.class, to=FullPostal.class)
Set getAddresses() { [...] }
}
当User使用Default组验证实例时,关联的地址将通过BasicPostal组进行验证。当User使用Complete组验证实例时,关联的地址将通过FullPostal组进行验证。
容器元素验证的组转换:
public class User {
Set<
@Valid
@ConvertGroup(from=Default.class, to=BasicPostal.class)
@ConvertGroup(from=Complete.class, to=FullPostal.class)
Address
> getAddresses() { [...] }
}
非法组转换:
public interface BasicPostal {}
public class Order { [...] }
public interface RetailOrderService {
@Valid
Order placeOrder(String itemNo, int quantity);
}
public interface B2BOrderService {
@Valid
@ConvertGroup(from=Default.class, to=BasicPostal.class)
Order placeOrder(String itemNo, int quantity);
}
public class OrderService implements RetailOrderService, B2BOrderService {
@Override
public Order placeOrder(String itemNo, int quantity) {
[...]
}
}
一、@Valid与Validated
@Valid为Bean Validation规范定义的注解,用于级联验证,可以标注在级联的属性、方法参数或方法返回类型上。
@Validated为Spring官方定义的,支持验证组的规范,通过value指定组,作用对象:
-
SpringMvc方法参数 。
-
标注在特定类上,与方法级验证一起使用(可以指定组),当标注在特定类上时,该类中的某些标注了校验注解的方法将会被验证(作为相应验证拦截器的切入点);注意: 不能再次将该注解标注在校验方法上,无效。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Phone {
private String type;
@IsMobileWithStart(mobileStart = "134")
private String mobile;
}
-----------------------------------------------------------
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@NotNull
private String name;
@NotNull
private Integer age;
@Valid
private Phone phone;
}
------------------------------------------------------------
@RequestMapping(value = "validation")
@RestController
@Validated
public class PhoneController {
@Autowired
private PhoneService phoneService;
@GetMapping(value = "/simplevalid")
public ResponseData simplevalid(@Validated(value = {Default.class}) User user, BindingResult resultError){
[。。。。]
}
}
2、标注特定类上,比如service类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Phone {
@NotNull(groups = Groups.GROUP1.class)
private String type;
@IsMobileWithStart(mobileStart = "134",type = IsMobileWithStart.Type.CHINATELECOM)
private String mobile;
}
--------------
@Validated(value = {Groups.GROUP1.class})
public interface PhoneService {
void insertPhone(@Valid Phone phone);
}
@Service
public class PhoneServiceImpl implements PhoneService {
@Override
public void insertPhone(Phone phone) {
if (phone != null) {
//插入逻辑
System.out.println("插入成功");
} else {
System.out.println("插入失败");
}
}
}
--------------
@GetMapping("/exceptionHandler")
public ResponseData ExceptonHandler(User user){
Phone phone = user.getPhone();
phone.setType(null);
phoneService.insertPhone(phone);
return ResponseData.success();
}
解析:
Phone中的type字段用@NotNull注解标注,且指定了group为Groups.GROUP1,mobile字段则使用了自定义的@IsMobileWithStart标注,默认为Default组。
PhoneService接口上标注了@Validated且指定了校验组Groups.GROUP1,且对insertPhone方法的Phone属性添加了@Valid,因为这会对Phone中满足Groups.GROUP1组的校验注解标注的目标进行校验。
ontroller层的ExceptonHandler方法,接受请求,并将Phone的type属性设置为null,用于@NotNull注解的验证。
错误情况:
@Validated(value = {Groups.GROUP1.class})
public interface PhoneService {
void insertPhone(@Validated Phone phone);
}
二、错误信息获取
对于错误消息的获取可以在方法参数中加入BindingResult参数
@GetMapping(value = "/simplevalid")
public ResponseData simplevalid(@Validated(value = {Default.class}) User user, BindingResult resultError){
if (resultError.hasErrors()) {
return ResponseData
.error(resultError.getAllErrors()
.stream()
.map(e -> e.getDefaultMessage())
.collect(Collectors.joining(",")));
}
phoneService.insertPhone(user.getPhone());
return ResponseData.success();
}
三、全局的异常处理
在一个一个的Controller方法中获取BindingResult对象,再进行相应的处理似乎有点单调且重复,那么就使用全局的异常处理来解决吧。主要是这两个异常:
BindException:对SpringMVC的参数进行校验是会出现该异常。
ValidationException:@Validated标注在特定类上,对某些方法上需要校验的Bean,出现校验失败时会出现该异常。
@RestControllerAdvice
public class ValidatorExceptionHandler {
@ExceptionHandler(value = {BindException.class})
private ResponseData handlerError(BindException bindException) {
String errFields = bindException.getBindingResult().getFieldErrors().stream().map(e -> e.getField()).collect(Collectors.joining(","));
String errorMsg = bindException.getAllErrors().stream().map(e ->e.getDefaultMessage()).collect(Collectors.joining(","));
return ResponseData.error("属性 = " + errFields + ", errorMsg = " + errorMsg).status(false);
}
@ExceptionHandler(value = {ValidationException.class})
private ResponseData handlerError(ValidationException exception){
return ResponseData.error("数据校验错误:" + exception.getMessage()).status(false);
}
}
四、分组校验
用于在不用的情况下进行不同的校验,关于分组校验,组继承、组序列、重定义默认组、以及组的转换可查看上面的介绍。
注意:对于自定义注解也同样适用
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GroupPhone {
@Pattern(regexp = "联通")
private String type;
@Max(10)
private Integer money;
@IsMobileWithStart(mobileStart = "134",type = IsMobileWithStart.Type.CHINATELECOM, groups = Groups.GROUP1.class)
private String mobile;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GroupUser {
@Pattern(regexp = "zc",groups = Groups.GROUP1.class)
private String name;
@Max(value = 12,groups = Groups.GROUP1.class)
private Integer age;
@Valid
private GroupPhone phone;
}
-------------------
@RestController
@RequestMapping("/user")
public class GroupPhoneController {
@GetMapping("/simpleGroup")
public ResponseData simpleGroup(@Validated(value = {Groups.GROUP1.class}) GroupUser user){
// 。。。
return ResponseData.success();
}
}
仅仅会校验所属组为Groups.GROUP1的校验注解所标注的数据,此处为GroupUser的name、agetype,GroupPhone的mobile。
localhost:8080/user/simpleGroup?name=zc&age=12&phone.type=联&phone.money=11&phone.mobile=132
注意: 此处的校验过程是递归的,且会校验@Valid标注Bean下所有的校验注解标注的属性,而不是只要一个不满足就报错。
将@Validated分组校验修改为Default.class,结果如下:
{
"success": false,
"code": 500,
"message": "属性 = phone.type,phone.money, errorMsg = 需要匹配正则表达式"联通",最大不能超过10",
"data": null
}
2、组序列
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Phone {
@Pattern(regexp = "联通")
private String type;
@IsMobileWithStart(mobileStart = "134",type = IsMobileWithStart.Type.CHINATELECOM,groups = Group.GROUP2.class)
private String mobile;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Pattern(regexp = "zc")
private String name;
@Max(value = 23,groups = Group.GROUP1.class)
private Integer age;
@Valid
private Phone phone;
}
---------------------------------
@GroupSequence(value = {Group.GROUP1.class,Default.class, Group.GROUP2.class})
public interface GroupSeq{
}
--------------------------------
@RequestMapping("/set")
public ResponseData setDefaultValidation(@Validated(value = GroupSeq.class) User user){
System.out.println(user);
return ResponseData.success();
}
会按照GroupSeq指定的顺序依次对元素进行校验,一旦前一个不满足后续的组就不会再校验,比如:(GROUP1组校验不过,Default,GROUP2的元素就不会进行校验)。
3、重定义默认组
只验证Default组时,无法验证GROUP1组、GROUP2组,为了确保完整的验证,用户必须在@Validated中使用该GroupSeq组,这破坏了可以预期的一些封装。可以通过重新定义Default组对给定类的含义来解决此问题。
示例一:
@Data
@NoArgsConstructor
@AllArgsConstructor
@GroupSequence(value = {Group.GROUP2.class,Phone.class})
public class Phone {
@Pattern(regexp = "联通")
private String type;
@IsMobileWithStart(mobileStart = "134",type = IsMobileWithStart.Type.CHINATELECOM,groups = Group.GROUP2.class)
private String mobile;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@GroupSequence(value = {Group.GROUP1.class,User.class})
public class User {
@Pattern(regexp = "zc")
private String name;
@Max(value = 23,groups = Group.GROUP1.class)
private Integer age;
@Valid
private Phone phone;
}
------------------------------------
@RequestMapping("/set")
public ResponseData setDefaultValidation(@Validated User user){
System.out.println(user);
return ResponseData.success();
}
localhost:8081/user/set?name=z&age=24&phone.type=联&phone.money=11&phone.mobile=132
{
"success": false,
"code": 500,
"message": "属性 = phone.mobile,age, errorMsg = 132不是以134开头-中国电信,最大不能超过23",
"data": null
}
User -> 校验age失败
Phone -> mobile校验失败
示例二:
@GroupSequence({ Minimal.class, Driver.class })
public class Driver {
@Min(value = 18, groups = Minimal.class)
int age;
@AssertTrue
Boolean passedDrivingTest;
@Valid
Car car;
// setter/getters
}
@GroupSequence({ Car.class, Later.class })
public class Car {
@NotNull
String type;
@AssertTrue(groups = Later.class)
Boolean roadWorthy;
// setter/getters
}
--------------------------
@GroupSequence({ Minimal.class, Later.class })
public interface SequencedGroups {
}
Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); Driver driver = new Driver(); driver.setAge(16); Car porsche = new Car(); driver.setCar(porsche); Set> violations = validator.validate( driver ); assert violations.size() == 2; violations = validator.validate( driver, SequencedGroups.class ); assert violations.size() == 1;
未指定SequencedGroups时:
两个错误:Driver的age、Car的type
指定SequencedGroups时:
一个错误:Driver的age,属于Later组的约束直到所有属于Minimal组的约束都得到验证才会进行验证。
此处针对校验注解进行国际化,且会重定义约束消息,以及自定义属性,如下:
重要注解与接口:Bean Validation 校验器注解的元注解 - @javax.validation.Constraint
作用:
- 标注在目标校验注解上,并指定Bean Validation 校验器的实现。
Bean Validation 校验器 - ConstraintValidator接口
作用:
- 实现该接口,定义Bean Validation 校验器校验逻辑,包含需要指定两个泛型:第一个为注解类型,第二个为处理参数类型
方法:
-
初始化方法 - #initialize 通过注解方法获取相关的元信息
-
校验方法 - #isValid 通过对象传入,并且控制 ConstraintValidatorContext
自定义校验注解:
@Target({TYPE,ANNOTATION_TYPE,FIELD,CONSTRUCTOR,PARAMETER,METHOD})
@Retention(RUNTIME)
@Constraint(validatedBy = {IsMobilevalidator.class})
@Repeatable(IsMobileWithStart.List.class)
public @interface IsMobileWithStart {
String message() default "{mobile.mobileIsNotStart}";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
Type type() default Type.CHINAMOBILE;
String mobileStart();
@Target({TYPE,ANNOTATION_TYPE,FIELD,CONSTRUCTOR,PARAMETER,METHOD})
@Retention(RUNTIME)
public @interface List{
IsMobileWithStart[] value();
}
enum Type{
CHINAMOBILE("中国移动"),
CHINATELECOM("中国电信"),
CHINAUNICOM("中国联通");
private String cnName;
Type(String cnName){
this.cnName = cnName;
}
public String getCnName(){
return cnName;
}
}
}
校验器:
public class IsMobilevalidator implements ConstraintValidator{ private String mobileStart; private IsMobileWithStart.Type type; private String mobileIsEmpty = ""; private String mobileIsNotStart = ""; @Override public void initialize(IsMobileWithStart constraintAnnotation) { this.mobileStart = constraintAnnotation.mobileStart(); this.type = constraintAnnotation.type(); } @Override public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { // 自定义属性值,可以通过{typeStr}获取 HibernateConstraintValidatorContext unwrap = constraintValidatorContext.unwrap(HibernateConstraintValidatorContext.class); unwrap.addMessageParameter("typeStr",type.getCnName()); //重定义约束消息 constraintValidatorContext.disableDefaultConstraintViolation(); // 此处之所以这样是在重定义约束消息后,无法通过${validatedValue}取到要验证的值,当然也可以通过自定义属性来取 mobileIsEmpty = s + "{mobile.mobileIsEmpty}"; mobileIsNotStart = s + "{mobile.mobileIsNotStart}"; if (s == null || "".equals(s)) { constraintValidatorContext .buildConstraintViolationWithTemplate(mobileIsEmpty) .addConstraintViolation(); return false; } if(!s.startsWith(mobileStart)){ constraintValidatorContext .buildConstraintViolationWithTemplate(mobileIsNotStart) .addConstraintViolation(); return false; } return true; } }
国际化
方式一:
此处介绍默认情况下的国际化,要求:只需要国际化文件名称必须为ValidationMessages即可
mobile.mobileIsEmpty=mobile is NULL
mobile.mobileIsNotStart=don't start with {mobileStart} - {typeStr}
mobile.mobileIsEmpty=电话号码为空
mobile.mobileIsNotStart=不是以{mobileStart}开头-{typeStr}
注意:可以通过${validatedValue}取到要校验的值,但如果发生了重定义约束消息后就无法取到。
方式二:
此处介绍通过配置MessageResolver与LocaleResolver来自定义的国际化,并不需要国际化文件名必须为 ValidationMessages。
LocaleResolver: 解析Request中的语言标志参数或者head中的Accept-Language参数, 并将解析后的参数保存到指定的LocaleContextHolder域中。
SpringMVC默认提供了四种实现了接口的类:cookieLocaleResolver, AcceptHeaderLocalResolver,FixdLocaleResolver,SessionLocaleResolver。
MessageResolver:国际化消息处理的顶层接口,存在两个实现类:ResourceBundleMessageResolver、PropertiesMessageResolver,当然你也可以实现AbstractMessageResolver来自定义MessageResolver
MessageSource:以用于支持信息的国际化和包含参数的信息的替换。
主要流程是通过配置springboot的LocaleResolver解析器,当请求打到springboot的时候对请求的所需要的语言进解析,并保存在LocaleContextHolder中。
MessageResolver解析器从MessageSource中获取到key对应的message,而locale值是从LocaleContextHolder获取的。
1、定义LocaleResolver,并注册bean
@Configuration
public class LocaleResolverConfig {
@Bean
public LocaleResolver localeResolver() {
AcceptHeaderLocaleResolver acceptHeaderLocaleResolver = new AcceptHeaderLocaleResolver();
acceptHeaderLocaleResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
return acceptHeaderLocaleResolver;
}
}
2、定义 MessageResolver
@RequiredArgsConstructor
public class MessageResolverConfig extends AbstractMessageResolver {
private final MessageSource messageSource;
@Override
protected String getMessage(String key) {
return messageSource.getMessage(key,null, LocaleContextHolder.getLocale());
}
}
3、将自定义的MessageResolver注册为bean
@RequiredArgsConstructor
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private final MessageSource messageSource;
@Bean
public MessageResolver messageResolver() {
return new MessageResolverConfig(messageSource);
}
@Bean
@Override
public LocalValidatorFactoryBean getValidator() {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource);
return bean;
}
}
4、配置国际化文件的位置
spring.messages.basename=message # 多个文件用逗号分隔,使用/表示层级



