做 Java Web 相关项目时,虽然使用过 @NotBlank
,@NotEmpty
之类的注解,但在读周志明老师《凤凰架构》一书中关于验证的内容时,才意识到自己的局限性。我之前并不了解这个东西背后的逻辑和思想,以及最佳实践,所以用这篇文章来补补课。
认知
什么是「验证」,上图就是一个简单直观的示例,笔者这里就不赘述了。如果说「认证」是“你是谁”,「授权」是“你能做什么”,那么「验证」就是“你做得对不对”。也许是因为相比黑客攻击这种叙事,数据验证实在太枯燥无聊了,以至于很多软件工程师并没有将「验证」划归到安全这一范畴之中。其实相比黑客攻击,由于数据验证不严谨导致的问题要多得多,只是风险大小不一而已。
而说到数据验证的枯燥无味,下面就是一个很经典的段子(摘抄于《凤凰架构》):
前 端: 提交一份用户数据(姓名:某, 性别:男, 爱好:女, 签名:xxx, 手机:xxx, 邮箱:null)
控制器: 发现邮箱是空的,抛ValidationException("邮箱没填")
前 端: 已修改,重新提交
安 全: 发送验证码时发现手机号少一位,抛RemoteInvokeException("无法发送验证码")
前 端: 已修改,重新提交
服务层: 邮箱怎么有重复啊,抛BusinessRuntimeException("不允许开小号")
前 端: 已修改,重新提交
持久层: 签名字段超长了插不进去,抛SQLException("插入数据库失败,SQL:xxx")
…… ……
前 端: 你们这些坑管挖不管埋的后端,各种异常都往前抛!
用 户: 这系统牙膏厂生产的?
校验少了,数据质量不够,校验多了呢?并不会使系统更健壮,反而在浪费机器资源,做无效的计算。而另一个争论的点是,放在哪一层做验证?我们以 Spring 中典型的分层结构为例:
1)控制层(Controller)验证:理由是服务层有同级调用(比如 ServiceA 调用 ServiceB),会出现参数重复校验的情况,所以放在控制层;
2)服务层(Service)验证:无业务含义的,前端表单已校验,有业务含义的放在控制层显然不合适;
3)控制层(Controller)和服务层(Service)各自验证:控制层做格式校验,服务层做业务校验,好像很合理,但这也是上面段子的出处;
4)持久层(Repository)验证:数据库的入口,把好这一关最重要;
5)……
争论不休,最佳实践是什么呢?没错,Java Bean Validation,这也是 Java 官方给出的答案。
简单来说,就是将校验从分层中解耦,不在任何一层中做,全部放在 Java Bean 中,比如我们都见过下面类似的代码:
public class XxxRequest {
@ApiModelProperty("协议类型")
@Pattern(regexp = "^(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH)$", message = "请求方法不合法")
private String method;
@ApiModelProperty("api名称")
@NotBlank(message = "接口名称不能为空")
private String name;
// ...
}
@RestController
@RequestMapping("/xxx")
@Validated
public class XxxController {
// ...
@PostMapping
public JsonResult<String> add(@Valid @RequestBody XxxRequest request) {
String id = service.xxx(request);
return new JsonResult(true, "添加成功", id);
}
// ...
}
遗憾的是,我们的服务层还是有很多下面这类代码:
public void Xxx(XxxRequest param) {
if (Objects.isNull(param.getXxx())) {
throw new RuntimeException("不行啊~");
}
}
这还是分层的思路,换成 Bean 校验的意思是,此处跟控制层校验一样(如果需要校验),即:
public void Xxx(@Valid XxxRequest param) {
}
此处小伙伴们可能会有疑惑,这会造成重复校验呀?别怕,有分组校验,下文将会提到。我们先来看看 Bean 校验的优势,周志明老师总结了以下几点:
1)无业务含义的格式校验,可以做到预置;
2)Bean 可以重用,于是有业务含义的校验在 Bean 上只需要实现一次,而放在方法上,则会产生冗余的代码;
3)集中化管理,比如格式化,国际化,统一认证的异常体系等;
4)避免输入数据的防御污染到业务代码,比如我们上面服务层校验的例子;
5)多个校验器统一执行处理,一次性返回所有错误信息,从而避免了上面段子中的挤牙膏做法。
遗憾的是,大多数情况下我们只是简单的在控制层使用了一些内置的校验组件,比如:
@AssertFalse, @AssertTrue, @DecimalMax, @DecimalMin, @Digits, @Email, @Future,
@FutureOrPresent, @Max, @Min, @Negative, @NegativeOrZero, @NotBlank, @NotEmpty,
@NotNull, @Null, @Past, @PastOrPresent, @Pattern, @Positive, @PositiveOrZero, @Size
其中 @Future
,@Past
等注解用来验证日期时间是将来或过去,而 @Digits
则用来限制数字整数部分和小数部分的位数。由于大部分注解都比较常见,笔者就不赘述了。需要重点说明的是,这些注解都脱胎于规范,即 JSR 303, JSR 349 和 JSR 380。
演化
1.0:JSR 303
Validating data is a common task that occurs throughout an application, from the presentation layer to the persistence layer. Often the same validation logic is implemented in each layer, proving to be time consuming and error-prone. To avoid duplication of these validations in each layer, developers often bundle validation logic directly into the domain model, cluttering domain classes with validation code that is, in fact, metadata about the class itself.
This JSR defines a metadata model and API for JavaBean validation. The default metadata source is annotations, with the ability to override and extend the meta-data through the use of XML validation descriptors.
The validation API developed by this JSR is not intended for use in any one tier or programming model. It is specifically not tied to either the web tier or the persistence tier, and is available for both server-side application programming, as well as rich client Swing application developers. This API is seen as a general extension to the JavaBeans object model, and as such is expected to be used as a core component in other specifications. Ease of use and flexibility have influenced the design of this specification.
2009 年发布的 JSR 303 是 1.0 版本,主要定义了 Java Bean 校验的基础模型和 API,并使用「注解」这一 Java 特性来实现。
/**
* The annotated element must not be {@code null}.
* Accepts any type.
*
* @author Emmanuel Bernard
*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { })
public @interface NotNull {
String message() default "{javax.validation.constraints.NotNull.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
/**
* Defines several {@link NotNull} annotations on the same element.
*
* @see javax.validation.constraints.NotNull
*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@interface List {
NotNull[] value();
}
}
上面是内置的 @NotNull
注解,我们将其作为一个示例来看看其内部的基本配置:
1)@Target
,@Retention
以及 @Documented
等都是之前我们熟悉的元注解(用来标注其他注解的注解),比如 @Documented
用于指示在使用 javadoc 等工具时,将当前注解的内容(即 @NotNull
)生成到注释文档中,否则 @NotNull
不会出现在 JavaDoc 文档中;
2)而比较陌生的 @Repeatable
和 @Constraint
则是我们比较陌生的元注解,其中 @Repeatable
表示可以在一个元素上重复使用该注解,而 @Constraint
用来声明校验的实现约束,具体的验证器实现类则会通过 validatedBy 属性进行指定,比如下面的例子:
@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 {};
}
那为什么默认的校验注解(比如 @NotNull
等),@Constraint
中的 validatedBy 属性都为空?这是因为这些默认注解的验证器实现类跟注解本身解耦了。
@Documented
@Target({ ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface Constraint {
// 验证器类需要实现 ConstraintValidator 接口
Class<? extends ConstraintValidator<?, ?>>[] validatedBy();
}
比如 hibernate-validator
就是一个具体实现,它实现了 validation-api
中定义的相关接口:
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
@NotNull
对应的验证器类 @NotNullValidator
就在 hibernate-validator
当中,我们来看看:
public class NotNullValidator implements ConstraintValidator<NotNull, Object> {
@Override
public boolean isValid(Object object, ConstraintValidatorContext constraintValidatorContext) {
return object != null;
}
}
实现逻辑很简单,就是判断当前被注解的对象是否为 null。也就是说,这些内置的注解,当它们被使用时,框架内部会去找他们的默认实现,将它们进行匹配,比如这里的 @NotNull
和 NotNullValidator
。
此时再看 ConstraintValidator
接口会发现它也很简单,initialize 方法用于初始化,isValid 方法则是逻辑实现。
public interface ConstraintValidator<A extends Annotation, T> {
// Initializes the validator in preparation for isValid(Object, ConstraintValidatorContext) calls.
default void initialize(A constraintAnnotation) {
}
// Implements the validation logic.
boolean isValid(T value, ConstraintValidatorContext context);
}
下面是一个简单的 Demo:
@log4j2
public class User {
@NotBlank(message = "姓名不能为空")
private String name;
@NotNull(message = "年龄不能为空")
private Integer age;
public User() {
}
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
public static void main(String[] args) {
User user = new User();
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<User>> violations = validator.validate(user);
violations.forEach(log::info);
}
}
输出:
16:17:07.439 [main] DEBUG org.jboss.logging - Logging Provider: org.jboss.logging.Log4j2LoggerProvider
16:17:07.440 [main] INFO org.hibernate.validator.internal.util.Version - HV000001: Hibernate Validator 8.0.1.Final
16:17:07.444 [main] DEBUG org.hibernate.validator.internal.xml.config.ValidationXmlParser - Trying to load META-INF/validation.xml for XML based Validator configuration.
16:17:07.444 [main] DEBUG org.hibernate.validator.internal.xml.config.ResourceLoaderHelper - Trying to load META-INF/validation.xml via TCCL
16:17:07.444 [main] DEBUG org.hibernate.validator.internal.xml.config.ResourceLoaderHelper - Trying to load META-INF/validation.xml via Hibernate Validator's class loader
16:17:07.444 [main] DEBUG org.hibernate.validator.internal.xml.config.ValidationXmlParser - No META-INF/validation.xml found. Using annotation based configuration only.
16:17:07.446 [main] DEBUG org.hibernate.validator.internal.engine.resolver.TraversableResolvers - Cannot find jakarta.persistence.Persistence on classpath. Assuming non JPA 2 environment. All properties will per default be traversable.
16:17:07.458 [main] DEBUG org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator - Loaded expression factory via original TCCL
16:17:07.491 [main] DEBUG org.hibernate.validator.internal.engine.ValidatorFactoryConfigurationHelper - HV000252: Using org.hibernate.validator.internal.engine.DefaultPropertyNodeNameProvider as property node name provider.
16:17:07.493 [main] DEBUG org.hibernate.validator.internal.engine.ValidatorFactoryConfigurationHelper - HV000234: Using org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator as ValidatorFactory-scoped message interpolator.
16:17:07.493 [main] DEBUG org.hibernate.validator.internal.engine.ValidatorFactoryConfigurationHelper - HV000234: Using org.hibernate.validator.internal.engine.resolver.TraverseAllTraversableResolver as ValidatorFactory-scoped traversable resolver.
16:17:07.493 [main] DEBUG org.hibernate.validator.internal.engine.ValidatorFactoryConfigurationHelper - HV000234: Using org.hibernate.validator.internal.util.ExecutableParameterNameProvider as ValidatorFactory-scoped parameter name provider.
16:17:07.493 [main] DEBUG org.hibernate.validator.internal.engine.ValidatorFactoryConfigurationHelper - HV000234: Using org.hibernate.validator.internal.engine.DefaultClockProvider as ValidatorFactory-scoped clock provider.
16:17:07.493 [main] DEBUG org.hibernate.validator.internal.engine.ValidatorFactoryConfigurationHelper - HV000234: Using org.hibernate.validator.internal.engine.scripting.DefaultScriptEvaluatorFactory as ValidatorFactory-scoped script evaluator factory.
16:17:07.527 [main] INFO jvm.tech.demonlee.tmp.model.User - ConstraintViolationImpl{interpolatedMessage='年龄不能为空', propertyPath=age, rootBeanClass=class jvm.tech.demonlee.tmp.model.User, messageTemplate='年龄不能为空'}
16:17:07.527 [main] INFO jvm.tech.demonlee.tmp.model.User - ConstraintViolationImpl{interpolatedMessage='姓名不能为空', propertyPath=name, rootBeanClass=class jvm.tech.demonlee.tmp.model.User, messageTemplate='姓名不能为空'}
注意日志的前面大部分内容,这些都是 Hibernate Validator 初始化所执行的步骤。
1.1:JSR 349
2013 年发布的 JSR 349 主要是对校验的动态性和灵活性进行升级,比如分组校验,方法出入参的校验,依赖注入支持等。分组校验是一个比较高频的需求,比如更新和插入使用的是一个 Bean,但校验的逻辑并不相同,此时就需要使用分组。
Bean Validation 1.1 focused on the following topics:
- openness of the specification and its process
- method-level validation (validation of parameters or return values)
- dependency injection for Bean Validation components
- integration with Context and Dependency Injection (CDI)
- group conversion
- error message interpolation using EL expressions
我们来看看分组校验的 Demo:
@Log4j2
public class User {
private interface Insert {
}
private interface Update {
}
@NotBlank(message = "id不能为空", groups = {Update.class})
private String id;
@NotBlank(message = "姓名不能为空", groups = {Insert.class})
private String name;
@NotNull(message = "年龄不能为空", groups = {Insert.class, Update.class})
private Integer age;
public User() {
}
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
public static void main(String[] args) {
User user = new User();
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
test("default checking...", validator.validate(user));
test("insert checking...", validator.validate(user, Insert.class));
test("update checking...", validator.validate(user, Update.class));
test("all checking...", validator.validate(user, Insert.class, Update.class));
}
private static void test(String message, Set<ConstraintViolation<User>> violations) {
log.info(message);
violations.forEach(v -> log.info(v.getMessage()));
}
}
输出日志(部分)为:
20:16:54.805 [main] INFO jvm.tech.demonlee.tmp.model.User - default checking...
20:16:54.827 [main] INFO jvm.tech.demonlee.tmp.model.User - insert checking...
20:16:54.829 [main] INFO jvm.tech.demonlee.tmp.model.User - 姓名不能为空
20:16:54.829 [main] INFO jvm.tech.demonlee.tmp.model.User - 年龄不能为空
20:16:54.829 [main] INFO jvm.tech.demonlee.tmp.model.User - update checking...
20:16:54.829 [main] INFO jvm.tech.demonlee.tmp.model.User - 年龄不能为空
20:16:54.829 [main] INFO jvm.tech.demonlee.tmp.model.User - id不能为空
20:16:54.830 [main] INFO jvm.tech.demonlee.tmp.model.User - all checking...
20:16:54.830 [main] INFO jvm.tech.demonlee.tmp.model.User - 姓名不能为空
20:16:54.830 [main] INFO jvm.tech.demonlee.tmp.model.User - 年龄不能为空
20:16:54.830 [main] INFO jvm.tech.demonlee.tmp.model.User - id不能为空
可以看到,这种分组能力应对不同的业务场景时,还是很香的。
2.0:JSR 380
2017 年发布的 JSR 380 带来了 2.0 版本,适配 Java 8,并提供了更精细化的校验,比如支持容器元素级校验。
Bean Validation 2.0 focused on the following topics:
- support for validating container elements by annotating type arguments of parameterized types e.g.
List<@Positive Integer> positiveNumbers
. This also includes:
- more flexible cascaded validation of container types
- support for
java.util.Optional
- support for the property types declared by JavaFX
- support for custom container types
- support for the new date/time data types (JSR 310) for
@Past
and@Future
- new built-in constraints:
@NotEmpty
,@NotBlank
,@Positive
,@PositiveOrZero
,@Negative
,@NegativeOrZero
,@PastOrPresent
and@FutureOrPresent
- leverage the JDK 8 new features (built-in constraints are marked repeatable, parameter names are retrieved via reflection)
这个版本新增了不少注解,比如前面已经简单介绍过的 @Past
和 @Future
,下面的示例用于演示容器元素的校验:
@Log4j2
public class User {
private interface Insert {
}
private interface Update {
}
private String id;
@NotEmpty(message = "分数不能为空", groups = {Insert.class, Update.class})
private List<@Positive(message = "分数必须为正整数", groups = {Insert.class, Update.class}) Integer> scores;
public User() {
}
public static void main(String[] args) {
User user = new User();
user.scores = List.of(5, 8, 0, 9, -3);
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
test("checking 1...", validator.validate(user, Insert.class, Update.class));
user.scores = List.of();
test("checking 2...", validator.validate(user, Insert.class, Update.class));
}
private static void test(String message, Set<ConstraintViolation<User>> violations) {
log.info(message);
violations.forEach(v -> log.info("{}: {}", v.getMessage(), v.getInvalidValue()));
}
}
输出日志(部分)为:
21:13:07.379 [main] INFO jvm.tech.demonlee.tmp.model.User - checking 1...
21:13:07.383 [main] INFO jvm.tech.demonlee.tmp.model.User - 分数必须为正整数: -3
21:13:07.383 [main] INFO jvm.tech.demonlee.tmp.model.User - 分数必须为正整数: 0
21:13:07.383 [main] INFO jvm.tech.demonlee.tmp.model.User - checking 2...
21:13:07.383 [main] INFO jvm.tech.demonlee.tmp.model.User - 分数不能为空: []
实践
但遗憾的是,大多数情况下我们只使用了简单的基础校验,对于复杂的业务逻辑校验则是手撸代码,所以周志明老师才说这是程序员版本的买椟还珠。比如,当我们要在创建员工的接口中判断手机号等信息是否重复时,大概率会在 Service 层写下如下类似的代码:
public String create(StaffRequest request) {
// ...
int count = repo.countByPhone(reqeust.getPhone());
if(count > 0) {
throw new RuntimeException("手机号与现有用户产生重复");
}
// ...
}
正确的做法应该是什么呢?没错,把这段逻辑定义成一个自定义校验注解,比如:
@Documented
@Constraint(validatedBy = NotConflictStaffValidator.class)
@Target({ METHOD, FIELD, PARAMETER, TYPE })
@Retention(RUNTIME)
public @interface NotConflictStaff {
String message() default "员工手机号等信息与现有员工产生重复";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
然后再实现 NotConflictStaffValidator:
@Slf4j
public class NotConflictStaffValidator implements ConstraintValidator<NotConflictStaff, Staff> {
@Resource
private StaffRepository repo;
@Override
public boolean isValid(Staff request, ConstraintValidatorContext context) {
return repo.countByPhone(request) == 0;
}
}
最后是配置 NotConflictStaff 这个注解。对此,笔者特地梳理了 @Valid
和 @Validated
,我们先看 JavaDoc 描述:
/**
* Marks a property, method parameter or method return type for validation cascading.
* <p>
* Constraints defined on the object and its properties are be validated when the
* property, method parameter or method return type is validated.
* <p>
* This behavior is applied recursively.
*/
@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
public @interface Valid {
}
/**
* Variant of JSR-303's {@link javax.validation.Valid}, supporting the
* specification of validation groups. Designed for convenient use with
* Spring's JSR-303 support but not JSR-303 specific.
*
* <p>Can be used e.g. with Spring MVC handler methods arguments.
* Supported through {@link org.springframework.validation.SmartValidator}'s
* validation hint concept, with validation group classes acting as hint objects.
*
* <p>Can also be used with method level validation, indicating that a specific
* class is supposed to be validated at the method level (acting as a pointcut
* for the corresponding validation interceptor), but also optionally specifying
* the validation groups for method-level validation in the annotated class.
* Applying this annotation at the method level allows for overriding the
* validation groups for a specific method but does not serve as a pointcut;
* a class-level annotation is nevertheless necessary to trigger method validation
* for a specific bean to begin with. Can also be used as a meta-annotation on a
* custom stereotype annotation or a custom group-specific validated annotation.
*
*/
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {
/**
* Specify one or more validation groups to apply to the validation step
* kicked off by this annotation.
* <p>JSR-303 defines validation groups as custom annotations which an application declares
* for the sole purpose of using them as type-safe group arguments, as implemented in
* {@link org.springframework.validation.beanvalidation.SpringValidatorAdapter}.
* <p>Other {@link org.springframework.validation.SmartValidator} implementations may
* support class arguments in other ways as well.
*/
Class<?>[] value() default {};
}
1)@Valid
是 JSR 303 中的原生标准注解,支持对象属性以及方法参数,不支持分组(即默认情况只校验 javax.validation.groups.Default 分组),并且支持「递归」地验证对象中的属性及其子属性,官方称之为Object graph validation
:
The
@Valid
annotation on a given association (i.e. object reference or collection, array,Iterable
of objects), dictates the Bean Validator implementation to apply recursively the Bean Validation routine on (each of) the associated object(s). This mechanism is recursive: an associated object can itself contain cascaded references.
2)@Validated
是 Spring 框架对 @Valid
注解的变体,支持分组,但如果想支持「递归」级联校验,需要与 @Valid
配合使用。
3)@Validated
能对 Spring Bean 中的方法参数和返回值进行校验,但需要在类上标注该注解进行激活;另外,方法上的 @Validated
注解会覆盖类上的 @Validated
。
下面是 Spring Service 层的一个使用示例:
@Service
@Validated // 开启校验
public class XxxFacadeService {
// ...
@Validated(XxxClient.class) // 覆盖上面的默认分组
@Transactional(rollbackFor = Exception.class)
@NotBlank(message = "创建完成,返回参数不能为空", groups = {XxxClient.class}) // 返回值校验
public String add(@NotConflictXxx(groups = {XxxClient.class}) @Valid XxxInsertRequest request) { // 此处 @Valid 会递归校验 XxxInsertRequest 中属于 XxxClient.class 分组的属性
// ...
}
// ...
}
怎么样?通过这种机制就能将散落在各处类似的校验逻辑「封装」起来,通过注解将其「切入」到主流程中,从而让业务逻辑更简洁清晰。当然,除了注解,我们也可以使用代码进行触发。正如前面所说的,我们并不限制在某一层(Controller,Service 或 Mapper 等)进行校验,而是将其封装到 Bean 中,需要消费这个 Bean 的地方,就可以择机开启校验,从而避免与任何一层耦合。
另外,周志明老师还给出两条建议:
1)将不带业务含义的校验放在 Bean 的类定义中,而带业务含义的校验则放在 Bean 的外面,也就是消费 Bean 的地方。
2)自定义 Bean Validator 要预置好默认的提示信息(message),像上面 NotConflictStaff 中定义的那样。
将校验放在类定义中,往往能自动运行。但对于带业务含义的校验,则不仅仅是多耗费一些时间和运算资源(比如数据库,网络等)的问题,更重要的是服务的幂等性。所以,我们要将其放在类定义的外面,然后手动配置是否需要开启校验。
当然,也有一些质疑,比如性能,但还是那句话:过早优化是万恶之源。如果确实有性能问题,请先给出具体的有说服力的测试数据。还有就是,我们这里讨论的校验更多的是面向外部的防御,比如用户输入的信息,第三方服务传入的参数等。针对服务内部自身的调用,开发人员可以使用 Bean Validator,也可以使用其他的方式,并没有绝对正确路径。
最后,本文并未对 Java Bean Validation 的实现给出更多技术细节的分析,如有需要,可结合官方文档和 AI 进行解锁。