问题背景
在微服务的 application.properties 文件中有一个test.container-name配置。原始配置如下:
/* by 01022.hk - online tools website : 01022.hk/zh/togif.html */ test.container-name=Tomcat同时有一个 Java 类TestConfigProperty中通过@ConfigurationProperties注解注入这个配置属性到它的变量containerName中,代码如下:
/* by 01022.hk - online tools website : 01022.hk/zh/togif.html */ @ConfigurationProperties(prefix = "test") @Component public class TestConfigProperty { private String containerName; public String getContainerName() { return containerName; } public void setContainerName(String containerName) { this.containerName = containerName; } }现在因为test.container-name配置包含敏感信息,不能直接配置原始的值,需要配置加密之后的值,在微服务启动的时候解密。现在是test.container-name配置引用了TEST_CONTAINER_NAME环境变量。配置如下:
test.container-name=${TEST_CONTAINER_NAME}然后在环境变量中配置了加密之后的值。在本案例中为了简化,这里加密就用的 Base64 编码作为示例演示。如下图所示:
在项目中有框架提供了在微服务启动时对加密后的字符串解密的能力,实现的基本原理是提供了一个DecryptEnvironmentPostProcessor类扩展了EnvironmentPostProcessor。
在它的postProcessEnvironment()方法中,判断环境变量配置的值是否是以ENC_开头,如果是则进行解密。解密之后放到一个MapPropertySource里面,然后添加到所有的PropertySource的前面。示例代码如下:
public class DecryptEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered { private static final String DECRYPTED_SOURCE_NAME = "decryptedSystemEnvironment"; @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { String systemEnvName = StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME; MapPropertySource systemEnvSource = (MapPropertySource) environment.getPropertySources().get(systemEnvName); Map<String, Object> decryptedMap = new HashMap<>(); if (systemEnvSource == null) { return; } systemEnvSource.getSource().forEach((key, value) -> { if (value instanceof String strVal) { // 这里进行了解密 if (StringUtils.isNotEmpty(strVal) && strVal.startsWith("ENC_")) { String plainText = new String(Base64.getDecoder().decode(strVal.substring(4))); decryptedMap.put(key, plainText); } } }); if (!decryptedMap.isEmpty()) { MapPropertySource decryptedSource = new MapPropertySource(DECRYPTED_SOURCE_NAME, decryptedMap); // 这里添加到所有的PropertySource的前面 environment.getPropertySources().addBefore(systemEnvName, decryptedSource); } } @Override public int getOrder() { return Ordered.LOWEST_PRECEDENCE; } }按照上述配置,通过调试发现类TestConfigProperty里面注入的还是加密之后的值,而并不是想要的解密之后的值。如下图所示:
查看Environment的getPropertySources()方法的返回值中,解密之后的环境变量属性配置确实是在未解密的环境变量属性配置之前,按照直观上的理解,那应该注入的是解密之后的值才对,但是实际结果却不是这样的。如下图所示:
问题原理
之前的文章这就是宽松的适配规则!里面讲了宽松适配的原理。在 Spring 的框架体系中是在ConfigurationPropertiesBindingPostProcessor中的postProcessBeforeInitialization()中实现对有@ConfigurationProperties注解修饰类的属性进行绑定的。
在它的内部实际上是通过调用ConfigurationPropertiesBinder的bind()来实现属性绑定的。代码如下:
public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor, PriorityOrdered, ApplicationContextAware, InitializingBean { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (!hasBoundValueObject(beanName)) { bind(ConfigurationPropertiesBean.get(this.applicationContext, bean, beanName)); } return bean; } private void bind(ConfigurationPropertiesBean bean) { if (bean == null) { return; } Assert.state(bean.asBindTarget().getBindMethod() != BindMethod.VALUE_OBJECT, "Cannot bind @ConfigurationProperties for bean '" + bean.getName() + "'. Ensure that @ConstructorBinding has not been applied to regular bean"); try { // 这里实际上是调用了ConfigurationPropertiesBinder的bind()方法 this.binder.bind(bean); } catch (Exception ex) { throw new ConfigurationPropertiesBindException(bean, ex); } } }在ConfigurationPropertiesBinder的bind()方法又调用了Binder的bind()方法。如下图所示:
在调用Binder的bind()方法时,会把注解上配置的前缀传进去,在本案例中就是test,并基于这个前缀创建一个ConfigurationPropertyName对象,然后最终调用到bindObject()方法。代码如下:
public class Binder { public <T> BindResult<T> bind(String name, Bindable<T> target, BindHandler handler) { // 这里基于test前缀创建了ConfigurationPropertyName对象 return bind(ConfigurationPropertyName.of(name), target, handler); } private <T> T bind(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler, Context context, boolean allowRecursiveBinding, boolean create) { try { Bindable<T> replacementTarget = handler.onStart(name, target, context); if (replacementTarget == null) { return handleBindResult(name, target, handler, context, null, create); } target = replacementTarget; // 调用bindObject()方法 Object bound = bindObject(name, target, handler, context, allowRecursiveBinding); return handleBindResult(name, target, handler, context, bound, create); } catch (Exception ex) { return handleBindError(name, target, handler, context, ex); } } }在bindObject()中首先调用findProperty()方法查找属性,因为当前只是前缀test,因此肯定是找不到对应的属性配置的。 因此往下走会调用到bindDataObject()方法。对于 JavaBean 来说,在Binder的bindDataObject()方法最终会调用到JavaBeanBinder的bind()方法。代码如下:
public class Binder { private <T> Object bindObject(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler, Context context, boolean allowRecursiveBinding) { ConfigurationProperty property = findProperty(name, target, context); if (property == null && context.depth != 0 && containsNoDescendantOf(context.getSources(), name)) { return null; } // 省略中间代码 //调用bindDataObject()方法 return bindDataObject(name, target, handler, context, allowRecursiveBinding); } private Object bindDataObject(ConfigurationPropertyName name, Bindable<?> target, BindHandler handler, Context context, boolean allowRecursiveBinding) { if (isUnbindableBean(name, target, context)) { return null; } Class<?> type = target.getType().resolve(Object.class); BindMethod bindMethod = target.getBindMethod(); if (!allowRecursiveBinding && context.isBindingDataObject(type)) { return null; } // 注意这里的lambda表达式,在JavaBeanBinder的bind()方法最终又会调用到这个lambda表达式 DataObjectPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(name.append(propertyName), propertyTarget, handler, context, false, false); // 这里会调用到JavaBeanBinder的bind()方法 return context.withDataObject(type, () -> fromDataObjectBinders(bindMethod, (dataObjectBinder) -> dataObjectBinder.bind(name, target, context, propertyBinder))); } }在JavaBeanBinder的bind()方法中会获取这个对象的所有的BeanProperty,然后又反调用回Binder中的lambda表达式了。代码如下:
class JavaBeanBinder implements DataObjectBinder { @Override public <T> T bind(ConfigurationPropertyName name, Bindable<T> target, Context context, DataObjectPropertyBinder propertyBinder) { boolean hasKnownBindableProperties = target.getValue() != null && hasKnownBindableProperties(name, context); Bean<T> bean = Bean.get(target, hasKnownBindableProperties); if (bean == null) { return null; } BeanSupplier<T> beanSupplier = bean.getSupplier(target); boolean bound = bind(propertyBinder, bean, beanSupplier, context); return (bound ? beanSupplier.get() : null); } private <T> boolean bind(DataObjectPropertyBinder propertyBinder, Bean<T> bean, BeanSupplier<T> beanSupplier, Context context) { boolean bound = false; for (BeanProperty beanProperty : bean.getProperties().values()) { // 获取这个对象上所有的BeanProperty属性 bound |= bind(beanSupplier, propertyBinder, beanProperty); context.clearConfigurationProperty(); } return bound; } private <T> boolean bind(BeanSupplier<T> beanSupplier, DataObjectPropertyBinder propertyBinder, BeanProperty property) { String propertyName = determinePropertyName(property); ResolvableType type = property.getType(); Supplier<Object> value = property.getValue(beanSupplier); Annotation[] annotations = property.getAnnotations(); Object bound = propertyBinder.bindProperty(propertyName, //这个地方实际上又反调用回Binder中的lambda表达式了 Bindable.of(type).withSuppliedValue(value).withAnnotations(annotations)); if (bound == null) { return false; } if (property.isSettable()) { property.setValue(beanSupplier, bound); } else if (value == null || !bound.equals(value.get())) { throw new IllegalStateException("No setter found for property: " + property.getName()); } return true; } }BeanProperty对象会将 JavaBean 中的属性统一为 Dash 格式。在本案例中属性名称是containerName,统一之后就变成了container-name。如下图所示:
在Binder中 lambda 表达式会将属性拼接到已有的ConfigurationPropertyName前缀上,在本案例中就变成了test.container-name。然后又递归调用bind()方法,然后又调用findProperty()方法尝试从从对应的ConfigurationPropertySource中获取对应的配置中查找这个属性。
Spring 提供了SpringIterableConfigurationPropertySource作为ConfigurationPropertySource实现类, 它实际是对PropertySource的一个适配,内部有一个propertySource表示真正的配置。通过调试contex.getSource()方法的返回值,可以看到加密之后的PropertySource确实是在没有加密的前面。代码如下:
DataObjectPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(name.append(propertyName), //这里将属性名称拼接到test前缀上 propertyTarget, handler, context, false, false); private <T> ConfigurationProperty findProperty(ConfigurationPropertyName name, Bindable<T> target, Context context) { if (name.isEmpty() || target.hasBindRestriction(BindRestriction.NO_DIRECT_PROPERTY)) { return null; } for (ConfigurationPropertySource source : context.getSources()) { ConfigurationProperty property = source.getConfigurationProperty(name); if (property != null) { return property; } } return null; }在getConfigurationProperty()方法中首先调用父类SpringConfigurationPropertySource的getConfigurationProperty()方法。在该方法中会调用PropertyMapper的map()方法对传入的ConfigurationPropertyName类型的name进行转换,然后根据转换后拿到的名称去PropertySource中获取对应的属性。
class SpringConfigurationPropertySource implements ConfigurationPropertySource { public ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName name) { if (name == null) { return null; } for (PropertyMapper mapper : this.mappers) { try { for (String candidate : mapper.map(name)) { // 这里先通过PropertyMapper转换名称 Object value = getPropertySource().getProperty(candidate); // 根据转换后的名称获取获取对应的属性 if (value != null) { Origin origin = PropertySourceOrigin.get(this.propertySource, candidate); return ConfigurationProperty.of(this, name, value, origin); } } } catch (Exception ex) { // Ignore } } return null; } }在创建SpringConfigurationPropertySource对象时,会根据PropertySource是MapPropertySource还是SystemEnvironmentPropertySource,从而设置不同的mappers属性,对于SystemEnvironmentPropertySource,它会多一个SystemEnvironmentPropertyMapper。代码如下:
class SpringConfigurationPropertySource implements ConfigurationPropertySource { private static final PropertyMapper[] DEFAULT_MAPPERS = { DefaultPropertyMapper.INSTANCE }; private static final PropertyMapper[] SYSTEM_ENVIRONMENT_MAPPERS = { SystemEnvironmentPropertyMapper.INSTANCE, DefaultPropertyMapper.INSTANCE }; static SpringConfigurationPropertySource from(PropertySource<?> source) { Assert.notNull(source, "Source must not be null"); PropertyMapper[] mappers = getPropertyMappers(source); if (isFullEnumerable(source)) { return new SpringIterableConfigurationPropertySource((EnumerablePropertySource<?>) source, mappers); } return new SpringConfigurationPropertySource(source, mappers); } private static PropertyMapper[] getPropertyMappers(PropertySource<?> source) { // 这里判断了如果是SystemEnvironmentPropertySource则会返回SYSTEM_ENVIRONMENT_MAPPERS,里面包含了SystemEnvironmentPropertyMapper if (source instanceof SystemEnvironmentPropertySource && hasSystemEnvironmentName(source)) { return SYSTEM_ENVIRONMENT_MAPPERS; } return DEFAULT_MAPPERS; } }对于DefaultPropertyMapper它的map()方法会直接返回ConfigurationPropertyName的名称,在本案例中就会直接返回test.container-name。代码如下:
final class DefaultPropertyMapper implements PropertyMapper { @Override public List<String> map(ConfigurationPropertyName configurationPropertyName) { // Use a local copy in case another thread changes things LastMapping<ConfigurationPropertyName, List<String>> last = this.lastMappedConfigurationPropertyName; if (last != null && last.isFrom(configurationPropertyName)) { return last.getMapping(); } // 这里直接返回ConfigurationPropertyName的名称 String convertedName = configurationPropertyName.toString(); List<String> mapping = Collections.singletonList(convertedName); this.lastMappedConfigurationPropertyName = new LastMapping<>(configurationPropertyName, mapping); return mapping; } }对于SystemEnvironmentPropertyMapper它会返回两个格式的名称,在本案例中就会返回TEST_CONTAINERNAME和TEST_CONTAINER_NAME两种格式。代码如下:
final class SystemEnvironmentPropertyMapper implements PropertyMapper { public static final PropertyMapper INSTANCE = new SystemEnvironmentPropertyMapper(); @Override public List<String> map(ConfigurationPropertyName configurationPropertyName) { String name = convertName(configurationPropertyName); String legacyName = convertLegacyName(configurationPropertyName); if (name.equals(legacyName)) { return Collections.singletonList(name); } // 这里会返回两个格式的名称 return Arrays.asList(name, legacyName); } private String convertName(ConfigurationPropertyName name) { return convertName(name, name.getNumberOfElements()); } private String convertName(ConfigurationPropertyName name, int numberOfElements) { StringBuilder result = new StringBuilder(); for (int i = 0; i < numberOfElements; i++) { if (!result.isEmpty()) { result.append('_'); } result.append(name.getElement(i, Form.UNIFORM).toUpperCase(Locale.ENGLISH)); } return result.toString(); } private String convertLegacyName(ConfigurationPropertyName name) { StringBuilder result = new StringBuilder(); for (int i = 0; i < name.getNumberOfElements(); i++) { if (!result.isEmpty()) { result.append('_'); } result.append(convertLegacyNameElement(name.getElement(i, Form.ORIGINAL))); } return result.toString(); } private Object convertLegacyNameElement(String element) { return element.replace('-', '_').toUpperCase(Locale.ENGLISH); } }在本案例中decryptedSystemEnvironment的PropertySource类型是MapPropertySource,存放的内容是TEST_CONTAINER_NAME=Tomcat。它只有DefaultPropertyMapper;
名称为systemEnvironment的PropertySource类型是SystemEnvironmentPropertySource,存放的内容是TEST_CONTAINER_NAME=ENC_VG9tY2F0。它有DefaultPropertyMapper和SystemEnvironmentPropertyMapper。
decryptedSystemEnvironment在顺序上排在systemEnvironment前面。这个时候开始查找传入名称为test.container-name的ConfigurationPropertyName,这个时候先从decryptedSystemEnvironment开始找,经过DefaultPropertyMapper转换之后拿到的属性名称是test.container-name,配置里面没有这个配置;然后从systemEnvironment开始找,经过SystemEnvironmentPropertyMapper转换之后拿到的属性名称是TEST_CONTAINERNAME和TEST_CONTAINER_NAME,根据TEST_CONTAINER_NAME就拿到了ENC_VG9tY2F0。这就解释了为啥配置类注入的还是加密之后的值了。
问题解决
知道问题的原理了之后,问题就好解决了。一种方法是可以在DecryptEnvironmentPostProcessor类的postProcessBeforeInitialization()方法中把添加的MapPropertySource类型改为SystemEnvironmentPropertySource就可以了。代码如下:
@Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { String systemEnvName = StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME; MapPropertySource systemEnvSource = (MapPropertySource) environment.getPropertySources().get(systemEnvName); Map<String, Object> decryptedMap = new HashMap<>(); if (systemEnvSource == null) { return; } systemEnvSource.getSource().forEach((key, value) -> { if (value instanceof String strVal) { if (StringUtils.isNotEmpty(strVal) && strVal.startsWith("ENC_")) { String plainText = new String(Base64.getDecoder().decode(strVal.substring(4))); decryptedMap.put(key, plainText); } } }); if (!decryptedMap.isEmpty()) { // 这里原来添加的是MapPropertySource类型,现在调整为SystemEnvironmentPropertySource类型 // MapPropertySource decryptedSource = new MapPropertySource(DECRYPTED_SOURCE_NAME, decryptedMap); environment.getPropertySources().addBefore(systemEnvName, new SystemEnvironmentPropertySource(DECRYPTED_SOURCE_NAME, decryptedMap)); System.out.println(""); } }