Few months ago I’ve started my journey with Play Framework and Akka. I loved their configuration syntax, the HOCON – Human-Optimized Config Object Notation.
If you don’t know what I’am talking writing about, you have to follow these links below to see what the word “awesome” means 😉
http://www.playframework.com/documentation/2.2.x/Configuration
https://github.com/typesafehub/config/blob/master/HOCON.md
I want to show you how easily HOCON can be adopted into your (JVM-based) projects.
Set up your project
This step will be indecently straightforward since everything you need to do is just ensure that appropriate JAR is present in the application’s class path. No matter what programming language are you using – Java, Scala, Groovy, yet another or maybe all of them.
If you are using Maven or SBT just add following dependency:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<dependency> | |
<groupId>com.typesafe</groupId> | |
<artifactId>config</artifactId> | |
<version>1.0.2</version> | |
</dependency> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"com.typesafe" % "config" % "1.0.2" |
Before We start..
Say hello to com.typesafe.config.Config interface which provides API for obtaining values (or even whole nested documents as a Config instance) for given path.
You can find more (up-to-date) informations in the Config’s JavaDoc.
You will be working almost only with this single interface, so I it’s recommended to read this JavaDoc. It is short and you will get comprehension about differences between Config vs ConfigObject, Keys vs Paths and immutability aspects.
It’s absolutely must-read if you want to know not only how to use library but also how it works and how do they did it.
Let’s create our first Config instance!
You can get Config instance using the com.typesafe.config.ConfigFactory, a dedicated factory.
The simplest way to start with Typesafe’s Config is to place application.conf containing JSON/HOCON configurations and load them using:
ConfigFactory.load()
You can find a lot of other methods in the ConfigFactory’s documentation.
Don’t omit description of the ConfigFactory.load() method – it provides useful informations about customizing config location on deployment environment (in a nutshell – using ConfigFactory.load() invocation you can set appropriate property and let Config to load contents from given URL or a file with specified path) or how to get a fresh configuration (instead of the cached one).
Examples source code
All of examples will be presented in Scala (because of conicise syntax).
If you aren’t familiar with Scala you can find these examples written in Java at my github repository -> https://github.com/mkubala/typesafe-config-examples.
I encourage you to cloning/forking my repo, modifying the code and having fun 😉
Basic usages
All of my examples uses the same HOCON file as a configuration source:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
my.organization { | |
project { | |
name = "DeathStar" | |
description = ${my.organization.project.name} "is a tool to take control over whole world. By world I mean couch, computer and fridge ;)" | |
} | |
team { | |
members = [ | |
"Aneta" | |
"Kamil" | |
"Lukasz" | |
"Marcin" | |
] | |
} | |
} | |
my.organization.team.avgAge = 26 |
First, Let’s try to read some values:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
val config = ConfigFactory.load() | |
config.getString("my.organization.project.name") // => DeathStar | |
config.getString("my.organization.project.description") // => DeathStar is a tool to take control over whole world. By world I mean couch, computer and fridge 😉 | |
config.getInt("my.organization.team.avgAge") // => 26 | |
config.getStringList("my.organization.team.members") // => [Aneta, Kamil, Lukasz, Marcin] |
Pretty simple, isn’t it?
As you may already noticed my cofiguration has tree-like structure.
You can easily extract only a particular bunch of branches (node with its children) to the new Config object:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
val config = ConfigFactory.load(); | |
val extractedConfig: Config = config.getConfig("my.organization.team"); | |
extractedConfig.getInt("avgAge") // => 26 | |
extractedConfig.getStringList("members") // => [Aneta, Kamil, Lukasz, Marcin] |
When it may be useful? Consider following use-case:
We want to allow further application’s administrator to define some set of predefined users.
We’ve defined UserObject which will be Config consumer:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class UserObject { | |
private final String name; | |
private final int age; | |
public UserObject(final Config params) { | |
name = params.getString("name"); | |
age = params.getInt("age"); | |
} | |
// getters, hashCode, equals, etc. | |
} |
We also provide some sample configuration:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
some { | |
namespace { | |
users = [ | |
{ | |
name = "firstUser" | |
age = 25 | |
} | |
{ | |
name = "secondUser" | |
age = 35 | |
} | |
] | |
} | |
} |
The rest is pretty simple:
- extract a list of nested user Configs (using .getConfigList(“path”))
- pass them with the UserObject’s constructor, transforming (mapping) list from step #1 into a collection of UserObjects
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import scala.collection.JavaConversions._ | |
val mainConfig = ConfigFactory.load("nestedConfExample") | |
mainConfig.getConfigList("some.namespace.users") map (new UserObject(_)) | |
// => ArrayBuffer(UserObject{name='firstUser', age=25}, UserObject{name='secondUser', age=35}) |
Fallbacks
You can also compose many Config instances into a fallback chain:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
val customs: Config = ConfigFactory.load("custom").withFallback(config) | |
customsval customs: Config = ConfigFactory.load("custom").withFallback(config).getString("my.organization.project.name") // => <Name overriden in custom.conf> | |
customs.getString("my.organization.team.avgAge") // => 26 |
It will be useful when you want to provide some default values which will be open for overriding and closed for modification.
Iterating over paths and values
Here is how I kill two birds with one stone – I get a rid of all unnecessary values outside a specific path (“my.organization”), iterate throught each of Config’s entries and extract the keys.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
config.withOnlyPath("my.organization").entrySet() map (_.getKey) | |
// => Set(my.organization.team.members, my.organization.project.name, | |
// my.organization.team.avgAge, my.organization.project.description) |
Method .withOnlyPath(..) used at the above example will clone the config, retaining only the given path (and its children) – all sibling paths will be removed.
Use Config.atPath(..) If you want to preserve siblings.
There’s also similar method – Config.atPath(..) which places the config inside another Config at the given path, preserving siblings:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
config.atPath("my.organization").entrySet() map (_.getKey) | |
// => Set(my.organization.java.library.path, my.organization.java.runtime.name, | |
// my.organization.sun.cpu.isalist, my.organization.awt.toolkit, my.organization.java.specification.version, | |
// my.organization.my.organization.project.description, my.organization.java.vm.specification.name, | |
// my.organization.http.nonProxyHosts, my.organization.java.version, my.organization.sun.boot.class.path, | |
/// and so on.. |
Values overriding
The section’s title doesn’t need explanation, so let’s look at the examples:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
config.getString("my.organization.project.name") // => DeathStar | |
// Let's override project's name with String "RebrandedProject" | |
val updatedConfig = config.withValue("my.organization.project.name", ConfigValueFactory.fromAnyRef("RebrandedProject") | |
updatedConfig.getString("my.organization.project.name") // => RebrandedProject | |
updatedConfig.getString("my.organization.project.description") // => DeathStar is a tool to take control over whole world. By world I mean couch, computer and fridge 😉 | |
// Because of immutability the original Config stay untouched | |
config.getString("my.organization.project.name") // => DeathStar |
Hey, what happened at line #5 – substitution expresions weren’t re-evaluated after replacing value! To be honest – I have no idea how to make it working.. Yet 😉
It’s worth to mention that you can override values using env variables.
Try to run examples with -Dmy.organization.project.description=”overriden by env” and see what happens 😉
You can find more examples at the Config’s repository, right here.
HOCON generating
Now it’s time for the missing part of the official Config’s examples – document generation and rendering.
Creating Configs
We have seen before how to load/create new Config objects using ConfigFactory.load, Config.atPath, Config.withOnlyPath, Config.getConfig and Config.getConfigList.
But how to create a Config from scratch?
You can use ConfigFactory.empty() factory method which returns an empty Config.
When you’ll be going to fill them using Config.withValue(..), you have to keep in mind that Config is immutable! This means that each invocation of withValue(..) produces a new Config instead of modifying object on which you invoke it.
I’m a bit hungry so the next example will be course-related. It represents pseudo-recipe for one of my favourite dish:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import scala.collection.JavaConversions._ | |
ConfigFactory.empty() | |
.withValue("dish.name", ConfigValueFactory.fromAnyRef("Zapiekanka")) | |
.withValue("dish.estimatedCost", ConfigValueFactory.fromAnyRef(10)) | |
.withValue("dish.ingredients", ConfigValueFactory.fromIterable( | |
List( | |
"potato", "bacon", "onion", "salt", "pepper" | |
))) |
Rendering Config as a HOCON
Ok, let’s try to produce a valid HOCON document (as a String) representing previously created recipe:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// See ConfigRenderOptions' JavaDoc for details. | |
val renderOpts = ConfigRenderOptions.defaults().setOriginComments(false).setComments(false).setJson(false); | |
val dishConfig = … // as in creating Conf example | |
dishConfig.root().render(renderOpts) | |
// => dish { | |
// ingredients=[ | |
// potato, | |
// bacon, | |
// onion, | |
// salt, | |
// pepper | |
// ] | |
// name=SomeCompany | |
// estimatedCost=10 | |
// } | |
Note: If you omit the .setJson(false) invocation, you will get a JSON document as an output, instead of HOCON.
Conclusion
Typesafe Config is a simple and powerful tool. It supports not only the HOCON but also Java properties and JSON as an input.
There is still few useful features not covered in this blog post.
You can find them, well described, at the official documentation (for example config validation).
Please feel free to share your reflections about library or this note.
English isn’t my native language therefore I’ll be grateful for any feedback about my grammar and/or vocabulary.
Thanks! Was looking for proper config rendering.
PS: I prefer curd `Zapiekanka` to potato =)
Documents generation & rendering is in my opinion one of the biggest missing part in official samples.
I’ll try to change this via pull requests as soon as I find enough spare time to prepare some additional samples.
PS: Good curd is never a bad option 😉
There is something special about ConfigFactory.load(). It definitely merges all the reference.conf files from other projects as well as your reference.conf. Then I think it calls .resolve(). That means in values overriding #5 you’d have to put your override into the load() method. Like this:
ConfigFactory.load(“custom-overrides”). If you do fallback chaining which have references in them you seem to have to call .resolve() on them like this:
ConfigFactory.load().withFallback(configWithOverrides).resolve()
Am I right? I am just learning this stuff myself. I’m using onejar/shadowjar and it’s been a bit tricky getting other reference confs in with my own. I’ve been trying to roll my own reference-app.conf and when you add your own config with a property that overrides some 3rd party value (eg. like akka.loglevel=”INFO”) you run into this issue.
http://doc.akka.io/docs/akka/snapshot/general/configuration.html#When_using_JarJar__OneJar__Assembly_or_any_jar-bundler
here is another nice piece of info:
http://lian-notes.readthedocs.org/en/latest/typesafe-config-notes.html
@MarcinKubala
Hello! I just wanted to say, this blog post has been super useful 🙂 Just wanted to point out that I think there’s a small but significant error. I believe the nestedConfExample.conf is missing a “,” between the elements in your array.
Can you clarify?
Thanks,
Simon
Hi Simon,
Thanks for feedback 🙂
Commas between array items and object fields are not mandatory as long as there is at least one ASCII newline between them: https://github.com/typesafehub/config/blob/master/HOCON.md#commas
To be honest, I don’t remember if I miss this comma intentionaly or it was a simple human mistake.
Anyway – thanks for being watchful!
Rightio! Thanks for the prompt reply.