赞
踩
这段时间在调整老系统相关的一些业务代码;发现一些模块,在无形中就被弄的有点乱了,由于每个开发人员技术水平不同、编码习惯差异;从而导致在请求、响应、异常这一块儿,出现了一些比较别扭的代码;但是归根究底,主要问题还是出在规范上面;不管是大到项目还是小到功能模块,对于请求、响应、异常这一块儿,应该是一块儿公共的模板化的代码,一旦定义清楚之后,是不需要做任何改动,而且业务开发过程中,也几乎是不需要动到他丝毫;所以,一个好的规范下,是不应该在这部分代码上出现混乱或者别扭的情况的;忍不住又得来整理一下这一块儿的东西;
作为一个后台的工程师,接受请求、处理业务、解决异常、响应数据,几乎覆盖了日常开发的全部;但是这个中间,除了业务代码是不可避免且无可替代之外;其他的三项操作,不管是啥功能,也都是大同小异的,那我们要如何来把这一块儿的东西抽离出来,让我们只需要去管业务,不用去管那些杂七杂八的的破事儿,从而腾出更多的时间学(mo)习(yu)呢?当然就是得去定义一个好的规则,运用优秀的轮子;让这部分重复的、可复用的工作给模板化、标准化;
这样,开发一遍,后面就不需要再去弄这些通用的东西了。
思考一下,关于请求、响应、异常,我们到底要注意些啥问题呢?
请求
1. 如何优雅的
接受
数据?
2. 如何优雅的校验
数据?
响应
1.
响应数据格式
如何统一?
2. 错误码如何规范
?
3.如何将业务功能和响应给剥离开来?
异常
1.
异常
如何捕获?
2. 业务异常、校验异常如何合理的转换为友好的标准响应?
3. 如何规避未捕获到的异常并优雅返回标准响应?
这一些列的问题,就衍生出,我们该如何去规范的问题?任何利用已有的优秀框架去解决这些问题?
接下来,就通过一个完整的示例,基于这三个大点下面的小问题,去把这个规范给讲清楚;
讲每个大的问题点之前,我会给大家一个或几个疑问;然后可以带着这些疑问,边思考边看。
下面的介绍,我们就以一个简单的用户信息(UserInfo)的CURD展开
hibernate-validator优雅的处理请求
疑问
主要的目的是为了减少一些非必要的DTO对象
@RestController @RequestMapping("user") public class UserController{ @PostMapping("add") public String add(@RequestBody UserAddRequestDto addInfo){ // ...... return "ok"; } @PutMapping("update") public void update(@RequestBody UserUpdateRequestDto updateInfo) { // ...... return "ok"; } }
这样?嗯!这样确实可以接受到请求参数,但是我们回归到上面的疑问;
参数如何校验?难道这样?
if(null==addInfo.getUserName()){
throw new Exceprion();
}
if(null==addInfo.getPassWord()){
throw new Exceprion();
}
// 。。。。
固然可以,这样真的好吗?很明显不好。。。。劳力伤神的事儿,咱可不干。
addInfo和updateInfo大部分属性都是一样的,添加的字段,大部分都是可以进行修改的,但是也有部分是不可以修改的;比如密码,一般都是单独写接口进行修改;
既然大部分都一样;有必要定义这么多个请求的DTO对象吗?有必要!!没办法啊!大部分一样,他也有不一样的地方!
那有没有能优雅的去解决参数校验问题,又可以将请求对象合多为一呢?
hibernate-validator就是一个可以完美的解决这些问题的优秀框架;
接下来,我们就详细的来看一下,如何使用这个工具。
优点
解耦
,数据的校验与业务逻辑进行分离,降低耦合度
到controller的对象就已经是校验过的对象了,接受到之后就只需要安心处理业务就好,不用再进行数据校验相关逻辑
规范的校验方式
,减少参数校验所带来的繁琐体力活
以注解的方式配置校验规则;大大减少校验的工作量,而且复用性强
简洁代码
,提高代码的可读性
以注解方式即可完成属性校验,去掉了各种冗长的校验代码;且所有的校验规则都定义在对象内部;使得代码结构更加清晰,可读性非常强。
注解说明
下面包含了validator的所有内置的注解
注解 | 作用 |
---|---|
@AssertFalse | 被注释的元素必须为 false |
@AssertTrue | 被注释的元素必须为 true |
@DecimalMin(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@DecimalMax(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@Digits (integer, fraction) | 被注释的元素必须是一个数字,其值必须在可接受的范围内 |
被注释的元素必须是电子邮箱地址 | |
@Future | 被注释的元素必须是一个将来的日期 |
@Length(min=,max=) | 被注释的字符串的大小必须在指定的范围内 |
@Min(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@Max(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@Negative | 该值必须小于0 |
@NegativeOrZero | 该值必须小于等于0 |
@Null | 被注释的元素必须为 null |
@NotNull | 被注释的元素必须不为 null |
@NotBlank(message =) | 验证字符串非null,且长度必须大于0 |
@NotEmpty | 被注释的字符串的必须非空 |
@Past | 被注释的元素必须是一个过去的日期 |
@Pattern(regex=,flag=) | 被注释的元素必须符合指定的正则表达式 |
@Positive | 该值必须大于0 |
@PositiveOrZero | 该值必须大于等于0 |
@Range(min=,max=,message=) | 被注释的元素必须在合适的范围内 |
@Size(max=, min=) | 数组大小必须在[min,max]这个区间 |
@URL(protocol=,host,port) | 检查是否是一个有效的URL,如果提供了protocol,host等,则该URL还需满足提供的条件 |
@Valid | 该注解主要用于字段为一个包含其他对象的集合或map或数组的字段,或该字段直接为一个其他对象的引用,这样在检查当前对象的同时也会检查该字段所引用的对象 |
第一步;引入依赖
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
第二步;属性添加对应的注解
按照上面表格的说明,根据自己定义属性的特点,添加相应的注解。
如下示例,用户名,密码,年龄不能为空;那我们就用@NotBlank @NotNull
去修饰,如果违背规则,就会按message的文本提示
年龄不能小于0岁、大于120岁;那么就用@min @max
进行约束
message描述了违背校验规则之后的描述。
@Data public class UserRequestDto { /** * 用户名 */ @NotBlank(message = "姓名不能为空") public String userName; /** * 密码 */ @NotBlank(message = "密码不能为空") public String passWord; /** * 年龄 */ @NotNull(message = "年龄不能为空") @Min(value = 0,message = "年龄不能小于0岁") @Max(value = 120,message = "年龄不能大于120岁") private Integer age; /** * 手机号码;使用正则进行匹配 */ @NotBlank(message = "手机号码不能为空") @Pattern(regexp = "^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\\d{8}$", message = "号码格式不正确!") private String phoneNum; // 。。。。 }
第三步,Controller的参数加上@Validated
@PostMapping("add")
public String add(@Validated @RequestBody UserRequestDto userRequestDto) {
// 。。。。
}
第四步,测试
第五步,异常处理
上面的操作可以看出,当请求参数如果不符合条件的话,就已经抛出异常并响应客户端了;
但是异常并没有针对性的处理,也没有进行友好的提示;前端收到错误之后,没办法根据错误信息准确的判断出是什么问题;因此对于的异常还需要进行特殊处理;
全局异常:
@RestControllerAdvice @Slf4j public class GlobalExceptionHandler { /** * 处理自定义异常 * */ @ExceptionHandler(value = BusinessException.class) public AjaxResult bizExceptionHandler(BusinessException e) { log.error(e.getMessage(), e); return AjaxResult.defineError(e); } @ExceptionHandler(value = MethodArgumentNotValidException.class) public AjaxResult exceptionHandler( MethodArgumentNotValidException e) { log.error(e.getMessage(), e); return AjaxResult.otherError(e.getFieldError().getDefaultMessage(),500); } /** *处理其他异常 * */ @ExceptionHandler(value = Exception.class) public AjaxResult exceptionHandler( Exception e) { log.error(e.getMessage(), e); return AjaxResult.otherError(ErrorEnum.INTERNAL_SERVER_ERROR); } }
异常枚举类
public enum ErrorEnum { // 数据操作错误定义 SUCCESS(200, "成功"), NO_PERMISSION(403,"你没得权限"), NO_AUTH(401,"未登录"), NOT_FOUND(404, "未找到该资源!"), INTERNAL_SERVER_ERROR(500, "服务器异常请联系管理员"), ; /** 错误码 */ private Integer errorCode; /** 错误信息 */ private String errorMsg; ErrorEnum(Integer errorCode, String errorMsg) { this.errorCode = errorCode; this.errorMsg = errorMsg; } public Integer getErrorCode() { return errorCode; } public String getErrorMsg() { return errorMsg; } }
自定义异常
public class BusinessException extends RuntimeException{ private static final long serialVersionUID = 1L; /** * 错误状态码 */ protected Integer errorCode; /** * 错误提示 */ protected String errorMsg; public BusinessException(){ } public BusinessException(Integer errorCode, String errorMsg) { this.errorCode = errorCode; this.errorMsg = errorMsg; } public Integer getErrorCode() { return errorCode; } public void setErrorCode(Integer errorCode) { this.errorCode = errorCode; } public String getErrorMsg() { return errorMsg; } public void setErrorMsg(String errorMsg) { this.errorMsg = errorMsg; } }
通用返回类
public class AjaxResult { //是否成功 private Boolean success; //状态码 private Integer code; //提示信息 private String msg; //数据 private Object data; public AjaxResult() { } //自定义返回结果的构造方法 public AjaxResult(Boolean success,Integer code, String msg,Object data) { this.success = success; this.code = code; this.msg = msg; this.data = data; } //自定义异常返回的结果 public static AjaxResult defineError(BusinessException de){ AjaxResult result = new AjaxResult(); result.setSuccess(false); result.setCode(de.getErrorCode()); result.setMsg(de.getErrorMsg()); result.setData(null); return result; } //其他异常处理方法返回的结果 public static AjaxResult otherError(ErrorEnum errorEnum){ AjaxResult result = new AjaxResult(); result.setMsg(errorEnum.getErrorMsg()); result.setCode(errorEnum.getErrorCode()); result.setSuccess(false); result.setData(null); return result; } public static AjaxResult otherError(String msg, Integer code){ AjaxResult result = new AjaxResult(); result.setMsg(msg); result.setCode(code); result.setSuccess(false); result.setData(null); return result; } public Boolean getSuccess() { return success; } public void setSuccess(Boolean success) { this.success = success; } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public Object getData() { return data; } public void setData(Object data) { this.data = data; } }
上面我们已经将请求的参数以一种比较优雅的方式给验证了;但是并没有将请求对象合并,依然还是使用的addInfo和updateInfo对参数进行接受的;下面就一起来看一下,如何将这边同质化的对象进行优雅的合并。
group分组校验说明
上面的业务场景中添加和修改用户信息,添加的时候,密码字段是必传的;修改的时候,密码是不需要传的;那我们能否把添加和修改所有用到的属性定义到一个对象中,然后根据不同的请求,去校验参数,比如,调用添加接口,密码是必传的;调用修改接口,就不需要传密码;为了能做到接口区分校验,就可以用到group这个关键参数;
group的理解
可以简单的理解就是把各个属性进行分组;校验的时候,会根据当前Controller指定的组进行校验,这些组里面包含了那些属性,就只校验那些属性,其他不在范围内的,就直接给忽略调掉。
group定义
group的定义是以接口为基本单元;也就是一个接口代表一个组;
使用示例
定义基础的、修改、添加的接口(group)
// 基础的校验接口,标识着所有操作都需要校验的字段
public interface UserRequestDtoSimpleValidate {};
// 修改的校验;继承自UserRequestDtoSimpleValidate
// 也就是说指定为这个组的时候在满足当前校验规则的同时还得校验simple接口的属性
public interface UserRequestDtoUpdateValidate extends UserRequestDtoSimpleValidate {}
// 原理同上
public interface UserRequestDtoAddValidate extends UserRequestDtoUpdateValidate {}
属性校验添加上分组配置
@Data public class UserRequestDtoGroups { /** * 用户名 */ @NotBlank(message = "姓名不能为空",groups = UserRequestDtoSimpleValidate.class) public String userName; /** * 密码 */ @NotBlank(message = "密码不能为空",groups = UserRequestDtoAddValidate.class) public String passWord; /** * 年龄 */ @NotNull(message = "年龄不能为空",groups = UserRequestDtoSimpleValidate.class) @Min(value = 0,message = "年龄不能小于0岁",groups = UserRequestDtoSimpleValidate.class) @Max(value = 120,message = "年龄不能大于120岁",groups = UserRequestDtoSimpleValidate.class) private Integer age; /** * 手机号码;使用正则进行匹配 */ @NotBlank(message = "手机号码不能为空",groups = UserRequestDtoAddValidate.class) @Pattern(regexp = "^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\\d{8}$", message = "号码格式不正确!",groups = UserRequestDtoAddValidate.class) private String phoneNum; }
Controller指定分组进行校验
如下@Validated中,指定分组接口类;可以一个,也可以多个,这样就会按照指定的分组进行参数校验
@PostMapping("add")
public String add(@Validated(UserRequestDtoAddValidate.class) @RequestBody UserRequestDtoGroups dtoGroups) {
// 后续业务
return "ok";
}
@PutMapping("update")
public void update(@Validated(UserRequestDtoUpdateValidate.class) @RequestBody UserRequestDtoGroups dtoGroups) {
// 后续业务
}
测试:
结果没有报错,因为熟悉走的是新增的校验group,而controller写的是修改的group,所以不会报错。
;修改属性的校验分组修改为修改的校验。
因为分组一致则进行了校验。
上面的所有校验,全部使用的是内置的注解,实际的使用过程中,不可避免的有一些特殊的业务场景,参数规则太过于个性化,内置的注解无法满足我们的需求时,要怎么办?比如说,文本必须全部是大写或者小写(该需求其实也可以通过正则表达式的方式进行);为了剧情需要,那我们可以基于这个需求,来自定义一个校验器;
定义大小写的枚举
用于注解使用的时候,来指定是校验规则是大写的还是小写的
public enum CaseMode {
//大写
UPPER,
//小写
LOWER;
}
定义校验大小写的注解
@Documented
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
//指定校验器
@Constraint(validatedBy = CaseCheckValidator.class)
public @interface CaseCheck {
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
CaseMode value() default CaseMode.UPPER;
}
这里面,我们将CaseMode枚举作为了注解中的value参数,可以根据需要动态设置大小写的参数,这里默认就是大写的;
@Constraint(validatedBy = CaseCheckValidator.class)
指明的使用CaseCheckValidator这个校验器进行数据校验;具体的校验规则,判断逻辑,就是写在这个校验器里面。
自定义校验器
public class CaseCheckValidator implements ConstraintValidator<CaseCheck, String> { //大小写的枚举 private CaseMode caseMode; @Override public void initialize(CaseCheck caseCheck) { this.caseMode = caseCheck.value(); } @Override public boolean isValid(String value, ConstraintValidatorContext context) { //如果文本是空,则不进行校验,因为有其他的注解是可以校验空或者空字符串的 if (null == value) { return true; } //文本只能是字母的正则 String pattern = "^[a-zA-Z]*$"; //校验传进来的是否是只包含了字母的文本 boolean isMatch = Pattern.matches(pattern, value); //如果存在其他字符则返回校验失败 if (!isMatch) { return false; } //如果没有指定方式,则直接返回false if (null == caseMode) { return false; } //判断是否符合大小写条件 if (caseMode == CaseMode.UPPER) { return value.equals(value.toUpperCase()); } else { return value.equals(value.toLowerCase()); } } }
泛型说明
该校验器继承自ConstraintValidator
这个接口;并传递了两个泛型参数;第一个是指明你自定义的注解;第二个是该注解作用的属性类型;
校验初始化
如果属性添加了该校验器对应的注解,就会初始化(initialize)该校验器时,将你加在属性上面的注解传递进来;
验证
初始化完会调用isValid方法·,并传递属性值;拿到属性值之后,就可以根据初始化传入的注解指定的规则,对属性值进行校验。验证通过返回true,并进行下一个属性的校验;验证失败返回false,并抛出异常;
测试
/**
* 用户名
*/
@NotBlank(message = "姓名不能为空",groups = UserRequestDtoSimpleValidate.class)
@CaseCheck(value = CaseMode.UPPER,message = "用户名必须大写字母",groups = UserRequestDtoSimpleValidate.class)
public String userName;
// 。。。。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。