背景
最近在做一个需求,需要用到 spring-retry重试机制,在做配置类的时候使用到了 @EnableRetry 注解。
如果不指定代理配置,会使用 JDK 动态代理。
最终,项目启动时加载一个外部依赖jar包,导致spring 容器启动失败。
前言
分析之前,说明一点。
容器启动失败,我们只关注 2 个地方即可,即:我的代码 和 依赖 jar的代码。
如果想快速知道结果的,直接看 2.1
和 2.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 {
}