Spring中获取Bean方法上的自定义注解问题解析

 更新时间:2023年06月14日 15:07:16   作者:墨、鱼  
这篇文章主要介绍了Spring中如何获取Bean方法上的自定义注解,基本的思路就是通过Spring提供的ApplicationContext#getBeansWithAnnotation+反射来实现,需要的朋友可以参考下

背景描述

项目中需要扫描出来所有 标注了自定义注解A的Service里面标注了自定义注解B的方法 来做后续处理。

基本的思路就是通过Spring提供的ApplicationContext#getBeansWithAnnotation+反射 来实现。

但是,随着在Service里面引入了声明式事务(@Transactional),上述的方法也就随之失效。

场景复现

这里通过构造一个case来说明问题

Service上的注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface MyService {
}

方法上的注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface MyAnno {
    String value() default "";
}

Service代码

public interface UserService {
    void print();
}
@MyService
@Component("annoUserService")
public class UserServiceImpl implements UserService {
    @Override
    @MyAnno("xujianadgdgagg")
    public void print() {
        System.out.println("写入数据库");
    }
}

自定义注解扫描代码

@Component
public class FindAnnotationService {
    @Autowired
    private ApplicationContext applicationContext;
    @PostConstruct
    public void init() {
    // 获取带有自定义注解的bean
        Map<String, Object> beanMap = applicationContext.getBeansWithAnnotation(MyService.class);
        for (Object bean : beanMap.values()) {
            Class<?> clazz = bean.getClass();
            Method[] declaredMethods = clazz.getDeclaredMethods();
            for (Method declaredMethod : declaredMethods) {
            // 寻找带有自定义注解的方法
            if (declaredMethod.isAnnotationPresent(MyAnno.class)) {
            // 如果方法上有自定义注解,则获取这个注解
                MyAnno annotation = declaredMethod.getDeclaredAnnotation(MyAnno.class);
                System.out.println(annotation.value());
            }
        }
    }
}

测试类

@SpringBootTest
public class FindAnnotationServiceTests {
    @Autowired
    private UserService annoUserService;
    @Test
    public void testPrint() {
        annoUserService.print();
    }
}

当对UserServiceImpl#print()方法加上@Transactional注解时,上面获取bean的地方,拿到的已经不是UserServiceImpl对象了,而是一个CGLIB代理类,如下所示:

在这里插入图片描述

我们都知道Spring确实会为声明式事物生成代理类。

对这个代理类通过反射并没有获取到带有自定义注解的方法。

问题追踪

最直接的原因推测是生成的代理类并不包含原始类中用户自定义的注解。

CGLIB动态代理以及生成的代理类可以参考《深入理解JVM字节码》。

为了验证猜想,我们自己手动为UserServiceImpl生成一个CGLIB代理类,同时去掉@Transactional注解。
这里通过BeanPostProcessor创建代理类:

@Component
public class MyBeanPostProcess implements BeanPostProcessor {
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof UserService) {
            // CGLIB动态代理
            MyMethodInterceptor myMethodInterceptor = new MyMethodInterceptor();
            myMethodInterceptor.setTarget(bean);
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(bean.getClass());
            enhancer.setCallback(myMethodInterceptor);
            return enhancer.create();
        } else {
            return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
        }
    }
}
public class MyMethodInterceptor implements MethodInterceptor {
    private Object target;
    public void setTarget(Object target) {
        this.target = target;
    }
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("cglib增强目标方法");
        return method.invoke(target,objects);
    }
}

结果跟刚才一样,由于生成了代理类而获取不到自定义注解。

解决方案

既然CGLIB代理类是罪魁祸首,那就得从它下手。

由于CGLIB生成的代理类继承了原始类,那在拿到这个代理类的时候,去找到它的父类(原始类),不就可以拿到自定义注解了吗?

对代码作如下改动:

@Component
public class FindAnnotationService {
    @Autowired
    private ApplicationContext applicationContext;
    @PostConstruct
    public void init() {
    // 获取带有自定义注解的bean
        Map<String, Object> beanMap = applicationContext.getBeansWithAnnotation(MyService.class);
        for (Object bean : beanMap.values()) {
            Class<?> clazz = bean.getClass();
            // 获取父类(代理类的原始类)
            clazz = clazz.getSuperclass();
            Method[] declaredMethods = clazz.getDeclaredMethods();
            for (Method declaredMethod : declaredMethods) {
            // 寻找带有自定义注解的方法
            if (declaredMethod.isAnnotationPresent(MyAnno.class)) {
                MyAnno annotation = declaredMethod.getDeclaredAnnotation(MyAnno.class);
                System.out.println(annotation.value());
            }
        }
    }
}

在这里插入图片描述

这样果然拿到了自定义注解。

对于这种情况,Spring早已预判到了,并提供了一个工具方法AnnotationUtils.findAnnotation用来获取bean方法上的注解,不管这个bean是否被代理。

通过这个工具方法优化代码如下:

@Component
public class FindAnnotationService {
    @Autowired
    private ApplicationContext applicationContext;
    @PostConstruct
    public void init() {
        Map<String, Object> beanMap = applicationContext.getBeansWithAnnotation(MyService.class);
        for (Object bean : beanMap.values()) {
            Class<?> clazz = bean.getClass();
            Method[] declaredMethods = clazz.getDeclaredMethods();
            for (Method declaredMethod : declaredMethods) {
            if (declaredMethod.isAnnotationPresent(MyAnno.class)) {
                MyAnno annotation = declaredMethod.getDeclaredAnnotation(MyAnno.class);
                System.out.println(annotation.value());
            }
        }
    }
}

扩展思考

既然CGLIB动态代理有这种问题,那JDK动态代理呢?

手动为UserServiceImpl生成JDK动态代理:

@Component
public class MyBeanPostProcess implements BeanPostProcessor {
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof UserService) {
            // JDK动态代理
            MyInvocationHandler myInvocationHandler = new MyInvocationHandler();
            myInvocationHandler.setTarget(bean);
            return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), bean.getClass().getInterfaces(), myInvocationHandler);
        } else {
            return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
        }
    }
}
public class MyInvocationHandler implements InvocationHandler {
    private Object target;
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("增强目标方法");
        return method.invoke(target,args);
    }
    public void setTarget(Object target) {
        this.target = target;
    }
}

在不使用AnnotationUtils.findAnnotation的时候果然还是获取不到自定义注解。

但是加上AnnotationUtils.findAnnotation以后发现还是获取不到!!!

为了探究原因,对AnnotationUtils.findAnnotation源码作简要分析以后发现:

AnnotationsScanner#processMethodHierarchy(C context, int[] aggregateIndex, Class<?> sourceClass, AnnotationsProcessor<C, R> processor, Method rootMethod, boolean includeInterfaces)

            // 如果当前代理类实现了接口(JDK动态代理方式)
            if (includeInterfaces) {
                Class[] var14 = sourceClass.getInterfaces();
                var9 = var14.length;
                for(var10 = 0; var10 < var9; ++var10) {
                    Class<?> interfaceType = var14[var10];
                    // 对实现的接口递归寻找注解
                    R interfacesResult = processMethodHierarchy(context, aggregateIndex, interfaceType, processor, rootMethod, true);
                    if (interfacesResult != null) {
                        return interfacesResult;
                    }
                }
            }
            // 如果当前代理类有父类(CGLIB动态代理方式)
            Class<?> superclass = sourceClass.getSuperclass();
            if (superclass != Object.class && superclass != null) {
                // 对父类递归寻找注解
                R superclassResult = processMethodHierarchy(context, aggregateIndex, superclass, processor, rootMethod, includeInterfaces);
                if (superclassResult != null) {
                    return superclassResult;
                }
            }

我们知道CGLIB代理是基于继承原始类来实现的,而JDK代理是基于实现接口来实现的。

从上面的源码可以大致判断出:对于CGLIB代理通过递归搜寻父类来找注解;对于JDK代理通过递归搜寻实现的接口来找注解。

那么在使用JDK生成代理的时候,把自定义注解放在接口UserService的方法上,而不是实现类UserServiceImpl上:

public interface UserService {
    @MyAnno("xujianadgdgagg")
    void print();
}

这样就可以通过AnnotationUtils.findAnnotation成功获取自定义注解了~

其实现在Spring大部分都是通过CGLIB生成的代理,所以无需将自定义注解放在接口上,毕竟放在实现类上才是常规操作。

到此这篇关于Spring中如何获取Bean方法上的自定义注解的文章就介绍到这了,更多相关Spring Bean注解内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • springboot调用webservice-soap接口的实现

    springboot调用webservice-soap接口的实现

    接口协议目前广泛使用的有http协议和RPC协议和webservice,本文主要介绍了springboot调用webservice-soap接口的实现,具有一定的参考价值,感兴趣的可以了解一下
    2024-03-03
  • Java使用System.currentTimeMillis()方法计算程序运行时间的示例代码

    Java使用System.currentTimeMillis()方法计算程序运行时间的示例代码

    System.currentTimeMillis() 方法的返回类型为 long ,表示毫秒为单位的当前时间,文中通过示例代码介绍了计算 String 类型与 StringBuilder 类型拼接字符串的耗时情况,对Java计算程序运行时间相关知识感兴趣的朋友一起看看吧
    2022-03-03
  • Java Web之限制用户多处登录实例代码

    Java Web之限制用户多处登录实例代码

    本篇文章主要介绍了Java Web之限制用户多处登录实例代码,可以限制单个用户在多个终端登录。非常具有实用价值,需要的朋友可以参考下。
    2017-03-03
  • RocketMQ消息发送流程源码剖析

    RocketMQ消息发送流程源码剖析

    这篇文章主要为大家介绍了RocketMQ消息发送流程源码剖析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08
  • SpringBoot混合使用StringRedisTemplate和RedisTemplate的坑及解决

    SpringBoot混合使用StringRedisTemplate和RedisTemplate的坑及解决

    这篇文章主要介绍了SpringBoot混合使用StringRedisTemplate和RedisTemplate的坑及解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-12-12
  • java.sql.SQLException: connection holder is null错误解决办法

    java.sql.SQLException: connection holder is null错误解决办法

    这篇文章主要给大家介绍了关于java.sql.SQLException: connection holder is null错误的解决办法,这个错误通常是由于连接对象为空或未正确初始化导致的,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2024-02-02
  • Spring Boot + Kotlin整合MyBatis的方法教程

    Spring Boot + Kotlin整合MyBatis的方法教程

    前几天由于工作需要,便开始学习了kotlin,java基础扎实学起来也还算比较快,对于kotlin这个编程语言自然是比java有趣一些,下面这篇文章主要给大家介绍了关于Spring Boot + Kotlin整合MyBatis的方法教程,需要的朋友可以参考下。
    2018-01-01
  • Java之while与do-while循环的用法详解

    Java之while与do-while循环的用法详解

    在上一篇文章中,给大家讲解了循环的概念,并重点给大家讲解了for循环的使用。但在Java中,除了for循环之外,还有while、do-while、foreach等循环形式。这篇文章给大家讲解while循环的使用
    2023-05-05
  • 项目总结之HttpURLConnection的disconnect的问题

    项目总结之HttpURLConnection的disconnect的问题

    这篇文章主要介绍了项目总结之HttpURLConnection的disconnect的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-06-06
  • Java实现购物管理系统

    Java实现购物管理系统

    这篇文章主要为大家详细介绍了Java实现购物管理系统,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-01-01

最新评论