关于dubbo 占位符无法解析问题

不知道大家有没有遇到过,你要开发一个新应用要使用 dubboapollo 等组件,在集成的过程中发现 dubbo 配置文件的占位符无法替换,wtf,配置明明和以前的项目一样,为啥就不行了。我前两天也遇到了这个问题,就一起来分析下。

简单配置介绍

项目大致使用了以下组件

  • apollo 0.11.1-内部版本
  • dubbo 2.6.0
  • mybatis-plus-boot-starter 3.2.0
  • mybatis-spring 2.0.2
  • spring-boot 2.2.5.RELEASE

provider.xml 配置文件如下,其中xyuanDepServiceImpl 的类有使用注解@Service声明,所以在这里直接 ref 引用即可,并且dubbo.register.address 是在 apollo 中配置的。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">


    <dubbo:application name="xuan_yuan"/>
	
    <dubbo:registry protocol="zookeeper" address="${dubbo.register.address}"/>

    <dubbo:protocol name="dubbo" port="20764"/>


    <dubbo:service ref="xyuanDepServiceImpl" interface="com.sucx.admin.services.XyuanDepService"/>


</beans>

consumer.xml 内容如下

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">


    <dubbo:reference id="ssoInfoService" interface="com.tuya.sso.service.ISsoInfoService" check="false" lazy="true"/>

    <dubbo:reference id="userSsoService" interface="com.tuya.sso.service.IUserSsoService" check="false" lazy="true"/>

</beans>

启动类

/**
 * @author scx
 */
@SpringBootApplication(scanBasePackages = "com.sucx")
@EnableApolloConfig
@MapperScan(basePackages = "com.sucx.*.mapper")
@ImportResource(locations = {"classpath:/dubbo/provider.xml", "classpath:/dubbo/consumer.xml"})

public class AdminApplication {

    public static void main(String[] args) {
        SpringApplication.run(AdminApplication.class, args);
    }

}

问题复现

执行 AdminApplication.main 方法抛出如下异常

2020-03-28 14:30:42.884  WARN 1647 --- [or-SendThread()] org.apache.zookeeper.ClientCnxn          : Session 0x0 for server ${dubbo.register.address}:9090, unexpected error, closing socket connection and attempting reconnect

java.lang.IllegalArgumentException: named capturing group is missing trailing '}'
	at java.util.regex.Matcher.appendReplacement(Matcher.java:841) ~[na:1.8.0_201]
	at java.util.regex.Matcher.replaceAll(Matcher.java:955) ~[na:1.8.0_201]
	at java.lang.String.replaceAll(String.java:2223) ~[na:1.8.0_201]
	at org.apache.zookeeper.ClientCnxn$SendThread.startConnect(ClientCnxn.java:997) ~[zookeeper-3.4.14.jar:3.4.14-4c25d480e66aadd371de8bd2fd8da255ac140bcf]
	at org.apache.zookeeper.ClientCnxn$SendThread.run(ClientCnxn.java:1060) ~[zookeeper-3.4.14.jar:3.4.14-4c25d480e66aadd371de8bd2fd8da255ac140bcf]

很明显,xml 中的占位符没有被替换,此时我首先想到的是,apollo 难道没加载配置?

排查apollo

apollo 通过在 META-INF/spring.factoriesApolloApplicationContextInitializer添加到 springbootApplicationContextInitializer

org.springframework.context.ApplicationContextInitializer=\
com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer

所以我们直接进入 ApplicationContextInitializer 进行 debug 查看dubbo.register.address 配置是否加载即可

在这里插入图片描述
debug 后可以发现,变量已经被 apollo 加载到 spring,那么问题还会出在哪里呢?

排查PropertySourcesPlaceholderConfigurer

大家应该知道 spring 是使用 PropertySourcesPlaceholderConfigurer 来对 xml等文件的占位符进行解析的,那么问题会是出在这里吗?继续进行 debug

在这里插入图片描述
在这里插入图片描述
从截图中可以发现,值也是有的,问题出在哪呢?

排查dubbo

经过上面两个分析,我们可以初步排除apollo和PropertySourcesPlaceholderConfigurer 的问题,唯有深入 dubbo 源码查看,由于我使用的 xml 配置的 dubbodubbo 是通过在META-INF/spring.handlers 文件中注册 schema 的解析器,来将 xml 配置转换为spring内部的 BeanDefinition

http\://code.alibabatech.com/schema/dubbo=com.alibaba.dubbo.config.spring.schema.DubboNamespaceHandler

进入 DubboNamespaceHandler

public class DubboNamespaceHandler extends NamespaceHandlerSupport {

    static {
        Version.checkDuplicate(DubboNamespaceHandler.class);
    }

    public void init() {
        registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));
        registerBeanDefinitionParser("module", new DubboBeanDefinitionParser(ModuleConfig.class, true));
        registerBeanDefinitionParser("registry", new DubboBeanDefinitionParser(RegistryConfig.class, true));
        registerBeanDefinitionParser("monitor", new DubboBeanDefinitionParser(MonitorConfig.class, true));
        registerBeanDefinitionParser("provider", new DubboBeanDefinitionParser(ProviderConfig.class, true));
        registerBeanDefinitionParser("consumer", new DubboBeanDefinitionParser(ConsumerConfig.class, true));
        registerBeanDefinitionParser("protocol", new DubboBeanDefinitionParser(ProtocolConfig.class, true));
        registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));
        registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));
        registerBeanDefinitionParser("annotation", new DubboBeanDefinitionParser(AnnotationBean.class, true));
    }

}

发现 <dubbo:registry protocol="zookeeper" address="${dubbo.register.address}"/> 是在 DubboBeanDefinitionParser 类中解析的,并且对应的bean classRegistryConfig.class,继续进行 debug

在这里插入图片描述
从截图中我们可以发现,dubbo 此时通过解析 xml 获取的 address 的值还没有被PropertySourcesPlaceholderConfigurer 替换,怎么回事?

思考

此时,只有一种想法,难道该 bean 的初始化早于 PropertySourcesPlaceholderConfigurer 的替换?也就是说,RegistryConfig 被提前初始化了

验证想法

我们应该知道 dubbo的消费者即:ReferenceBean 实现了InitializingBean的接口的afterPropertiesSet方法,在所有变量完成设置后会回调该方法。

并且 ReferenceBean 实现了 FactoryBean 接口的 getObject 方法。该 bean 会使用动态代理 ProxyFactory.getProxy封装为一个对象,在我们调用某个方法时,在反射对象中使用 rpc 请求服务者返回执行结果,我们的ReferenceBean 中也持有RegistryConfig这个rpc配置对象。

所以当 springbean 属性被设置完成后,会调用afterPropertiesSet方法。
此时我在上面的PropertySourcesPlaceholderConfigurer#getPropertydubboReferenceBean#afterPropertiesSet 这里全部加上断点,果然发现 ReferenceBean 先执行 afterPropertiesSetPropertySourcesPlaceholderConfigurer 后执行 getProperty 变量的操作。此时可以断定,ReferenceBean 被提前初始化无疑了。

在这里插入图片描述

PropertySourcesPlaceholderConfigurer 是在 BeanFactoryPostProcessor 时触发的,也就是说,beanBeanFactoryPostProcessor触发之前初始化了,为什么会被提前初始化呢?
有了 idea 就很简单,我们可以直接在 RegistryConfig.setAddress 打断点,看它的调用栈即可。
在这里插入图片描述
调用栈如下:

setAddress:123, RegistryConfig (com.alibaba.dubbo.config)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
setValue:332, BeanWrapperImpl$BeanPropertyHandler (org.springframework.beans)
processLocalProperty:458, AbstractNestablePropertyAccessor (org.springframework.beans)
setPropertyValue:278, AbstractNestablePropertyAccessor (org.springframework.beans)
setPropertyValue:266, AbstractNestablePropertyAccessor (org.springframework.beans)
setPropertyValues:97, AbstractPropertyAccessor (org.springframework.beans)
setPropertyValues:77, AbstractPropertyAccessor (org.springframework.beans)
applyPropertyValues:1732, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
populateBean:1444, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
doCreateBean:594, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
createBean:517, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
lambda$doGetBean$0:323, AbstractBeanFactory (org.springframework.beans.factory.support)
getObject:-1, 700631078 (org.springframework.beans.factory.support.AbstractBeanFactory$$Lambda$181)
getSingleton:222, DefaultSingletonBeanRegistry (org.springframework.beans.factory.support)
doGetBean:321, AbstractBeanFactory (org.springframework.beans.factory.support)
getBean:202, AbstractBeanFactory (org.springframework.beans.factory.support)
getBeansOfType:617, DefaultListableBeanFactory (org.springframework.beans.factory.support)
getBeansOfType:1250, AbstractApplicationContext (org.springframework.context.support)
beansOfTypeIncludingAncestors:378, BeanFactoryUtils (org.springframework.beans.factory)
afterPropertiesSet:129, ReferenceBean (com.alibaba.dubbo.config.spring)
invokeInitMethods:1855, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
initializeBean:1792, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
doCreateBean:595, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
createBean:517, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
lambda$doGetBean$0:323, AbstractBeanFactory (org.springframework.beans.factory.support)
getObject:-1, 700631078 (org.springframework.beans.factory.support.AbstractBeanFactory$$Lambda$181)
getSingleton:222, DefaultSingletonBeanRegistry (org.springframework.beans.factory.support)
doGetBean:321, AbstractBeanFactory (org.springframework.beans.factory.support)
getTypeForFactoryBean:1643, AbstractBeanFactory (org.springframework.beans.factory.support)
getTypeForFactoryBean:895, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
isTypeMatch:613, AbstractBeanFactory (org.springframework.beans.factory.support)
doGetBeanNamesForType:533, DefaultListableBeanFactory (org.springframework.beans.factory.support)
getBeanNamesForType:491, DefaultListableBeanFactory (org.springframework.beans.factory.support)
getBeansOfType:613, DefaultListableBeanFactory (org.springframework.beans.factory.support)
getBeansOfType:605, DefaultListableBeanFactory (org.springframework.beans.factory.support)
getBeansOfType:1242, AbstractApplicationContext (org.springframework.context.support)
processPropertyPlaceHolders:367, MapperScannerConfigurer (org.mybatis.spring.mapper)
postProcessBeanDefinitionRegistry:338, MapperScannerConfigurer (org.mybatis.spring.mapper)
invokeBeanDefinitionRegistryPostProcessors:275, PostProcessorRegistrationDelegate (org.springframework.context.support)
invokeBeanFactoryPostProcessors:125, PostProcessorRegistrationDelegate (org.springframework.context.support)
invokeBeanFactoryPostProcessors:706, AbstractApplicationContext (org.springframework.context.support)
refresh:532, AbstractApplicationContext (org.springframework.context.support)
refresh:141, ServletWebServerApplicationContext (org.springframework.boot.web.servlet.context)
refresh:747, SpringApplication (org.springframework.boot)
refreshContext:397, SpringApplication (org.springframework.boot)
run:315, SpringApplication (org.springframework.boot)
run:1226, SpringApplication (org.springframework.boot)
run:1215, SpringApplication (org.springframework.boot)
main:22, AdminApplication (com.tuya.admin)

最后我终于发现在MapperScannerConfigurer这里由于processPropertyPlaceHolderstrue导致进入processPropertyPlaceHolders方法,该方法调用了 applicationContext.getBeansOfType(PropertyResourceConfigurer.class);方法

在这里插入图片描述
最后导致在DefaultListableBeanFactory#doGetBeanNamesForType 遍历了所有的BeanDefinition 导致提前初始化。
在这里插入图片描述

两个项目为什么不一样

最后发现在我的另外一个项目中 mybatis-spring 的版本是 2.0.1 而这个版本中是 2.0.2. 唯一不同的是 2.0.2 版本中注册 MapperScannerConfigurerBeanDefinition 时设置了processPropertyPlaceHolderstrue。而 2.0.1 默认为 false 的。
在这里插入图片描述

解决办法

知道原因了,解决办法也很简单。

降低版本

降低 mybatis-spring 的版本,很简单,就不再赘述

注册MapperScannerConfigurer的bean

还记得最上面我们的启动类的MapperScan注解吗?去掉该注解,然后增加一个配置类

@Component
public class SpringBeans {
    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer() {
        MapperScannerConfigurer configurer = new MapperScannerConfigurer();
        configurer.setProcessPropertyPlaceHolders(false);
        configurer.setBasePackage("com.sucx.*.mapper");
        return configurer;
    }

}

自定义MapperScannerRegistrar 注解

自定义一个MapperScan注解,重写@Import注解内的MapperScannerRegistrar类,把processPropertyPlaceHolders值设置为false即可
在这里插入图片描述

小彩蛋

其实之前我也遇到过一次 dubbo 占位符无法解析的状况,经过细密的排查最后发现是使用了 spring-boot 的热部署 spring-boot-devtools 导致第二次加载不到资源。解决办法是去掉热部署的 spring-boot-devtools 就好了,可惜当时没有写文章记录下来。希望这个小彩蛋能帮助到你。

展开阅读全文
©️2020 CSDN 皮肤主题: 像素格子 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值