Spring代理采坑指南 之 CGLib 和 JDK 动态代理

背景

最近在做一个需求,需要用到 spring-retry重试机制,在做配置类的时候使用到了 @EnableRetry 注解。

如果不指定代理配置,会使用 JDK 动态代理。

最终,项目启动时加载一个外部依赖jar包,导致spring 容器启动失败。

前言

分析之前,说明一点。
容器启动失败,我们只关注 2 个地方即可,即:我的代码依赖 jar的代码
如果想快速知道结果的,直接看 2.12.6即可。

分析过程

2.1 我的代码

这个就是配置类,我没有指定代理对象,默认使用 JDK 代理。
如果不想了解@EnableRetry原理的可以直接跳到 2.6部分来看。

@Configuration
@EnableRetry
public class RetryConfig {
 
}

2.2 EnableRetry实现

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@EnableAspectJAutoProxy(proxyTargetClass = false) // 启用 AspectJ 自动代理,设置代理目标类为false,也就是走的 JDK 代理。
@Import(RetryConfiguration.class)
@Documented
public @interface EnableRetry {
 
    /**
     * Indicate whether subclass-based (CGLIB) proxies are to be created as opposed
     * to standard Java interface-based proxies. The default is {@code false}.
     *
     * @return whether to proxy or not to proxy the class
     */
    boolean proxyTargetClass() default false;
 
}

2.3 EnableAspectJAutoProxy的实现

我们只关注 proxyTargetClass 配置即可,它的具体实现在AspectJAutoProxyRegistrar 里。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {
 
    /**
     * 
     * Indicate whether subclass-based (CGLIB) proxies are to be created as opposed
     * to standard Java interface-based proxies. The default is {@code false}.
     */
    boolean proxyTargetClass() default false;
 
    /**
     * Indicate that the proxy should be exposed by the AOP framework as a {@code ThreadLocal}
     * for retrieval via the {@link org.springframework.aop.framework.AopContext} class.
     * Off by default, i.e. no guarantees that {@code AopContext} access will work.
     * @since 4.3.1
     */
    boolean exposeProxy() default false;
 
}

2.4 AspectJAutoProxyRegistrard的实现

接下来,我们看一下AspectJAutoProxyRegistrar的源码

class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
 
    /**
     * Register, escalate, and configure the AspectJ auto proxy creator based on the value
     * of the @{@link EnableAspectJAutoProxy#proxyTargetClass()} attribute on the importing
     * {@code @Configuration} class.
     */
    @Override
    public void registerBeanDefinitions(
            AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
 
        AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
 
        AnnotationAttributes enableAspectJAutoProxy =
                AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
        if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
            // 强制自动代理创建器使用代理类
            AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
        }
        if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
            AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
        }
    }
 
}

2.5 AopConfigUtils的实现

我们只需要关注AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);的实现就行,其他的别管。

public abstract class AopConfigUtils {
 
    /**
     * The bean name of the internally managed auto-proxy creator.
     */
    public static final String AUTO_PROXY_CREATOR_BEAN_NAME =
            "org.springframework.aop.config.internalAutoProxyCreator";
 
    public static void forceAutoProxyCreatorToUseClassProxying(BeanDefinitionRegistry registry) {
        // BeanDefinitionRegistry如果包含internalAutoProxyCreator对象,将配置添加进去
        if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
            BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
            definition.getPropertyValues().add("proxyTargetClass", Boolean.TRUE);
        }
    }
}

好了,到这里我们就知道,代码的@EnableRetry可以指定指定代理目标对象的原理了。
接下来我们来看下那个jar 包做了什么。

2.6 外部依赖jar 的代码

这个类的其实就干一件事,找出带有@JmsListener注解的类,将注册信息放入listenerMap
而导致容器启动失败的原因,是因为JmsListener jmsListener = AnnotationUtils.findAnnotation(listener.getClass(), 获取到的值为null 导致后面使用空指针了。

public class JmsListenerAutoExporter implements InitializingBean  {
 // 省略其他代码
 
 @Override
 private void afterPropertiesSet() throws Exception {
        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
 
        Map<String, Object> beans = applicationContext.getBeansWithAnnotation(JmsListener.class);
        Iterator<Map.Entry<String, Object>> it = beans.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, Object> entry = it.next();
            if (!(entry.getValue() instanceof MessageListener)) {
                throw new Exception();
            }
            MessageListener listener = ((MessageListener) entry.getValue());
            // 获取目标类的JmsListener注解
            // 如果spring 使用的是 JDK 动态代理,按照AnnotationUtils的实现是无法找到JmsListener,而使用 CGLib 代理就可以。
            JmsListener jmsListener = AnnotationUtils.findAnnotation(listener.getClass(), JmsListener.class);
            try {
                // 解析 dsub 配置,这一行也是问题代码。
                String destinationName = beanFactory.resolveEmbeddedValue(jmsListener.destinationName());
                destinationName = messageUtil.formatName(destinationName);
                listenerMap.put(destinationName, listener);
            } catch (Exception e) {
                //增加初始化监听器失败时日志,输出具体的className
                LOGGER.error("initListener error! class:{},destinationName:{}", listener.getClass().getName(), jmsListener.destinationName());
                throw e;
            }
        }
    }
// 省略其他代码
 
}

总结

这个问题的核心在于:目标类和代理对象的问题, 代理对象身上是没有目标类身上的注解的,所以如果使用 JDK 代理的话,是无法获取到注解的,想取到可以获取目标类本身。

解决方案

方案一:外部 jar 做兼容(推荐)

private void initListenerMap() throws Exception {
    DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
 
    Map<String, Object> beans = applicationContext.getBeansWithAnnotation(JmsListener.class);
    Iterator<Map.Entry<String, Object>> it = beans.entrySet().iterator();
    while (it.hasNext()) {
        Map.Entry<String, Object> entry = it.next();
        if (!(entry.getValue() instanceof MessageListener)) {
            throw new Exception();
        }
        Object clazz = entry.getValue();
        // 增加如下判断
        // import org.springframework.aop.support.AopUtils
        // 如果是 JDK 动态代理,获取目标类
        if (AopUtils.isJdkDynamicProxy(clazz)){
            clazz = AopTargetUtils.getTarget(clazz);
        }
        MessageListener listener = ((MessageListener) clazz);
        JmsListener jmsListener = AnnotationUtils.findAnnotation(listener.getClass(), JmsListener.class);
        try {
            String destinationName = beanFactory.resolveEmbeddedValue(jmsListener.destinationName());
            destinationName = messageUtil.formatName(destinationName);
            listenerMap.put(destinationName, listener);
        } catch (Exception e) {
            LOGGER.error("initListener error! class:{},destinationName:{}", listener.getClass().getName(), jmsListener.destinationName());
            throw e;
        }
    }
}

方案二:调用方指定使用 CGLIB 代理(不是那么的推荐)

@Configuration
@EnableRetry(proxyTargetClass = true)
public class RetryConfig {
 
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值