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

1404

被折叠的 条评论
为什么被折叠?



