如何使用SpringAOP优雅的实现接口参数校验

本文介绍了如何在Java中通过自定义注解和AOP实现参数校验,避免接口参数错误。首先定义了@NotNull和@NotEmpty两个注解,然后在ServiceAspect切面中使用Spring AOP的@Around注解处理参数校验,针对String类型和对象类型的参数进行了详细的校验逻辑实现。在Spring Boot 2.x中,由于默认使用Cglib动态代理,无法获取接口注解,因此需要调整配置使用JDK Proxy。此外,文章还分享了在编译时启用获取参数名的技巧,并提供了测试用例验证校验功能。

1.前言

我们在写接口的时候无法避免对参数进行校验

public interface HelloService {

    /**
     * 测试
     * @param userName 用户名
     * @return hello + userName
     */
    String sayHello(@NotEmpty String userName);
}

就像下面的代码,但是一旦接口传入的参数多起来,光参数校验就要写很多行代码,代码看起来也非常的不美观。

public class HelloServiceImpl implements HelloService {

    @Override
    public String sayHello(String userName) {
        if (null == userName || userName.equals("")) {
            return "wrong name";
        }
        return "Hello " + userName;

    }
}

很多人肯定会想到了spring validation通过注解如:@NotNull,@NotEmpty 等来校验controller的参数,我也参考测试了下,在当接口不是controller的时候,spring validation没有生效。那么,对于自己的PRC接口需要校验参数怎么办(其实很多RPC框架都有自己的实现),这里我们自己来实现一个。

2 通过注解和AOP实现参数校验

2.1 从定义注解开始

这里我们根据需要就定义两个注解,Target可以是parameter和field,runtime类型。

  • @NotNull 用于校验非字符串对象
  • @NotEmpty 用于校验字符串
/**
 * @author : xiangyida
 * @date : 10:11 下午 2021/3/18
 * 用于校验非字符串对象
 */
@Target({ElementType.PARAMETER,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotNull {
}
/**
 * @author : xiangyida
 * @date : 10:19 下午 2021/3/18
 * 用于校验字符串
 */
@Target({ElementType.PARAMETER,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotEmpty {
}

2.2 引入AOP依赖

这里我们基于Springboot使用AOP

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.3.2.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>

开始定义我们的切面,注意这里我们around advice,因为我们检测到参数非法时需要对方法进行拦截。
这里的切点可以定义在接口或者实现类上。

@Aspect
@Component
public class ServiceAspect {
    @Around(value = "execution(* com.xyh.api..*(..))")
    public Object handleParam(ProceedingJoinPoint joinPoint) throws Throwable {
        return joinPoint.proceed();
    }
}

2.3 校验String类型的参数

先给参数打上注解

public interface HelloService {

    /**
     * 测试
     * @param userName 用户名
     * @return hello + userName
     */
    String sayHello(@NotEmpty String userName);
}

先实现对String类型参数的校验

@Aspect
@Component
public class ServiceAspect {

    @Around(value = "execution(* com.xyh.api..*(..))")
    public Object handleParam(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取参数的值
        Object[] args = joinPoint.getArgs();
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        //获取method
        Method method = methodSignature.getMethod();
        //通过method获取参数
        Parameter[] parameters = method.getParameters();
        for (int i = 0; i < parameters.length; i++) {
            Parameter parameter = parameters[i];
            if (parameter.isAnnotationPresent(NotEmpty.class) && StringUtils.isEmpty(args[i])) {
                return "参数不能为空: " + parameter.toString();
            }
        }
        return joinPoint.proceed();
    }
}

测试一下效果

    @SpringBootTest
    @RunWith(SpringRunner.class)
    public class TestHelloService {
        @Autowired
        private HelloService helloService;

        /**
         * 测试字符串类型参数校验
         */
        @Test
        public void testSayHello(){
            System.out.println(helloService.sayHello("Tom"));
            System.out.println(helloService.sayHello(""));
        }
    }

在这里插入图片描述
这里需要注意两个地方

  • 这里有个比较关键的地方(严重踩坑的地方),正常情况下来说,我们的注解肯定是打在接口上的,但是即使切点位于接口方法上,织入的目标也是到接口实现类中的方法上,所以在使用AOP获取参数上的注解的时候,我们需要获取到接口方法上的注解。这个时候需要使用JDKProxy,因为JDKProxy是基于接口来实现的。而对于SpringBoot1.x的动态代理默认的是JDKProxy,对于SpringBoot2.x默认使用的Cglib。cglib是基于字节码来实现的动态代理,这样是无法获取到接口上的注解的。这里我们可以修改下配置,让SpringBoot2.x默认使用JDKProxy

    spring:
      aop:
        proxy-target-class: false
    
  • 这里打印了参数名,Java8是支持获取参数名的,不过需要在编译的时候加入参数

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.3</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <compilerArgs>
                        <arg>-parameters</arg>
                    </compilerArgs>
                </configuration>
            </plugin>
        </plugins>
    </build>

2.4 校验对象类型的参数

2.4.1 先定义个对象类型的入参

这里直接把对象嵌套的情况考虑进去了吧。

@Data
public class UserInfo {
    /**
     * 用户id
     */
    @NotEmpty
    private String UserId;
    /**
     * 用户详细信息
     */
    @NotNull
    private UserDetail userDetail;
}
@Data
public class UserDetail {
    /**
     * 用户名
     */
    @NotEmpty
    private String userName;
    /**
     * 电话号码
     */
    @NotNull
    private Integer phoneNumber;

    /**
     * 地址
     * 可为空
     */
    private String address;
}
2.4.2 增加一个接口
public interface HelloService {

    /**
     * 测试
     * @param userName 用户名
     * @return hello + userName
     */
    String sayHello(@NotEmpty String userName);

    /**
     * 用户注册
     * @param userInfo 用户信息
     */
    String userRegister(@NotNull UserInfo userInfo);
}

2.4.3 在切面方法中增加对象类型参数校验

校验对象类型的参数多了个步骤,先检验对象是否为空,再检验对象内部的属性是否为空
这里需要注意的是,考虑到对象的嵌套,所以这里还是需要递归调用。

@Aspect
@Component
public class ServiceAspect {

    @Around(value = "execution(* com.xyh.api..*(..))")
    public Object handleParam(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取参数的值
        Object[] args = joinPoint.getArgs();
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        //获取method
        Method method = methodSignature.getMethod();
        //通过method获取参数
        Parameter[] parameters = method.getParameters();
        for (int i = 0; i < parameters.length; i++) {
            Parameter parameter = parameters[i];
            //校验String类型参数
            if (parameter.isAnnotationPresent(NotEmpty.class) && StringUtils.isEmpty(args[i])) {
                return "参数不能为空: " + parameter.toString();
            }
            //校验对象类型参数
            if (parameter.isAnnotationPresent(NotNull.class)) {
                if (args[i] == null) {
                    return "参数不能为空: " + parameter.toString();
                }
                //校验对象内部
                String msg = checkObjectInternField(args[i]);
                if (msg != null) {
                    return msg;
                }
            }
        }
        return joinPoint.proceed();
    }

    /**
     * 校验对象内部参数,注意递归调用
     * @param obj arg
     * @return 校验不通过返回不通过信息,通过则返回null
     * @throws IllegalAccessException filed.get(obj)异常
     */
    public String checkObjectInternField(Object obj) throws IllegalAccessException {
        Field[] fields = obj.getClass().getDeclaredFields();
        for (Field field : fields) {
            //属性为String类型
            field.setAccessible(true);
            if (field.isAnnotationPresent(NotEmpty.class) && StringUtils.isEmpty(field.get(obj))) {
                return "参数不能为空: " + field.toString();
            }
            //属性为对象类型
            if (field.isAnnotationPresent(NotNull.class)) {
                if (field.get(obj) == null) {
                    return "参数不能为空: " + field.toString();
                }
                return checkObjectInternField(field.get(obj));
            }
        }
        return null;
    }
}
2.4.4 测试
  	/**
     * 测试校验对象类型参数
     */
    @Test
    public void testUserRegister(){
        UserInfo userInfo = new UserInfo();
        userInfo.setUserId("123");
        System.out.println(helloService.userRegister(null));
        System.out.println(helloService.userRegister(userInfo));
        UserDetail userDetail = new UserDetail();
        userDetail.setPhoneNumber(123456);
        userInfo.setUserDetail(userDetail);
        System.out.println(helloService.userRegister(userInfo));
    }

在这里插入图片描述

3.最后

这篇文章还是涉及到了许多的知识点,反射,SpringAOP/动态代理。其中最大的坑莫过于SpringBoot不同版本默认的动态代理方式不一样,
我也是在使用不同SpringBoot版本后才发现的,话说还蛮幸运是先用的1.x的版本测试成功了:)。
最后附上这篇文章涉及到的代码的Github:

https://github.com/xiangyida/ApiUtils

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值