我们在项目开发中,经常会对一些参数进行校验,比如非空校验、长度校验,以及定制的业务校验规则等,如果使用if/else语句来对请求的每一个参数一一校验,就会出现大量与业务逻辑无关的代码,繁重不堪且繁琐的校验,会大大降低我们的工作效率,而且准确性也无法保证。为保证数据的正确性、完整性,前后端都需要进行数据检验。本文对开源 boot-admin 项目的后端校验实践进行总结,以飨码友。

boot-admin 是一款采用前后端分离模式、基于 SpringCloud 微服务架构的SaaS后台管理框架。系统内置基础管理、权限管理、运行管理、定义管理、代码生成器和办公管理6个功能模块,集成分布式事务 Seata、工作流引擎 Flowable、业务规则引擎 Drools、后台作业调度框架 Quartz 等,技术栈包括 Mybatis-plus、Redis、Nacos、Seata、Flowable、Drools、Quartz、SpringCloud、Springboot Admin Gateway、Liquibase、jwt、Openfeign、I18n等。

项目源码仓库github

项目源码仓库gitee

引入Maven依赖

<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>

参数校验实践

定义校验对象

@Data
/** 组合校验注解(方式1) **/
@OverallValid(value = "check1" ,message="女士不得小于16岁。")
@OverallValid(value = "check2" ,message="男士不得小于18岁。")
public class User {
//字符个数检测(内置注解)
@Size(min = 1,max = 10,message = "姓名长度必须为1到10")
//占用空间长度检测(自定义注解)
@StringLength(min = 1,max = 12,message = "姓名的保存长度不允许超过12个字节。")
private String name;
//利用枚举类检测(自定义注解)
@EnumValid(target = SexEnum.class, message = "性别的取值范围是【1】和【2】")
private String sex;
//注意 @NotNull @NotEmpty @NotBlank 的区别
@NotBlank(message = "姓氏是必填项。")
private String firstName; @Min(value = 10,message = "年龄最小为10")
@Max(value = 100,message = "年龄最大为100")
private Integer age; @Past(message = "出生时间必须为过去时间")
private Date birth; @NotEmpty(message = "兴趣不能为空")
private List<String> interest; //嵌套检测
@Valid
private List<User> children;
@Valid
private User father;
@Valid
private User mother; /** 组合校验(方式2) **/
@BooleanValid(message = "男性年龄需在60岁以下")
public boolean getValid1(){
if(sex.equalsIgnoreCase("1") && age >= 60 ){
return false;
}
return true;
}
/** 组合校验(方式2) **/
@BooleanValid(message = "女性年龄需在55岁以下")
public boolean getValid2(){
if(sex.equalsIgnoreCase("2") && age >= 55 ){
return false;
}
return true;
}
/** 组合校验(方式1)方法 **/
public boolean check1(){
if(sex.equalsIgnoreCase("2") && age < 16 ){
return false;
}
return true;
}
/** 组合校验(方式1)方法 **/
public boolean check2(){
if(sex.equalsIgnoreCase("1") && age < 18 ){
return false;
}
return true;
}
}

相关枚举类:

public enum SexEnum {
男("1"),女("2");
private final String value;
SexEnum(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}

参数校验(在 Controller 中使用)

@RestController
@RequestMapping("/api/system")
@Slf4j
public class DemoController {
//注入校验信息采集器
@Resource
private FormValidator formValidator; @PostMapping("/free/user/check")
public ResultDTO check(@Valid @RequestBody User user, BindingResult bindingResult, HttpServletRequest request) throws Exception{
/** 参数校验 **/
if (bindingResult.hasErrors()) {
return formValidator.generateMessage(bindingResult);
}
/** 继续执行业务逻辑 **/
return ResultDTO.success();
}
}

在Controller中使用的校验结果信息采集器实现

接口定义:

public interface FormValidator {
ResultDTO generateMessage(BindingResult bindingResult) throws Exception;
}

类实现:

@Service
@Slf4j
public class FormValidatorImpl implements FormValidator {
@Override
public ResultDTO generateMessage(BindingResult bindingResult) throws Exception {
String msg = this.getMessage(bindingResult);
return ResultDTO.failureCustom(msg);
}
/**
* 生成校验结果
* @param bindingResult
* @return
*/
private String getMessage(BindingResult bindingResult){
log.info(bindingResult.toString());
List<ObjectError> objectErrorList=bindingResult.getAllErrors();
String msg= this.getFormValidErrsMsgNoBr(objectErrorList);
log.info(msg);
return msg;
}
private String getFormValidErrsMsgNoBr(List<ObjectError> objectErrorList) {
if (objectErrorList==null) {
return "";
}
StringBuffer csv = new StringBuffer();
csv.append("数据验证未通过:[");
for (int i = 0; i < objectErrorList.size(); i++){
if (i > 0){
csv.append("],[");
}
csv.append(objectErrorList.get(i).getDefaultMessage());
}
csv.append("]");
return csv.toString();
}
}

相关注解介绍

JSR-303 规范常用注解

以下列举常用内置注解,可直接使用。

注解 描述
@Valid 对po实体尽心校验
@AssertFalse 所注解的元素必须是Boolean类型,且值为false
@AssertTrue 所注解的元素必须是Boolean类型,且值为true
@DecimalMax 所注解的元素必须是数字,且值小于等于给定的值
@DecimalMin 所注解的元素必须是数字,且值大于等于给定的值
@Digits 所注解的元素必须是数字,且值必须是指定的位数
@Future 所注解的元素必须是将来某个日期
@Max 所注解的元素必须是数字,且值小于等于给定的值
@Min 所注解的元素必须是数字,且值大于等于给定的值
@Range 所注解的元素需在指定范围区间内
@NotNull 所注解的元素值不能为null
@NotBlank 所注解的元素值有内容
@Null 所注解的元素值为null
@Past 所注解的元素必须是某个过去的日期
@PastOrPresent 所注解的元素必须是过去某个或现在日期
@Pattern 所注解的元素必须满足给定的正则表达式
@Size 所注解的元素必须是String、集合或数组,且长度大小需保证在给定范围之内
@Email 所注解的元素需满足Email格式

自定义注解

仅仅使用内置的注解,无法满足复杂的业务需求,故扩展下面几个自定义注解。

UTF-8 字符串长度校验

对字符串长度的校验目的,一般是用于保证数据表字段可以容纳,当字符串内容是中文时,内置的 @Size 是不适用的,此时就需要自行扩展 UTF-8 字符串长度校验。

注解类:

@Target( {
METHOD,
FIELD,
ANNOTATION_TYPE,
CONSTRUCTOR,
PARAMETER
})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {StringLengthValidator.class})
public @interface StringLength {
int max() default 4000;
int min() default 0;
String message() default "字符串长度不符合要求。";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

注解类实现:

@Slf4j
public class StringLengthValidator implements ConstraintValidator<StringLength, String> {
private int max;
private int min;
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
try {
if(StringUtils.isBlank(value)){
if(min > 0){
return false;
}else {
return true;
}
}
byte[] tmpbyte = value.getBytes("UTF-8");
int length = tmpbyte.length;
if(length < min || length > max){
return false;
}
return true;
}catch (Exception ex){
log.error("注解校验StringLength发生异常。");
log.error(ex.getMessage(),ex);
return false;
}
}
@Override
public void initialize(StringLength constraintAnnotation) {
max = constraintAnnotation.max();
min = constraintAnnotation.min();
}
}

手机号码校验

注解类:

@Target( {
METHOD,
FIELD,
ANNOTATION_TYPE,
CONSTRUCTOR,
PARAMETER
})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {MobileValidator.class})
public @interface Mobile {
String regexp() default "";
String message() default "手机号码格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

注解类实现:

public class MobileValidator implements ConstraintValidator<Mobile, String> {
/**
* 手机号的正则表达式.
*/
private static Pattern pattern = Pattern.compile(
"^0?(13[0-9]|14[0-9]|15[0-9]|16[0-9]|17[0-9]|18[0-9]|19[0-9])[0-9]{8}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
Matcher m = pattern.matcher(value);
return m.matches();
}
@Override
public void initialize(Mobile constraintAnnotation) {}
}

这里对手机号码的校验使用了正则表达式,也可以直接使用内置注解 @Pattern 定义校验规则。

枚举类整数值校验

有时需要校验参数值必须是系统定义的枚举值(整数值),此时需要扩展以下注解。

注解类:

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {EnumIntegerValidator.class})
public @interface EnumIntegerValid {
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/** * 目标枚举类 */
Class<?> target() default Class.class;
/** * 是否忽略空值 */
boolean ignoreEmpty() default true;
}

注解类实现:

@Slf4j
public class EnumIntegerValidator implements ConstraintValidator<EnumIntegerValid, Integer> {
/** 枚举校验注解 */
private EnumIntegerValid annotation;
@Override
public void initialize(EnumIntegerValid constraintAnnotation) {
annotation = constraintAnnotation;
}
@Override
public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
boolean result = false;
Class<?> cls = annotation.target();
boolean ignoreEmpty = annotation.ignoreEmpty();
// target为枚举,并且value有值,或者不忽视空值,才进行校验
if (cls.isEnum() && value != null) {
Object[] objects = cls.getEnumConstants();
try {
Method method = cls.getMethod("getValue");
for (Object obj : objects) {
Object code = method.invoke(obj);
if (value.compareTo((Integer) code) == 0) {
result = true;
break;
}
}
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
log.warn("EnumValidator call isValid() method exception.");
result = false;
}
} else {
result = true;
}
return result;
}
}

枚举类字符串校验

有时需要校验参数值必须是系统定义的枚举值(字符串),此时需要扩展以下注解。

注解类:

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {EnumValidator.class})
public @interface EnumValid {
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/** * 目标枚举类 */
Class<?> target() default Class.class;
/** * 是否忽略空值 */
boolean ignoreEmpty() default true;
}

注解类实现:

@Slf4j
public class EnumValidator implements ConstraintValidator<EnumValid, String> {
/** 枚举校验注解 */
private EnumValid annotation;
@Override
public void initialize(EnumValid constraintAnnotation) {
annotation = constraintAnnotation;
}
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
boolean result = false;
Class<?> cls = annotation.target();
boolean ignoreEmpty = annotation.ignoreEmpty();
// target为枚举,并且value有值,或者不忽视空值,才进行校验
boolean fitCheck = cls.isEnum() && (isNotEmpty(value) || !ignoreEmpty);
if (fitCheck) {
Object[] objects = cls.getEnumConstants();
try {
Method method = cls.getMethod("getValue");
for (Object obj : objects) {
Object code = method.invoke(obj);
if (value.equals(code.toString())) {
result = true;
break;
}
}
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
log.warn("EnumValidator call isValid() method exception.");
result = false;
}
} else {
result = true;
}
return result;
}
}

Bean 内多属性组合校验(组合校验)

此类校验一般属于业务逻辑校验,常常要求多个属性符合一定的逻辑设定。此时需要在Bean中编写校验方法,并在类定义前面添加自定义注解 @OverallValid 或者在方法前面加上自定义注解 @BooleanValid

方式1:

注解在类定义前面,类方法要求:

  1. 方法的可访问属性:public
  2. 方法的返回类型: boolean

    @OverallValid注解类:
@Target({METHOD, FIELD,TYPE})
@Retention(RUNTIME)
@Repeatable(OverallValids.class)
@Documented
@Constraint(validatedBy = {OverallValidImpl.class})
public @interface OverallValid {
String value() default "overallValid";
String message() default "组合校验未通过。";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

上面注解要求可重复使用,使用了 @Repeatable(OverallValids.class),OverallValids 代码如下:

@Target({METHOD, FIELD,TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OverallValids {
OverallValid[] value();
}

使用注入的方法名,通过反射执行该方法,得到校验结果。注解实现如下:

@Slf4j
public class OverallValidImpl implements ConstraintValidator<OverallValid, Object> {
private String functionName;
@Override
public void initialize(OverallValid overallValid) {
functionName = overallValid.value();
}
@Override
public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
try {
//得到方法对象
Method checkMethod = o.getClass().getMethod(functionName);
//调用方法,得到返回值
Object checkRet = checkMethod.invoke(o);
return Boolean.valueOf(checkRet.toString());
}catch (Exception ex){
log.error("综合校验异常。");
log.error(ex.getMessage(),ex);
}
return false;
}
}

方式2:

注解在方法前面,类方法要求:

  1. 方法的可访问属性:public
  2. 方法的返回类型: boolean
  3. 方法名称格式:get+首字母大写驼峰,如 getValid1

@BooleanValid注解类:

@Target( {
METHOD,
FIELD,
ANNOTATION_TYPE,
CONSTRUCTOR,
PARAMETER
})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {BooleanValidImpl.class})
public @interface BooleanValid {
boolean value() default true;
String message() default "综合校验未通过。";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

类实现:

@Slf4j
public class BooleanValidImpl implements ConstraintValidator<BooleanValid, Boolean> {
@Override
public boolean isValid(Boolean value, ConstraintValidatorContext constraintValidatorContext) {
return value;
}
@Override
public void initialize(BooleanValid constraintAnnotation) {
}
}

嵌套校验

在成员属性上加注解 @Valid ,意味着对该成员属性进行嵌套校验,校验规则按该成员的内部校验注解执行。

boot-admin开源项目中有关后端参数校验的最佳实践的更多相关文章

  1. Java Bean Validation(参数校验) 最佳实践

    转载来自:http://www.cnblogs.com 参数校验是我们程序开发中必不可少的过程.用户在前端页面上填写表单时,前端js程序会校验参数的合法性,当数据到了后端,为了防止恶意操作,保持程序的 ...

  2. 【Spring Boot】利用 Spring Boot Admin 进行项目监控管理

    利用 Spring Boot Admin 进行项目监控管理 一.Spring Boot Admin 是什么 Spring Boot Admin (SBA) 是一个社区开源项目,用于管理和监视 Spri ...

  3. [转]C,C++开源项目中的100个Bugs

    [转]C,C++开源项目中的100个Bugs http://tonybai.com/2013/04/10/100-bugs-in-c-cpp-opensource-projects/ 俄罗斯OOO P ...

  4. android studio 使用jar包,arr包和怎么使用githup开源项目中的aar包或module

    我这里的android studio的版本是2.2.3版本 一.现在大家都用android studio了,就有人问怎么使用jar包 其实使用jar包比较简单 直接吧jar放入工程的app目录下的li ...

  5. dotnet 是 前30个增长最快速度的开源项目中排名第一的开发平台

    CNCF 的博客 发了一篇文章 <Update on CNCF and Open Source Project Velocity 2020>,中文翻译参见 2020年CNCF和开源项目开发 ...

  6. python项目使用jsonschema进行参数校验

    python项目使用jsonschema进行参数校验 最近想要给一个新的openstack项目加上参数校验,过完年回来准备开工的时候,发现其他人已经在做了,对应的patch是:https://revi ...

  7. github中fork分支和pullrequest的最佳实践

    github中fork分支和pullrequest的最佳实践 */--> code {color: #FF0000} pre.src {background-color: #002b36; co ...

  8. Java 编程中关于异常处理的 10 个最佳实践

    异常处理是Java 开发中的一个重要部分.它是关乎每个应用的一个非功能性需求,是为了处理任何错误状况,比如资源不可访问,非法输入,空输入等等.Java提供了几个异常处理特性,以try,catch 和 ...

  9. paip.提升性能--多核编程中的java .net php c++最佳实践 v2.0 cah

    paip.提升性能--多核编程中的java .net php c++最佳实践  v2.0 cah 作者Attilax  艾龙,  EMAIL:1466519819@qq.com  来源:attilax ...

  10. 细数Android开源项目中那些频繁使用的并发库中的类

    这篇blog旨在帮助大家 梳理一下前面分析的那些开源代码中喜欢使用的一些类,这对我们真正理解这些项目是有极大好处的,以后遇到类似问题 我们就可以自己模仿他们也写 出类似的代码. 1.ExecutorS ...

随机推荐

  1. 实验1.SDN拓扑实践

    实验1:SDN拓扑实践 一.基本要求 (一)Mininet运行结果截图 (二) 使用Mininet的命令行生成如下拓扑: 1. 3台交换机,每个交换机连接1台主机,3台交换机连接成一条线. 2. 3台 ...

  2. 2---JVM启动参数有哪些?

    也要以说JVM启动的时候可以调整哪些参数,来进行调优.JVM 为垃圾收集器.堆大小和运行时编译器提供与平台相关的默认选择. 性能取决于堆的大小.应用程序维护的实时数据量以及可用处理器的数量和速度.. ...

  3. 10. XrmToolBox

    XrmToolBox是一个连接到MicrosoftDataverse的Windows应用程序,可以为任何在MicrosoftDataverse上构建的产品提供了简化定制.配置和操作任务的工具,包括Dy ...

  4. 讨论django并发能力及提供并发解决方案

    django 的并发能力真的是令人担忧,这里就使用 nginx + uwsgi 提供高并发 nginx 的并发能力超高,单台并发能力过万(这个也不是绝对),在纯静态的 web 服务中更是突出其优越的地 ...

  5. 关于windows cmd 控制台输出中文

    由于中文在window 输出总是优乱码可能性  ,先建cmd.reg  负责下面内容  ,双击运行即可. Windows Registry Editor Version 5.00 [HKEY_CURR ...

  6. Java-01enum常量特定方法

    OnJava8-Enum-常量特定方法 用枚举实现责任链模式 责任链(Chain Of Responsibility)设计模式先创建了一批用于解决目标问题的不同方法,然后将它们连成一条"链& ...

  7. Spring--AOP简介+入门案例

    AOP简介 面向切面编程:在不惊动原始设计的基础上,进行功能增强 各个要应用该功能的对象叫做连接点,那个功能叫做通知,表面上的代码没有发生变化,私下里发生变化的连接点,会出现切入点,切入点与通知通过切 ...

  8. es6中clss做了些什么 怎么继承

    我的理解是clss实际是一种语法糖 凡是es6中clss能做的 我们通过es5也同样可以完成传统的javascript中只有对象,没有类的概念.它是基于原型的面向对象语言.原型对象特点就是将自身的属性 ...

  9. 迁移学习《Pseudo-Label : The Simple and Efficient Semi-Supervised Learning Method for Deep Neural Networks》

    论文信息 论文标题:Pseudo-Label : The Simple and Efficient Semi-Supervised Learning Method for Deep Neural Ne ...

  10. PySimpleGU之运行多个窗口

    这是PySimpleGUI继续简单的地方,但是问题空间刚刚进入了"复杂"领域. 如果您希望在事件循环中运行多个窗口,则有两种方法可以执行此操作. 当第二个窗口可见时,第一个窗口不会 ...