Strong statement, but it's true. We've all come across different ways of managing configuration data. The Java Preferences API is one of those little things in the JDK that are often overlooked but are very useful. It handles a lot of the "heavy lifting" of managing external data and reduces your code's plumbing code.
I've been actively developing Java since 2002 and I still ask "Where do I put configuration data?". Usually the answer is "Wherever your users want it dummy!". There are so many choices, though. Which one do you want?
A plain old Java Properties file has been my go-to choice for configuration for years (and it should be your's too). They're so easy to access and understand. But where do you put the file and how does your code read it? Do you put it on the classpath? Do you put it in an external directory? Do you change the code so that the application just "knows" where to look? Do you add support for fancy Url-style names for your file?
These questions create a point of frustration for a lot of developers and customers. It'd be better if Java at least gave us a default that could be built off of like they do for the JDK Logging configuration where a configuration file can be stored and then overridden with a system property.
Another question I've often asked is how often does a user actually change their configuration? My experience says the answer is very rarely, if ever. So while a configuration file is easily accessible and changable, that ease of access rendered moot if it's not going to be changed often (think YAGNI).
Sometimes you think need a different format from Properties so you try something else like XML, JSON, YAML, or whatever else (like the God-awful HCL). This is all well and good and there good "reasons" (rationalizations) for choosing a different file format. What you're doing though is adding to your user's cognitive load. A special configuation file format doesn't add anything to your application whatsoever. And if your customer won't be changing their configuration much after it's working, what's the point (see above)?
But let's say that you have decided to use a different file format anyway. Now you need code to process it. You could write it. That'd be fine. Or would it? Code, debug, test, repeat till defects are gone. That's a waste of time that adds nothing to the functionality of your application.
Ok, you say, I'll follow Josh Bloch's advice in Effective Java and just use a stable library that's widely used because the bugs have been flushed out by everyone. Using a library is great advice. Except now you've added a dependency to your application. If you depenedency graph is already complex, this is going to make it worse. There are lots of libraries to choose from for parsing configuration. Oh, and don't forget to write your tests for your configuration. When it comes to configuration your user couldn't care less if the application doesn't work right.
Don't. Now you've added another integration point/dependency to your application.
All of this tongue firmly planted in cheek discussion. Everyone has reasons for doing what they do. We all know that configuration should enhance your application or service, not be front and center. And there's an easy way to do that for Java-based applications. And that way is to use the Preferences API that's already part of the JDK (and has been since version 1.4) which is a solution to these common problems.
Java Preferences are data that is stored in a platform-specific way and accessed in a platform-agnostic way that is used to store configurations that seldom change. Confused? Let's break it down.
Preference data is stored in a way that's best/common for the platform the JVM is currently running on. For example, your JVM knows it's running on Windows so the concrete implementation of the PreferencesFactory will use the Windows Registry to store its data. Other platforms will use what's appropriate for them. This is all OK because the API in the JDK that's used to access the Preference data is the same no matter what platform you're on, and it let's Java make up its own mind release-to-release how best to store these data. The details have been (very appropriately) abstracted away from you to let you concentrate on reading/writing the data you need to make your application work.
So now that you don't have to worry about accessing your platform's backing store, how do I change it? Well, since the data is stored in a way that is platform-specific this may mean you're not able to easily access it without operating system-specific tools. Most of the time that's fine, right? I mean, if you're constantly tweaking your application then that's one thing but if you just care about getting the thing set up and going without worrying about changing things run-to-run, then this is the best fit. For example, to read or change Preferences data in Windows, you'd likely use Regedit and navigate to the key that you wanted to change. Rather than doing that though, this API is easy enough to use and understand that you can write your own tools to do the job for you using the API and letting your JRE worry about the editing details.
The best part about using and editing Preferences in this way is that there is a standard format for sharing Preferences data from platform-to-platform. This is totally editable outside the backing store and can be imported to another backing store with relative ease.
Preferences are trees. The tree has roots with many nodes. The roots are for scoping the child nodes (either to the current user or the entire system). The nodes are names under the root. Each node can have another node or a map as children. Finally, a map has child entries that are the key-value pairs of data.
The convention for nodes is to name them similar to unix-style paths, naming them with the fully-qualified class name (including the package). For example under the user root, there may be a node named
com/github/argherna/LocalDirectoryServer where each piece of the "path name" is a node under a node. At the lowest level node, there may be a map containing keys and values. The Preferences API lets you read them as Strings, booleans, integers, byte arrays and other data types, and it requires you to set a default value if the key-value pair isn't in the map (Preferences don't like null values).
There are limitations to the components of Preferences. They're meant to work on multiple platforms and as such they have to match the lowest common denominator for those platforms. So there's a maximum length for the name of a node, the name of a key , and the length for a value as a string.
When you compare reading and writing properties files to reading and writing Preferences, you notice that things you used to worry about just go away.
The steps you should follow to access a Preference are:
- Get the root (either user or system).
- Get the node under your root.
- Read the key-value pair under the node.
If the key-value pair isn't under the node, it will be created in the backing store.
Preferences myConnectionPrefs = Preferences.userRoot().node("com/github/argherna/MyApplication/connections"); String url = myConnectionPrefs.get("url", "http://example.com"); // Now start doing stuff with the url.
Similar to above, the steps you should follow to save a Preference are:
- Get the root (either user or system).
- Get the node under your root.
- Save the key-value pair under the node.
This part of the Preferences API represents a small leaky abstraction to me. The methods used to save data start with the verb
put which is associated with the Map interface. Perhaps that was intentional. If it were, I should be able to get the Map data structure under the node and operate directly on it, then set it back to the node and let the API persist the settings.
String newUrl = "https://secure.example.com/"; Preferences myConnectionPrefs = Preferences.userRoot().node("com/github/argherna/MyApplication/connections"); // Save the newUrl. myConnectionPrefs.put("url", newUrl);
Preferences can be deleted, exported and imported. The export and import operations rely on the aforementioned file format linked in this post. You can check for children of nodes. You can check for values associated with a node. The API is fully capable of doing anything you can think of with a Preference.
The great part about the Prefrences API is that it's extensible. See the Preferences API. This means that if your IT shop centralizes its settings by putting them in a database or LDAP (as I groused about previously), you can write your own PreferencesFactory implementation that connnects to it and manages the read-write operations as well as the data structures. For nearly all purposes though, the default implementation is enough.
Fair question. There are the length limitations mentioned earlier. And that editing Preferences requires some tooling (either provided by your platform or yourself).
One of the biggest drawbacks is that the Preferences API doesn't scale up very well. You can only associate 1 name-value pair with each node (no Collections-based values are allowed). But depending on how you use them, this should never be a concern.
Knowing when to use them too is important. I use Preferences when:
- I can keep the amount of configuration small.
- I need to rely on the JRE to make the right decisions about where to store settings.
- The settings don't change often, if ever.
- The application can have an editor for Preferences built into it (including web applications) or I have tooling that lets me edit them.
Yes I do. See some of the utilities in my Dotfiles repository. There you'll see the Preferences tools I wrote to support my applications. Also, see my project pike. Preferences fit the way I develop some things. They aren't just for GUI applications. It's one of the little things the JDK has that does the job well.