SpringBoot+@Validated实现参数验证(非空、类型、范围、格式等)-若依前后端导入Excel数据并校验为例
原文链接: 霸道的程序猿的博客 (cnblogs.com)
若依管理系统前后端分离版基于ElementUI和SpringBoot怎样实现Excel导入和导出:
https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/108278834
SpringBoot+Vue实现excel导入带格式化的时间参数(moment格式化明天日期并设置el-date-picker默认值):
https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/127821880
在上面搭建SpringBoot+Vue并实现Excel导入的基础上,为避免导入的excel中数据格式不规范
导致产生大量脏数据,所以需要对excel的数据进行校验,比如不能为空、类型是否为数字、数字范围等等规则。
若依官方对这块有专门说明
http://doc.ruoyi.vip/ruoyi/document/htsc.html#%E5%8F%82%E6%95%B0%E9%AA%8C%E8%AF%81
说明比较简单,具体实现流程如下,也可参考其用户导入的实现代码和流程。
这里对用户账号做了非空校验,需要在实体类上对应属性添加
@Xss(message = "用户账号不能包含脚本字符") @NotBlank(message = "用户账号不能为空") @Size(min = 0, max = 30, message = "用户账号长度不能超过30个字符") public String getUserName() { return userName; }
查看@NotBlank注解的实现
发现其来自javax.validation。
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
关注公众号
霸道的程序猿
获取编程相关电子书、教程推送与免费下载。
实现
后台实现流程
Hibernate Validator 是 Bean Validation 的参考实现 。Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,
除此之外还有一些附加的 constraint 在日常开发中,Hibernate Validator经常用来验证bean的字段,基于注解,方便快捷高效。
在SpringBoot中可以使用@Validated,注解Hibernate Validator加强版,也可以使用@Valid原来Bean Validation java版本
添加pom依赖
<!-- 自定义验证注解 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
若依框架添加在common模块中
使用 Validation API 进行参数效验步骤整个过程如下图所示,用户访问接口,然后进行参数效验 ,
如果效验通过,则进入业务逻辑,否则抛出异常,交由全局异常处理器进行处理
自定义全局捕获异常
/** * 全局异常处理器 * * @author ruoyi */ @RestControllerAdvice public class GlobalExceptionHandler { private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); /** * 自定义验证异常 */ @ExceptionHandler(BindException.class) public AjaxResult handleBindException(BindException e) { log.error(e.getMessage(), e); String message = e.getAllErrors().get(0).getDefaultMessage(); return AjaxResult.error(message); } /** * 自定义验证异常 */ @ExceptionHandler(MethodArgumentNotValidException.class) public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { log.error(e.getMessage(), e); String message = e.getBindingResult().getFieldError().getDefaultMessage(); return AjaxResult.error(message); } }
若依框架中将其定义在framework模块下
@ExceptionHandler用于指定异常处理方法。当与@RestControllerAdvice配合使用时,用于全局处理控制器里的异常。
在需要校验的实体类属性上或者get方法上添加校验规则注解 ,比如下面
@Xss(message = "用户昵称不能包含脚本字符") @Size(min = 0, max = 30, message = "用户昵称长度不能超过30个字符") public String getNickName() { return nickName; } public void setNickName(String nickName) { this.nickName = nickName; } @Xss(message = "用户账号不能包含脚本字符") @NotBlank(message = "用户账号不能为空") @Size(min = 0, max = 30, message = "用户账号长度不能超过30个字符") public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } @Email(message = "邮箱格式不正确") @Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符") public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } @Size(min = 0, max = 11, message = "手机号码长度不能超过11个字符") public String getPhonenumber() { return phonenumber; }
注解参数说明
格式校验的注解可自行搜索,用法较多。
如果是在Controller中传参时进行校验,可以直接添加注解@Validated
比如添加用户的Controller
@PreAuthorize("@ss.hasPermi('system:user:add')") @Log(title = "用户管理", businessType = BusinessType.INSERT) @PostMapping public AjaxResult add(@Validated @RequestBody SysUser user) { }
但是形如Excel导入时,Controller接收的是MultipartFile数据,无法在Controller添加校验
@PostMapping("/importData") public AjaxResult importData(MultipartFile file, String planDateString) throws Exception { ExcelUtil<LimitQuotaStatistics> util = new ExcelUtil<LimitQuotaStatistics>(LimitQuotaStatistics.class); List<LimitQuotaStatistics> limitQuotaStatisticsList = util.importExcel(file.getInputStream()); String message =limitQuotaStatisticsService.insertLimitQuotaStatisticsBatch(limitQuotaStatisticsList,planDateString); return success(message); }
就需要在serviceImpl中通过如下方式进行注入和使用
先通过
@Autowired protected Validator validator;
注入依赖
再通过
BeanValidators.validateWithException(validator, limitQuotaStatistics);
进行参数校验
@Service public class LimitQuotaStatisticsServiceImpl implements ILimitQuotaStatisticsService { private static final Logger log = LoggerFactory.getLogger(LimitQuotaStatisticsServiceImpl.class); @Autowired private LimitQuotaStatisticsMapper limitQuotaStatisticsMapper; @Autowired protected Validator validator; @Override public String insertLimitQuotaStatisticsBatch(List<LimitQuotaStatistics> limitQuotaStatisticsList, String planDateString) throws ParseException { if (StringUtils.isNull(limitQuotaStatisticsList) || limitQuotaStatisticsList.size() == 0) { throw new ServiceException("导入数据不能为空!"); } int successNum = 0; int failureNum = 0; StringBuilder successMsg = new StringBuilder(); StringBuilder failureMsg = new StringBuilder(); Date plaeDate = new SimpleDateFormat("yyyy-MM-dd").parse(planDateString); for (LimitQuotaStatistics limitQuotaStatistics : limitQuotaStatisticsList) { try { BeanValidators.validateWithException(validator, limitQuotaStatistics); limitQuotaStatistics.setPlanDate(plaeDate); this.insertLimitQuotaStatistics(limitQuotaStatistics); successNum++; successMsg.append("<br/>" + successNum + "、" + limitQuotaStatistics.getDeptName() + " 导入成功"); } catch (Exception e) { failureNum++; String msg = "<br/>" + failureNum + "、" + limitQuotaStatistics.getDeptName() + " 导入失败:"; failureMsg.append(msg + e.getMessage()); log.error(msg, e); } } if (failureNum > 0) { failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:"); throw new ServiceException(failureMsg.toString()); } else { successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:"); } return successMsg.toString(); } }
其中BeanValidators的实现如下
package com.bdtd.limit.common.utils.bean; import java.util.Set; import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; import javax.validation.Validator; /** * bean对象属性验证 * * @author ruoyi */ public class BeanValidators { public static void validateWithException(Validator validator, Object object, Class<?>... groups) throws ConstraintViolationException { Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups); if (!constraintViolations.isEmpty()) { throw new ConstraintViolationException(constraintViolations); } } }
这里要校验的实体类添加规则为
@Data @AllArgsConstructor @NoArgsConstructor @Builder public class LimitQuotaStatistics extends BaseEntity { private static final long serialVersionUID = 1L; /** id */ private Long id; /** 部门id */ private Long deptId; /** 部门名称 */ @Excel(name = "部门名称") @NotNull(message = "部门名称为空") private String deptName; /** 夜班人数 */ @Excel(name = "夜班人数") @NotNull(message = "夜班人数为空或格式不对") @Range(max = 1000, min = 1, message = "夜班人数需在1-1000之间") private Long nightShiftNum; }
部分字段,仅供参考。
前端页面组件实现
组件实现代码参考若依官方文档中示例
<template> <!-- 用户导入对话框 --> <el-dialog :title="title" :visible.sync="open" width="400px" append-to-body> <div class="block"> <span class="demonstration">计划日期: </span> <el-date-picker v-model="planDate" type="date" placeholder="选择计划日期" size="small" value-format="yyyy-MM-dd" > </el-date-picker> </div> <br /> <el-upload ref="upload" :limit="1" accept=".xlsx, .xls" :headers="headers" :action="upLoadUrl + '?planDateString=' + this.planDate" :disabled="isUploading" :on-progress="handleFileUploadProgress" :on-success="handleFileSuccess" :auto-upload="false" drag > <i class="el-icon-upload"></i> <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div> <div class="el-upload__tip text-center" slot="tip"> <span>仅允许导入xls、xlsx格式文件。</span> </div> </el-upload> <div slot="footer" class="dialog-footer"> <el-button type="primary" @click="submitFileForm">确 定</el-button> <el-button @click="open = false">取 消</el-button> </div> </el-dialog> </template> <script> import { getToken } from "@/utils/auth"; import moment from "moment"; export default { data() { return { // 是否显示弹出层(用户导入) open: false, // 弹出层标题(用户导入) title: "", // 是否禁用上传 isUploading: false, //计划日期 planDate: new Date(), // 设置上传的请求头部 headers: { Authorization: "Bearer " + getToken() }, // 上传的地址 upLoadUrl: "", }; }, mounted() { //默认计划日期为明天 this.planDate = moment().subtract(-1, "days").format("YYYY-MM-DD"); }, methods: { /** 导入按钮操作 */ handleImport(data) { this.title = data.title; this.upLoadUrl = process.env.VUE_APP_BASE_API + data.upLoadUrl; this.open = true; }, // 提交上传文件 submitFileForm() { this.$refs.upload.submit(); }, // 文件上传中处理 handleFileUploadProgress() { this.isUploading = true; }, // 文件上传成功处理 handleFileSuccess(response) { this.open = false; this.isUploading = false; this.$refs.upload.clearFiles(); this.$alert( "<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "导入结果", { dangerouslyUseHTMLString: true } ); //上传数据成功后重新请求数据 this.$emit("getList"); }, }, }; </script> <style> </style>
导入测试效果