springboot项目读取自定义yml文件失效问题及解决方案

821 阅读3分钟

在resource下创建自定义yml文件user.yml,文件内容如下

user:
  id: 1
  name: zhangshan
  email: desmond@163.com

然后创建Customer类读取配置文件

@PropertySource(value = "user.yml")
@ConfigurationProperties(prefix = "user")
@Configuration
public class Customer {

    private Integer id;
    private String name;
    private String email;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

最后在controller中测试能否获取到user.yml文件中的值

image.png 从图中可以看出,Custom的Bean对象没有读到user.yml文件中的值,那为什么name会有Annsky呢 从源码中可以看到,读取xml文件配置并将相应的值赋给对应bean上是由ConfigurationClassParser这个类实现的,首先给Bean赋值方法如下

protected final ConfigurationClassParser.SourceClass doProcessConfigurationClass(ConfigurationClass configClass, ConfigurationClassParser.SourceClass sourceClass, Predicate<String> filter) throws IOException {
    if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
        this.processMemberClasses(configClass, sourceClass, filter);
    }

    Iterator var4 = AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(), PropertySources.class, PropertySource.class).iterator();

    AnnotationAttributes importResource;
    while(var4.hasNext()) {
        importResource = (AnnotationAttributes)var4.next();
        if (this.environment instanceof ConfigurableEnvironment) {
        
       //这个方法是关键的方法
            this.processPropertySource(importResource);
        } else {
            this.logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() + "]. Reason: Environment must implement ConfigurableEnvironment");
        }
    }
/*****后面就不贴了

}
private void processPropertySource(AnnotationAttributes propertySource) throws IOException {
    String name = propertySource.getString("name");
    if (!StringUtils.hasLength(name)) {
        name = null;
    }

    String encoding = propertySource.getString("encoding");
    if (!StringUtils.hasLength(encoding)) {
        encoding = null;
    }

    String[] locations = propertySource.getStringArray("value");
    Assert.isTrue(locations.length > 0, "At least one @PropertySource(value) location is required");
    boolean ignoreResourceNotFound = propertySource.getBoolean("ignoreResourceNotFound");
    Class<? extends PropertySourceFactory> factoryClass = propertySource.getClass("factory");
    PropertySourceFactory factory = factoryClass == PropertySourceFactory.class ? DEFAULT_PROPERTY_SOURCE_FACTORY : (PropertySourceFactory)BeanUtils.instantiateClass(factoryClass);
    String[] var8 = locations;
    int var9 = locations.length;

    for(int var10 = 0; var10 < var9; ++var10) {
        String location = var8[var10];

        try {
            String resolvedLocation = this.environment.resolveRequiredPlaceholders(location);
            Resource resource = this.resourceLoader.getResource(resolvedLocation);
            //关键在这个方法,这个方法是将user.yml配置解析为一个PropertySource类,并加入到集合中
            this.addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding)));
        } catch (FileNotFoundException | UnknownHostException | SocketException | IllegalArgumentException var14) {
            if (!ignoreResourceNotFound) {
                throw var14;
            }

            if (this.logger.isInfoEnabled()) {
                this.logger.info("Properties location [" + location + "] not resolvable: " + var14.getMessage());
            }
        }
    }

}

image.png 截图中可以看到yml文件读到四个键值对,这是错误的,正确应该是user.name ---> "zhangsan",三条这样的类型数据,那这是为什么,返回到上我上面的代码 this.addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding))); 参数是由factory.createPropertySource(name, new EncodedResource(resource, encoding))创建出来的,那么回头上去看

image.png factory是DefaultPropertySourceFactory,这个工厂是不能解析yaml类型的文件的数据

image.png 虽然能读取user.yml,但是结果如第一张图所示,完全读成错误的数据,那么怎么办呢? 解决思路 既然是工厂不对,那么我们可以创建自定义的工厂

public class CustomYmlPropertySourceFactory implements PropertySourceFactory {

    @Override
    public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
        PropertySource custmoSetting = name != null ?
                new YamlPropertySourceLoader().load(name,resource.getResource()).get(0)
                : new YamlPropertySourceLoader().load(getNameForResource(resource.getResource()),resource.getResource()).get(0);
        return custmoSetting;
    }


    private static String getNameForResource(Resource resource) {
        String name = resource.getFilename();
        if (!org.springframework.util.StringUtils.hasText(name)) {
            name = resource.getClass().getSimpleName() + "@" + System.identityHashCode(resource);
        }
        return name;
    }

}

然后杳然Custom类指定由我们自定义的工厂来创建并解析配置文件然后赋值

@PropertySource(value = "user.yml",encoding = "utf-8",factory = CustomYmlPropertySourceFactory.class)
@ConfigurationProperties(prefix = "user")
@Configuration
public class Customer {

    private Integer id;
    private String name;
    private String email;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

这样加入到PropertySource集合中的Bean对象就是正确的数据

image.png

但是Custom对象的值中name的值还是不对,还是Annnsky image.png 这是因为在系统环境变量配置有同名的user.name

image.png

image.png

所以应该怎么解决呢? 思路:首先将user.yml文件中的配置名修改成不冲突的,第二种将自己的自定义的yml文件解析出来的PropertySource配置类加到MutablePropertySources中PropertySourceList集合中的首节点,那么按照顺序加载文件并赋值的规则,自定义的user.name 优先于systemProperties中的user.name 第二种解决办法代码如下

public class ParserBeanfactoryprocesser implements BeanFactoryPostProcessor, ApplicationContextAware, BeanPostProcessor {

    private ApplicationContext applicationContext;
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        printPropertySourceNames();
        String propertySourceName = "user.yml";
        PropertySource propertySource = getPropertySource(propertySourceName);
        if(!ObjectUtils.isEmpty(propertySource)){
            //bean初始化之前,修改属性文件加载顺序
            getEnvironment().getPropertySources().remove(propertySourceName);
            getEnvironment().getPropertySources().addFirst(propertySource);
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }


    private AbstractEnvironment getEnvironment(){
        return (AbstractEnvironment)applicationContext.getEnvironment();
    }
    private PropertySource getPropertySource(String propertySourceName) {
        return getEnvironment().getPropertySources().get(propertySourceName);
    }
    private void printPropertySourceNames(){
        getEnvironment().getPropertySources().stream().forEach(p-> System.out.println(p.getName()));
    }
}