校验参数的6大神功!

新手司机翻车实录

"哥,注册接口又被刷爆了!

"某一个周末下午,我接到电话,打开日志一看,NullPointerException堆栈里有38个不同位置的校验逻辑。

原来新人小王在Controller里写满了这样的代码:

java 复制代码
// 典型错误示范(转载自某小厂祖传代码)
public String register(UserDTO user) {
    if (user.getName() == null) {
        return "名字不能为空";
    }
    if (user.getAge() == null) {
        return "年龄不能为空";
    }
    if (user.getAge() < 18) {
        return "年龄不能小于18岁";
    }
    if (!user.getPhone().matches("^1[3-9]\\d{9}$")) {
        return "手机号不合法";
    }
    // ...后续还有20个if...
}

这才是代码界的"九转大肠"------每个入口都让人窒息。

作为一位有很多开发经验的老司机,今天,老夫带你修炼参数校验的6大神功。

希望对你会有所帮助。

第一重:JSR规范基础功

1.1 HibernateValidator瞬炼大法

可以使用Hibernate中Validator框架做参数校验,具体代码如下:

java 复制代码
public class UserDTO {
    @NotBlank(message = "名称要填,皮这一下很开心?")
    private String name;

    @NotNull
    @Min(value = 18, message = "未成年禁止入内")
    @Max(60)
    private Integer age;

    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "这手机号是哪国来的?")
    private String phone;
}

// Controller层启用校验(新手必知第一步)
@PostMapping("/register")
public Result register(@Valid @RequestBody UserDTO user) {
    // 业务代码...
}

技术要点

  • 引入spring-boot-starter-validation依赖(调料包记得加)
  • @Valid注解要放在入参侧(别贴在DTO类上)
  • 错误信息会进BindingResult(打扫战场需要手动处理)

第二重:全局异常擒龙手

2.1 统一异常拦截器

我们需要对异常进行统一拦截。

这样在出现参数校验异常,比如空指针时,不会把服务的内部错误信息直接输出给用户。

通过@RestControllerAdvice和@ExceptionHandler注解实现统一异常拦截器的功能。

具体代码如下:

java 复制代码
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    // 专治各种不服校验
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result handleValidException(MethodArgumentNotValidException e) {
        BindingResult result = e.getBindingResult();
        return Result.fail(result.getFieldError().getDefaultMessage());
    }
}

// 返回格式规范(示例)
public class Result<T> {
    private Integer code;
    private String msg;
    private T data;
    
    public static <T> Result<T> fail(String message) {
        return new Result<>(500, message, null);
    }
}

反爬虫机制

  • 禁止直接暴露字段名给前端(攻击者会利用字段名信息)
  • 错误信息字典化管理(后面会教国际化这招)

第三重:自定义校验屠龙技

3.1 手机/邮箱二元校验

有时候,Hibernate Validator框架或者其他校验框架定义的校验不满足需求,我们需要自定义校验规则。

则可以自定义注解,实现ConstraintValidator接口,来实现具体的自定义的校验逻辑。

自定义注解@Contact在字段上使用。

具体代码如下:

java 复制代码
@Target({FIELD, PARAMETER})
@Retention(RUNTIME)
@Constraint(validatedBy = ContactValidator.class)
public @interface Contact {
    String message() default "联系方式格式错误";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// 校验逻辑实现(不要相信前端的下拉框!)
public class ContactValidator implements ConstraintValidator<Contact, String> {
    
    private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
    private static final Pattern EMAIL_PATTERN = Pattern.compile("^\\w+@\\w+\\.\\w+$");

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return PHONE_PATTERN.matcher(value).matches() 
               || EMAIL_PATTERN.matcher(value).matches();
    }
}

六边形战士培养计划

  • 可通过context.buildConstraintViolationWithTemplate()动态修改错误信息
  • 支持DI注入Spring管理的Bean(比如从数据库加载正则)

第四重:分组校验北冥功

4.1 增删改查不同校验规则

对于增删改查中,对于实体对象中的同一个参数,在不同的应用场景中需要做不同分组校验。

具体代码如下:

java 复制代码
// 定义校验组别(划分阵营)
public interface CreateGroup {}
public interface UpdateGroup {}

// DTO根据场景应用分组
public class ProductDTO {
    @Null(groups = UpdateGroup.class)
    @NotNull(groups = CreateGroup.class)
    private Long id;

    @NotBlank(groups = {CreateGroup.class, UpdateGroup.class})
    private String name;
}

// 控制层按需激活校验组  
@PostMapping("/create")
public Result create(@Validated(CreateGroup.class) @RequestBody ProductDTO dto) {
    // 创建逻辑
}

多副本作战手册

  • Default组始终生效(除非使用groups显式配置)
  • 妙用@ConvertGroup进行分组转换

第五重:跨界校验凌波微步

5.1 跨字段关系校验

如果存在跨字段关系校验的情况,即组合条件校验,比如:用户密码和确认密码,可以将自定义注解作用在类上。

具体代码如下:

java 复制代码
@Target(TYPE)
@Retention(RUNTIME)
@Constraint(validatedBy = PasswordValidator.class)
public @interface PasswordValid {
    String message() default "两次密码不一致";
    // ...
}

public class PasswordValidator implements ConstraintValidator<PasswordValid, UserDTO> {
    
    @Override
    public boolean isValid(UserDTO user, ConstraintValidatorContext context) {
        return user.getPassword().equals(user.getConfirmPassword());
    }
}

// 应用到类级别
@PasswordValid
public class UserDTO {
    private String password;
    private String confirmPassword;
}

风控新法

  • 适用于订单金额与优惠券匹配等业务规则
  • DDD值对象的天然场景

第六重:规则引擎之天机策

天机殿的自动化战场

新来的产品小妹指着参数校验文档:"每次改个手机号正则都要等发版?

"我默默掏出了祖传的规则引擎。

这种政商联动的需求,是时候施展大型工程的必杀技了!

6.1 规则引擎的三层境界

第一境:硬编码校验 (青铜段位的if-else)
第二境:配置化校验 (黄金段位的数据库规则表)
第三境:热力场作战(王者段位的动态规则引擎)

6.2 Drools天机大阵部署实录

战场场景:信贷额度动态校验(每小时调整风控模型) 。

天机规则文件如下:

java 复制代码
// 天机规则文件(credit_rule.drl)
rule "白领贷基础校验"
    when
        $req : LoanRequest(
            occupation == "白领", 
            salary > 10000, 
            age >= 25 && age <= 45
        )
    then
        $req.setRiskScore(-10); //加分项
end

rule "高危行业拦截"
    when
        $req : LoanRequest(
            industry in ("赌博业", "传销"), 
            location.contains("缅甸")
        )
    then
        throw new ValidationException("阁下莫非是缅北战神?"); 
end

布阵心法

阵法要诀

  1. 规则文件按业务线拆分(金融/电商/社交各立山头)
  2. 使用kie-maven-plugin自动编译规则文件
  3. KieScanner监听规则变更(天机更新不重启服务)

6.3 SpringBoot接引天机大阵

法咒集成

java 复制代码
@Configuration
public class DroolsConfig {
    
    @Bean
    public KieContainer kieContainer() {
        KieServices ks = KieServices.Factory.get();
        KieFileSystem kfs = ks.newKieFileSystem();
        
        // 加载天机卷轴(规则文件)
        Resource resource = new ClassPathResource("rules/credit_rule.drl");
        kfs.write(ks.getResources().newInputStreamResource(resource.getInputStream())
                    .setTargetPath("credit_rule.drl"));
        
        KieBuilder kieBuilder = ks.newKieBuilder(kfs).buildAll();
        return ks.newKieContainer(kieBuilder.getKieModule().getReleaseId());
    }
}

// Controller层调用天尊之力
@PostMapping("/apply")
public Result applyLoan(@RequestBody LoanRequest request) {
    kieSession.insert(request);
    kieSession.fireAllRules(); // 执行天机推演
    return riskService.process(request);
}

天机沙箱防御

  1. 限制规则中eval()的使用次数(防CPU过载)
  2. 为每个请求创建独立KieSession(防线程污染)
  3. 设置规则执行超时熔断(天机殿也有算不动的时候)

6.4 天机策反制诀窍

某次上线后,规则引擎的神操作:

复制代码
rule "特殊时段放水"
    when
        $req : LoanRequest(hour > 2 && hour < 5)
    then
        $req.setCreditLimit(50000); //给值夜班的兄弟开后门
end

反制方案

  1. 规则提交走审批流(太上长老团联署制)
  2. 生产环境禁用update/modify关键字(防自动夺舍)
  3. 规则版本回滚机制(祭出玄天宝镜倒转时空)

祖师爷级参数校验纲领

段位 招式名称 修炼难度 适用场景 破坏力
青铜 if-else硬编码 ★☆☆ 小型工具类 ⚡⚡⚡
白银 JSR注解大法 ★★☆ 常规CRUD ⚡⚡
黄金 全局异常拦截 ★★★ RESTful API
铂金 定制校验规则 ★★★☆ 复杂业务规则
钻石 组合条件校验 ★★★★ 跨字段业务约束
王者 规则引擎整合 ★★★★★ 动态风控场景

避坑法门

  1. 不过三:Controller层校验不要超过三层(应该转给Service)
  2. 见好就收:业务规则校验与基础格式校验分离
  3. 防君子更防小人:服务端校验必须存在(前端校验是防君子用的)
  4. 语义明确:错误提示避免暴露敏感信息(比如"用户不存在"改为"账号或密码错误")

最后提醒各位大侠:好的参数校验就像空气------你平时感受不到它的存在,但一旦失去它,整个系统瞬间崩塌!(代码fields正提刀赶来)

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。