一、前言
在Spring Boot项目开发过程中,我们经常会使用到自定义注解的方式进行业务逻辑开发,此时注解我们一般是放在方法或者类上面
,通过AOP
切面拦截的方式进行自定义业务逻辑填充。但是如果自定义注解放在类的字段上
,此时应该如何进行解析呢?
二、技术思路
自定义注解放在类的字段上其实解析起来也是比较容易的,我这里采用的思路是:
自定义一个MethodInterceptor
拦截器拦截执行的业务方法,获取到方法参数数组,遍历每个参数,获取每个参数中所有字段,判断字段上是否有自定义注解,筛选出所有标识了自定义注解的字段,然后进行自定义业务逻辑填充。
三、技术实践
按照上面的思路,我们进行一下技术实践,这里我模拟一个案例。
1. 需求
自定义一个注解,该注解作用于类的字段上,带有该标识注解的字段,在使用时,如果字段没有设置值,则采用注解设置的默认值。
2. 新建spring boot项目,导入相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
3. 自定义注解
示例代码如下:
import java.lang.annotation.*;
@Target({ElementType.FIELD}) // 标识此注解适用于字段上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DefaultValueAnnotation {
String value() default "";
}
4. 字段注解获取工具类
示例代码:
import lombok.Data;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.util.ReflectionUtils;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* 字段上注解查找工具类
*/
public class FieldAnnotationUtils {
/**
* 字段注解信息返回VO
*/
@Data
public static class FieldAnnotationInfo {
/**
* 类变量
*/
private Object obj;
/**
* 携带该注解的字段对象
*/
private Field field;
/**
* 注解对象
*/
private Annotation annotation;
}
/**
* 判断类变量中是否字段上存在指定的注解,如果存在,则返回字段和注解信息
*
* @param obj 类变量
* @param annotationType 注解类型
* @return
*/
public static List<FieldAnnotationInfo> parseFieldAnnotationInfo(Object obj, Class annotationType) {
return parseFieldAnnotationInfo(obj, annotationType, null);
}
/**
* 判断类变量中是否字段上存在指定的注解,如果存在,则返回字段和注解信息
*
* @param obj 类变量
* @param annotationType 注解类型
* @param filterFieldClazz 注解适用的字段类型(不适用的字段类型即使字段上面添加改注解也不生效)
* @return
*/
public static List<FieldAnnotationInfo> parseFieldAnnotationInfo(Object obj, Class annotationType, Set<Class> filterFieldClazz) {
if (obj == null) {
return null;
}
List<FieldAnnotationInfo> resultList = new ArrayList<>();
/**
* 获取该对象的所有字段,进行遍历判断
*/
ReflectionUtils.doWithFields(obj.getClass(), field -> {
// 判断该字段上是否存在注解
Annotation annotation = AnnotatedElementUtils.findMergedAnnotation(field, annotationType);
if (annotation != null) { // 如果存在指定注解
boolean flag = true;
if (filterFieldClazz != null && !filterFieldClazz.isEmpty()) { // 如果指定了适用的字段的类型
for (Class c : filterFieldClazz) {
if (c.isAssignableFrom(field.getType())) { // 判断该字段类型是否符合使用类型,使用isAssignableFrom方法是为了父类也进行判断
break;
}
flag = false;
}
}
if (flag) { // 如果该字段类型符合,则返回字段注解信息
FieldAnnotationInfo fieldAnnotationInfo = new FieldAnnotationInfo();
fieldAnnotationInfo.setObj(obj);
fieldAnnotationInfo.setField(field);
fieldAnnotationInfo.setAnnotation(annotation);
resultList.add(fieldAnnotationInfo);
}
}
});
return resultList;
}
}
5. 自定义MethodInterceptor
示例代码:
public class MyMethodInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// 获取执行方法的所有参数
Object[] arguments = invocation.getArguments();
// 参数列表不为空
if (arguments != null && arguments.length != 0) {
// 由于我这里注解定义简单,我只让String类型的字段生效
Set<Class> clazzSet = new HashSet<>();
clazzSet.add(CharSequence.class);
for (int i = 0; i < arguments.length; i++) { // 判断所有参数
// 从每个参数中获取字段上有@DefaultValueAnnotation注解标识,并且类型是CharSequence的字段信息
List<FieldAnnotationUtils.FieldAnnotationInfo> fieldAnnotationInfos = FieldAnnotationUtils.parseFieldAnnotationInfo(arguments[i], DefaultValueAnnotation.class, clazzSet);
if (!CollectionUtils.isEmpty(fieldAnnotationInfos)) { // 如果存在符合条件的字段信息
for (int m = 0; m < fieldAnnotationInfos.size(); m++) { // 判断所有符合条件的字段
FieldAnnotationUtils.FieldAnnotationInfo fni = fieldAnnotationInfos.get(m);
Field field = fni.getField(); // 获取字段
field.setAccessible(true); // 设置可访问,突破private权限
Object value = field.get(fni.getObj());
if (value == null) { // 判断当前字段是否已经有值
// 如果当前字段没有值,利用反射的方式,把注解中的值设置进去
field.set(fni.getObj(), ((DefaultValueAnnotation) fni.getAnnotation()).value());
}
field.setAccessible(false);
}
}
}
}
// 放行方法执行
return invocation.proceed();
}
}
6. 把自定义的MethodInterceptor织入到Spring容器中
示例代码:
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyInterceptorConfig {
@Bean
public DefaultPointcutAdvisor DefaultPointcutAdvisor() {
DefaultPointcutAdvisor defaultPointcutAdvisor = new DefaultPointcutAdvisor();
defaultPointcutAdvisor.setAdvice(new MyMethodInterceptor()); // 自定义MethodInterceptor
AspectJExpressionPointcut aspectJExpressionPointcut = new AspectJExpressionPointcut();
aspectJExpressionPointcut.setExpression("execution(* com.learn..*(..))"); // 指定需要拦截的切面
defaultPointcutAdvisor.setPointcut(aspectJExpressionPointcut);
return defaultPointcutAdvisor;
}
}
7. 使用自定义注解
示例代码:
import lombok.Data;
@Data
public class TestParamPojo {
@DefaultValueAnnotation("我是注解设置的值.")
private String name;
@DefaultValueAnnotation("100")
private Integer age;
}
8. 定义方法入参
示例代码:
@Component
public class TestService {
public void test() {
System.out.println("test empty...");
}
public void test(TestParamPojo pojo) {
System.out.println("test pojo: " + pojo.toString());
}
public void test(TestParamPojo pojo, String testStr) {
System.out.println("test pojo: " + pojo.toString() + " testStr: " + testStr);
}
}
9.测试
-
测试代码:
@Autowired private TestService testService; @PostConstruct public void init() { System.out.println("========================="); testService.test(); TestParamPojo testParamPojo = new TestParamPojo(); testService.test(testParamPojo); testService.test(testParamPojo, null); testParamPojo.setName("我是自己设置的..."); testService.test(testParamPojo, "hello, world."); System.out.println("========================="); }
-
测试结果:
-
测试结论
可以看到,在没有设置值的时候,可以从注解中获取默认值进行设置,同时,不是指定的字段类型即使标识了自定义注解也不会生效。
四、写在最后
本文是对Spring Boo项目中方法参数对象中字段上存在的注解如何进行拦截解析的技术探讨,以上只是技术思路的一种实现方式,仅供大家参考。