一、前言
在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*/@Datapublic 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 {@Overridepublic 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 {@Beanpublic DefaultPointcutAdvisor DefaultPointcutAdvisor() {DefaultPointcutAdvisor defaultPointcutAdvisor = new DefaultPointcutAdvisor();defaultPointcutAdvisor.setAdvice(new MyMethodInterceptor()); // 自定义MethodInterceptorAspectJExpressionPointcut 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.测试
-
测试代码:
@Autowiredprivate TestService testService;@PostConstructpublic 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项目中方法参数对象中字段上存在的注解如何进行拦截解析的技术探讨,以上只是技术思路的一种实现方式,仅供大家参考。