java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Spring Bean注解

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

作者:墨、鱼

这篇文章主要介绍了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注解内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文