在Java中从配置文件中读取配置参数的最佳方法是什么?

31
假设在运行时我们不知道配置的详细信息(用户可能需要在应用程序运行之前在“config”文件中配置这些参数)。
我想要读取这些配置详细信息,并在需要它们的任何地方重复使用。为此,我想将它们作为全局常量("public static final")。
那么,我的疑问是,如果我直接从所需类中的“config”文件中读取,是否会有任何性能影响?因为运行时值无法直接放入单独的“Interface”。
我认为这将会对性能产生影响。请建议我更好的方法。
更新:我能否使用单独的final类来存储配置详细信息? 将所有配置详细信息作为常量放入单独的 "public final class" 中 (一次性从配置文件中读取所有配置详细信息并将其存储为全局常量以供应用程序后续使用)

你能具体一点吗?例如,在J2EE应用程序中配置数据源,应该使用JNDI。在Spring应用程序中配置某种类型的超时,应该使用Spring。没有通用的最佳实践。 - flup
你是否使用了像Spring这样的框架?如果是的话,它们可能有自己管理属性的方式。例如,Spring有一个Environment对象,它封装了所有环境变量和属性文件。 - SergeyB
11个回答

32

我认为这会影响性能。

我怀疑这不是真的。

假设应用程序在启动时只读取一次配置文件,则读取文件所需的时间可能与应用程序的整体性能无关。实际上,应用程序运行的时间越长,启动时间就越不重要。

标准建议是仅在具有确凿证据(即测量)表明性能是一个重大问题时才优化应用程序性能。然后,仅优化那些分析告诉您真正成为性能瓶颈的代码部分。


我可以使用单独的final类作为配置详细信息吗?

是的,这是可能的。没有人会阻止你1。然而,这是一个坏主意。任何导致需要重新编译代码以更改配置参数的操作都是一个坏主意。依我看。


从配置文件一次性读取所有配置详细信息,并将它们存储为全局常量,以供应用程序稍后使用。

啊... 所以您实际上想要读取“常量”的值,而不是硬编码它们。

是的,这是可能的。它比将配置参数硬编码到代码中更有意义。但这仍然不是一个好主意(依我看)。

为什么?让我们看看代码应该是什么样子:

public final class Config { 
    public static final int CONST_1;
    public static final String CONST_2;
   
    static {
        int c1;
        String c2;
        try (Scanner s = new Scanner(new File("config.txt"))) {
            c1 = s.nextInt();
            c2 = s.next();
        } catch (IOException ex) {
            throw RuntimeException("Cannot load config properties", ex);
        }
        CONST_1 = c1;
        CONST_2 = c2; 
    }
}

首先需要注意的是,将类声明为final并没有影响字段是否被声明为final,只有将字段声明为final才会使它们成为常量。 (将类声明为final可以防止子类化,但这对static字段没有影响。静态字段不受继承的影响。)

接下来需要注意的是,此代码在许多方面都很脆弱:

  • 如果在静态初始化程序块中出现错误,则程序块引发的未经检查的异常将被包装为ExceptionInInitializerError(是的...它是一个Error!!),并且Config类将被标记为出错。

  • 如果发生这种情况,就没有实际的希望进行恢复,并且尝试诊断Error甚至可能是一个不好的想法。

  • 上面的代码在初始化Config类时执行,但确定何时发生这种情况可能很棘手。

  • 如果配置文件名是参数,则必须解决获取参数值的问题...在触发静态初始化之前。

此外,与将状态加载到实例变量中相比,代码非常混乱。这种混乱主要是由于必须在静态初始化器的约束下工作。如果改为使用final实例变量,则代码如下所示。

public final class Config { 
    public final int CONST_1;
    public final String CONST_2;
   
    public Config(File file) throws IOException {
        try (Scanner s = new Scanner(file)) {
            CONST_1 = s.nextInt();
            CONST_2 = s.next();
        } 
    }
}

最后,static final 字段与 final 字段相比在性能上的优势微乎其微:

  • 每次访问常量时,可能只有一两条机器指令的差异。

  • 如果 JIT 编译器很聪明并且您适当处理了单例 Config 引用,则可能根本没有性能差异。

无论如何,在绝大多数情况下,这些优势都是微不足道的。


1 - 如果您的代码经过代码审查,那么可能会有人阻止您这样做。


使用非静态实现时,您需要实例化一个Configuration对象来加载配置。当其他类需要配置参数时该怎么办?每个类在执行操作之前都必须实例化一个配置对象吗?还是将您在第一次创建的配置对象传递给其他类? - Luke
这似乎是一个简单的全局单例模式的明显案例。将您的配置对象(commons-configuration或自定义对象,如果您有理由)放在其中。或者在上面的情况下,将该对象本身转换为单例模式。无需静态初始化块,也无需在各处传递配置对象。 - Manius
@Manius - 单例模式也可能是一件坏事。它们会使测试变得更加复杂。如果配置对象是不可变的单例,那么如何运行涉及许多不同配置的单元测试?(好吧,有方法...)这些天来,在许多情况下,依赖注入被认为是最好的方式。 - Stephen C
@StephenC 我不能说我完全不同意,但我相信我们可以争论“最佳”取决于上下文。也许我想到了一些极简主义的情况,在这种情况下,由于某种原因避免使用 DI 框架依赖和连接,因此单例模式非常实用。太懒了,不想重新阅读整个页面来回忆这里的上下文。 - Manius

24

你听说过Apache Commons Configuration http://commons.apache.org/proper/commons-configuration/吗?它是我找到的最好的配置读取器,我甚至在我的应用程序中使用它,该应用程序已经运行了一年。从未发现任何问题,非常易于理解和使用,性能出色。我知道它对你的应用程序有些依赖,但相信我,你会喜欢它。

你需要做的就是

Configuration config = new ConfigSelector().getPropertiesConfiguration(configFilePath);
String value = config.getString("key");
int value1 = config.getInt("key1");
String[] value2 = config.getStringArray("key2");
List<Object> value3 = config.getList("key3");

就是这样。您的配置对象将保留所有配置值,您可以将该对象传递给任意数量的类。有了如此多可用的帮助方法,您可以提取所需的任何类型的键。


谢谢你提到其他选择。Apache配置文件支持在配置文件中添加注释(包括基于属性的注释)。很不错。 - Jayan
1
一个人真的必须努力思考才能不使用commons-configuration。从文件中读取属性很容易,但是一旦你开始考虑重新加载文件,https://commons.apache.org/proper/commons-configuration/userguide_v1.10/howto_filebased.html#Automatic_Reloading可以节省很多时间。 :) - cringe
1
我们有一个配置对象。如果你的解决方案是将该对象“只传递给尽可能多的类”,那么你必须重写每个类的每个方法,以接受额外的配置参数... - Luke
@Luke,依赖注入或单例模式可以解决这个问题,但是我对此感到困扰的是它带来了重大的依赖负担(包括一个活跃的安全漏洞,如果IntelliJ的警告系统准确的话,它说commons-text是一个依赖项)。https://devhub.checkmarx.com/cve-details/CVE-2022-42889/ - 真可爱啊?就像Log4j2一样烂。这些东西已经变得太“聪明”了,不值得去做了。更不用说你会在这个解决方案中随处可见随机密钥字符串。 - Manius

5

如果您将它们放在属性文件中,并在应用程序启动时读取该文件并将所有参数初始化为系统参数(System.setProperty),则只需付一次成本,然后在代码中定义常量即可。

public static final String MY_CONST = System.getProperty("my.const");

但要确保在应用程序启动之前进行初始化,以免加载任何其他类。


5

有不同类型的配置。

通常需要某种引导配置,例如连接到数据库或服务,才能启动应用程序。在 J2EE 中,指定数据库连接参数的方式是通过在容器的 JNDI 注册表(Glassfish、JBoss、Websphere 等)中指定“数据源”。然后,在 persistence.xml 中按名称查找此数据源。在非 J2EE 应用程序中,更常见的是在 Spring 上下文或甚至.properties 文件中指定这些内容。无论如何,您通常需要一些东西来将应用程序连接到某种数据存储。

在将数据存储引导后,一种选项是在此数据存储中管理配置值。例如,如果您有一个数据库,可以使用单独的表(例如应用程序中的JPA实体)来表示配置值。如果您不需要这种灵活性,可以改为使用简单的.properties文件。Java(ResourceBundle)和像Spring这样的框架都对.properties文件提供了良好的支持。原始ResourceBundle仅加载属性一次,Spring助手提供可配置的缓存和重新加载功能(这有助于您提到的性能方面)。注意:您还可以使用支持数据存储而不是文件的Properties。

通常在应用程序中同时存在这两种方法。在已部署的应用程序中永远不会改变的值(如应用程序名称)可以从属性文件中读取。可能需要在运行时由应用程序维护者更改的值(例如会话超时间隔)最好保留在可重新加载的.properties文件或数据库中。可以由应用程序用户更改的值应保存在应用程序的数据存储中,并且通常具有应用程序屏幕来编辑它们。
因此,我的建议是将配置设置分为类别(例如引导、部署、运行时和应用程序),并选择适当的机制来管理它们。这也取决于您的应用程序范围,即它是J2EE Web应用程序、桌面应用程序、命令行实用程序还是批处理过程?

4
你有什么样的配置文件需求?如果是属性文件,可能适合你:
public class Configuration {

    // the configuration file is stored in the root of the class path as a .properties file
    private static final String CONFIGURATION_FILE = "/configuration.properties";

    private static final Properties properties;

    // use static initializer to read the configuration file when the class is loaded
    static {
        properties = new Properties();
        try (InputStream inputStream = Configuration.class.getResourceAsStream(CONFIGURATION_FILE)) {
            properties.load(inputStream);
        } catch (IOException e) {
            throw new RuntimeException("Failed to read file " + CONFIGURATION_FILE, e);
        }
    }

    public static Map<String, String> getConfiguration() {
        // ugly workaround to get String as generics
        Map temp = properties;
        Map<String, String> map = new HashMap<String, String>(temp);
        // prevent the returned configuration from being modified 
        return Collections.unmodifiableMap(map);
    }


    public static String getConfigurationValue(String key) {
        return properties.getProperty(key);
    }

    // private constructor to prevent initialization
    private Configuration() {
    }

}

您可以立即从getConfiguration()方法中返回Properties对象,但是此时其他访问该对象的代码可能会修改其内容。使用Collections.unmodifiableMap()不能使配置信息变为不可变的(因为Properties实例是在创建后通过load()方法获取其值的),但是由于它被包装在一个不可修改的map中,因此其他类无法更改配置信息。

谢谢@matsev,这个方法非常有效。自2014年以来,有更好的方法吗? - Vijay Kumar

4
这是一个普遍存在的问题,每个人都会遇到。解决这个问题的方法是创建一个单例类,该类具有与配置文件中相同的实例变量和默认值。其次,该类应该有一个名为getInstance()的方法,该方法读取属性一次并在每次返回相同对象(如果它存在)。我们可以使用环境变量来获取路径或类似于System.getenv("Config_path");的东西来读取文件。读取属性(readProperties() 方法) 应从配置文件中读取每个项目,并将值设置为单例对象的实例变量。因此,现在一个对象包含所有配置参数的值,如果参数为空,则考虑默认值。

2

另一种方法是定义一个类并在该类中读取属性文件。这个类需要在应用程序级别,并且可以标记为单例。将该类标记为单例将避免创建多个实例。


0

我建议使用JAXB或类似的绑定框架来处理基于文本的文件。由于JAXB实现是JRE的一部分,因此很容易使用。就像Denis所说,我不建议使用配置键。

这里有一个简单的示例,可以使用XML和JAXB轻松配置应用程序,并且仍然非常强大。当您使用DI框架时,只需将类似的配置对象添加到DI上下文中即可。

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class ApplicationConfig {

    private static final JAXBContext CONTEXT;
    public static final ApplicationConfig INSTANCE;

    // configuration properties with defaults
    private int number = 0;
    private String text = "default";
    @XmlElementWrapper
    @XmlElement(name = "text")
    private List<String> texts = new ArrayList<>(Arrays.asList("default1", "default2"));

    ApplicationConfig() {
    }

    static {
        try {
            CONTEXT = JAXBContext.newInstance(ApplicationConfig.class);
        } catch (JAXBException ex) {
            throw new IllegalStateException("JAXB context for " + ApplicationConfig.class + " unavailable.", ex);
        }
        File applicationConfigFile = new File(System.getProperty("config", new File(System.getProperty("user.dir"), "config.xml").toString()));
        if (applicationConfigFile.exists()) {
            INSTANCE = loadConfig(applicationConfigFile);
        } else {
            INSTANCE = new ApplicationConfig();
        }
    }

    public int getNumber() {
        return number;
    }

    public String getText() {
        return text;
    }

    public List<String> getTexts() {
        return Collections.unmodifiableList(texts);
    }

    public static ApplicationConfig loadConfig(File file) {
        try {
            return (ApplicationConfig) CONTEXT.createUnmarshaller().unmarshal(file);
        } catch (JAXBException ex) {
            throw new IllegalArgumentException("Could not load configuration from " + file + ".", ex);
        }
    }

    // usage
    public static void main(String[] args) {
        System.out.println(ApplicationConfig.INSTANCE.getNumber());
        System.out.println(ApplicationConfig.INSTANCE.getText());
        System.out.println(ApplicationConfig.INSTANCE.getTexts());
    }
}

配置文件看起来像这样:

<?xml version="1.0" encoding="UTF-8"?>
<applicationConfig>
    <number>12</number>
    <text>Test</text>
    <texts>
        <text>Test 1</text>
        <text>Test 2</text>
    </texts>
</applicationConfig>

0
  protected java.util.Properties loadParams() throws IOException {
  // Loads a ResourceBundle and creates Properties from it
  Properties prop = new Properties();
  URL propertiesFileURL = this.getClass().getResource("/conf/config.properties");
  prop.load(new FileInputStream(new File(propertiesFileURL.getPath())));
  return prop;
  }


Properties prop = loadParams();
String prop1=(String) prop.get("x.y.z");

0
将配置键直接放入类中是不好的做法:配置键会分散在代码中。最佳实践是将应用程序代码和配置代码分离。通常使用依赖注入框架(如Spring)。它会加载配置文件并使用配置值构建对象。如果您的类需要某些配置值,则应为此值创建一个setter。Spring将在上下文初始化期间设置此值。

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