我曾多次被 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) {
        // All parameters validated before this runs
    }
}

Enter fullscreen mode Exit fullscreen mode

就是這樣。不需要設定 ValidatorFactory 或宣告 bean。只要在類別上加上 @Valid 就能啟用所有處理方法的驗證。

用於實體(巢狀)驗證的 @Validated

當參數是帶有自身驗證規則的 DTO 時,請使用 @Validated

@Valid
@Controller
public class UserController {

    @Mapping("/user/register")
    public void register(@Validated RegisterRequest req) {
        // req fields are validated
    }
}

@Data
public class RegisterRequest {
    @NotNull
    @Length(min = 2, max = 50)
    private String name;

    @Email
    @NotNull
    private String email;

    @Validated  // Nested entity validation
    @NotNull
    @Size(min = 1)
    private List<Order> orderList;
}

Enter fullscreen mode Exit fullscreen mode

分組驗證

針對更新與建立的不同情境,可使用分組:

public interface UpdateGroup {}

@Data
public class User {
    @NotNull(groups = UpdateGroup.class)  // Only required on update
    private Long id;

    @NotNull
    private String name;
}

// Controller
@Valid
@Controller
public class UserController {
    @Mapping("/user/update")
    public void update(@Validated(UpdateGroup.class) User user) {
        // Only validates fields in UpdateGroup
    }
}

Enter fullscreen mode Exit fullscreen mode

20 多個註解一覽表

Solon 的驗證外掛內建豐富的註解集合。完整清單如下:

註解 作用範圍 用途
@Valid 控制器類別 啟用驗證
@Validated 參數/欄位 驗證實體欄位
@NotNull 方法/參數/欄位 不可為 null
@Null 方法/參數/欄位 必須為 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) {
        // Validation works here too
    }
}

Enter fullscreen mode Exit fullscreen mode

使用 ValidUtils 進行手動驗證

你也可以在任何地方以程式方式驗證:

User user = new User();
user.setName(null);
ValidUtils.validateEntity(user);  // throws ValidatorException if invalid

Enter fullscreen mode Exit fullscreen mode

處理驗證錯誤

在過濾器中捕捉 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()));
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

預設情況下,驗證遇到第一個錯誤就會停止。若要收集所有錯誤,請設定:

solon.validation.validateAll: true

Enter fullscreen mode Exit fullscreen mode

業務感知驗證器(四個檢查器)

四個註解需要你提供檢查器實作。這正是 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);
    }
}

Enter fullscreen mode Exit fullscreen mode

@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);
    }

Enter fullscreen mode Exit fullscreen mode

@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);
    }

Enter fullscreen mode Exit fullscreen mode

@Logined

@Component
public class LoginedCheckerImpl implements LoginedChecker {
    @Override
    public boolean check(Logined anno, Context ctx, String userKeyName) {
        return ctx.sessionAsLong("userId") > 0;
    }

Enter fullscreen mode Exit fullscreen mode

自訂驗證器:建立自己的註解

此框架設計為可擴充。以下是建立自訂 @Phone 驗證器的完整範例:

步驟 1:定義註解

@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Phone {
    String message() default "Invalid phone number";
    Class<?>[] groups() default {};
}

Enter fullscreen mode Exit fullscreen mode

步驟 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);
    }
}

Enter fullscreen mode Exit fullscreen mode

步驟 3:註冊

@Configuration
public class ValidationConfig {
    @Bean
    public void registerValidators() {
        ValidatorManager.register(Phone.class, new PhoneValidator());
    }
}

Enter fullscreen mode Exit fullscreen mode

步驟 4:使用

@Valid
@Controller
public class UserController {
    @Mapping("/user/phone")
    public void setPhone(@Phone String phone) {
        // Validated
    }
}

Enter fullscreen mode Exit fullscreen mode

我想改進的地方

兩件事:

  1. 沒有內建 @Phone@URL — 這些常見的驗證你會期待有,但必須自己撰寫。雖然容易,但若能內建就更好了。
  2. @Validated 註解名稱 — 其功能與 JSR 380 的 @Valid 及 Spring 的 @Validated 不同。在 Solon 中,@Validated 專門用於觸發參數上的實體欄位驗證。了解後沒問題,但與 JSR 380 的命名重疊曾讓我困惑。

結論

Solon 的驗證是一個完整且獨立的系統。你可直接取得 20 多個註解、實體驗證、分組驗證,以及乾淨的自訂驗證器擴充點 — 完全不需要任何 javax.validationjakarta.validation 依賴。

業務感知註解(@Whitelist@NoRepeatSubmit@Logined)是其亮點。它們讓你能在驗證層級而非在處理常式中散落檢查,即可強制執行安全性與冪等性限制。

對於希望保持依賴圖精簡的專案,Solon 的驗證是一個穩健的選擇。