Lesson 04 / 19
Configuration Sources
A setting value comes from one of three sources, and the precedence order is the IoC container's own rule — it never appears anywhere in the source: a value given from outside overrides a file source, and a file source overrides an embedded default. A misspelled setting name never disturbs the container at all — the program silently falls to the embedded default and reads empty instead of full.
Published
In this lesson
- Measures the precedence order among three configuration sources by running it across four separate settings
- Counts which source each setting won from and shows that the winning source never appears in the source at all
- Proves by running it that a misspelled setting name falls to the embedded default with no error at all
Contents
The previous lesson showed that prepare() filled the status value by reading the startup
field, but it never asked where that field itself came from — it only assumed it was already
filled by the time of initialization. The line
@Container.Value(name = "store.startup", defaultValue = "empty") says two things: this field
may have a value from outside, and if it is not given, "empty" is used. But “a value given from
outside” does not come from a single place. This lesson splits that source into three and
measures the precedence order among them.
The third lesson had split a managed component’s construction into three steps: constructor, filling the value fields, init method — and the order of these three steps was the container’s own line order, not a number written in the source. This lesson takes the same pattern one level deeper: filling the value fields is not a single step in itself, it is querying three separate sources with their own order. The previous lesson’s “which step first” question turns here into “which source first,” and the answer is in the same place again: in the container’s own code, not in the source.
Three Sources, One Value
A setting value can come from one of three places: a value given from outside (supplied
directly by the party running the program, for example a command-line argument or a runtime
flag), a file source (a configuration set read while the container is built, carrying more
than one setting at once), and an embedded default (the annotation’s own defaultValue()
value, the last resort placed by the class’s author). All three can exist at the same time, and
the container must choose a single winner for every setting.
Choosing the winner is a chain: the external source is checked first, if it is not there the
file is checked, if it is not there either it falls to the embedded default. This chain’s order
is the container’s own rule; no setting declaration says “check the external source first” —
this behavior comes from the order of the fill method’s code lines:
if (externalSource.containsKey(...)) ... else if (fileSource.containsKey(...)) ... else ....
If this order were reversed (file checked first, then the external source), the same three
sources would produce a different winner — no setting definition in the source can change this
order, because the order lives inside the container’s own code, not the setting definitions.
This ordering has its own logic, but that logic, too, is nowhere in the source: the external
source gets the highest priority because the party running the program may want to give a
different value every single time — so the same class, unchanged, can work with two different
values across two separate runs. The file source is one step below it, because it belongs to
the whole of a setup, not specific to a single run. The embedded default is at the bottom,
because it is the last resort saying “if nothing is given, use this,” from the person who wrote
the class. All three steps rest on a reasonable justification, but the justification that orders
the three does not stand in the source even as a comment — it is embedded only in the fill
method’s if-else order.
The containsKey check itself also carries a detail: even if a source gave an empty string
as a value for a key, that source is still considered the winner, because the check asks not for
a null check but for the key’s existence. There is no such case in this measurement — none
of the four settings is defined with an empty string — but the mechanism itself makes this
distinction: “value is empty” and “value was never given” are two separate states, and fill
only moves to the next source in the second case.
- CC16. The precedence chain is a fixed three steps: external source, file, embedded default. This measurement does not add a fourth source (an environment variable, for example).
- CC17. The external source and the file are not read from a real file system or command
line; both are
Map<String, String>objects held in memory. - CC18. Which source a setting comes from is determined only by the
containsKeycheck; even if the key’s value is an empty string, that source is still considered a “winner.” - CC19. Four settings represent three separate scenarios: one that is in both the external source and the file, one that is only in the file (two settings), one that is in neither.
- CC20. The winning source count is kept in a map; the counter starts at zero and only counts the four settings this measurement covers.
- CC21. There is no duration, file path, or real command-line argument in this measurement; the only thing carried is a setting name and a string value.
// Configuration.java — precedence order among three configuration sources: external source, file source, embedded default
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;
public class Configuration {
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD)
@interface Value { String name(); String defaultValue(); }
static final Map<String, Integer> winnerCount = new LinkedHashMap<>();
static void won(String source) { winnerCount.merge(source, 1, Integer::sum); }
final Map<String, String> externalSource;
final Map<String, String> fileSource;
Configuration(Map<String, String> externalSource, Map<String, String> fileSource) {
this.externalSource = externalSource;
this.fileSource = fileSource;
}
void fill(Object instance) throws Exception {
for (Field f : instance.getClass().getDeclaredFields()) {
Value v = f.getAnnotation(Value.class);
if (v == null) continue;
f.setAccessible(true);
String value; String source;
if (externalSource.containsKey(v.name())) { value = externalSource.get(v.name()); source = "external-source"; }
else if (fileSource.containsKey(v.name())) { value = fileSource.get(v.name()); source = "file"; }
else { value = v.defaultValue(); source = "embedded-default"; }
f.set(instance, value);
won(source);
System.out.printf("%-24s %-6s winner: %s%n", v.name(), value, source);
}
}
public static void main(String[] args) throws Exception {
Map<String, String> external = new LinkedHashMap<>();
external.put("store.connection-pool", "50");
Map<String, String> file = new LinkedHashMap<>();
file.put("store.connection-pool", "10");
file.put("store.timeout", "30");
file.put("store.startup.status", "full");
Configuration c = new Configuration(external, file);
ConfiguredComponent b = new ConfiguredComponent();
System.out.println("-- which source each setting came from --");
c.fill(b);
System.out.println();
System.out.println("-- winning source count --");
winnerCount.forEach((k, v) -> System.out.printf("%-20s %d%n", k, v));
System.out.println();
System.out.println("values read:");
System.out.println("pool size : " + b.poolSize);
System.out.println("timeout : " + b.timeout);
System.out.println("startup status : " + b.startupStatus);
System.out.println("cache size : " + b.cacheSize);
}
}
class ConfiguredComponent {
@Configuration.Value(name = "store.connection-pool", defaultValue = "5") String poolSize;
@Configuration.Value(name = "store.timeout", defaultValue = "10") String timeout;
@Configuration.Value(name = "store.startup.status", defaultValue = "empty") String startupStatus;
@Configuration.Value(name = "store.cache.size", defaultValue = "100") String cacheSize;
}
-- which source each setting came from -- store.connection-pool 50 winner: external-source store.timeout 30 winner: file store.startup.status full winner: file store.cache.size 100 winner: embedded-default -- winning source count -- external-source 1 file 2 embedded-default 1 values read: pool size : 50 timeout : 30 startup status : full cache size : 100
Four Settings, Four Different Winners
store.connection-pool is defined both in the external source (50) and in the file (10), and
the winner is the external source — the chain’s first link. This shows that how many places a
setting is defined in does not matter: the only thing that determines the winner is which source
is queried earlier in the chain. store.timeout and store.startup.status exist only in the
file, not in the external source; both win from the file because the first check
(externalSource.containsKey) returns false and moves to the second check.
store.cache.size is in neither source — it falls to the chain’s third link, the embedded
default.
winnerCount is kept on a LinkedHashMap, not a HashMap — even though correct counting is the
only thing this lesson needs, LinkedHashMap preserves insertion order, so the output prints in
the same order on every run too. Even this small choice is a reminder: the container’s own
internal data structure choices affect the readability of the output, even when they do not
affect the measured behavior — and this choice, too, appears in none of the setting declarations
in the source.
The count line summarizes this: one of four settings came from the external source, two from the file, one from the embedded default. This number is the measure of the precedence order’s effect — it shows how many settings depend on which source “winning.” The Infrastructure as Code course measured whether a value given from outside is recorded or not; this lesson takes the same “value given from outside” idea but asks a different question — not recording, but which component actually sees that value.
The f.setAccessible(true) call appears here again too, because ConfiguredComponent‘s fields
are visible at package level and the fill method reaches these fields through a general
Object — the same access check bypass seen in the second lesson’s Service constructor, this
time for fields. The difference: here only a value is written, no method is called.
The setting name itself is also not a type, only a string — String name(). This is a choice
from the same class as the third lesson’s scope name observation: the compiler cannot check
whether a setting name really corresponds to an existing source key, because all it has is a
character sequence. "store.connection-pool" and "store.connection_pool" (underscore instead
of hyphen) are two strings equally valid as far as the compiler is concerned, but two completely
separate keys as far as containsKey is concerned. This lesson uses correctly written names in
all four settings; the next section deliberately breaks this assumption.
When the Order Changes, the Winner Changes
The previous section’s claim — that the order is embedded in the container’s own code — remains an unproven assertion if left in plain prose. Running the same two sources, the same two values, with only the query order reversed, confirms this claim:
// ReversedOrder.java — same two sources, only the query order changed, and the winner changes too
import java.util.*;
public class ReversedOrder {
static String filePriority(Map<String, String> externalSource, Map<String, String> file, String name, String defaultValue) {
if (file.containsKey(name)) return file.get(name);
if (externalSource.containsKey(name)) return externalSource.get(name);
return defaultValue;
}
public static void main(String[] args) {
Map<String, String> external = Map.of("store.connection-pool", "50");
Map<String, String> file = Map.of("store.connection-pool", "10");
System.out.println("winner when external source queried first : " + external.get("store.connection-pool"));
System.out.println("winner when file queried first : "
+ filePriority(external, file, "store.connection-pool", "5"));
}
}
winner when external source queried first : 50 winner when file queried first : 10
The content of the external and file maps never changed between these two lines — the key
store.connection-pool stands with the same values in both. The only thing that changed is which
map the filePriority method looks at first. Configuration.fill queries the external
source first, and 50 wins; the filePriority method written here queries the file first, and
10 wins. The data in the sources is the same, the setting name is the same, the component
itself does not even exist in this measurement — only the order of two if lines inside a method
reversed, and the winner changed. This is direct proof that the precedence order depends not on
any setting declaration in the source, but only on the order of the code that writes the
container.
Limiting Measurement: A Spelling Difference
The key store.startup.status sits in the file with the value "full". A component wants to
read this value, but writes the key wrong in its annotation — a hyphen where a dot should be:
// MisspelledSettingName.java — the value sitting in the file never wins at all, because of a typo in the setting name
public class MisspelledSettingName {
public static void main(String[] args) throws Exception {
java.util.Map<String, String> external = java.util.Map.of();
java.util.Map<String, String> file = java.util.Map.of("store.startup.status", "full");
Configuration c = new Configuration(external, file);
Misspelled b = new Misspelled();
c.fill(b);
System.out.println("value sitting in the file : full");
System.out.println("value the component read : " + b.status);
}
}
class Misspelled {
@Configuration.Value(name = "store-startup.status", defaultValue = "empty") String status;
}
store-startup.status empty winner: embedded-default value sitting in the file : full value the component read : empty
Misspelled‘s name value is "store-startup.status" (a hyphen after “startup”), while the key
in the file is "store.startup.status" (a dot) — the two strings are completely different as far
as Java’s equals comparison is concerned, containsKey returns false for both, and fill
falls to the chain’s third link. There is no exception, no warning, no log line saying “key not
found” — the program silently reads "empty"; the "full" value sitting in the file is never
seen at all.
This is the exact opposite of the second lesson’s unbound interface error: there, the container threw an exception immediately and loudly; here, the container makes no sound at all. Just as the precedence order stands nowhere in the source, a source “losing” leaves no trace in the source either — the losing source is treated as if it never existed at all. The only way to notice this defect is to compare, by hand, the value sitting in the file against the value the component reads; the code itself never points out this difference.
It is worth looking at why this behavior does not change: the fill method carries no mechanism
that could notice a setting name “almost matching” in the file, because such a mechanism would
itself become a new source of defects — who decides which spelling difference is “acceptable”?
The container never asks this question at all, it only looks for an exact match. This is the
same design choice as the third lesson’s unrecognized scope name: instead of making validation
early and automatic, leaving validation out entirely and showing the result only through
behavior. In both cases the defect is not an exception, it is an output that looks different
from expected but valid — in WrongScope it was two separate objects, here it is "empty"
instead of "full".
These two observations — the winner changing when the order changes, no source winning at all
because of a spelling difference — are two faces of the same fact. In the first, all three
sources were defined with the correct name, and the order change determined which one showed up.
In the second, the order never changed at all, but because the setting name was misspelled, even
the chain’s first two links could never be queried at all — the containsKey question was asked
of the wrong key. Both defects come from the same root: which value a setting will carry is a
result that cannot be read directly from any line in the source, it can only be extracted by
knowing the container’s rule and what the sources contain at that moment.
Summary
- A setting value can come from three sources: a value given from outside, a file source, and the annotation’s embedded default; the chain that determines the winner is the container’s own code order.
- A setting being defined in more than one source does not matter — only the source queried earlier in the chain wins.
- One of four settings came from the external source, two from the file, one from the embedded default; this distribution is the direct measure of the precedence order’s effect.
- The precedence order itself stands nowhere in the source; knowing which source wins requires knowing the container’s code.
- A misspelled setting name throws no error at all — the container silently falls to the embedded default, and the real value sitting in the file is never seen.
Next Step
The fourth setting (store.cache.size) was not defined in any source and fell to the embedded
default — but this did not mean the container “did nothing,” it only meant it reached the
chain’s last link. In some cases, the container itself notices that a component was never
declared at all and brings in its own default — not a setting value, but the component
itself. The next lesson measures this different kind of default: what the container does when a
component is never written at all, and why this decision never shows up anywhere in the source
with a “conditional” word.
To keep your progress and take notes, Log in
My notes
Log in to take notes.