基于SpringBoot参数校验器拓展自定义参数校验

2,544 阅读5分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

想必工作中大家为了保证接口的稳定性与安全性都会对入参进行校验。五花八门的校验写法会让我们的代码不够整洁,本文将介绍如何使用SpringBoot为我们提供的参数校验器,并对其进行扩展,让其能够实现自定义校验。当然在一些互联网项目中,为保证接口的高性能,校验都是放在前端做的,但是在阿里开发规约中是这样说的越是简单的接口越不需要进行参数校验,越是复杂的接口越需要参数校验,因为复杂的接口试错成本很高,校验对接口性能的影响微乎其微。

工程中的使用可参照我的开源项目:gitee.com/zhuhuijie/b…

在common-web模块中引入在example-business中使用

SpringBoot 参数校验器的使用

本章通过怎么引入SpringBoot的参数校验器,让大家能够搭建一个简单的Demo,文章的第二部分,自定义扩展才是本文的重头戏。

1 首先,pom文件引入参数校验器的依赖

<!--参数校验-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

2 入参VO,加入相关的注解

常用注解:

  • @NotNull 非空验证

  • @Min(value = 1, message = "年龄不能小于1") @Max(value = 25, message = "年龄不能超过25")

    值区间验证

  • @Email(message = "必须是邮箱格式") 邮箱格式验证

  • @Past(message = "生日范围不正确,生日必须是今天以前的")

    验证是否是过去的时间

import com.fasterxml.jackson.annotation.JsonFormat;
import com.zhj.business.protocol.validhandler.BirthdayValidHandler;
import com.zhj.common.web.valid.annotation.MyValid;
import lombok.Data;
​
import javax.validation.constraints.*;
import java.util.Date;
​
/**
 * @author zhj
 */
@Data
public class StudentInput {
​
    @NotNull(message = "名字不能为空")
    private String name;
    @Min(value = 1, message = "年龄不能小于1")
    @Max(value = 25, message = "年龄不能超过25")
    private Integer age;
    @NotNull(message = "邮箱不能为空")
    @Email(message = "必须是邮箱格式")
    private String email;
    @NotNull(message = "性别不能为空")
    private Integer sex;
    @NotNull(message = "生日不能为空")
    @Past(message = "生日范围不正确,生日必须是今天以前的")
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd")
    private Date birthday;
}

3 Controller 中开启校验,切记开启才会生效

import com.zhj.business.protocol.input.StudentInput;
import com.zhj.business.service.StudentService;
import com.zhj.common.core.result.Result;
import com.zhj.common.core.util.ResultUtils;
import com.zhj.data.entity.example.Student;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
​
import javax.validation.Valid;
import java.util.List;
​
@Slf4j
@RestController
@RequestMapping("/student")
public class StudentController {
​
    @Autowired
    private StudentService studentService;
​
    @PostMapping("/add")
    public Result add(@Valid @RequestBody StudentInput studentInput) {
        log.info("接收到的学生信息:" + studentInput);
        Student student = new Student();
        BeanUtils.copyProperties(studentInput, student);
        boolean result = studentService.save(student);
        log.info("保存学生的结果" + result);
        return ResultUtils.createSuccess(student);
    }
}

SpringBoot 参数校验器的扩展

本章将通过实现年龄与生日是否匹配的校验为例,为我们展示如何通过注解实现这类复杂的参数校验

1 首先呢,我们扩展需要参数校验器需要通过自定义注解实现

import com.zhj.common.web.valid.constraint.MyParameterValid;
import com.zhj.common.web.valid.handler.MyValidHandler;
​
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
​
/**
 * 自定义校验注解
 * @author zhj
 */
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MyParameterValid.class) // 对应校验类型
public @interface MyValid {
​
    /**
     * 失败提示
     * @return
     */
    String message() default "校验失败";
​
    /**
     * 校验分组
     * @return
     */
    Class<?>[] groups() default {};
​
    /**
     * 校验的负载
     * @return
     */
    Class<? extends Payload>[] payload() default {};
​
    /**
     * 扩展校验方法
     * @return
     */
    Class<? extends MyValidHandler> handler();
}

2 实现对Springboot参数校验器的自定义扩展

  • 通过实现ConstraintValidator<MyValid, Object> 官方为我们提供的扩展接口,完成自定义注解的初始化

  • 从新实现校验方法

    • 通过Spring容器获取自定义参数校验处理器MyValidHandler
    • 然后将注解与校验对象传入自定义的校验方法
    • 执行自定义校验方法,根据返回结果判断是否校验通过
import com.zhj.common.web.util.ApplicationContextUtils;
import com.zhj.common.web.valid.annotation.MyValid;
import com.zhj.common.web.valid.handler.MyValidHandler;
import lombok.extern.slf4j.Slf4j;
​
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Optional;
​
/**
 * 自定义参数校验类
 * @author zhj
 */
@Slf4j
public class MyParameterValid implements ConstraintValidator<MyValid, Object> {
​
    private MyValid myValid;
​
    @Override
    public void initialize(MyValid constraintAnnotation) {
        this.myValid = constraintAnnotation;
    }
​
    /**
     * 校验方法
     * @param o
     * @param constraintValidatorContext
     * @return
     */
    @Override
    public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
        log.info("自定义参数校验触发" + o);
        if (null != o) {
            Class<? extends MyValidHandler> handler = myValid.handler();
            // 交给 MyValidHandler 处理校验
            MyValidHandler myValidHandler = ApplicationContextUtils.getBean(handler);
            return Optional
                    .ofNullable(myValidHandler)
                    .map(myValidHandler1 -> {
                return myValidHandler.valid(myValid, o);
            }).orElse(false);
        }
        return true;
    }
}

3 自定义处理接口

自定义校验方法通过实现该接口,重写校验方法

通过接口的多实现从而实现各种自定义处理。

import com.zhj.common.web.valid.annotation.MyValid;
​
/**
 * 扩展接口,开发者实现该接口,扩展校验方式
 * @author zhj
 */
public interface MyValidHandler<T> {
​
    /**
     * 实现校验方法
     * @param data
     * @return
     */
    boolean valid(MyValid myValid, T data);
}

4 获取Spring容器中对象的工具类

通过Spring上下文根据类名查找对应的实现

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
​
import javax.annotation.PostConstruct;
​
/**
 * Spring 容器工具方法
 * @author zhj
 */
@Component
public class ApplicationContextUtils {
​
    @Autowired
    private ApplicationContext applicationContext;
​
    /**
     * 静态容器对象
     */
    private static ApplicationContext staticApplicationContext;
​
    @PostConstruct
    private void init() {
        ApplicationContextUtils.staticApplicationContext = applicationContext;
    }
​
    public static <T> T getBean(Class<T> cls) {
        if (staticApplicationContext != null) {
            return staticApplicationContext.getBean(cls);
        }
        return null;
    }
}

5 在入参VO类上添加自定义注解

handler = BirthdayValidHandler.class 将自定义校验方法的类名传入,该类需要实现自定义处理接口

import com.fasterxml.jackson.annotation.JsonFormat;
import com.zhj.business.protocol.validhandler.BirthdayValidHandler;
import com.zhj.common.web.valid.annotation.MyValid;
import lombok.Data;
​
import javax.validation.constraints.*;
import java.util.Date;
​
/**
 * @author zhj
 */
@Data
@MyValid(message = "年龄和生日不匹配", handler = BirthdayValidHandler.class)
public class StudentInput {
​
    @NotNull(message = "名字不能为空")
    private String name;
    @Min(value = 1, message = "年龄不能小于1")
    @Max(value = 25, message = "年龄不能超过25")
    private Integer age;
    @NotNull(message = "邮箱不能为空")
    @Email(message = "必须是邮箱格式")
    private String email;
    @NotNull(message = "性别不能为空")
    private Integer sex;
    @NotNull(message = "生日不能为空")
    @Past(message = "生日范围不正确,生日必须是今天以前的")
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd")
    private Date birthday;
}

6 创建注解所写类名,实现自定义校验

切记需要将该对象注册进Spring容器中,否则扩展方式无法获取到该实例

本示例是实现,年龄与生日关系的校验,年龄和生日必须匹配才会校验成功

import com.zhj.business.protocol.input.StudentInput;
import com.zhj.common.web.valid.annotation.MyValid;
import com.zhj.common.web.valid.handler.MyValidHandler;
import org.springframework.stereotype.Component;
​
import java.util.Calendar;
import java.util.Date;
​
/**
 * @author zhj
 */
@Component
public class BirthdayValidHandler implements MyValidHandler<StudentInput> {
​
    @Override
    public boolean valid(MyValid myValid, StudentInput data) {
        Integer age = data.getAge();
        Date birthday = data.getBirthday();
        if (age == null || birthday == null) return true;
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
        int currYear = calendar.get(Calendar.YEAR);
        calendar.setTime(birthday);
        int birYear = calendar.get(Calendar.YEAR);
        return currYear - birYear == age;
    }
}