我被 JSR 380 折腾过多次,因此特别欣赏一个不需要拖入整个依赖生态的验证框架。Solon 的验证系统位于 solon-security-validation —— 一个单一插件,提供 20+ 注解、实体验证和自定义验证器,且无需引入 javax.validation 或 jakarta.validation。
下面我分享一些我认为有用的内容。
两阶段验证模型
Solon 将验证分为两个阶段:
- 上下文验证(注入前)—— 在方法参数解析前运行。方法上的注解检查请求头、Cookie、IP 或请求级约束。
- 参数验证(注入后)—— 在参数解析后运行。针对单个参数或实体字段的注解。
这对应两种不同的实现机制:
- 上下文验证底层使用
@Addition(Filter.class) - 参数验证使用
@Around(Interceptor.class)
大多数时候你不需要考虑这些,但它解释了为什么某些注解放在方法上,另一些放在参数上。
入门:控制器上的 @valid
在控制器类(或其基类)上添加 @Valid,然后为参数添加注解:
@Valid
@Controller
public class UserController {
@Mapping("/user/add")
public void addUser(
@NotNull String name,
@Email String email,
@Pattern("^https?://") String avatarUrl,
@Length(min = 6, max = 20) String password) {
// 所有参数在此方法运行前已完成验证
}
}
进入全屏模式 退出全屏模式
就是这样。无需配置 ValidatorFactory 或声明 Bean。在类上添加 @Valid 即可为所有处理器方法启用验证。
@Validated 用于实体(嵌套)验证
当参数是一个带有自身验证规则的 DTO 时,使用 @Validated:
@Valid
@Controller
public class UserController {
@Mapping("/user/register")
public void register(@Validated RegisterRequest req) {
// req 字段将得到验证
}
}
@Data
public class RegisterRequest {
@NotNull
@Length(min = 2, max = 50)
private String name;
@Email
@NotNull
private String email;
@Validated // 嵌套实体验证
@NotNull
@Size(min = 1)
private List<Order> orderList;
}
进入全屏模式 退出全屏模式
分组验证
对于更新与创建场景,使用分组:
public interface UpdateGroup {}
@Data
public class User {
@NotNull(groups = UpdateGroup.class) // 仅在更新时必填
private Long id;
@NotNull
private String name;
}
// 控制器
@Valid
@Controller
public class UserController {
@Mapping("/user/update")
public void update(@Validated(UpdateGroup.class) User user) {
// 仅验证 UpdateGroup 中的字段
}
}
进入全屏模式 退出全屏模式
20+ 注解一览表
Solon 的验证插件提供了全面的注解集。以下是完整列表:
| 注解 | 作用域 | 用途 |
|---|---|---|
@Valid |
控制器类 | 启用验证 |
@Validated |
参数/字段 | 验证实体字段 |
@NotNull |
方法/参数/字段 | 非空 |
@Null |
方法/参数/字段 | 为空 |
@NotBlank |
方法/参数/字段 | 非空白字符串 |
@NotEmpty |
方法/参数/字段 | 非空字符串 |
@NotZero |
方法/参数/字段 | 非零 |
@Min(value) |
参数/字段 | >= 值 |
@Max(value) |
参数/字段 | <= 值 |
@DecimalMin(value) |
参数/字段 | >= 十进制值 |
@DecimalMax(value) |
参数/字段 | <= 十进制值 |
@Length(min, max) |
参数/字段 | 字符串长度范围 |
@Size |
参数/字段 | 集合大小范围 |
@Email |
参数/字段 | 邮箱格式 |
@Pattern(value) |
参数/字段 | 正则匹配 |
@Date |
参数/字段 | 日期格式 |
@Numeric |
参数/字段 | 数字格式 |
@Logined |
控制器/方法 | 用户已登录 |
@NoRepeatSubmit |
控制器/方法 | 禁止重复提交 |
@Whitelist |
控制器/方法 | IP 在白名单中 |
@NotBlacklist |
控制器/方法 | IP 不在黑名单中 |
其中部分注解(@Logined、@NoRepeatSubmit、@Whitelist、@NotBlacklist)属于“业务感知型”——需要你实现检查器接口。下文会进一步说明。
非 Web 组件上的验证
这让我感到意外:Solon 的验证同样适用于普通的 @Component 类,而不仅限于控制器:
@Valid
@Component
public class UserService {
public void addUser(@NotNull String name, @Email String email) {
// 验证同样在此处生效
}
}
进入全屏模式 退出全屏模式
使用 ValidUtils 进行手动验证
你也可以在任何地方以编程方式进行验证:
User user = new User();
user.setName(null);
ValidUtils.validateEntity(user); // 无效时抛出 ValidatorException
进入全屏模式 退出全屏模式
处理验证错误
在过滤器中捕获 ValidatorException:
@Component
public class ValidationFilter implements Filter {
@Override
public void doFilter(Context ctx, FilterChain chain) throws Throwable {
try {
chain.doFilter(ctx);
} catch (ValidatorException e) {
ctx.render(Result.failure(e.getCode(), e.getMessage()));
}
}
}
进入全屏模式 退出全屏模式
默认情况下,验证在首次错误时停止。如需收集所有错误,请设置:
solon.validation.validateAll: true
进入全屏模式 退出全屏模式
业务感知型验证器(四种检查器)
四个注解需要你提供检查器实现。这正是 Solon 设计的亮点——框架负责拦截,你只需插入业务逻辑。
@NoRepeatSubmit
@Component
public class NoRepeatSubmitCheckerImpl implements NoRepeatSubmitChecker {
@Override
public boolean check(NoRepeatSubmit anno, Context ctx,
String submitHash, int limitSeconds) {
return LockUtils.tryLock(Solon.cfg().appName, submitHash, limitSeconds);
}
}
进入全屏模式 退出全屏模式
@Whitelist
@Component
public class WhitelistCheckerImpl implements WhitelistChecker {
@Override
public boolean check(Whitelist anno, Context ctx) {
String ip = ctx.realIp();
return CloudClient.list().inListOfIp("whitelist", ip);
}
}
进入全屏模式 退出全屏模式
@NotBlacklist
@Component
public class NotBlacklistCheckerImpl implements NotBlacklistChecker {
@Override
public boolean check(NotBlacklist anno, Context ctx) {
String ip = ctx.realIp();
return !CloudClient.list().inListOfIp("blacklist", ip);
}
}
进入全屏模式 退出全屏模式
@Logined
@Component
public class LoginedCheckerImpl implements LoginedChecker {
@Override
public boolean check(Logined anno, Context ctx, String userKeyName) {
return ctx.sessionAsLong("userId") > 0;
}
}
进入全屏模式 退出全屏模式
自定义验证器:创建自己的注解
该框架支持扩展。以下是创建自定义 @Phone 验证器的完整示例:
步骤 1:定义注解
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Phone {
String message() default "Invalid phone number";
Class<?>[] groups() default {};
}
进入全屏模式 退出全屏模式
步骤 2:实现验证器
public class PhoneValidator implements Validator<Phone> {
private static final Pattern PHONE_PATTERN =
Pattern.compile("^1[3-9]\\d{9}$");
@Override
public String message(Phone anno) {
return anno.message();
}
@Override
public Class<?>[] groups(Phone anno) {
return anno.groups();
}
@Override
public Result validateOfValue(Phone anno, Object val, StringBuilder tmp) {
if (val == null) return Result.succeed();
if (val instanceof String == false) return Result.failure();
if (PHONE_PATTERN.matcher((String) val).matches()) {
return Result.succeed();
}
return Result.failure();
}
@Override
public Result validateOfContext(Context ctx, Phone anno,
String name, StringBuilder tmp) {
String val = ctx.param(name);
if (val == null) return Result.succeed();
if (PHONE_PATTERN.matcher(val).matches()) {
return Result.succeed();
}
return Result.failure(name);
}
}
进入全屏模式 退出全屏模式
步骤 3:注册
@Configuration
public class ValidationConfig {
@Bean
public void registerValidators() {
ValidatorManager.register(Phone.class, new PhoneValidator());
}
}
进入全屏模式 退出全屏模式
步骤 4:使用
@Valid
@Controller
public class UserController {
@Mapping("/user/phone")
public void setPhone(@Phone String phone) {
// 已验证
}
}
进入全屏模式 退出全屏模式
我想改进的地方
两点:
-
未内置
@Phone或@URL—— 这些是你期望的常见注解。自己编写很容易,但如果能作为预置选项会更好。 -
@Validated注解名称 —— 它的行为与 JSR 380 的@Valid和 Spring 的@Validated都不同。在 Solon 中,@Validated专门用于触发参数上的实体字段验证。了解后没问题,但与 JSR 380 的命名重叠曾让我困惑片刻。
总结
Solon 的验证是一个完整、自包含的系统。你开箱即得 20+ 注解、实体验证、分组验证,以及自定义验证器的干净扩展点——无需任何 javax.validation 或 jakarta.validation 依赖。
业务感知型注解(@Whitelist、@NoRepeatSubmit、@Logined)是亮点。它们允许你在验证层而非在处理器中散落检查来强制执行安全性和幂等性约束。
对于希望保持依赖图精简的项目,Solon 的验证是一个稳健的选择。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.