Spring: 当有多个解析器时如何解析一个属性?

Spring: How to Resolve a Property When There Are Multiple Resolvers?

我们正在使用 Spring 云框架构建多个微服务。其中一项服务依赖于一些遗留共享库,并为 bean 配置导入各种 XML 文件。我们面临的问题是,通过这些导入,引入了多个 属性 解析器,因此 AbstractBeanFactory 中的以下代码无法解析 spring.application.name 因为值是 ${spring.application.name:unknown} 第一个解析器无法解析,因此将 result 设置为 unknownembeddedValueResolver 确实有一个可以解析 属性 的解析器,但是因为 属性 被以前的解析器设置为默认值,所以它没有机会。这导致 Eureka 的服务注册失败并出现 NPE。

@Override
public String resolveEmbeddedValue(String value) {
    String result = value;
    for (StringValueResolver resolver : this.embeddedValueResolvers) {
        if (result == null) {
            return null;
        }
        result = resolver.resolveStringValue(result);
    }
    return result;
}

为了回答我自己的问题,我使用 BeanDefinitionRegistryPostProcessor 解决了这个问题。相关 JIRA SPR-6428 已由另一用户提交但已关闭。

/**
 * Removes {@link org.springframework.beans.factory.config.PropertyPlaceholderConfigurer} classes that come before
 * {@link PropertySourcesPlaceholderConfigurer} and fail to resolve Spring Cloud properties, thus setting them to default.
 * One such property is {@code spring.application.name} that gets set to 'unknown' thus causing registration with
 * discovery service to fail. This class collects the {@code locations} from these offending
 * {@code PropertyPlaceholderConfigurer} and later adds to the end of property sources available from
 * {@link org.springframework.core.env.Environment}.
 * <p>
 * c.f. https://jira.spring.io/browse/SPR-6428
 *
 * @author Abhijit Sarkar
 */
@Component
@Slf4j
public class PropertyPlaceholderConfigurerPostProcessor implements BeanDefinitionRegistryPostProcessor {
    private final Set<String> locations = new HashSet<>();

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
        String[] beanDefinitionNames = beanDefinitionRegistry.getBeanDefinitionNames();

        List<String> propertyPlaceholderConfigurers = Arrays.stream(beanDefinitionNames)
                .filter(name -> name.contains("PropertyPlaceholderConfigurer"))
                .collect(toList());

        for (String name : propertyPlaceholderConfigurers) {
            BeanDefinition beanDefinition = beanDefinitionRegistry.getBeanDefinition(name);
            TypedStringValue location = (TypedStringValue) beanDefinition.getPropertyValues().get("location");

            if (location != null) {
                String value = location.getValue();
                log.info("Found location: {}.", location);
                /* Remove 'classpath:' prefix, if present. It later creates problem with reading the file. */
                locations.add(removeClasspathPrefixIfPresent(value));

                log.info("Removing bean definition: {}.", name);

                beanDefinitionRegistry.removeBeanDefinition(name);
            }
        }
    }

    private String removeClasspathPrefixIfPresent(String location) {
        int classpathPrefixIdx = location.lastIndexOf(':');

        return classpathPrefixIdx > 0 ? location.substring(++classpathPrefixIdx) : location;
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        PropertySourcesPlaceholderConfigurer configurer =
                beanFactory.getBean(PropertySourcesPlaceholderConfigurer.class);

        MutablePropertySources propertySources = getPropertySources(configurer);

        locations.stream()
                .map(locationToPropertySrc)
                .forEach(propertySources::addLast);
    }

    private MutablePropertySources getPropertySources(PropertySourcesPlaceholderConfigurer configurer) {
        /* I don't like this but PropertySourcesPlaceholderConfigurer has no getter for environment. */
        Field envField = null;
        try {
            envField = PropertySourcesPlaceholderConfigurer.class.getDeclaredField("environment");
            envField.setAccessible(true);
            ConfigurableEnvironment env = (ConfigurableEnvironment) envField.get(configurer);

            return env.getPropertySources();
        } catch (ReflectiveOperationException e) {
            throw new ApplicationContextException("Our little hack didn't work. Failed to read field: environment.", e);
        }
    }

    Function<String, PropertySource> locationToPropertySrc = location -> {
        ClassPathResource resource = new ClassPathResource(location);
        try {
            Properties props = PropertiesLoaderUtils.loadProperties(resource);
            String filename = getFilename(location);

            log.debug("Adding property source with name: {} and location: {}.", filename, location);

            return new PropertiesPropertySource(filename, props);
        } catch (IOException e) {
            throw new ApplicationContextException(
                    String.format("Failed to read from location: %s.", location), e);
        }
    };

    private String getFilename(String location) {
        return location.substring(location.lastIndexOf('/') + 1);
    }
}