Properties and Configuration Files

Load key=value configuration with java.util.Properties, layer defaults and overrides, and turn strings into a validated, typed config at startup.

SyntaxProperties props = new Properties(defaults); props.load(reader); String value = props.getProperty("key", "fallback"); Set<String> keys = props.stringPropertyNames();

java.util.Properties reads and writes the .properties format: one key=value per line, # or ! for comments. It is the format of Spring Boot's application.properties, of many JDBC drivers and of countless older services, so it is worth knowing its rules exactly.

Loading

Properties props = new Properties();
try (Reader in = Files.newBufferedReader(Path.of("app.properties"))) {
    props.load(in);
}
String url = props.getProperty("db.url");            // null if missing
int port = Integer.parseInt(props.getProperty("server.port", "8080"));

Load from a Reader. load(InputStream) always decodes ISO-8859-1, so any non-Latin-1 text written in UTF-8 comes back garbled. Files.newBufferedReader uses UTF-8.

To load a file packaged in your JAR, use getClass().getResourceAsStream("/app.properties") wrapped in an InputStreamReader with UTF-8. It returns null when the resource is missing, so check before loading.

The format's surprises

  • The separator can be =, : or plain whitespace, and spaces around it are ignored.
  • A backslash is an escape character. C:\temp becomes C: followed by a tab and emp. Write C:\\temp or use forward slashes.
  • A line ending in a backslash continues on the next line, with that line's leading whitespace removed.
  • Every value is a String. "true " with a trailing space is not true to Boolean.parseBoolean, so strip values before parsing.

Typed configuration and failing fast

Do not scatter getProperty calls through the code. Read everything once at startup into a record, parse numbers, durations and booleans there, and validate ranges. A missing database URL should stop the service at boot with a clear message, not cause a NullPointerException on the first request an hour later. In Kubernetes, a pod that fails at startup never receives traffic and the bad rollout is caught straight away.

Layering

Real services combine sources: defaults packaged with the code, a file per environment, then environment variables and system properties that win over both. new Properties(defaults) gives a fallback chain for getProperty, and environment variables are conventionally the dotted key in upper case with underscores, such as DB_POOL_SIZE for db.pool.size. Spring Boot applies the same idea with more sources and relaxed name matching.

Secrets

Keep passwords and API keys out of properties files in the repository. Read them from environment variables or a secret store at runtime, and never log the full config object if it holds them.

Properties extends Hashtable<Object, Object>, so iteration order is not the file order. Use stringPropertyNames() and sort when you need a stable listing.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Load database, port and timeout settings once at startup into a validated record and refuse to start when one is missing.
  • Ship sensible defaults inside the JAR and let each environment override them with environment variables in its deployment manifest.
  • Read a supplier's properties-based configuration file that contains Windows paths and non-English text without corrupting either.

More examples

Defaults, file values and overrides

The defaults passed to the constructor are not copied into the new Properties; they are consulted only when a key is missing, which is why keySet sees one entry and stringPropertyNames sees three. Taking the environment as a parameter also makes the lookup easy to test.

Example · java
static String lookup(String key, Map<String, String> env, Properties file) {
    String fromSystem = System.getProperty(key);
    if (fromSystem != null) {
        return fromSystem + " (system property)";
    }
    String envName = key.toUpperCase(Locale.ROOT).replace('.', '_');
    String fromEnv = env.get(envName);
    if (fromEnv != null) {
        return fromEnv + " (environment " + envName + ")";
    }
    return file.getProperty(key) + " (properties)";
}

void main() {
    Properties defaults = new Properties();
    defaults.setProperty("server.port", "8080");
    defaults.setProperty("log.level", "INFO");
    defaults.setProperty("db.pool.size", "10");

    Properties file = new Properties(defaults);   // falls back to defaults
    file.setProperty("log.level", "DEBUG");

    // In a real service: System.getenv(). A fixed map keeps the demo repeatable.
    Map<String, String> env = Map.of("DB_POOL_SIZE", "20");
    System.setProperty("server.port", "9090");     // as if started with -Dserver.port=9090

    for (String key : List.of("server.port", "log.level", "db.pool.size")) {
        IO.println(key + " = " + lookup(key, env, file));
    }
    IO.println("keys in file alone: " + file.keySet().size()
            + ", visible through getProperty: " + file.stringPropertyNames().size());
}

// Output:
// server.port = 9090 (system property)
// log.level = DEBUG (properties)
// db.pool.size = 20 (environment DB_POOL_SIZE)
// keys in file alone: 1, visible through getProperty: 3

Why load(Reader) matters for UTF-8 files

The accented letter is two bytes in UTF-8, and the stream version turns each byte into its own character. The bug only shows up with non-English text, which is why it often reaches production through a shop name or an email template.

Example · java
void main() throws IOException {
    byte[] bytes = "shop.name=caf\u00e9\n".getBytes(StandardCharsets.UTF_8);

    Properties fromStream = new Properties();
    fromStream.load(new ByteArrayInputStream(bytes));   // decodes as ISO-8859-1

    Properties fromReader = new Properties();
    fromReader.load(new InputStreamReader(new ByteArrayInputStream(bytes), StandardCharsets.UTF_8));

    String expected = "caf\u00e9";
    String a = fromStream.getProperty("shop.name");
    String b = fromReader.getProperty("shop.name");
    IO.println("InputStream: length " + a.length() + ", correct " + a.equals(expected));
    IO.println("Reader:      length " + b.length() + ", correct " + b.equals(expected));
}

// Output:
// InputStream: length 5, correct false
// Reader:      length 4, correct true

Backslashes, continuations and separators

The properties text holds single backslashes in upload.dir, which the loader treats as escapes and drops; doubling them keeps the Windows path. The continuation line loses its leading spaces, so a long comma-separated list can be wrapped without adding spaces to the value.

Example · java
void main() throws IOException {
    String file = """
        upload.dir=C:\\apps\\shop
        upload.dir.fixed=C:\\\\apps\\\\shop
        allowed.origins=https://shop.example,\\
            https://admin.example
        greeting   Hello there
        """;
    Properties props = new Properties();
    props.load(new StringReader(file));
    // Properties has no defined order; sort the names for stable output.
    for (String key : new TreeSet<>(props.stringPropertyNames())) {
        IO.println(key + " -> [" + props.getProperty(key) + "]");
    }
}

// Output:
// allowed.origins -> [https://shop.example,https://admin.example]
// greeting -> [Hello there]
// upload.dir -> [C:appsshop]
// upload.dir.fixed -> [C:\apps\shop]

Discussion

  • Be the first to comment on this lesson.