springboot写自定义参数验证方式

简介: 本次发表文章距上次发表已近有两月有余,原因是两月前离开了上家公司(离开原因可能会在年终终结叙述,本篇暂且忽略),来到了现在所在的京东集团,需要花时间熟悉环境和沉淀一下新的东西,因此写文章也暂时没那么勤奋了,不得不说这次是机遇也是对自己职业生涯的一次重要决定。

本次发表文章距上次发表已近有两月有余,原因是两月前离开了上家公司(离开原因可能会在年终终结叙述,本篇暂且忽略),来到了现在所在的京东集团,需要花时间熟悉环境和沉淀一下新的东西,因此写文章也暂时没那么勤奋了,不得不说这次是机遇也是对自己职业生涯的一次重要决定。

话说本篇内容主要分享的是自定义方法参数的验证,参数的基本校验在对外接口或者公用方法时经常所见,用过hibernate的验证方式的朋友一定不会陌生,读完本篇内容能够很好的帮助各位朋友对自定义参数验证方式有一定了解:

  • 自定义参数验证的思路
  • 实战参数验证的公用方法
  • aop结合方法参数验证实例

自定义参数验证的思路

对于自定义参数验证来说,需要注意的步骤有以下几步:

  1. 怎么区分需要验证的参数,或者说参数实体类中需要验证的属性(答案:可用注解标记)
  2. 对于参数要验证哪几种数据格式(如:非空、邮箱、电话以及是否满足正则等格式)
  3. 怎么获取要验证的参数数据(如:怎么获取方法参数实体传递进来的数据)
  4. 验证失败时提示的错误信息描述(如:统一默认校验错误信息,或者获取根据标记验证注解传递的错误提示文字暴露出去)
  5. 在哪一步做校验(如:进入方法内部时校验,或是可以用aop方式统一校验位置)

实战参数验证的公用方法

根据上面思路描述,我们首先需要有注解来标记哪些实体属性需要做不同的校验,因此这里创建两种校验注解(为了本章简短性):IsNotBlank(校验不能为空)和RegExp(正则匹配校验),如下代码:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface IsNotBlank {
    String des() default "";
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface RegExp {
    String pattern();

    String des() default "";
}

然后为了统一这里创建公用的验证方法,此方法需要传递待验证参数的具体实例,其主要做的工作有:

  1. 通过传递进来的参数获取该参数实体的属性
  2. 设置field.setAccessible(true)允许获取对应属性传进来的数据
  3. 根据对应标记属性注解来验证获取的数据格式,格式验证失败直接提示des描述

这里有如下公用的验证方法:

public class ValidateUtils {

    public static void validate(Object object) throws IllegalAccessException {
        if (object == null) {
            throw new NullPointerException("数据格式校验对象不能为空");
        }
        //获取属性列
        Field[] fields = object.getClass().getDeclaredFields();
        for (Field field : fields) {
            //过滤无验证注解的属性
            if (field.getAnnotations() == null || field.getAnnotations().length <= 0) {
                continue;
            }
            //允许private属性被访问
            field.setAccessible(true);
            Object val = field.get(object);
            String strVal = String.valueOf(val);

            //具体验证
            validField(field, strVal);
        }
    }

    /**
     * 具体验证
     *
     * @param field  属性列
     * @param strVal 属性值
     */
    private static void validField(Field field, String strVal) {
        if (field.isAnnotationPresent(IsNotBlank.class)) {
            validIsNotBlank(field, strVal);
        }
        if (field.isAnnotationPresent(RegExp.class)) {
            validRegExp(field, strVal);
        }
        /** add... **/
    }

    /**
     * 匹配正则
     *
     * @param field
     * @param strVal
     */
    private static void validRegExp(Field field, String strVal) {
        RegExp regExp = field.getAnnotation(RegExp.class);
        if (Strings.isNotBlank(regExp.pattern())) {
            if (Pattern.matches(regExp.pattern(), strVal)) {
                return;
            }
            String des = regExp.des();
            if (Strings.isBlank(des)) {
                des = field.getName() + "格式不正确";
            }
            throw new IllegalArgumentException(des);
        }
    }

    /**
     * 非空判断
     *
     * @param field
     * @param val
     */
    private static void validIsNotBlank(Field field, String val) {
        IsNotBlank isNotBlank = field.getAnnotation(IsNotBlank.class);
        if (val == null || Strings.isBlank(val)) {
            String des = isNotBlank.des();
            if (Strings.isBlank(des)) {
                des = field.getName() + "不能为空";
            }
            throw new IllegalArgumentException(des);
        }
    }
}

有了具体验证方法,我们需要个测试实例,如下测试接口和实体:

public class TestRq extends BaseRq implements Serializable {

    @IsNotBlank(des = "昵称不能为空")
    private String nickName;
    @RegExp(pattern = "\\d{10,20}", des = "编号必须是数字")
    private String number;
    private String des;
    private String remark;
}
    @PostMapping("/send")
    public BaseRp<TestRp> send(@RequestBody TestRq rq) throws IllegalAccessException {
        ValidateUtils.validate(rq);
        return testService.sendTestMsg(rq);
    }

image

aop结合方法参数验证实例

上面是围绕公用验证方法来写的,通常实际场景中都把它和aop结合来做统一验证;来定制两个注解,MethodValid方法注解(是否验证所有参数)和ParamValid参数注解(标记方法上的某个参数):

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
public @interface MethodValid {
    /**
     * 验证所有参数
     *
     * @return true
     */
    boolean isValidParams() default true;
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.PARAMETER})
public @interface ParamValid {
}

有了两个标记注解再来创建aop,我这里是基于springboot框架的实例,所有引入如下mvn:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

然后aop需要做如下逻辑:

  1. 获取方法上传递参数(param1,param2...)
  2. 遍历每个参数实体,如有验证注解就做校验
  3. 遍历标记有ParamValid注解的参数,如有验证注解就做校验

这里特殊的地方是,想要获取方法参数对应的注解,需要method.getParameterAnnotations()获取所有所有参数注解后,再用索引来取参数对应的注解;如下aop代码:

package com.shenniu003.common.validates;

import com.shenniu003.common.validates.annotation.MethodValid;
import com.shenniu003.common.validates.annotation.ParamValid;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;

/**
 * des:
 *
 * @author: shenniu003
 * @date: 2019/12/01 11:04
 */
@Aspect
@Component
public class ParamAspect {

    @Around(value = "@annotation(methodValid)", argNames = "joinPoint,methodValid")
    public Object validMethod(ProceedingJoinPoint joinPoint, MethodValid methodValid) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        System.out.println("method:" + method.getName());
        String strArgs = Arrays.toString(joinPoint.getArgs());
        System.out.println("params:" + strArgs);

        //获取方法所有参数的注解
        Annotation[][] parametersAnnotations = method.getParameterAnnotations();

        for (int i = 0; i < joinPoint.getArgs().length; i++) {
            Object arg = joinPoint.getArgs()[i];
            if (arg == null) {
                continue; //
            }

            if (methodValid.isValidParams()) {
                //验证所有参数
                System.out.println(arg.getClass().getName() + ":" + arg.toString());
                ValidateUtils.validate(arg);
            } else {
                //只验证参数前带有ParamValid注解的参数
                //获取当前参数所有注解
                Annotation[] parameterAnnotations = parametersAnnotations[i];
                //是否匹配参数校验注解
                if (matchParamAnnotation(parameterAnnotations)) {
                    System.out.println(Arrays.toString(parameterAnnotations) + " " + arg.getClass().getName() + ":" + arg.toString());
                    ValidateUtils.validate(arg);
                }
            }
        }
        return joinPoint.proceed();
    }

    /**
     * 是否匹配参数的注解
     *
     * @param parameterAnnotations 参数对应的所有注解
     * @return 是否包含目标注解
     */
    private boolean matchParamAnnotation(Annotation[] parameterAnnotations) {
        boolean isMatch = false;
        for (Annotation parameterAnnotation : parameterAnnotations) {
            if (ParamValid.class == parameterAnnotation.annotationType()) {
                isMatch = true;
                break;
            }
        }
        return isMatch;
    }
}

这里编写3中方式的测试用例,验证方法所有参数、无参数不验证、验证方法参数带有@ParamValid的参数,以此达到不同需求参数的校验方式:

    //验证方法所有参数
    @MethodValid
    public void x(TestRq param1, String param2) {
    }
    //无参数不验证
    @MethodValid
    public void xx() {
    }
    //验证方法参数带有@ParamValid的参数
    @MethodValid(isValidParams = false)
    public void xxx(TestRq param1, @ParamValid String param2) {
    }

同样用send接口作为测试入口,调用上面3种方法:

    @PostMapping("/send")
    @MethodValid
    public BaseRp<TestRp> send(@RequestBody TestRq rq) throws IllegalAccessException {
//        ValidateUtils.validate(rq);
        testController.x(rq, "验证方法所有参数");
        testController.xx();
        testController.xxx(rq, "验证方法参数带有@ParamValid的参数");
        return testService.sendTestMsg(rq);
    }

image

相关文章
|
2月前
|
SQL 监控 druid
springboot-druid数据源的配置方式及配置后台监控-自定义和导入stater(推荐-简单方便使用)两种方式配置druid数据源
这篇文章介绍了如何在Spring Boot项目中配置和监控Druid数据源,包括自定义配置和使用Spring Boot Starter两种方法。
|
26天前
|
并行计算 Java 数据处理
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
123 0
|
15天前
|
JSON 安全 算法
|
1月前
|
前端开发 Java 数据安全/隐私保护
用户登录前后端开发(一个简单完整的小项目)——SpringBoot与session验证(带前后端源码)全方位全流程超详细教程
文章通过一个简单的SpringBoot项目,详细介绍了前后端如何实现用户登录功能,包括前端登录页面的创建、后端登录逻辑的处理、使用session验证用户身份以及获取已登录用户信息的方法。
125 2
用户登录前后端开发(一个简单完整的小项目)——SpringBoot与session验证(带前后端源码)全方位全流程超详细教程
|
26天前
|
人工智能 自然语言处理 前端开发
SpringBoot + 通义千问 + 自定义React组件:支持EventStream数据解析的技术实践
【10月更文挑战第7天】在现代Web开发中,集成多种技术栈以实现复杂的功能需求已成为常态。本文将详细介绍如何使用SpringBoot作为后端框架,结合阿里巴巴的通义千问(一个强大的自然语言处理服务),并通过自定义React组件来支持服务器发送事件(SSE, Server-Sent Events)的EventStream数据解析。这一组合不仅能够实现高效的实时通信,还能利用AI技术提升用户体验。
137 2
|
2月前
|
Java Spring
springboot静态资源目录访问,及自定义静态资源路径,index页面的访问
本文介绍了Spring Boot中静态资源的访问位置、如何进行静态资源访问测试、自定义静态资源路径和静态资源请求映射,以及如何处理自定义静态资源映射对index页面访问的影响。提供了两种解决方案:取消自定义静态资源映射或编写Controller来截获index.html的请求并重定向。
springboot静态资源目录访问,及自定义静态资源路径,index页面的访问
|
27天前
|
JSON NoSQL Java
springBoot:jwt&redis&文件操作&常见请求错误代码&参数注解 (九)
该文档涵盖JWT(JSON Web Token)的组成、依赖、工具类创建及拦截器配置,并介绍了Redis的依赖配置与文件操作相关功能,包括文件上传、下载、删除及批量删除的方法。同时,文档还列举了常见的HTTP请求错误代码及其含义,并详细解释了@RequestParam与@PathVariable等参数注解的区别与用法。
|
1月前
|
缓存 NoSQL Java
Springboot自定义注解+aop实现redis自动清除缓存功能
通过上述步骤,我们不仅实现了一个高度灵活的缓存管理机制,还保证了代码的整洁与可维护性。自定义注解与AOP的结合,让缓存清除逻辑与业务逻辑分离,便于未来的扩展和修改。这种设计模式非常适合需要频繁更新缓存的应用场景,大大提高了开发效率和系统的响应速度。
48 2
|
25天前
|
监控 Java Maven
springboot学习二:springboot 初创建 web 项目、修改banner、热部署插件、切换运行环境、springboot参数配置,打包项目并测试成功
这篇文章介绍了如何快速创建Spring Boot项目,包括项目的初始化、结构、打包部署、修改启动Banner、热部署、环境切换和参数配置等基础操作。
100 0
|
27天前
|
前端开发 Java 数据库
springBoot:template engine&自定义一个mvc&后端给前端传数据&增删改查 (三)
本文介绍了如何自定义一个 MVC 框架,包括后端向前端传递数据、前后端代理配置、实现增删改查功能以及分页查询。详细展示了代码示例,从配置文件到控制器、服务层和数据访问层的实现,帮助开发者快速理解和应用。