SpringBoot 2.X Kotlin系列之数据校验和异常处理
在开发项目时,我们经常需要在前后端都校验用户提交的数据,判断提交的数据是否符合我们的标准,包括字符串长度,是否为数字,或者是否为手机号码等;这样做的目的主要是为了减少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())
}
}
这里如果有参数错误就直接抛出参数异常,然后交给全局异常处理器来捕获。
Controller层编写
@PostMapping("/student")
fun create(@RequestBody studentForm: StudentForm): ResultVo<StudentDTO> {
this.validate(studentForm)
val studentDTO = StudentDTO()
BeanUtils.copyProperties(studentForm, studentDTO)
return ResultVo(200, "", studentDTO)
}
1.传入一个空对象: 返回结果:
{
"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": "学校"
}
}
本章内容就到此结束了,如果错误的地方欢迎大家及时指出,觉得有用的话就点个赞,谢谢❤
13 声望
2 粉丝
推荐阅读
与RabbitMQ有关的一些知识
工作中用过一段时间的Kafka,不过主要还是RabbitMQ用的多一些。今天主要来讲讲与RabbitMQ相关的一些知识。一些基本概念,以及实际使用场景及一些注意事项。
lpe234赞 8阅读 1.9k
spring boot 锁
由于当前的项目中由于多线程操作同一个实体,会出现数据覆盖的问题,后保存的实体把先保存的实体的数据给覆盖了。于是查找了锁的实现的几种方式。但写到最后发现,其实自己可以写sql 更新需要更新的字段即可,这...
weiewiyi赞 3阅读 9.2k
利用Docker部署管理LDAP及其初次使用
前言:本周主要写了gitlabWebhook转github的项目,总体上没有遇到什么大问题,这周接触到了LDAP,于是就花时间实际操作了解了一下。
李明赞 5阅读 891
记录本周问题
项目里两个地方都用到了hashmap。但是感觉自己用的时候并没有感觉非常的清晰。同时发现hashmap有线程不安全问题,而自己用的时候就是多线程来使用。于是在这里介绍一下。
weiewiyi赞 5阅读 705
记录java 在遍历中删除元素 以及 mysql5.6版本添加unique失败
遍历中删除List或Queue等数据结构中,如何一边遍历一遍删除?1. 常犯错误ArrayList可能没遇到坑过的人会用增强for循环这么写: {代码...} 但是一运行,结果却抛 java.util.ConcurrentModificationException 异常即...
weiewiyi赞 4阅读 774
Spring Security + JWT
Spring Security默认是基于session进行用户认证的,用户通过登录请求完成认证之后,认证信息在服务器端保存在session中,之后的请求发送上来后SecurityContextPersistenceFilter过滤器从session中获取认证信息、...
福赞 4阅读 1.4k
Reactive Spring实战 -- 理解Reactor的设计与实现
Reactor是Spring提供的非阻塞式响应式编程框架,实现了Reactive Streams规范。 它提供了可组合的异步序列API,例如Flux(用于[N]个元素)和Mono(用于[0 | 1]个元素)。
binecy赞 3阅读 4.7k评论 2
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。