Failed to bind properties under ‘spring.datasource.druid‘ to javax.sql.DataSource;Nacos支持加密配置项动态刷新
使用版本:
SpringBoot:2.1.18.RELEASE
SpringCloud: Greenwich.SR6
SpringCloud Alibaba: 2.1.4.RELEASE
jasypt-spring-boot-starter:2.1.2
druid-spring-boot-starter: 1.1.24
问题说明:
通过jasypt对Nacos中的敏感配置项进行加密处理后,当更新nacos更新配置项时,后台报错。
org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'spring.datasource.druid' to javax.sql.DataSource
at org.springframework.boot.context.properties.bind.Binder.handleBindError(Binder.java:242)
at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:218)
at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:202)
at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:185)
at org.springframework.boot.context.properties.ConfigurationPropertiesBinder.bind(ConfigurationPropertiesBinder.java:78)
at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.bind(ConfigurationPropertiesBindingPostProcessor.java:101)
at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.postProcessBeforeInitialization(ConfigurationPropertiesBindingPostProcessor.java:89)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:413)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1761)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:404)
at org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder.rebind(ConfigurationPropertiesRebinder.java:102)
at org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder.rebind(ConfigurationPropertiesRebinder.java:83)
at org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder.onApplicationEvent(ConfigurationPropertiesRebinder.java:128)
at org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder.onApplicationEvent(ConfigurationPropertiesRebinder.java:50)
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:403)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:360)
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:403)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:360)
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$$$capture(FutureTask.java:266)
at java.util.concurrent.FutureTask.run(FutureTask.java)
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:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.IllegalStateException: Unable to set value for property username
at org.springframework.boot.context.properties.bind.JavaBeanBinder$BeanProperty.setValue(JavaBeanBinder.java:339)
at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:86)
at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:70)
at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:54)
at org.springframework.boot.context.properties.bind.Binder.lambda$null$4(Binder.java:329)
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
at java.util.ArrayList$ArrayListSpliterator.tryAdvance(ArrayList.java:1351)
at java.util.stream.ReferencePipeline.forEachWithCancel(ReferencePipeline.java:126)
at java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:498)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:485)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
at java.util.stream.FindOps$FindOp.evaluateSequential(FindOps.java:152)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.ReferencePipeline.findFirst(ReferencePipeline.java:464)
at org.springframework.boot.context.properties.bind.Binder.lambda$bindBean$5(Binder.java:330)
at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:429)
at org.springframework.boot.context.properties.bind.Binder$Context.withBean(Binder.java:415)
at org.springframework.boot.context.properties.bind.Binder$Context.access$400(Binder.java:372)
at org.springframework.boot.context.properties.bind.Binder.bindBean(Binder.java:328)
at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:269)
at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:214)
... 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:336)
... 60 common frames omitted
Caused by: java.lang.UnsupportedOperationException: null
at com.alibaba.druid.pool.DruidAbstractDataSource.setUsername(DruidAbstractDataSource.java:1123)
... 65 common frames omitted
原因分析:
由于数据库用户名和密码采用了jasypt进行了加密处理,刷新nacos配置时,触发EnvironmentChangeEvent事件,触发JavaBeanBinder重新给DruidAbstractDataSource的username重新复制,这时候由于赋值的内容是加密过的(nacos不支持加密配置项动态刷新),和第一次赋值的解密的username不一样了,并且DruidAbstractDataSource的inited已经是初始化过了,导致触发UnsupportedOperationException的异常。
解决思路:
从源码看,只要让setUsername中的StringUtils.equals(this.username, username))生效就可以避免该问题。
public void setUsername(String username) {
//只要让该条件生效,就可以解决该问题
if (StringUtils.equals(this.username, username)) {
return;
}
if (inited) {
throw new UnsupportedOperationException();
}
this.username = username;
}
解决办法:
1.既然是username加密导致这个问题,最简单粗暴的就是取消username的jasypt加密
参考: 修改了Nacos配置后,Druid抛出大量数据库连接异常日志_legendaryhaha的博客-CSDN博客修改了Nacos配置后,Druid抛出大量数据库连接异常日志起因分析复现解决起因某个周末,运营同事反馈账单数据不对且客户需要开始执行交易了。起初,还以为只是简单的数据问题,核对一下就好了。但在生产日志排查的过程种,发现日志大量抛出了大量的数据库连接异常:ERROR DruidDataSource[run 2755] create connection SQLException异常信息下还有如下提示: Access denied for user 'root'@'X.XX.XX.XXX' (ushttps://blog.csdn.net/legendaryhaha/article/details/118458241
2.既然是由于刷新后赋值时,这个username没有被解密造成的,那么就让Nacos支持加密配置项动态刷新
参考:让Nacos支持加密配置项的配置刷新 - 剑握在手 - 博客园2021年4月1日(本内容非愚人节恶搞)新增: jasypt在新版本3.0.3.Release中已经解决了这个问题,请大家直接用新版就行。 传送门 ——————————————————————————https://www.cnblogs.com/flying607/p/12520009.html
按照这个文章进行修改,好像并没有解决,最后采用如下的代码使得Nacos支持加密配置项动态刷新:
/*
* Copyright 2013-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.nacos.client;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.cloud.nacos.NacosConfigProperties;
import com.alibaba.cloud.nacos.NacosPropertySourceRepository;
import com.alibaba.cloud.nacos.refresh.NacosContextRefresher;
import com.alibaba.nacos.api.config.ConfigService;
import com.ulisesbocchio.jasyptspringboot.EncryptablePropertyFilter;
import com.ulisesbocchio.jasyptspringboot.EncryptablePropertyResolver;
import com.ulisesbocchio.jasyptspringboot.EncryptablePropertySourceConverter;
import com.ulisesbocchio.jasyptspringboot.InterceptionMode;
import com.ulisesbocchio.jasyptspringboot.detector.DefaultPropertyDetector;
import com.ulisesbocchio.jasyptspringboot.filter.DefaultPropertyFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* @author xiaojing
* @author pbting
* @Description 覆盖nacos三方包里面的类,解决采用jasypt加密nacos敏感配置项后二次刷新,ENC(*****)无法解析的问题
*/
@Order(0)
public class NacosPropertySourceLocator implements PropertySourceLocator {
private static final Logger log = LoggerFactory
.getLogger(NacosPropertySourceLocator.class);
private static final String NACOS_PROPERTY_SOURCE_NAME = "NACOS";
private static final String SEP1 = "-";
private static final String DOT = ".";
private NacosPropertySourceBuilder nacosPropertySourceBuilder;
private NacosConfigProperties nacosConfigProperties;
private NacosConfigManager nacosConfigManager;
@Autowired
private ConfigurableListableBeanFactory beanFactory;
@Autowired
private ConfigurableApplicationContext context;
/**
* com.ulisesbocchio.jasyptspringboot.configuration.EncryptablePropertyResolverConfiguration.RESOLVER_BEAN_NAME;
*/
static final String RESOLVER_BEAN_NAME = "lazyEncryptablePropertyResolver";
/**
* com.ulisesbocchio.jasyptspringboot.configuration.EncryptablePropertyResolverConfiguration.FILTER_BEAN_NAME;
*/
static final String FILTER_BEAN_NAME = "lazyEncryptablePropertyFilter";
/**
* recommend to use
* {@link NacosPropertySourceLocator#NacosPropertySourceLocator(com.alibaba.cloud.nacos.NacosConfigManager)}.
* @param nacosConfigProperties nacosConfigProperties
*/
@Deprecated
public NacosPropertySourceLocator(NacosConfigProperties nacosConfigProperties) {
this.nacosConfigProperties = nacosConfigProperties;
}
public NacosPropertySourceLocator(NacosConfigManager nacosConfigManager) {
this.nacosConfigManager = nacosConfigManager;
this.nacosConfigProperties = nacosConfigManager.getNacosConfigProperties();
}
@Override
public PropertySource<?> locate(Environment env) {
nacosConfigProperties.setEnvironment(env);
ConfigService configService = nacosConfigManager.getConfigService();
if (null == configService) {
log.warn("no instance of config service found, can't load config from nacos");
return null;
}
long timeout = nacosConfigProperties.getTimeout();
nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService,
timeout);
String name = nacosConfigProperties.getName();
String dataIdPrefix = nacosConfigProperties.getPrefix();
if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = name;
}
if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = env.getProperty("spring.application.name");
}
CompositePropertySource composite = new CompositePropertySource(
NACOS_PROPERTY_SOURCE_NAME);
loadSharedConfiguration(composite);
loadExtConfiguration(composite);
loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
return composite;
}
/**
* load shared configuration.
*/
private void loadSharedConfiguration(
CompositePropertySource compositePropertySource) {
List<NacosConfigProperties.Config> sharedConfigs = nacosConfigProperties
.getSharedConfigs();
if (!CollectionUtils.isEmpty(sharedConfigs)) {
checkConfiguration(sharedConfigs, "shared-configs");
loadNacosConfiguration(compositePropertySource, sharedConfigs);
}
}
/**
* load extensional configuration.
*/
private void loadExtConfiguration(CompositePropertySource compositePropertySource) {
List<NacosConfigProperties.Config> extConfigs = nacosConfigProperties
.getExtensionConfigs();
if (!CollectionUtils.isEmpty(extConfigs)) {
checkConfiguration(extConfigs, "extension-configs");
loadNacosConfiguration(compositePropertySource, extConfigs);
}
}
/**
* load configuration of application.
*/
private void loadApplicationConfiguration(
CompositePropertySource compositePropertySource, String dataIdPrefix,
NacosConfigProperties properties, Environment environment) {
String fileExtension = properties.getFileExtension();
String nacosGroup = properties.getGroup();
// load directly once by default
loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,
fileExtension, true);
// load with suffix, which have a higher priority than the default
loadNacosDataIfPresent(compositePropertySource,
dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
// Loaded with profile, which have a higher priority than the suffix
for (String profile : environment.getActiveProfiles()) {
String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
fileExtension, true);
}
}
private void loadNacosConfiguration(final CompositePropertySource composite,
List<NacosConfigProperties.Config> configs) {
for (NacosConfigProperties.Config config : configs) {
String dataId = config.getDataId();
String fileExtension = dataId.substring(dataId.lastIndexOf(DOT) + 1);
loadNacosDataIfPresent(composite, dataId, config.getGroup(), fileExtension,
config.isRefresh());
}
}
private void checkConfiguration(List<NacosConfigProperties.Config> configs,
String tips) {
for (int i = 0; i < configs.size(); i++) {
String dataId = configs.get(i).getDataId();
if (dataId == null || dataId.trim().length() == 0) {
throw new IllegalStateException(String.format(
"the [ spring.cloud.nacos.config.%s[%s] ] must give a dataId",
tips, i));
}
}
}
private void loadNacosDataIfPresent(final CompositePropertySource composite,
final String dataId, final String group, String fileExtension,
boolean isRefreshable) {
if (null == dataId || dataId.trim().length() < 1) {
return;
}
if (null == group || group.trim().length() < 1) {
return;
}
NacosPropertySource propertySource = this.loadNacosPropertySource(dataId, group,
fileExtension, isRefreshable);
//主要是结合jasypt的实现原理,对nacos生成的propertySource进行包装,包装成EncryptableXXX
EncryptablePropertyResolver propertyResolver = beanFactory.getBean(RESOLVER_BEAN_NAME, EncryptablePropertyResolver.class);
EncryptablePropertyFilter encryptablePropertyFilter = beanFactory.getBean(FILTER_BEAN_NAME,EncryptablePropertyFilter.class);
propertySource = (NacosPropertySource) EncryptablePropertySourceConverter.makeEncryptable(InterceptionMode.PROXY, propertyResolver,encryptablePropertyFilter, propertySource);
this.addFirstPropertySource(composite, propertySource, false);
}
private NacosPropertySource loadNacosPropertySource(final String dataId,
final String group, String fileExtension, boolean isRefreshable) {
if (NacosContextRefresher.getRefreshCount() != 0) {
if (!isRefreshable) {
return NacosPropertySourceRepository.getNacosPropertySource(dataId,
group);
}
}
return nacosPropertySourceBuilder.build(dataId, group, fileExtension,
isRefreshable);
}
/**
* Add the nacos configuration to the first place and maybe ignore the empty
* configuration.
*/
private void addFirstPropertySource(final CompositePropertySource composite,
NacosPropertySource nacosPropertySource, boolean ignoreEmpty) {
if (null == nacosPropertySource || null == composite) {
return;
}
if (ignoreEmpty && nacosPropertySource.getSource().isEmpty()) {
return;
}
composite.addFirstPropertySource(nacosPropertySource);
}
public void setNacosConfigManager(NacosConfigManager nacosConfigManager) {
this.nacosConfigManager = nacosConfigManager;
}
}
采用上面的代码后,再次刷新nacos配置,可以看到 username已经被解密,StringUtils.equals(this.username, username) 条件生效。
转载请注明:Failed to bind properties under ‘spring.datasource.druid‘ to javax.sql.DataSource;Nacos支持加密配置项动态刷新 | 胖虎的工具箱-编程导航