使用Spring的AOP优雅的进行参数校验【三种方式】

背景

在开发中服务端给前端或者其他业务方使用接口,会在进入核心逻辑前进行参数校验,目的是防止不完整的参数导致逻辑出现异常,比如校验空字符串校验字段为null、或者是正则匹配手机号等等。

哈哈,看上面小郭和小李的沟通,我猜测小李在自测的时候,把参数传完整了,所以系统运行一切正常,但是小郭在调试时没有传完整,导致系统出现了空指针异常,或者称为一个bug吧。发现参数校验在系统中还是很重要的一个环节。

没有参数校验多可怕,少传一个参数系统就崩溃了,但是做参数校验怎样才算更优雅呢?

开篇提问

  1. 开发中常用的参数校验方式有哪些?
    AOP学习文章请进入:什么是AOP

开发中常用的参数校验方式有哪些?

开发一个用户注册接口,需要提供用户的姓名、年龄、手机号、住址(非必填)等信息,按照传统方式的实现思路是进入接口后,通过非空判断,正则匹配等技术校验参数。

一、普通校验方式

模拟注册用户接口,普通的参数拦截需要针对每个参数做校验,尤其是正则表达式需要写多行代码校验,代码美观性不说,给人的感觉代码冗余可读性差。

注册用户接口 【UserController 】

@RestController
@RequestMapping("/user")
public class UserController {

    /**
     * 注册用户
     *
     * @return
     */
    @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包依赖

<!--SpringBoot依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--参数验证插件-->
<!--官网:https://www.validationapi.com/-->
<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 {
    /**
     * 注册用户
     *
     * @return
     */
    @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// 这个注解表明 使用spring boot 的aop,需要开启aop spring.aop.auto=true
@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>();
        // 访问目标方法的参数名称parameterNames和方法method
        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
        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();

        // NonCheck注解中的两个参数数量不能为空并且保持一致
        if (clazzs.length > 0 && clazzs.length == parameters.size()) {
            for (String parameter : parameterNames) {
                if (parameters.contains(parameter)) {
                    if (parameterMap.get(parameter) instanceof String) {
                        // 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");
        }
    }

    /**
     * 绑定参数
     *
     * @param parameterNames
     * @param args
     * @return
     */
    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;
    }

    /**
     * 绑定注解类
     *
     * @param parameters
     * @param clazzs
     * @return
     */
    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 {
    /**
     * 注册用户
     *
     * @return
     */
    @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被注释的元素大小必须介于指定边界(包括)之间
  • 8
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值