在开发项目时,我们经常需要在前后端都校验用户提交的数据,判断提交的数据是否符合我们的标准,包括字符串长度,是否为数字,或者是否为手机号码等;这样做的目的主要是为了减少SQL注入攻击的风险以及脏数据的插入。提到数据校验我们通常还会提到异常处理,因为为了安全起见,后端出现的异常我们通常不希望直接抛到客户端,而是经过我们的处理之后再返回给客户端,这样做主要是提升系统安全性,另外就是给予用户友好的提示。
class StudentForm() { @NotBank(message = '生日不能为空') var birthday: String = "" @NotBlank(message = "Id不能为空") var id:String = "" @NotBlank(message = "年龄不能为空") var age:String = "" @NotEmpty(message = "兴趣爱好不能为空") var Interests:List<String> = Collections.emptyList() @NotBlank(message = "学校不能为空") var school: String = "" override fun toString(): String { return ObjectMapper().writeValueAsString(this) } }
这里首先使用的是基础校验注解,位于 javax.validation.constraints
下,常见注解有 @NotNull
、 @NotEmpty
、 @Max
、 @Email
、 @NotBank
、 @Size
、 @Pattern
,当然出了这些还有很多注解,这里就不在一一讲解,想了解更多的可以咨询查看jar包。
这里简单讲解一下注解的常见用法:
@NotNull
: 校验一个对象是否为 Null
@NotBank
: 校验字符串是否为空串 @NotEmpty
: 校验 List
、 Map
、 Set
是否为空 @Email
: 校验是否为邮箱格式 @Max @Min
: 校验 Number
或 String
是否在指定范围内 @Size
: 通常需要配合 @Max @Min
一期使用 @Pattern
: 配合自定义正则表达式校验 enum class ResultEnums(var code:Int, var msg:String) { SUCCESS(200, "成功"), SYSTEM_ERROR(500, "系统繁忙,请稍后再试"), }
这里主要是参数校验,所以定义一个运行时异常,代码如下:
class ParamException(message: String?) : RuntimeException(message) { var code:Int = ResultEnums.SUCCESS.code constructor(code:Int, message: String?):this(message) { this.code = code } }
class ResultVo<T> { var status:Int = ResultEnums.SUCCESS.code var msg:String = "" var data:T? = null constructor() constructor(status:Int, msg:String, data:T) { this.status = status this.data = data this.msg = msg } override fun toString(): String { return ObjectMapper().writeValueAsString(this) } }
这里的全局异常处理,是指请求到达Controller层之后发生异常处理。代码如下:
@RestControllerAdvice class RestExceptionHandler { private val logger:Logger = LoggerFactory.getLogger(this.javaClass) @ExceptionHandler(Exception::class) @ResponseBody fun handler(exception: Exception): ResultVo<String> { logger.error("全局异常:{}", exception) return ResultVo(500, "系统异常", "") } @ExceptionHandler(ParamException::class) @ResponseBody fun handler(exception: ParamException): ResultVo<String> { logger.error("参数异常:{}", exception.localizedMessage) return ResultVo(exception.code, exception.localizedMessage, "") } }
这里得和Java处理的方式大同小异,无疑就是更加简洁了而已。
object ValidatorUtils { private val validator = Validation.buildDefaultValidatorFactory().validator /** * 校验对象属性 * @param obj 被校验对象 * @param <T> 泛型 * @return Map </T> */ fun validate(obj: Any): Map<String, String> { var errorMap: Map<String, String>? = null val set = validator.validate(obj, Default::class.java) if (CollectionUtils.isEmpty(set)) { return emptyMap() } errorMap = set.map { it.propertyPath.toString() to it.message }.toMap() return errorMap } /** * 校验对象属性 * @param obj 被校验对象 * @param <T> 泛型 * @return List </T> */ fun validata(obj: Any): List<String> { val set = validator.validate(obj, Default::class.java) return if (CollectionUtils.isEmpty(set)) { emptyList() } else set.stream() .filter {Objects.nonNull(it)} .map { it.message } .toList() } }
因为校验是通用的,几乎大部分接口都需要检验传入参数,所以我们把校验方法抽出来放在通用Controller层里,通用层这里不建议使用 Class
或者是 抽象类
,而是使用 interface
,定义如下:
@Throws(ParamException::class) fun validate(t:Any) { val errorMap = ValidatorUtils.validate(t).toMutableMap() if (errorMap.isNotEmpty()) { throw ParamException(ResultEnums.SYSTEM_ERROR.code, errorMap.toString()) } }
这里如果有参数错误就直接抛出参数异常,然后交给全局异常处理器来捕获。
@PostMapping("/student") fun create(@RequestBody studentForm: StudentForm): ResultVo<StudentDTO> { this.validate(studentForm) val studentDTO = StudentDTO() BeanUtils.copyProperties(studentForm, studentDTO) return ResultVo(200, "", studentDTO) }
{ "status": 500, "msg": "{school=学校不能为空, id=Id不能为空, age=年龄不能为空, Interests=兴趣爱好不能为空}", "data": "" }
本篇文章开始之前我们提到过 @Pattern
,这个注解主要是方便我们定义自己的校验规则,假如我这里需要校验前端传入的生日,是否符合我所需要的格式,如下所示:
@NotBlank(message = "生日不能为空") @Pattern(regexp="^(19|20)//d{2}-(1[0-2]|0?[1-9])-(0?[1-9]|[1-2][0-9]|3[0-1])$", message="不是生日格式") var birthday: String = ""
这里的校验逻辑可能不完善,大家使用的时候需要注意。
修改完成后我再次请求
入参: { "age": "10", "id": "1", "school": "学校", "interests": ["户外运动"], "birthday": "" } 出参: { "status": 500, "msg": "{birthday=生日不能为空}", "data": "" }
入参: { "age": "10", "id": "1", "school": "学校", "interests": ["户外运动"], "birthday": "1989-20-20" } 出参: { "status": 500, "msg": "{birthday=不是生日格式}", "data": "" }
入参: { "age": "10", "id": "1", "school": "学校", "interests": ["户外运动"], "birthday": "1999-01-01" } 出参: { "status": 200, "msg": "", "data": { "id": "1", "birthday": "1999-01-01", "age": "10", "school": "学校" } }
本章内容就到此结束了,如果错误的地方欢迎大家及时指出,觉得有用的话就点个赞,谢谢❤