JVM Languages

Solving the Configuration Problem for Java Apps

By Allen Holub, February 21, 2012

Storing data items for program configuration is a constant problem for Java server apps. Enums are a surprisingly robust solution.

Program configuration is a persistent and pervasive annoyance. Most realistic server-side programs need to use configuration information that can't be compiled into the code. Things that change from installation to installation — IP addresses and port numbers, URLs of remote services, database user names and passwords — almost always need to be defined outside of the program proper. An irritating amount of code is often required to fetch these values, given the parsing problems, file I/O issues, and simple human error that can arise anytime you don't have a compiler to find bugs for you. A simple misspelling, for example, becomes a nasty runtime error, with all sorts of code required to guard yourself against that eventuality. More to the point, you don't want to clutter up your code with 20 lines of error handling every time you need to fetch a configuration option.

Server-side programs typically use either environment variables or configuration files to hold configuration information, so the problems all center around error handling. It's true that you can put some configuration information in places like web.xml (in the case of a servlet), but in practice, that's not particularly useful because you'll want to modify either the WAR file or its expansion every time you deploy to a new environment. Because they're completely distinct from both the WAR file and the servlet container, the environment-variable and external-file approaches are much easier to use in practice.

The Problem

In general, here are the problems to solve:

Configuration information might be in more than one place (both an environment variable and a -D VM-command-line argument could be used to define one).

Configuration files might be hard to find. Putting them at a fixed location in the file system is risky and may not be possible.

Variable names can be misspelled, thus hard to find.

Required variables might be missing.

Variables might not have legitimate values or the value can be missing.

Values are not typed — they're strings — so there's no guarantee that they are usable.

The value strings could be malformed. For example, a value for a number might have illegal alphabetic characters in it.

Files and directories identified by configuration options might not exist (or might exist when they shouldn't).

Error detection is often performed way too late, after the server has been running for much too long.

To see where I'm going, let's look at an example. I'll specify a location for temporary files in one of two ways: defining a TMP environment variable (in a bash shell, I'd put an export "TMP=/tmp" or equivalent in my .profile file), or I'll invoke my server application with a -D VM-command-line option, like this:

java -DTMP=/tmp -jar myProject.jar

To create a file in that directory, I'd put the following line into my program.

File foo = Places.TMP.file("foo.tmp");

That line returns a File representing foo.tmp, located in the place defined with the -DTMP=xxx VM switch; or if there's no -D switch, in the TMP environment; or if neither exist, in a default place (~/tmp). The directory is created if it doesn't already exist.

That's 100% of the required code. All of the environment-variable and -D parsing, associated error detection, directory creation, etc. is done for you in the background. More to the point, the variable foo is guaranteed to have a valid value in it. There's no need to check for null, catch any exceptions, or worry about error conditions. I'm doing all this using the magic of enums; I'll examine the code in depth shortly.

One more example, here's how to create a temporary file in the TMP directory:

Places.TMP.directory() returns a File object that's guaranteed to represent an existing directory, as defined using -DTMP=..., the TMP environment variable, or the default value (~/tmp). Note that the tilde (~) is replaced by the value returned from System.getProperties("user.home"). That's not the same thing as the HOME environment in all cases. (Some servlet containers modify the property, for example.)

Underlying Principles

The best way to understand why I did things the way I did is to look at the basic programming principles involved. Here's a list:

1. Ask for help, not information (Delegation)
In general, all the work required to do a given task should be contained within the methods of an object. You should never ask another object for information that you need to do a bit of work; rather, ask the object that has the information to do the work for you. I'll show you an example under the next principle in the list.

2. Make it simple
The most-common operation should be as simple as possible to execute. This principle is really a corollary of "ask for help." A configuration option is typically read only once, but it's used many times. Consequently, the code at the point of use should be very simple, even if that simplicity comes at the cost of considerably more work elsewhere.

If you find yourself surrounding a method call with identical code every time you call the method, then something is seriously wrong. Perhaps the method isn't doing enough work, perhaps it's not handling errors that it should handle, perhaps the problem is a more-serious architectural problem.

A classic example of a violation of this principle is the misuse of getters and setters, which also violates the "ask for help" rule. Consider a Money class with getValue() and getCurrency() methods on it. Every time you use an instance of Money, you have to do something like the following:

This code is not only too ugly for words, it (or code like it) will have to be duplicated everywhere you use your Money class, greatly inflating the code size. More to the point, it is error prone. What if you accidentally swap the two arguments to getExchangeRate() and reverse the source and destination currencies?

A far better solution is to ask the object that has the information to do the work. If the Money object does the work (adding together two values), then the currency-conversion code moves into the Money class's add() method, and you get:

Money item = new Money(...); // Could be any currency.
Money total = new Money(Currency.DOLLARS);
total.add( item ); // convert currencies, then add

The currency-conversion code still exists, of course, it's just moved, but the operation that's exercised most often (adding) is now much simpler.

When I talk about not using get/set methods, I'm often asked how to do that. The answer is that you need to think about what your objects do, not what they contain. If you implement the "doing" in your methods, you probably don't need to import or export anything. Just do the work where the information naturally resides.

3. Leverage the compiler
Whenever possible, you want errors to be handled by the compiler, not at runtime. You'd like the compiler to find spelling errors for you, for example. By using an enum (Places.TMP in the earlier example), it's no longer possible to misspell the name of the environment variable or command-line switch.

4. Things should just work
For example, if you access a configuration option, you should be guaranteed that the value is a good one without having to worry about things like parse errors or falling back to defaults every time you use the value. In general, method calls (or equivalent) that have to be surrounded by error-handling code should raise a red flag. It's always better if the method itself can deal with the error and do something reasonable if it finds one.

5. Front-load error detection
You should find all errors as early as possible, so you can write your code without having to worry about those errors. For example, if methods check for illegal-argument values (e.g., null) at the very top of the method and deal with the problem then, the code for the rest of the method is vastly simpler. You can safely use the argument without having to worry about its state or about NullPointerExceptions being thrown at awkward places. In the case of configuration options, all errors should be found very early in the boot process, and the server shouldn't be allowed to run at all if an error is present. In my Places class, all error detection is made for all places the first time any place is accessed. Thereafter, I am guaranteed that everything is valid, so I don't have to check for errors.

Configuration Errors

Now let's look at some code. First of all, I'll be detecting errors (like a missing value) very early in the runtime process; and if I find one, I want to terminate the program. My assumption is that the program simply can't work if I can't configure it.

It violates "Delegation" because I'm not delegating error handling to the Places.TMP object.

It violates "Make it simple" because I'd have to detect the null every time I call the method.

It violates the "Front-load" principle because this call might not happen into long after the server boots.

I'll go through these problems one at a time, but in all error situations, I'm throwing objects of type ConfigurationError (see Listing One). This is a java.lang.Error object, not a java.lang.Exception. Errors are for serious problems that will abruptly shut down the program. If you catch them at all, you'll catch them in main() (or in the run method of a thread). Consequently, there's no requirement that you catch them, and you don't need a throws statement in the declarations of the methods. The class implementation is straightforward since it doesn't need to add any functionality to the base class. It just provides constructors that chain to base-class equivalents.

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task.
However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

Video

This month's Dr. Dobb's Journal

This month,
Dr. Dobb's Journal is devoted to mobile programming. We introduce you to Apple's new Swift programming language, discuss the perils of being the third-most-popular mobile platform, revisit SQLite on Android
, and much more!