写在前面,有感而发
上个月,XS 开源库被曝运营者恶意植入后门,虽然与 Java 关系不是很大,但是其所象征的 “开源=免费+安全” 已经变成 “开源=白嫖” 了
事件的来龙去脉可以在网上查看,总的来说就是 XS 库的作者无力维护,来了个人积极提 PR 同时“逼宫”作者,最终将这个人加入了社区运营者,最终让他有了机会
说到底,开源就像是用爱发电,没有收入和赞助,大部分开源作者都是难以为继,如 Python 的 request 库和 LayUI
就这样吧
一、现象
项目开启了自动刷新 Nacos 配置,但是无法刷新配置,并且每隔一段时间报错:
org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'spring.datasource.druid.xxxxxxx' to javax.sql.DataSource
at org.springframework.boot.context.properties.bind.Binder.handleBindError(Binder.java:363)
at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:323)
at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:308)
at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:238)
at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:225)
at org.springframework.boot.context.properties.ConfigurationPropertiesBinder.bind(ConfigurationPropertiesBinder.java:90)
at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.bind(ConfigurationPropertiesBindingPostProcessor.java:89)
at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.postProcessBeforeInitialization(ConfigurationPropertiesBindingPostProcessor.java:78)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:415)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1786)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:406)
at org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder.rebind(ConfigurationPropertiesRebinder.java:108)
at org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder.rebind(ConfigurationPropertiesRebinder.java:84)
at org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder.onApplicationEvent(ConfigurationPropertiesRebinder.java:142)
at org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder.onApplicationEvent(ConfigurationPropertiesRebinder.java:51)
at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:172)
at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:165)
at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:139)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:404)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:361)
at org.springframework.cloud.context.refresh.ContextRefresher.refreshEnvironment(ContextRefresher.java:96)
at org.springframework.cloud.context.refresh.ContextRefresher.refresh(ContextRefresher.java:85)
at org.springframework.cloud.endpoint.event.RefreshEventListener.handle(RefreshEventListener.java:72)
at org.springframework.cloud.endpoint.event.RefreshEventListener.onApplicationEvent(RefreshEventListener.java:61)
at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:172)
at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:165)
at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:139)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:404)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:361)
at com.alibaba.cloud.nacos.refresh.NacosContextRefresher$1.innerReceive(NacosContextRefresher.java:133)
at com.alibaba.nacos.api.config.listener.AbstractSharedListener.receiveConfigInfo(AbstractSharedListener.java:40)
at com.alibaba.nacos.client.config.impl.CacheData$1.run(CacheData.java:210)
at com.alibaba.nacos.client.config.impl.CacheData.safeNotifyListener(CacheData.java:241)
at com.alibaba.nacos.client.config.impl.CacheData.checkListenerMd5(CacheData.java:181)
at com.alibaba.nacos.client.config.impl.ClientWorker$LongPollingRunnable.run(ClientWorker.java:629)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.IllegalStateException: Unable to set value for property username
at org.springframework.boot.context.properties.bind.JavaBeanBinder$BeanProperty.setValue(JavaBeanBinder.java:360)
at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:101)
at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:83)
at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:59)
at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$5(Binder.java:451)
at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:571)
at org.springframework.boot.context.properties.bind.Binder$Context.withDataObject(Binder.java:557)
at org.springframework.boot.context.properties.bind.Binder$Context.access$300(Binder.java:512)
at org.springframework.boot.context.properties.bind.Binder.bindDataObject(Binder.java:449)
at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:390)
at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:319)
... 40 common frames omitted
Caused by: java.lang.reflect.InvocationTargetException: null
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.boot.context.properties.bind.JavaBeanBinder$BeanProperty.setValue(JavaBeanBinder.java:357)
... 50 common frames omitted
Caused by: java.lang.UnsupportedOperationException: null
at com.alibaba.druid.pool.DruidAbstractDataSource.setUsername(DruidAbstractDataSource.java:1135)
... 55 common frames omitted
直觉告诉我,就是这个报错导致的自动刷新配置失效
仔细一看,什么 bind、refresh,应该就是在刷新数据的时候发生报错了
也就是说,Nacos 的自动刷新配置生效了,但是报错导致失败
二、跟源码
从这个方法入手:
at com.alibaba.druid.pool.DruidAbstractDataSource.setUsername(DruidAbstractDataSource.java:1135)
报错在这里,看起来是在绑定 username 时发生了报错,首先会判断当前的 username 和即将注入的字符串是否相同,发现不相同,往下走又发现已经完成了初始化,最终导致报错
这里的原因在于配置项中数据库的用户名和密码都是加密的,在项目启动注入 Bean 的时候会进行解密操作,因此配置项和服务的用户名自然不一样
由此问题从“为什么报错”变成了“如何不刷新指定的配置项”
接下来往这走:
at com.alibaba.cloud.nacos.refresh.NacosContextRefresher$1.innerReceive(NacosContextRefresher.java:133)
因为它的类名 NacosContextRefresher,引起了我的注意
看起来像是注册了一个 Spring 监听事件,其中的 RefreshEvent 就是这个刷新事件,并且报错就是从这里出来的
接下来看这里:
at org.springframework.cloud.context.refresh.ContextRefresher.refreshEnvironment(ContextRefresher.java:96)
虽然现在说的简单,但是跟源码就是一个一个方法点进去看,提取有用的信息,因此写代码时方法名的选取真的很重要——除非是不想让人轻易发现方法的作用,防御性编程
在这个方法里打断点,可以发现 before 里存储的就是所有配置项,keys 里存储的就是发生变化的配置项(只有第一次发生修改的时候 keys 里有值,更新失败之后的轮询中 keys 中不会再有值)
同时,这里又触发了新的事件——EnvironmentChangeEvent
这就很明显了,触发了环境变更事件,需要进行更新了
接下来就直接跳到:
at org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder.rebind(ConfigurationPropertiesRebinder.java:84)
很明显,对所有 Beans 的配置变量进行重新绑定
进入这个 rebind 方法
(org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder#rebind(java.lang.String))
注意到这里,会进行是否刷新的判断,再进入 getNeverRefreshable 这个方法
(org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder#getNeverRefreshable):
这就非常非常明显了,SpringCloud 在刷新变量前会先获取不进行更新的类名,当类名匹配到时就会跳过此类的更新
因此,当我不想更新 com.alibaba.druid.pool.DruidDataSource 这个类的时候,就需要在配置文件中加入
spring:
cloud:
refresh:
never-refreshable: com.zaxxer.hikari.HikariDataSource,com.alibaba.druid.pool.DruidDataSource
不同类之间用英文逗号分隔,其中 com.zaxxer.hikari.HikariDataSource 是默认值,因此加上以防万一
至此,解决了 Nacos 自动刷新配置报错的问题