Spring环境支持Typesafe Config

22

我希望在我的项目中使用类型安全的配置(HOCON配置文件),以便轻松有序地进行应用程序配置。目前我正在使用普通的Java属性文件(application.properties),在大型项目中难以处理。

我的项目是Spring MVC(不是Spring Boot项目)。有没有办法将我的Spring Environment(我正在向服务注入的Environment)备份为类型安全的配置,而不会破坏我现有的Environment使用,如@Value注释,@Autowired Environment等。

我该如何以最小的工作量和代码更改来实现这一点。

这是我的当前解决方案:是否有其他更好的方法?

@Configuration
public class PropertyLoader{
    private static Logger logger = LoggerFactory.getLogger(PropertyLoader.class);

    @Bean
    @Autowired
    public static PropertySourcesPlaceholderConfigurer properties(Environment env) {
        PropertySourcesPlaceholderConfigurer pspc = new PropertySourcesPlaceholderConfigurer();

        Config conf = ConfigFactory.load();
        conf.resolve();
        TypesafePropertySource propertySource = new TypesafePropertySource("hoconSource", conf);

        ConfigurableEnvironment environment = (StandardEnvironment)env;
        MutablePropertySources propertySources = environment.getPropertySources();
        propertySources.addLast(propertySource);
        pspc.setPropertySources(propertySources);

        return pspc;
    }
}

class TypesafePropertySource extends PropertySource<Config>{
    public TypesafePropertySource(String name, Config source) {
        super(name, source);
    }

    @Override
    public Object getProperty(String name) {
        return this.getSource().getAnyRef(name);
    }
}

使用ApplicationContextInitializer添加属性源是更好的解决方案,尝试像你目前做的那样硬塞进去并不是最好的解决办法。 - M. Deinum
4个回答

17

我认为我想到了一种比手动添加PropertySource到属性源更习惯用语的方法。创建一个PropertySourceFactory并使用@PropertySource引用它。

首先,我们有一个与您几乎完全相同的TypesafeConfigPropertySource

public class TypesafeConfigPropertySource extends PropertySource<Config> {
    public TypesafeConfigPropertySource(String name, Config source) {
        super(name, source);
    }

    @Override
    public Object getProperty(String path) {
        if (source.hasPath(path)) {
            return source.getAnyRef(path);
        }
        return null;
    }
}

接下来,我们创建一个返回该属性源的属性源工厂

public class TypesafePropertySourceFactory implements PropertySourceFactory {

    @Override
    public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
        Config config = ConfigFactory.load(resource.getResource().getFilename()).resolve();

        String safeName = name == null ? "typeSafe" : name;
        return new TypesafeConfigPropertySource(safeName, config);
    }

}

最后,在我们的配置文件中,我们只需像其他PropertySource一样引用属性源,而不必自己添加PropertySource:

@Configuration
@PropertySource(factory=TypesafePropertySourceFactory.class, value="someconfig.conf")
public class PropertyLoader {
    // Nothing needed here
}

1
我需要在 getProperty 方法中添加 if (path.contains("[")) return null; if (path.contains(":")) return null; 来忽略其他 PropertySource 实现中存在的特殊字符。 - raisercostin
我特别处理了“:”版本。在Spring中,它提供了一个默认值。所以,我按“:”分割,对第一部分进行搜索,如果不存在,则返回第二部分。 - Laplie Anderson

5
你可以创建一个PropertySource类,代码如下。这个类和你的相似,不同的是它必须返回一个值或者null,不能让库抛出缺失异常。
public class TypesafeConfigPropertySource extends PropertySource<Config> {

    private static final Logger LOG = getLogger(TypesafeConfigPropertySource.class);

    public TypesafeConfigPropertySource(String name, Config source) {
        super(name, source);
    }

    @Override
    public Object getProperty(String name) {
        try {
            return source.getAnyRef(name);
        } catch (ConfigException.Missing missing) {
            LOG.trace("Property requested [{}] is not set", name);
            return null;
        }
    }
}

第二步是按照以下方式定义一个bean。
    @Bean
    public TypesafeConfigPropertySource provideTypesafeConfigPropertySource(
        ConfigurableEnvironment env) {

        Config conf = ConfigFactory.load().resolve();
        TypesafeConfigPropertySource source = 
                          new TypesafeConfigPropertySource("typeSafe", conf);
        MutablePropertySources sources = env.getPropertySources();
        sources.addFirst(source); // Choose if you want it first or last
        return source;

    }

在您想要自动装配属性到其他bean的情况下,需要使用注释@DependsOn来指定propertysource bean,以确保它首先被加载。
希望对您有所帮助。

即使这是正确的答案,我认为Laplie Anderson的答案更加简洁明了。但两者都可以使用。 - Ysak
如果你返回一个 ConfigException.Missing,我认为它不会起作用。 - tbo

2

Laplie Anderson提出了一些小的改进:

  • 如果找不到资源,抛出异常
  • 忽略包含[:字符的路径

TypesafePropertySourceFactory.java

import java.io.IOException;

import org.springframework.core.env.PropertySource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertySourceFactory;

import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigParseOptions;
import com.typesafe.config.ConfigResolveOptions;

public class TypesafePropertySourceFactory implements PropertySourceFactory {

  @Override
  public PropertySource<?> createPropertySource(String name, EncodedResource resource)
      throws IOException {
    Config config = ConfigFactory
        .load(resource.getResource().getFilename(),
            ConfigParseOptions.defaults().setAllowMissing(false),
            ConfigResolveOptions.noSystem()).resolve();

    String safeName = name == null ? "typeSafe" : name;
    return new TypesafeConfigPropertySource(safeName, config);
  }
}

TypesafeConfigPropertySource.java

import org.springframework.core.env.PropertySource;

import com.typesafe.config.Config;

public class TypesafeConfigPropertySource extends PropertySource<Config> {
  public TypesafeConfigPropertySource(String name, Config source) {
    super(name, source);
  }

  @Override
  public Object getProperty(String path) {
    if (path.contains("["))
      return null;
    if (path.contains(":"))
      return null;
    if (source.hasPath(path)) {
      return source.getAnyRef(path);
    }
    return null;
  }
}

我建议在TypesafePropertySourceFactory中使用ConfigResolveOptions.defaults(),而不是.noSystem(),因为我想人们可能不知道为什么他们的环境变量不能正确地替换到他们的配置文件中。 - coderatchet

2
我尝试了上述所有方法但都失败了。我遇到的一个特定问题是Bean的初始化顺序。例如,我们需要Flyway支持来获取一些来自类型安全配置的覆盖属性,还需要同样的方式处理其他属性。
正如m-deinum在其中一条评论中建议的那样,对于我们来说以下解决方案有效,同时也依赖其他答案提供的信息。通过在加载主应用程序时使用ApplicationContextInitializer,我们确保在应用程序启动时加载属性,并正确合并到"env"中:
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.annotation.Import;

@SpringBootConfiguration
@Import({MyAppConfiguration.class})
public class MyApp {

    public static void main(String[] args) {
        new SpringApplicationBuilder(MyApp.class)
            .initializers(new MyAppContextInitializer())
            .run(args);
    }
}
ContextInitializer看起来像这样:
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;

public class MyAppContextInitializer implements
    ApplicationContextInitializer<ConfigurableApplicationContext> {

    @Override
    public void initialize(ConfigurableApplicationContext ac) {    
        PropertiesLoader loader = new PropertiesLoader(ac.getEnvironment());
        loader.addConfigToEnv();
    }

} 
< p > PropertiesLoader 的工作方式是从配置文件中加载属性,并将其放入环境中:

import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;

class PropertiesLoader {

    private ConfigurableEnvironment env;

    public PropertiesLoader(ConfigurableEnvironment env) {
        this.env = env;
    }

    public void addConfigToEnv() {
        MutablePropertySources sources = env.getPropertySources();

        Config finalConfig = ConfigFactory.load().resolve();
        // you can also do other stuff like: ConfigFactory.parseFile(), use Config.withFallback to merge configs, etc.
        TypesafeConfigPropertySource source = new TypesafeConfigPropertySource("typeSafe", finalConfig);

        sources.addFirst(source);
    }

}

我们还需要适用于typesafe配置的TypesafeConfigPropertySource

import com.typesafe.config.Config;
import org.springframework.core.env.PropertySource;

public class TypesafeConfigPropertySource extends PropertySource<Config> {

    public TypesafeConfigPropertySource(String name, Config source) {
        super(name, source);
    }

    @Override
    public Object getProperty(String path) {
        if (path.contains("["))
            return null;
        if (path.contains(":"))
            return null;
        if (source.hasPath(path)) {
            return source.getAnyRef(path);
        }
        return null;
    }

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接