You don't need a framework for configuration. There are plenty of configuration libraries that the frameworks use directly.

Typesafe Config

A common use case for configs is to have defaults (application.conf) and overrides (application.{env}.conf) per environment. Typesafe Config is great for this use case. Here is a mini wrapper for building Typesafe Config objects.

public class Configs {
    private static final Logger log = LoggerFactory.getLogger(Configs.class);

    private Configs() { }

    /*
     * I am letting the typesafe configs bleed out on purpose here.
     * We could abstract out and delegate but its not worth it.
     * I am gambling on the fact that I will not switch out the config library.
     */
    private static final Config system = ConfigFactory.systemProperties();
    private static final Config properties = new Builder().withSecureConf().envAwareApp().build();

    public static Config system() {
        return system;
    }

    public static Config properties() {
        return properties;
    }

    // This should return the current executing user path
    public static String getExecutionDirectory() {
        return system.getString("user.dir");
    }

    public static Map<String, Object> asMap(Config config) {
        return Seq.seq(config.entrySet())
                  .toMap(e -> e.getKey(), e -> e.getValue().unwrapped());
    }

    public static <T> T getOrDefault(Config config, String path, BiFunction<Config, String, T> extractor, T defaultValue) {
        if (config.hasPath(path)) {
            return extractor.apply(config, path);
        }
        return defaultValue;
    }

    public static <T> T getOrDefault(Config config, String path, BiFunction<Config, String, T> extractor, Supplier<T> defaultSupplier) {
        if (config.hasPath(path)) {
            return extractor.apply(config, path);
        }
        return defaultSupplier.get();
    }

    public static class Builder {
        private Config conf;

        public Builder() {
            log.info("Loading configs first row is highest priority, second row is fallback and so on");
        }

        public Builder withResource(String resource) {
            conf = returnOrFallback(ConfigFactory.parseResources(resource));
            log.info("Loaded config file from resource ({})", resource);
            return this;
        }

        public Builder withOptionalFile(String path) {
            File secureConfFile = new File(path);
            if (secureConfFile.exists()) {
                log.info("Loaded config file from path ({})", path);
                conf = returnOrFallback(ConfigFactory.parseFile(secureConfFile));
            } else {
                log.info("Attempted to load file from path ({}) but it was not found", path);
            }
            return this;
        }

        public Builder envAwareApp() {
            String env = system.hasPath("env") ? system.getString("env") : "local";
            String envFile = "application." + env + ".conf";
            return withResource(envFile).withResource("application.conf");
        }

        public Builder withSecureConf() {
            return withOptionalFile(getExecutionDirectory() + "/secure.conf");
        }

        public Config build() {
            // Resolve substitutions.
            conf = conf.resolve();
            if (log.isDebugEnabled()) {
                log.debug("Logging properties. Make sure sensitive data such as passwords or secrets are not logged!");
                log.debug(conf.root().render(ConfigRenderOptions.concise().setFormatted(true)));
            }
            return conf;
        }

        private Config returnOrFallback(Config config) {
            if (this.conf == null) {
                return config;
            }
            return this.conf.withFallback(config);
        }
    }

    public static void main(String[] args) {
        Configs.properties();
    }
}

From this code you have two global configurations set up.

  • System - This has tons of useful JVM properties as well as any -D params you pass to the JVM.
  • Properties - All of your application.{env}.conf and application.conf properties merged.
    • application.conf - All general configs and defaults.
    • application.local.conf - Overrides for local env. .gitignore this file so each developer can have their own version.
    • application.prod.conf - Prod config, to use it just pass -Denv=prod to the JVM.