我被 JSR 380 折腾过多次,因此特别欣赏一个不需要拖入整个依赖生态的验证框架。Solon 的验证系统位于 solon-security-validation —— 一个单一插件,提供 20+ 注解、实体验证和自定义验证器,且无需引入 javax.validationjakarta.validation

下面我分享一些我认为有用的内容。

两阶段验证模型

Solon 将验证分为两个阶段:

  1. 上下文验证(注入前)—— 在方法参数解析前运行。方法上的注解检查请求头、Cookie、IP 或请求级约束。
  2. 参数验证(注入后)—— 在参数解析后运行。针对单个参数或实体字段的注解。

这对应两种不同的实现机制:

  • 上下文验证底层使用 @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) {
        // 已验证
    }
}

进入全屏模式 退出全屏模式

我想改进的地方

两点:

  1. 未内置 @Phone@URL —— 这些是你期望的常见注解。自己编写很容易,但如果能作为预置选项会更好。
  2. @Validated 注解名称 —— 它的行为与 JSR 380 的 @Valid 和 Spring 的 @Validated 都不同。在 Solon 中,@Validated 专门用于触发参数上的实体字段验证。了解后没问题,但与 JSR 380 的命名重叠曾让我困惑片刻。

总结

Solon 的验证是一个完整、自包含的系统。你开箱即得 20+ 注解、实体验证、分组验证,以及自定义验证器的干净扩展点——无需任何 javax.validationjakarta.validation 依赖。

业务感知型注解(@Whitelist@NoRepeatSubmit@Logined)是亮点。它们允许你在验证层而非在处理器中散落检查来强制执行安全性和幂等性约束。

对于希望保持依赖图精简的项目,Solon 的验证是一个稳健的选择。