前言
两年前我刚步入社会接受毒打时,对整个开发流程还没什么概念,当时我以为的开发工作=“CRUD+接口对接”,所以代码写的那叫一个为所欲为(Map传参、不加注释、不打日志等等等等)。后来我被分到另一个项目组,原来的代码被我一个同事接手,后来这位同事辞职了。。。。。
老大瞅了一眼我写的代码,差点没把早上吃的两个包子吐出来,然后拉着我就是长达一个小时的谈话,啥代码可读性、接口可用性、系统健壮性啥的,咱也听不懂呀,从头到尾就听明白了一句话:“系统的Bug 80%以上都是因为没有做入参校验”。
小何内心ps:曾经有一个机会摆在我面前,我没有好好珍惜,如果上天再给我个机会,我一定会做好入参校验,如果非要给这件事情加个期限,我希望是Just Now
环境准备
假设我们现在需要编写一个下单接口,这个接口的参数包含商户订单号(orderId)、订单金额(money)、支付方式(payType)、平台商户号(pfMchId)、子商户号(subMchId)等字段,按照之前的写法,这个接口应该长如下模样
@RestController
public class OrderController {
@PostMapping("/unifiedOrder")
public String unifiedOrder(@RequestBody Map map){
if (map.get("orderId") != null) {
if (map.get("orderMoney") != null) {
if (map.get("payType") != null) {
if(map.get("mchId") !=null&& map.get("subMchId")!=null){
// TO DO SOMETHING
return "SUCCESS";
}else{
return "平台商户号与子商户号不能同时为空";
}
}else{
return "支付类型不能为空";
}
}else{
return "订单金额不能为空";
}
}else{
return "订单号不能为空";
}
}
}
看完这段代码不知道你怎么想,反正放到现在肯定是要被我DS的,下面就开始着手于此接口的改造。
Spring validation
引入依赖
如果是基于SpringBoot 2.3之前版本的项目会默认引入此依赖。2.3之后的版本则需要手动引入此依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
创建数据模型
这一步其实是为了避免使用Map传递数据,毕竟Map的可读性实在是不怎么理想(
觉得这一步没有必要的拉出去毒打一顿)于是将接口的入参模型Map改为如下数据模型
/**
* 下单接口入参模型
* @author hcq
* @date 2021/3/13 13:23
*/
@Data
public class OrderModel {
/**
* 订单Id
*/
private String orderId;
/**
* 订单金额
*/
private String money;
/**
* 支付方式
*/
private String payType;
/**
* 平台商户号
*/
private String pfMchId;
/**
* 子商户号
*/
private String subMchId;
}
接口变更如下
@RestController
public class OrderController {
@PostMapping("/unifiedOrder")
public String unifiedOrder(@RequestBody OrderModel order){
if (order.getOrderId() != null) {
if (order.getMoney() != null) {
if (order.getPayType() != null) {
if(order.getPfMchId()!=null&& order.getSubMchId()!=null){
// TO DO SOMETHING
return "SUCCESS";
}else{
return "平台商户号与子商户号不能同时为空";
}
}else{
return "支付类型不能为空";
}
}else{
return "订单金额不能为空";
}
}else{
return "订单号不能为空";
}
}
}
好吧~看起来几乎没啥变动,重头戏还在后面
SpringValidation(划重点了呃)
Spring的Validation组件使用起来十分简洁,只需要简单的两步
-
在需要校验的接口参数前加上@Valid或者是@Validated 注解
@RestController public class OrderController { @PostMapping("/unifiedOrder") public String unifiedOrder(@Validated @RequestBody OrderModel order) { if (order.getPfMchId() != null && order.getSubMchId() != null) { // TO DO SOMETHING return "SUCCESS"; } else { return "平台商户号与子商户号不能同时为空"; } } }
-
在需要校验的字段上添加校验规则及提示信息
@Data public class OrderModel { /** * 订单Id */ @NotNull(message = "订单Id不能为空") private String orderId; /** * 订单金额 */ @NotNull(message = "订单金额不能为空") private String money; /** * 支付方式 */ @NotNull(message = "支付方式不能为空") private String payType; /** * 平台商户号 */ private String pfMchId; /** * 子商户号 */ private String subMchId; }
做完这两步后,一个简单的入参校验就已经完成了。如果@NotNull修饰的字段为null值,那么后端服务器将会抛出BindException参数绑定异常,json类型入参则抛出MethodArgumentNotValidException异常,两种异常内部都包含着所有不符合规则的字段提示信息,此时可以由全局异常处理器捕获到此异常并进行异常响应(不清楚全局异常处理器怎样使用的可以参考我之前的文章)。
执行结果分析
PostMan发起请求,后端服务器抛出的MethodArgumentNotValidException异常被默认异常处理器DefaultHandlerExceptionResolver拦截,然后打印了相关异常信息
Validation常见的校验注解
- @NotNull :该字段不允许为null值
- @NotEmpty:该字段不允许为null值或空值,此注解同样适用于校验集合不允许为空
- @Null:该注解与@NotNull正好相反,标识该字段必须为Null
- @Pattern:通过正则表达式进行匹配,若该值无法匹配成功则抛出异常
- @Max:通常使用在数字类型字段,标识该字段最大取值
- @Min:通常使用在数字类型字段,标识该字段最小取值
- @Lenth:标识该字段长度范围
自定义参数校验注解
我们会发现Validation提供的注解大多时候只能满足一些简单的校验场景,稍微复杂一点的场景就不适用于此规则了,例如最常见的一些接口规则有:多选一必填(Or)、只允许某些值中的一个(In)、多个字段不能同时上送(Mutex)等,这个时候我们可以通过自定义注解来完成相关参数的校验。还是拿刚才的栗子来讲,我们可以通过定义一个@Or自定义注解实现"pfMchId与subMchId二选一必填"的校验规则
接口改造如下
@PostMapping("/unifiedOrder")
public String unifiedOrder(@Validated @RequestBody OrderModel order) {
// TO DO SOMETHING
return "SUCCESS";
}
创建自定义注解Or
@Target({ElementType.TYPE})
@Repeatable(List.class)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = InVerifyHandler.class)
public @interface Or {
String[] fields() default {};
String message() default "Invalid ID!";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@interface List{
Or[] value();
}
}
-
@Target({ElementType.TYPE}):标识该注解的作用域(作用Class上)
-
@Repeatable(List.class):标志该注解可以在同一对象上重复声明
-
@Retention(RetentionPolicy.RUNTIME):标识该注解的生命周期(保留至运行时)
-
@Constraint(validatedBy = InVerifyHandler.class):标识该注解的处理类
注解处理类
@Slf4j
public class InVerifyHandler implements ConstraintValidator<Or,Object> {
private String[] fields;
@Override
public void initialize(Or targetAnno) {
fields = targetAnno.fields();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if(fields.length==0){
return true;
}
try {
for (String field : fields) {
Field file = value.getClass().getDeclaredField(field);
file.setAccessible(true);
if(file.get(value)!=null){
return true;
}
}
}catch (Exception e){
log.error("自定义校验注解异常:",e);
}
return false;
}
}
使用介绍
@Data
@Or(fields = {"pfMchId","subMchId"},message = "pfMchId、subMchId不能同时为空")
public class OrderModel {
/**
* 平台商户号
*/
private String pfMchId;
/**
* 子商户号
*/
private String subMchId;
}
通过Validation注解+自定义参数校验注解几乎可以覆盖绝大多数的校验场景,当然也不可能做到百分之百覆盖,一些业务场景上的校验还是有必要手动处理一下的。
本期文章就到这里啦,你学废了吗?