背景
在开发中服务端给前端或者其他业务方使用接口,会在进入核心逻辑前进行参数校验 ,目的是防止不完整的参数导致逻辑出现异常,比如校验空字符串、校验字段为null、或者是正则匹配手机号等等。
哈哈,看上面小郭和小李的沟通,我猜测小李在自测的时候,把参数传完整了,所以系统运行一切正常,但是小郭在调试时没有传完整,导致系统出现了空指针异常,或者称为一个bug吧。发现参数校验在系统中还是很重要的一个环节。
没有参数校验多可怕,少传一个参数系统就崩溃了,但是做参数校验怎样才算更优雅呢?
开篇提问
- 开发中常用的参数校验方式有哪些?
AOP学习文章请进入:什么是AOP
开发中常用的参数校验方式有哪些?
开发一个用户注册接口,需要提供用户的姓名、年龄、手机号、住址(非必填)等信息,按照传统方式的实现思路是进入接口后,通过非空判断,正则匹配等技术校验参数。
一、普通校验方式
模拟注册用户接口,普通的参数拦截需要针对每个参数做校验,尤其是正则表达式需要写多行代码校验,代码美观性不说,给人的感觉代码冗余可读性差。
注册用户接口 【UserController 】
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("/registerUser")
public String registerUser(@RequestBody UserInfoRequest request) {
if (StringUtils.isBlank(request.getName())) {
return "用户姓名为空";
}
if (Objects.isNull(request.getAge())) {
return "年龄为空";
}
if (StringUtils.isBlank(request.getPhone())) {
return "手机号码为空";
}
Pattern pattern = Pattern.compile("0?(13|14|15|17|18)[0-9]{9}");
Matcher matcher = pattern.matcher(request.getPhone());
if (!matcher.find()) {
return "手机号码不正确";
}
System.out.println("插入收据库成功");
return "注册成功";
}
}
使用postman模拟调用,参数传空对象时,按照校验顺序,返回“用户姓名为空”
参数传入name=“小李”,系统按照顺序校验第二个参数age,年龄为空
其他参数填写规范,手机号未按照规范填写时,正则匹配不通过
所有参数填写完整,用户注册成功 虽然校验功能也完成了,如果字段太多的话,一个一个这样写就太麻烦了,稍不留神容易漏下某个字段。上线后的风险就太大了
二、使用SpringBoot方式入参校验
普通校验方式会导致业务代码非常臃肿且不易维护,下面用SpringBoot的AOP方式校验参数。
具体实现方式如下: 1、构建环境,引入相关jar包依赖 2、validation-api注解标注对象 3、在方法参数上增加@Validated 注解 4、增加Controller异常拦截机制 5、使用postman验证
1、构建环境,引入相关jar包依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>2.0.2</version>
</dependency>
2、validation-api注解标注对象
字段上使用注解描述校验的规则,用户信息对象引用了爱好对象是用来验证SpringBoot方式是否可以校验嵌套对象的字段
@Data
public class UserInfoRequest {
@NotBlank(message = "name不能为空")
private String name;
@NotNull(message = "age不能为空")
@Min(value = 0, message = "age不能小于0")
private Integer age;
@NotBlank(message = "phone不能为空")
@Pattern(regexp = "0?(13|14|15|17|18)[0-9]{9}", message = "手机号不正确")
private String phone;
@NotNull(message = "爱好不能为空")
private UserHobbyDTO userHobbyDTO;
private String address;
}
用户爱好对象
@Data
public class UserHobbyDTO implements Serializable {
@NotBlank(message = "hobbyName不能为空")
private String hobbyName;
}
3、在方法参数上增加@Validated 注解
暴露Controller接口,使用@Validated 注解进行参数校验,接口中不需要再写大量参数校验逻辑,让程序员关注业务,代码瞬间变得清爽多了
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("/registerUser2")
public String registerUser2(@RequestBody @Validated UserInfoRequest request) {
System.out.println("插入收据库成功");
return "注册成功";
}
}
4、增加Controller 异常拦截机制
当被校验的对象不符合规范时,通过@ControllerAdvice 进行Controller异常拦截
@ControllerAdvice
public class ControllerExceptionHandler {
private Logger logger = LoggerFactory.getLogger(ControllerExceptionHandler.class);
@ExceptionHandler(value = MethodArgumentNotValidException.class)
@ResponseBody
public Object MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
logger.error("ControllerExceptionHandler.MethodArgumentNotValidExceptionHandler", e);
StringBuilder sb = new StringBuilder();
BindingResult bindingResult = e.getBindingResult();
if (bindingResult.hasErrors()) {
for (ObjectError error : bindingResult.getAllErrors()) {
if (StringUtils.isBlank(sb.toString())) {
sb.append(error.getDefaultMessage());
} else {
sb.append(" | ").append(error.getDefaultMessage());
}
}
}
return sb.toString();
}
}
5、使用postman验证
①什么参数都不传的情况
不传参数时,先看age 字段命中了@NotNull(message = "age不能为空") ,phone字段也是这样,因为这两个字段上配置了两个拦截注解,因为参数为空,所以空判断注解先校验拦截
@NotNull(message = "age不能为空")
@Min(value = 0, message = "age不能小于0")
private Integer age;
@NotBlank(message = "phone不能为空")
@Pattern(regexp = "0?(13|14|15|17|18)[0-9]{9}", message = "手机号不正确")
private String phone;
②把年龄字段传一个小于0的值看看会怎样
当把age参数传入后,@NotNull 注解校验通过,但是@Min 注解校验拦截住了,可以看到返回的是 age不能小于0
③把name和age都填写正确的情况
手机号的校验原理同age情况一样,先校验@NotBlank ,再校验@Pattern 正则,正则未通过,所以返回 手机号不正确
④把爱好字段传一个空对象判断是否可以嵌套校验
传入 userHobbyDTO 字段为空时,爱好对象中的字段并未校验,说明SpringBoot的校验方式无法完成对象嵌套的校验功能
三、自定义AOP方式校验参数
通过上面方式二使用SpringBoot可以优雅的完成对象验参逻辑,如果请求对象中没有引用自定义对象,使用该方式就完全可以了 但是,针对无法校验嵌套对象可以通过自定义开发AOP解决,在SpringBoot中以注解方式接入接口,提升程序员生产力的小工具从此而生,我们开始搭建
具体实现方式如下: 1、开发自定义注解 2、AOP前置拦截器 3、增加Controller异常拦截机制 4、在项目中使用功能 5、使用postman验证
1、开发自定义注解
@NonCheck 可以作用在方法上,通过@Target 来标注,目的是在访问Controller接口时,拦截请求校验参数
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NonCheck {
Class[] clazzs() default {};
String[] params() default {};
}
开启参数校验注解,放在类上使用,可以作为启用参数校验的开关,做成模块功能可快速插拔。这样如果即使在方法上使用了@NonCheck 注解,但是在启动类上未使用@EnableNonCheck ,校验功能也不会生效的
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({Inspect.class})
public @interface EnableNonCheck {
}
2、AOP前置拦截器
@Aspect
@Component
public class Inspect {
private static final Logger logger = LoggerFactory.getLogger(Inspect.class);
@Before(value = "@annotation(com.xtol.common.annotation.NonCheck)")
public void before(JoinPoint point) {
this.logger.info(">>>>>Inspect.before");
Map<String, StringBuffer> errorMap = new HashMap<String, StringBuffer>();
MethodSignature methodSignature = (MethodSignature) point.getSignature();
String[] parameterNames = methodSignature.getParameterNames();
Method method = methodSignature.getMethod();
Map parameterMap = this.bindParameter(parameterNames, point.getArgs());
Map<String, Class> clazzMap = null;
Class[] clazzs = null;
List<String> parameters = null;
if (method.isAnnotationPresent(NonCheck.class)) {
clazzs = method.getAnnotation(NonCheck.class).clazzs();
parameters = Lists.newArrayList(method.getAnnotation(NonCheck.class).params());
clazzMap = this.bindClazz(parameters, clazzs);
this.logger.info("拦截对象:{}", JSONObject.toJSONString(clazzs));
this.logger.info("拦截参数:{}", JSONObject.toJSONString(parameters));
}
Result result = Result.fail();
if (clazzs.length > 0 && clazzs.length == parameters.size()) {
for (String parameter : parameterNames) {
if (parameters.contains(parameter)) {
if (parameterMap.get(parameter) instanceof String) {
Object obj = JSONObject.parseObject((String) parameterMap.get(parameter), clazzMap.get(parameter));
try {
result = VerifyUtils.validateField(obj, errorMap);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
} else {
try {
result = VerifyUtils.validateField(parameterMap.get(parameter), errorMap);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
if (!result.isSuccess()) {
this.logger.info("参数校验失败:{}", JSONObject.toJSONString(errorMap));
this.logger.info("<<<<<Inspect.before");
throw new ValidateException(result.getMessage());
} else {
this.logger.info("参数校验成功");
this.logger.info("<<<<<Inspect.before");
}
}
private Map bindParameter(String[] parameterNames, Object[] args) {
Map map = Maps.newHashMap();
for (int i = 0; i < parameterNames.length; i++) {
map.put(parameterNames[i], args[i]);
}
return map;
}
private Map<String, Class> bindClazz(List<String> parameters, Class[] clazzs) {
Map<String, Class> map = Maps.newHashMap();
for (int i = 0; i < parameters.size(); i++) {
map.put(parameters.get(i), clazzs[i]);
}
return map;
}
}
3、增加Controller异常拦截机制
@ControllerAdvice
public class ControllerExceptionHandler {
private Logger logger = LoggerFactory.getLogger(ControllerExceptionHandler.class);
@ExceptionHandler(value = ValidateException.class)
@ResponseBody
public Result ValidateExceptionHandler(ValidateException e) {
logger.warn("ControllerExceptionHandler.ValidateExceptionHandler: {}", e.getMessage());
return Result.fail(e.getMessage());
}
}
4、在项目中使用功能
在SpringBoot启动类中增加启用注解@EnableNonCheck
@EnableNonCheck
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
在接口方法上增加@NonCheck 注解
注意: clazzs是入参对象的类型,多个类型用逗号分隔,params是入参名称,多个名称可以用逗号分隔
@RestController
@RequestMapping("/user")
public class UserController {
@NonCheck(clazzs = {UserInfoRequest.class}, params = {"request"})
@PostMapping("/registerUser3")
public String registerUser3(@RequestBody UserInfoRequest request) {
System.out.println("插入收据库成功");
return "注册成功";
}
}
5、使用postman验证
这次传入参数userHobbyDTO 是一个空对象,自定义AOP参数校验功能可以验证嵌套对象中的字段,可以看到返回结果已经被拦截了,该功能在日常使用中对于参数校验能帮助研发提效很多,满足日常的业务使用,非常方便。
validation-api常用校验注解
参考链接:https://blog.csdn.net/Hello_World_QWP/article/details/116129788
Validation-API | 描述 |
---|
@AssertFalse | 被注释的元素必须为 false | @AssertTrue | 被注释的元素必须为 true | @DecimalMax | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 | @DecimalMin | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 | @Digits | 被注释的元素必须是一个在可接受范围内的数字 | @Email | 被注释的元素必须是正确格式的电子邮件地址 | @Future | 被注释的元素必须是将来的日期 | @FutureOrPresent | 被注释的元素必须是现在或将来的日期 | @Past | 被注释的元素必须是一个过去的日期 | @PastOrPresent | 被注释的元素必须是过去或现在的日期 | @Max | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 | @Min | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 | @Negative | 被注释的元素必须是一个严格的负数(0为无效值) | @NegativeOrZero | 被注释的元素必须是一个严格的负数(包含0) | @NotBlank | 被注释的元素同StringUtils.isNotBlank,只作用在String上,在String属性上加上@NotBlank约束后,该属性不能为null且trim()之后size>0 | @NotEmpty | 被注释的元素同StringUtils.isNotEmpty,作用在集合类上面,在Collection、Map、数组上加上@NotEmpty约束后,该集合对象是不能为null的,并且不能为空集,即size>0 | @NotNull | 被注释的元素不能是Null,作用在Integer上(包括其它基础类),在Integer属性上加上@NotNull约束后,该属性不能为null,没有size的约束;@NotNull作用在Collection、Map或者集合对象上,该集合对象不能为null,但可以是空集,即size=0(一般在集合对象上用@NotEmpty约束) | @Null | 被注释的元素元素是Null | @Pattern | 被注释的元素必须符合指定的正则表达式 | @Positive | 被注释的元素必须严格的正数(0为无效值) | @PositiveOrZero | 被注释的元素必须严格的正数(包含0) | @Szie | 被注释的元素大小必须介于指定边界(包括)之间 |
|