Three new article modules: configuration binding, profiles and config data, Spring AOP
configuration-properties/ @ConfigurationProperties vs @Value on Spring Boot 4.1.1. The relaxed-binding matrix is generated by binding each spelling rather than transcribed, and re-checked against real processes -- the in-process probe was wrong twice before it was right. Records the three findings that came out of it: @Value does get relaxed resolution inside Spring Boot (Boot attaches ConfigurationPropertySources), the configuration processor silently stops generating metadata on JDK 23+ when declared as a plain dependency, and @Valid is not what makes nested constraints run. profiles-and-config/ Precedence, profiles, spring.config.import and config trees. /precedence reports every source holding a property in rank order with file and line, which turns "my profile file had no effect" into a two-line answer. Also pins the counterintuitive one: an imported file outranks the file that imported it. spring-aop/ Designators, proxy types, and aspects that do not fire. One advice per supported designator so the reference table is generated from real matches; all fourteen unsupported designators fed to the parser. Two corrections to the reference documentation: unsupported designators throw UnsupportedPointcutPrimitiveException (extends RuntimeException, not IllegalArgumentException), and spring-boot-starter-aop was renamed to spring-boot-starter-aspectj in Boot 4. 19 contract tests across the three modules, 15 captured transcripts, all regenerated by scripts/run-all.sh. Verified on Spring Boot 4.1.1, Spring Framework 7.0.9, JDK 25.0.4.1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gip4srpzMwjgoba6uEfbr5
This commit is contained in:
74
profiles-and-config/README.md
Normal file
74
profiles-and-config/README.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Spring Boot profiles, config import and config trees
|
||||
|
||||
Companion project for [**Spring Boot Profiles Done Right**](https://ankurm.com/) on ankurm.com.
|
||||
|
||||
The question the project answers: you set a value in `application-prod.yaml`, deployed with
|
||||
`prod` active, and the old value is still in effect. Why?
|
||||
|
||||
## Versions
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Spring Boot | 4.1.1 |
|
||||
| Spring Framework | 7.0.9 |
|
||||
| JDK | Eclipse Temurin 25.0.4.1 (LTS) |
|
||||
|
||||
## Quickstart
|
||||
|
||||
```bash
|
||||
export JAVA_HOME=/path/to/jdk-25
|
||||
mvn -DskipTests package
|
||||
./scripts/run-all.sh # regenerate every transcript in docs/output/
|
||||
mvn test # 6 contract tests
|
||||
```
|
||||
|
||||
## Profiles
|
||||
|
||||
| Profile | What it demonstrates |
|
||||
|---|---|
|
||||
| `prod` | a profile group expanding to `prod`, `prod-db`, `prod-metrics` |
|
||||
| `import` | `spring.config.import`, and the imported file winning |
|
||||
| `multidoc` | one file, three documents, activated by condition |
|
||||
| `badactivation` | `spring.profiles.active` set from a profile-specific document — refused |
|
||||
| `staging` | the conditional document inside `application-multidoc.yaml` |
|
||||
|
||||
## Endpoints
|
||||
|
||||
| Endpoint | Purpose |
|
||||
|---|---|
|
||||
| `GET /precedence?name=` | every source holding a property, ranked, with file and line |
|
||||
| `GET /sources` | the live property-source stack in precedence order |
|
||||
|
||||
Diagnostics. Delete before shipping, or use Actuator's `/actuator/env`, which sanitises.
|
||||
|
||||
## Documentation
|
||||
|
||||
1. [The precedence list](docs/01-the-precedence-list.md)
|
||||
2. [Profiles: files, documents and groups](docs/02-profiles.md)
|
||||
3. [Seeing precedence instead of reasoning about it](docs/03-seeing-precedence.md)
|
||||
4. [Why your profile-specific file lost](docs/04-why-your-profile-file-lost.md)
|
||||
5. [`spring.config.import`](docs/05-config-import.md)
|
||||
6. [Config trees and Kubernetes ConfigMaps](docs/06-config-trees-and-configmaps.md)
|
||||
|
||||
## Captured output
|
||||
|
||||
| File | Produced by |
|
||||
|---|---|
|
||||
| [`00-versions.txt`](docs/output/00-versions.txt) | `scripts/demo-versions.sh` |
|
||||
| [`01-precedence.txt`](docs/output/01-precedence.txt) | `scripts/demo-precedence.sh` |
|
||||
| [`02-profile-file-loses.txt`](docs/output/02-profile-file-loses.txt) | `scripts/demo-profile-file-loses.sh` |
|
||||
| [`03-config-tree.txt`](docs/output/03-config-tree.txt) | `scripts/demo-config-tree.sh` |
|
||||
| [`04-import-and-multidoc.txt`](docs/output/04-import-and-multidoc.txt) | `scripts/demo-import-and-multidoc.sh` |
|
||||
|
||||
## The short answer
|
||||
|
||||
Config data — every file you write, profile-specific ones included — is item 3 in Spring Boot's
|
||||
documented precedence list. OS environment variables are item 5. Later items win. A
|
||||
profile-specific file beats other files and loses to the weakest environment variable on the
|
||||
host.
|
||||
|
||||
## The one that surprises people
|
||||
|
||||
`spring.config.import` does not behave like `#include`. The **imported** file is processed after
|
||||
the file that declared the import, so it **wins**. Import a shared baseline expecting to
|
||||
override it and every key it sets will quietly beat yours.
|
||||
56
profiles-and-config/docs/01-the-precedence-list.md
Normal file
56
profiles-and-config/docs/01-the-precedence-list.md
Normal file
@@ -0,0 +1,56 @@
|
||||
[Index](../README.md) · [Profiles →](02-profiles.md)
|
||||
|
||||
# 1. The precedence list
|
||||
|
||||
Spring Boot's documented order, lowest precedence first. Later entries win.
|
||||
|
||||
| # | Source |
|
||||
|---|---|
|
||||
| 1 | Default properties (`SpringApplication.setDefaultProperties`) |
|
||||
| 2 | `@PropertySource` on `@Configuration` classes |
|
||||
| 3 | **Config data** — `application.properties`, `application.yaml`, profile-specific files, `spring.config.import` |
|
||||
| 4 | `RandomValuePropertySource` (`random.*`) |
|
||||
| 5 | **OS environment variables** |
|
||||
| 6 | Java system properties (`-D`) |
|
||||
| 7 | JNDI attributes from `java:comp/env` |
|
||||
| 8 | `ServletContext` init parameters |
|
||||
| 9 | `ServletConfig` init parameters |
|
||||
| 10 | `SPRING_APPLICATION_JSON` |
|
||||
| 11 | Command-line arguments |
|
||||
| 12 | `properties` on `@SpringBootTest` |
|
||||
| 13 | `@DynamicPropertySource` |
|
||||
| 14 | `@TestPropertySource` |
|
||||
| 15 | Devtools global settings |
|
||||
|
||||
## The two rows that matter
|
||||
|
||||
**Item 3 covers every file you write.** `application.yaml`, `application-prod.yaml`, an
|
||||
imported config tree, a mounted ConfigMap — all of it is config data, all of it at rank 3.
|
||||
|
||||
**Item 5 is above it.** Every environment variable outranks every file.
|
||||
|
||||
Profile-specific files beat non-profile files, and later imports beat earlier ones, but those
|
||||
are orderings *within* item 3. Nothing inside item 3 can reach item 5.
|
||||
|
||||
That single fact explains the bug this project exists for, and
|
||||
[chapter 4](04-why-your-profile-file-lost.md) walks through it with a transcript.
|
||||
|
||||
## Seeing it for real
|
||||
|
||||
`/sources` prints the live stack, which is more useful than the table because it shows exactly
|
||||
which files were loaded:
|
||||
|
||||
```
|
||||
3. SimpleCommandLinePropertySource commandLineArgs
|
||||
6. PropertiesPropertySource systemProperties
|
||||
7. OriginAwareSystemEnvironmentPropertySource systemEnvironment
|
||||
9. OriginTrackedMapPropertySource application-prod-metrics.yaml
|
||||
10. OriginTrackedMapPropertySource application-prod-db.yaml
|
||||
11. OriginTrackedMapPropertySource application-prod.yaml
|
||||
12. OriginTrackedMapPropertySource application.yaml
|
||||
```
|
||||
|
||||
Note rank 2 in the real stack, which the table does not mention:
|
||||
`ConfigurationPropertySourcesPropertySource`, named `configurationProperties`. That is the
|
||||
source Spring Boot attaches to give `${...}` placeholders the binder's relaxed name matching —
|
||||
see the [binding project's chapter 2](../../configuration-properties/docs/02-relaxed-binding.md).
|
||||
70
profiles-and-config/docs/02-profiles.md
Normal file
70
profiles-and-config/docs/02-profiles.md
Normal file
@@ -0,0 +1,70 @@
|
||||
[← Precedence list](01-the-precedence-list.md) · [Index](../README.md) · [Seeing precedence →](03-seeing-precedence.md)
|
||||
|
||||
# 2. Profiles: files, documents and groups
|
||||
|
||||
## Profile-specific files
|
||||
|
||||
`application-<profile>.yaml`, loaded from the same locations as `application.yaml`, and always
|
||||
overriding it. With several profiles active, last one wins:
|
||||
`--spring.profiles.active=prod,live` means `application-live.yaml` beats
|
||||
`application-prod.yaml`.
|
||||
|
||||
## Multi-document files
|
||||
|
||||
The same effect without multiplying files. Documents are separated by `---` and activated by
|
||||
condition:
|
||||
|
||||
```yaml
|
||||
demo:
|
||||
greeting: from-multidoc-default-document
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: staging
|
||||
demo:
|
||||
greeting: from-multidoc-staging-document
|
||||
```
|
||||
|
||||
Later documents win over earlier ones, so an unconditional first document acts as the default
|
||||
and each conditional document overrides it. Measured in
|
||||
[`04-import-and-multidoc.txt`](output/04-import-and-multidoc.txt).
|
||||
|
||||
`spring.config.activate.on-cloud-platform` and `spring.config.activate.on-profile` can be
|
||||
combined; both must match.
|
||||
|
||||
## Profile groups
|
||||
|
||||
One profile that activates several:
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
profiles:
|
||||
group:
|
||||
prod: prod-db,prod-metrics
|
||||
```
|
||||
|
||||
`--spring.profiles.active=prod` reports all three as active, and all three
|
||||
`application-<name>.yaml` files are loaded. Groups are resolved before config data is
|
||||
processed, which is why declaring a group in `application.yaml` can still affect which files
|
||||
get loaded.
|
||||
|
||||
## The activation Spring Boot refuses
|
||||
|
||||
`spring.profiles.active` cannot be set from a document that is itself profile-specific:
|
||||
|
||||
```
|
||||
InvalidConfigDataPropertyException: Property 'spring.profiles.active' imported from location
|
||||
'class path resource [application-badactivation.yaml]' is invalid in a profile specific
|
||||
resource [origin: ... - 12:13]
|
||||
```
|
||||
|
||||
A profile that activates itself would change which files are loaded after the set of files had
|
||||
already been decided. Boot refuses rather than half-applying it. `spring.profiles.include` has
|
||||
the same restriction; `spring.config.activate.on-profile` is how you express the condition.
|
||||
|
||||
## `@Profile` is a different mechanism
|
||||
|
||||
`@Profile("prod")` on a bean is evaluated when the context is built, long after config data is
|
||||
resolved. It decides which *beans* exist, not which *properties* are set. The two use the same
|
||||
profile names and nothing else.
|
||||
59
profiles-and-config/docs/03-seeing-precedence.md
Normal file
59
profiles-and-config/docs/03-seeing-precedence.md
Normal file
@@ -0,0 +1,59 @@
|
||||
[← Profiles](02-profiles.md) · [Index](../README.md) · [Why your profile file lost →](04-why-your-profile-file-lost.md)
|
||||
|
||||
# 3. Seeing precedence instead of reasoning about it
|
||||
|
||||
Endpoint: [`PrecedenceEndpoint`](../src/main/java/com/ankurm/profiles/web/PrecedenceEndpoint.java).
|
||||
Transcript: [`01-precedence.txt`](output/01-precedence.txt).
|
||||
|
||||
Set `demo.greeting` from five places at once and ask which won:
|
||||
|
||||
```
|
||||
$ DEMO_GREETING=from-environment-variable \
|
||||
java -Ddemo.greeting=from-system-property \
|
||||
-jar profiles-and-config-1.0.0.jar --spring.profiles.active=prod \
|
||||
--demo.greeting=from-command-line-argument
|
||||
```
|
||||
|
||||
```
|
||||
"effectiveValue": "from-command-line-argument",
|
||||
"activeProfiles": ["prod", "prod-db", "prod-metrics"],
|
||||
"holders": [
|
||||
{ "rank": 1, "value": "from-command-line-argument", "source": "commandLineArgs" },
|
||||
{ "rank": 2, "value": "from-system-property", "source": "systemProperties" },
|
||||
{ "rank": 3, "value": "from-environment-variable", "source": "systemEnvironment" },
|
||||
{ "rank": 4, "value": "from-application-prod-yaml", "origin": "application-prod.yaml - 4:13" },
|
||||
{ "rank": 5, "value": "from-application-yaml", "origin": "application.yaml - 21:13" }
|
||||
],
|
||||
"shadowedCount": 4
|
||||
```
|
||||
|
||||
Five sources hold the property. Four of them lose. Each one that came from a file names its
|
||||
line.
|
||||
|
||||
## The whole implementation
|
||||
|
||||
```java
|
||||
for (ConfigurationPropertySource source : ConfigurationPropertySources.get(environment)) {
|
||||
ConfigurationProperty property =
|
||||
source.getConfigurationProperty(ConfigurationPropertyName.of(name));
|
||||
if (property != null) {
|
||||
// rank = position, property.getValue(), property.getOrigin()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`ConfigurationPropertySources.get(...)` returns the sources in precedence order. Iterate,
|
||||
collect every hit, and the first is the winner. That is the entire diagnostic.
|
||||
|
||||
## Why this beats reading the list
|
||||
|
||||
The documented order is correct but abstract. It does not tell you that a `DEMO_GREETING` left
|
||||
over from a shell three weeks ago is sitting at rank 3, and that is the actual question.
|
||||
|
||||
## Alternatives if you would rather not add an endpoint
|
||||
|
||||
- Actuator's `/actuator/env` gives the same information with sanitisation, and
|
||||
`/actuator/env/{name}` narrows to one property. Prefer it in anything real.
|
||||
- `logging.level.org.springframework.boot.context.config=TRACE` logs which config data
|
||||
resources were loaded and in what order.
|
||||
- `--debug` does *not* show this. It prints the auto-configuration report.
|
||||
73
profiles-and-config/docs/04-why-your-profile-file-lost.md
Normal file
73
profiles-and-config/docs/04-why-your-profile-file-lost.md
Normal file
@@ -0,0 +1,73 @@
|
||||
[← Seeing precedence](03-seeing-precedence.md) · [Index](../README.md) · [Config import →](05-config-import.md)
|
||||
|
||||
# 4. Why your profile-specific file lost
|
||||
|
||||
Transcript: [`02-profile-file-loses.txt`](output/02-profile-file-loses.txt).
|
||||
|
||||
The bug: you set a value in `application-prod.yaml`, deploy with `prod` active, and the old
|
||||
value is still in effect.
|
||||
|
||||
## Two runs, one difference
|
||||
|
||||
```
|
||||
--- 1. prod profile active, no environment variable ---
|
||||
effective value : jdbc:postgresql://prod-db:5432/orders
|
||||
1. jdbc:postgresql://prod-db:5432/orders <- application-prod.yaml
|
||||
2. jdbc:h2:mem:default <- application.yaml
|
||||
```
|
||||
|
||||
Working as intended. Now with one leftover variable in the environment:
|
||||
|
||||
```
|
||||
--- 2. identical, plus one leftover environment variable ---
|
||||
effective value : jdbc:postgresql://leftover:5432/orders
|
||||
1. jdbc:postgresql://leftover:5432/orders <- systemEnvironment
|
||||
2. jdbc:postgresql://prod-db:5432/orders <- application-prod.yaml
|
||||
3. jdbc:h2:mem:default <- application.yaml
|
||||
```
|
||||
|
||||
The profile file was still loaded. It still holds the right value. It is at rank 2.
|
||||
|
||||
## Why it feels wrong
|
||||
|
||||
Profile-specific files *do* override — the mental model is not wrong, it is incomplete. They
|
||||
override other config data. Config data as a whole sits at item 3 in the precedence list and
|
||||
environment variables at item 5, so the strongest file loses to the weakest variable.
|
||||
|
||||
## Where the leftover variables come from
|
||||
|
||||
Every one of these is real:
|
||||
|
||||
- A Kubernetes `Deployment` with an `env:` block that predates the ConfigMap and was never
|
||||
removed. `envFrom` a `ConfigMap` produces environment variables, not config data.
|
||||
- A `docker-compose.yml` `environment:` entry copied from a colleague.
|
||||
- Spring Cloud Kubernetes or a service mesh injecting `SPRING_DATASOURCE_URL`.
|
||||
- A CI runner exporting variables for a different service.
|
||||
- `SPRING_APPLICATION_JSON`, which is item 10 and beats almost everything.
|
||||
|
||||
## Diagnosing it in one step
|
||||
|
||||
If a property is not what the file says, look for a variable:
|
||||
|
||||
```bash
|
||||
kubectl exec deploy/my-app -- env | grep -i datasource
|
||||
```
|
||||
|
||||
or ask the running application, which reports every holder including the one you did not know
|
||||
about.
|
||||
|
||||
## Living with it
|
||||
|
||||
**Prefer environment variables in containers, files for defaults.** The precedence order is
|
||||
designed for exactly this: the image carries defaults, the deployment overrides them. Fighting
|
||||
it means fighting the design.
|
||||
|
||||
**Do not set the same key in both places.** If a value is per-environment, keep it out of the
|
||||
profile files entirely so there is only ever one source.
|
||||
|
||||
**Name environment variables specifically.** `DEMO_DATASOURCE_URL` collides with nothing;
|
||||
`SPRING_DATASOURCE_URL` collides with every Spring application on the host.
|
||||
|
||||
**Mount configuration as a config tree instead.** Still config data, still below environment
|
||||
variables, but at least it is one mechanism rather than two —
|
||||
[chapter 6](06-config-trees-and-configmaps.md).
|
||||
58
profiles-and-config/docs/05-config-import.md
Normal file
58
profiles-and-config/docs/05-config-import.md
Normal file
@@ -0,0 +1,58 @@
|
||||
[← Why your profile file lost](04-why-your-profile-file-lost.md) · [Index](../README.md) · [Config trees →](06-config-trees-and-configmaps.md)
|
||||
|
||||
# 5. `spring.config.import`
|
||||
|
||||
Transcript: [`04-import-and-multidoc.txt`](output/04-import-and-multidoc.txt).
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
config:
|
||||
import: "optional:classpath:/imported.yaml"
|
||||
```
|
||||
|
||||
## The imported file wins
|
||||
|
||||
This is the part that catches people, and it catches them in the direction opposite to the one
|
||||
they brace for:
|
||||
|
||||
```
|
||||
effective value : from-imported-yaml
|
||||
1. from-imported-yaml <- imported.yaml
|
||||
2. from-application-import-yaml <- application-import.yaml (declared the import)
|
||||
3. from-application-yaml <- application.yaml
|
||||
```
|
||||
|
||||
The importing file declared the import and then lost to it. An imported document is processed
|
||||
*after* the document that declared it, and later documents win.
|
||||
|
||||
`#include` semantics would give the opposite. So would treating the import as a set of
|
||||
defaults, which is what people usually intend when they import a shared baseline. If you import
|
||||
a company-wide `common.yaml` expecting your own file to override it, every key `common.yaml`
|
||||
sets will quietly beat yours.
|
||||
|
||||
To get defaults-style behaviour, put your overrides somewhere that outranks config data — an
|
||||
environment variable or a command-line argument — or import from a *later* document in your own
|
||||
file so the ordering is explicit.
|
||||
|
||||
## Prefixes
|
||||
|
||||
| Prefix | Meaning |
|
||||
|---|---|
|
||||
| `optional:` | do not fail if it is missing |
|
||||
| `file:` | a filesystem path |
|
||||
| `classpath:` | a classpath resource |
|
||||
| `configtree:` | a directory of value-per-file entries |
|
||||
|
||||
They compose: `optional:configtree:/etc/config/`.
|
||||
|
||||
Without `optional:`, a missing location is `ConfigDataLocationNotFoundException` at startup.
|
||||
That is usually what you want for a secret mount and never what you want for a developer
|
||||
machine.
|
||||
|
||||
## Where imports are legal
|
||||
|
||||
`spring.config.import` is only honoured in config data — `application.yaml` and friends. Setting
|
||||
it as an environment variable or a command-line argument works too, because those are processed
|
||||
before config data is loaded. Setting it anywhere else does nothing.
|
||||
|
||||
Imports are processed depth-first, and a cycle is detected and reported rather than looping.
|
||||
85
profiles-and-config/docs/06-config-trees-and-configmaps.md
Normal file
85
profiles-and-config/docs/06-config-trees-and-configmaps.md
Normal file
@@ -0,0 +1,85 @@
|
||||
[← Config import](05-config-import.md) · [Index](../README.md)
|
||||
|
||||
# 6. Config trees and Kubernetes ConfigMaps
|
||||
|
||||
Transcript: [`03-config-tree.txt`](output/03-config-tree.txt).
|
||||
|
||||
## What Kubernetes actually mounts
|
||||
|
||||
A ConfigMap mounted as a volume is not a properties file. Kubernetes writes **one file per
|
||||
key**, named after the key, containing only the value with no trailing newline:
|
||||
|
||||
```
|
||||
<configmap-mount>/demo.datasource-url
|
||||
<configmap-mount>/demo.greeting
|
||||
<configmap-mount>/demo.pool-size
|
||||
<configmap-mount>/demo/nested/value
|
||||
<secret-mount>/demo.api-key
|
||||
```
|
||||
|
||||
```
|
||||
$ cat <configmap-mount>/demo.greeting
|
||||
from-configmap-volume
|
||||
```
|
||||
|
||||
There is no syntax to parse. The filename is the key.
|
||||
|
||||
## Reading it
|
||||
|
||||
```
|
||||
--spring.config.import=configtree:/etc/config/,configtree:/etc/secrets/
|
||||
```
|
||||
|
||||
A trailing `/` is required — the location is a directory. Values arrive as properties:
|
||||
|
||||
```
|
||||
demo.pool-size = 25
|
||||
demo.nested.value = from-nested-directory
|
||||
demo.api-key = sk_live_not_a_real_key
|
||||
```
|
||||
|
||||
A directory under the mount becomes a nested property, so `demo/nested/value` is
|
||||
`demo.nested.value`. That is how a ConfigMap whose keys contain slashes arrives.
|
||||
|
||||
Secrets mount identically. The only difference is file permissions, which is why the same
|
||||
mechanism reads both and why nothing in your application needs to know which it got.
|
||||
|
||||
## Why this beats mounting a properties file
|
||||
|
||||
- **Per-key updates.** Changing one key rewrites one file. Kubernetes propagates it to the
|
||||
volume without a restart, and `spring.config.import` supports `configtree` reloading through
|
||||
Spring Cloud Kubernetes if you want to act on it.
|
||||
- **No parse step**, so no chance of one malformed line taking out the whole file.
|
||||
- **Secrets and config read the same way.**
|
||||
- **Values can contain anything.** No escaping, no quoting, no YAML surprises — a value of
|
||||
`yes` stays the string `yes`.
|
||||
|
||||
## Wildcards
|
||||
|
||||
```
|
||||
--spring.config.import=optional:configtree:/etc/config/*/
|
||||
```
|
||||
|
||||
Reads every immediate subdirectory, which is the shape you get when several ConfigMaps are
|
||||
mounted under one parent. Useful for "one ConfigMap per component" layouts.
|
||||
|
||||
## It is still config data
|
||||
|
||||
An imported config tree outranks `application.yaml` — and still loses to an environment
|
||||
variable:
|
||||
|
||||
```
|
||||
effective value : from-environment-variable
|
||||
1. from-environment-variable <- systemEnvironment
|
||||
2. from-configmap-volume <- ConfigTreePropertySource
|
||||
3. from-application-yaml <- application.yaml
|
||||
```
|
||||
|
||||
If you mount a ConfigMap *and* set `envFrom` on the same Deployment — which is a common way to
|
||||
migrate from one to the other — the environment variables win and the ConfigMap looks broken.
|
||||
|
||||
## There is no profile-specific config tree
|
||||
|
||||
No `<mount>-prod` convention exists. Per-environment configuration is a different ConfigMap
|
||||
chosen by the Deployment, not by `spring.profiles.active`. This is a feature: the environment
|
||||
is decided by what you deploy, not by a string inside the image.
|
||||
83
profiles-and-config/docs/output/01-precedence.txt
Normal file
83
profiles-and-config/docs/output/01-precedence.txt
Normal file
@@ -0,0 +1,83 @@
|
||||
== every source sets demo.greeting at once ==
|
||||
|
||||
$ DEMO_GREETING=from-environment-variable \
|
||||
java -Ddemo.greeting=from-system-property \
|
||||
-jar target/profiles-and-config-1.0.0.jar --spring.profiles.active=prod \
|
||||
--demo.greeting=from-command-line-argument
|
||||
|
||||
{
|
||||
"property": "demo.greeting",
|
||||
"effectiveValue": "from-command-line-argument",
|
||||
"activeProfiles": [
|
||||
"prod",
|
||||
"prod-db",
|
||||
"prod-metrics"
|
||||
],
|
||||
"holders": [
|
||||
{
|
||||
"rank": 1,
|
||||
"source": "SimpleCommandLinePropertySource {name='commandLineArgs'}",
|
||||
"value": "from-command-line-argument",
|
||||
"origin": "\"demo.greeting\" from property source \"commandLineArgs\""
|
||||
},
|
||||
{
|
||||
"rank": 2,
|
||||
"source": "PropertiesPropertySource {name='systemProperties'}",
|
||||
"value": "from-system-property",
|
||||
"origin": "\"demo.greeting\" from property source \"systemProperties\""
|
||||
},
|
||||
{
|
||||
"rank": 3,
|
||||
"source": "OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}",
|
||||
"value": "from-environment-variable",
|
||||
"origin": "System Environment Property \"DEMO_GREETING\""
|
||||
},
|
||||
{
|
||||
"rank": 4,
|
||||
"source": "OriginTrackedMapPropertySource {name='Config resource 'class path resource [application-prod.yaml]' via location 'optional:classpath:/''}",
|
||||
"value": "from-application-prod-yaml",
|
||||
"origin": "class path resource [application-prod.yaml] from profiles-and-config-1.0.0.jar - 4:13"
|
||||
},
|
||||
{
|
||||
"rank": 5,
|
||||
"source": "OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.yaml]' via location 'optional:classpath:/''}",
|
||||
"value": "from-application-yaml",
|
||||
"origin": "class path resource [application.yaml] from profiles-and-config-1.0.0.jar - 21:13"
|
||||
}
|
||||
],
|
||||
"shadowedCount": 4
|
||||
}
|
||||
|
||||
== and with the environment variable removed, nothing else changed ==
|
||||
./scripts/demo-precedence.sh: line 41: 5735 Killed DEMO_GREETING=from-environment-variable setsid nohup java -Ddemo.greeting=from-system-property -jar "$JAR" --spring.profiles.active=prod --demo.greeting=from-command-line-argument > /tmp/profiles-precedence.log 2>&1 < /dev/null
|
||||
{
|
||||
"property": "demo.greeting",
|
||||
"effectiveValue": "from-system-property",
|
||||
"activeProfiles": [
|
||||
"prod",
|
||||
"prod-db",
|
||||
"prod-metrics"
|
||||
],
|
||||
"holders": [
|
||||
{
|
||||
"rank": 1,
|
||||
"source": "PropertiesPropertySource {name='systemProperties'}",
|
||||
"value": "from-system-property",
|
||||
"origin": "\"demo.greeting\" from property source \"systemProperties\""
|
||||
},
|
||||
{
|
||||
"rank": 2,
|
||||
"source": "OriginTrackedMapPropertySource {name='Config resource 'class path resource [application-prod.yaml]' via location 'optional:classpath:/''}",
|
||||
"value": "from-application-prod-yaml",
|
||||
"origin": "class path resource [application-prod.yaml] from profiles-and-config-1.0.0.jar - 4:13"
|
||||
},
|
||||
{
|
||||
"rank": 3,
|
||||
"source": "OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.yaml]' via location 'optional:classpath:/''}",
|
||||
"value": "from-application-yaml",
|
||||
"origin": "class path resource [application.yaml] from profiles-and-config-1.0.0.jar - 21:13"
|
||||
}
|
||||
],
|
||||
"shadowedCount": 2
|
||||
}
|
||||
./scripts/demo-precedence.sh: line 41: 5796 Killed setsid nohup java -Ddemo.greeting=from-system-property -jar "$JAR" --spring.profiles.active=prod > /tmp/profiles-precedence2.log 2>&1 < /dev/null
|
||||
41
profiles-and-config/docs/output/02-profile-file-loses.txt
Normal file
41
profiles-and-config/docs/output/02-profile-file-loses.txt
Normal file
@@ -0,0 +1,41 @@
|
||||
== does application-prod.yaml win? ==
|
||||
|
||||
demo.datasource-url is set in application.yaml and again in application-prod.yaml.
|
||||
|
||||
--- 1. prod profile active, no environment variable ---
|
||||
$ java -jar target/profiles-and-config-1.0.0.jar --spring.profiles.active=prod
|
||||
active profiles : prod, prod-db, prod-metrics
|
||||
effective value : jdbc:postgresql://prod-db:5432/orders
|
||||
1. jdbc:postgresql://prod-db:5432/orders <- 'file application-prod.yaml' via location 'optional:classpath:/''}
|
||||
2. jdbc:h2:mem:default <- 'file application.yaml' via location 'optional:classpath:/''}
|
||||
holders that lost: 1
|
||||
|
||||
--- 2. identical, plus one leftover environment variable ---
|
||||
$ DEMO_DATASOURCE_URL=jdbc:postgresql://leftover:5432/orders \
|
||||
java -jar target/profiles-and-config-1.0.0.jar --spring.profiles.active=prod
|
||||
active profiles : prod, prod-db, prod-metrics
|
||||
effective value : jdbc:postgresql://leftover:5432/orders
|
||||
1. jdbc:postgresql://leftover:5432/orders <- OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
|
||||
2. jdbc:postgresql://prod-db:5432/orders <- 'file application-prod.yaml' via location 'optional:classpath:/''}
|
||||
3. jdbc:h2:mem:default <- 'file application.yaml' via location 'optional:classpath:/''}
|
||||
holders that lost: 2
|
||||
|
||||
The profile-specific file is still loaded and still holds its value -- it is listed,
|
||||
and it lost. Config data is item 3 in the documented precedence list; OS environment
|
||||
variables are item 5, and later items win.
|
||||
|
||||
== the full property-source stack, in order ==
|
||||
$ curl -s localhost:8080/sources
|
||||
1. MapPropertySource server.ports
|
||||
2. ConfigurationPropertySourcesPropertySource configurationProperties
|
||||
3. SimpleCommandLinePropertySource commandLineArgs
|
||||
4. StubPropertySource servletConfigInitParams
|
||||
5. ServletContextPropertySource servletContextInitParams
|
||||
6. PropertiesPropertySource systemProperties
|
||||
7. OriginAwareSystemEnvironmentPropertySource systemEnvironment
|
||||
8. RandomValuePropertySource random
|
||||
9. OriginTrackedMapPropertySource Config resource 'class path resource [application-prod-metrics.yaml]' via location 'optional:classpath:/'
|
||||
10. OriginTrackedMapPropertySource Config resource 'class path resource [application-prod-db.yaml]' via location 'optional:classpath:/'
|
||||
11. OriginTrackedMapPropertySource Config resource 'class path resource [application-prod.yaml]' via location 'optional:classpath:/'
|
||||
12. OriginTrackedMapPropertySource Config resource 'class path resource [application.yaml]' via location 'optional:classpath:/'
|
||||
13. ApplicationInfoPropertySource applicationInfo
|
||||
45
profiles-and-config/docs/output/03-config-tree.txt
Normal file
45
profiles-and-config/docs/output/03-config-tree.txt
Normal file
@@ -0,0 +1,45 @@
|
||||
== what Kubernetes actually mounts ==
|
||||
$ find /tmp/demo-configmap /tmp/demo-secret -type f | sort
|
||||
<configmap-mount>/demo.datasource-url
|
||||
<configmap-mount>/demo.greeting
|
||||
<configmap-mount>/demo.pool-size
|
||||
<configmap-mount>/demo/nested/value
|
||||
<secret-mount>/demo.api-key
|
||||
|
||||
$ cat <configmap-mount>/demo.greeting; echo
|
||||
from-configmap-volume
|
||||
|
||||
Each file holds a bare value with no trailing newline and no key. There is no
|
||||
properties syntax to parse -- the filename is the key.
|
||||
|
||||
== importing it ==
|
||||
$ java -jar target/profiles-and-config-1.0.0.jar \
|
||||
--spring.config.import=configtree:/tmp/demo-configmap/,configtree:/tmp/demo-secret/
|
||||
|
||||
active profiles : (none)
|
||||
effective value : from-configmap-volume
|
||||
1. from-configmap-volume <- ConfigTreePropertySource {name='Config tree '/tmp/demo-configmap''}
|
||||
2. from-application-yaml <- 'file application.yaml' via location 'optional:classpath:/''}
|
||||
holders that lost: 1
|
||||
|
||||
demo.pool-size = 25
|
||||
demo.nested.value = from-nested-directory
|
||||
demo.api-key = sk_live_not_a_real_key
|
||||
|
||||
A directory under the mount becomes a nested property: demo/nested/value is
|
||||
demo.nested.value. That is how a ConfigMap with slashes in its keys arrives.
|
||||
|
||||
== the part that surprises people ==
|
||||
An imported config tree outranks application.yaml, but it is still config data,
|
||||
so it still loses to an environment variable:
|
||||
|
||||
active profiles : (none)
|
||||
effective value : from-environment-variable
|
||||
1. from-environment-variable <- OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
|
||||
2. from-configmap-volume <- ConfigTreePropertySource {name='Config tree '/tmp/demo-configmap''}
|
||||
3. from-application-yaml <- 'file application.yaml' via location 'optional:classpath:/''}
|
||||
holders that lost: 2
|
||||
|
||||
There is also no such thing as a profile-specific config tree. There is no
|
||||
<mount>-prod directory convention; a per-environment ConfigMap is a different mount
|
||||
chosen by the deployment, not by spring.profiles.active.
|
||||
60
profiles-and-config/docs/output/04-import-and-multidoc.txt
Normal file
60
profiles-and-config/docs/output/04-import-and-multidoc.txt
Normal file
@@ -0,0 +1,60 @@
|
||||
== spring.config.import: which document wins? ==
|
||||
|
||||
application-import.yaml imports imported.yaml. Both set demo.greeting.
|
||||
$ java -jar target/profiles-and-config-1.0.0.jar --spring.profiles.active=import
|
||||
|
||||
active profiles : import
|
||||
effective value : from-imported-yaml
|
||||
1. from-imported-yaml <- 'file imported.yaml' via location 'optional:classpath:/imported.yaml''}
|
||||
2. from-application-import-yaml <- 'file application-import.yaml' via location 'optional:classpath:/''}
|
||||
3. from-application-yaml <- 'file application.yaml' via location 'optional:classpath:/''}
|
||||
holders that lost: 2
|
||||
|
||||
demo.imported-only = yes-this-file-was-read
|
||||
|
||||
The imported file WON. spring.config.import does not behave like #include, and it
|
||||
does not behave like a default either: the imported document is processed AFTER the
|
||||
document that declared the import, so it outranks the file that pulled it in.
|
||||
If you import a shared baseline expecting your own file to override it, every key
|
||||
the baseline sets will quietly beat yours.
|
||||
|
||||
== one file, several documents, activated by condition ==
|
||||
--- spring.profiles.active=<none> (with the multidoc profile) ---
|
||||
active profiles : multidoc
|
||||
effective value : from-multidoc-default-document
|
||||
1. from-multidoc-default-document <- 'file application-multidoc.yaml' via location 'optional:classpath:/' (document ...
|
||||
2. from-application-yaml <- 'file application.yaml' via location 'optional:classpath:/''}
|
||||
holders that lost: 1
|
||||
|
||||
--- spring.profiles.active=staging (with the multidoc profile) ---
|
||||
active profiles : multidoc, staging
|
||||
effective value : from-multidoc-staging-document
|
||||
1. from-multidoc-staging-document <- 'file application-multidoc.yaml' via location 'optional:classpath:/' (document ...
|
||||
2. from-multidoc-default-document <- 'file application-multidoc.yaml' via location 'optional:classpath:/' (document ...
|
||||
3. from-application-yaml <- 'file application.yaml' via location 'optional:classpath:/''}
|
||||
holders that lost: 2
|
||||
|
||||
--- spring.profiles.active=prod (with the multidoc profile) ---
|
||||
active profiles : multidoc, prod, prod-db, prod-metrics
|
||||
effective value : from-application-prod-yaml
|
||||
1. from-application-prod-yaml <- 'file application-prod.yaml' via location 'optional:classpath:/''}
|
||||
2. from-multidoc-prod-document <- 'file application-multidoc.yaml' via location 'optional:classpath:/' (document ...
|
||||
3. from-multidoc-default-document <- 'file application-multidoc.yaml' via location 'optional:classpath:/' (document ...
|
||||
4. from-application-yaml <- 'file application.yaml' via location 'optional:classpath:/''}
|
||||
holders that lost: 3
|
||||
|
||||
Later documents in the same file win over earlier ones, so the unconditional first
|
||||
document acts as the default and each conditional document overrides it.
|
||||
|
||||
== the activation Spring Boot refuses ==
|
||||
application-badactivation.yaml tries to set spring.profiles.active from a document
|
||||
that is itself conditional on a profile.
|
||||
$ java -jar target/profiles-and-config-1.0.0.jar --spring.profiles.active=badactivation,staging
|
||||
|
||||
org.springframework.boot.context.config.InvalidConfigDataPropertyException: Property
|
||||
'spring.profiles.active' imported from location 'class path resource
|
||||
[application-badactivation.yaml]' is invalid in a profile specific resource [origin: class path
|
||||
resource [application-badactivation.yaml] from profiles-and-config-1.0.0.jar - 12:13]
|
||||
at
|
||||
org.springframework.boot.context.config.InvalidConfigDataPropertyException.lambda$throwIfPropert
|
||||
yFound$1(InvalidConfigDataPropertyException.java:123)
|
||||
44
profiles-and-config/pom.xml
Normal file
44
profiles-and-config/pom.xml
Normal file
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>4.1.1</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.ankurm</groupId>
|
||||
<artifactId>profiles-and-config</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>profiles-and-config</name>
|
||||
<description>Spring Boot profiles, config import, config trees and ConfigMaps</description>
|
||||
|
||||
<properties>
|
||||
<java.version>25</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webmvc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
66
profiles-and-config/scripts/demo-config-tree.sh
Executable file
66
profiles-and-config/scripts/demo-config-tree.sh
Executable file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env bash
|
||||
# Config trees: what a Kubernetes ConfigMap or Secret actually looks like to Spring Boot.
|
||||
#
|
||||
# A ConfigMap mounted as a volume is not a properties file. Kubernetes writes one file per
|
||||
# key, named after the key, containing only the value. `configtree:` is the loader that reads
|
||||
# that shape. This script builds the same directory layout on disk, so the demonstration is
|
||||
# the real mechanism rather than a description of it.
|
||||
set -euo pipefail
|
||||
set +m
|
||||
cd "$(dirname "$0")/.."
|
||||
source scripts/env.sh
|
||||
|
||||
TREE="${TMPDIR:-/tmp}/demo-configmap"
|
||||
SECRET="${TMPDIR:-/tmp}/demo-secret"
|
||||
rm -rf "$TREE" "$SECRET"; mkdir -p "$TREE" "$SECRET"
|
||||
|
||||
# Exactly what `kubectl create configmap demo --from-literal=demo.greeting=...` produces
|
||||
# once mounted: one file per key, the filename IS the property name.
|
||||
printf 'from-configmap-volume' > "$TREE/demo.greeting"
|
||||
printf 'jdbc:postgresql://configmap-db:5432/o' > "$TREE/demo.datasource-url"
|
||||
printf '25' > "$TREE/demo.pool-size"
|
||||
# Nested keys use a directory per level, or a dotted filename. Both work.
|
||||
mkdir -p "$TREE/demo/nested"
|
||||
printf 'from-nested-directory' > "$TREE/demo/nested/value"
|
||||
# A Secret mount looks identical; only the permissions differ.
|
||||
printf 'sk_live_not_a_real_key' > "$SECRET/demo.api-key"
|
||||
|
||||
{
|
||||
echo "== what Kubernetes actually mounts =="
|
||||
echo "\$ find $TREE $SECRET -type f | sort"
|
||||
find "$TREE" "$SECRET" -type f | sort | sed "s|$TREE|<configmap-mount>|;s|$SECRET|<secret-mount>|"
|
||||
echo
|
||||
echo "\$ cat <configmap-mount>/demo.greeting; echo"
|
||||
cat "$TREE/demo.greeting"; echo
|
||||
echo
|
||||
echo "Each file holds a bare value with no trailing newline and no key. There is no"
|
||||
echo "properties syntax to parse -- the filename is the key."
|
||||
echo
|
||||
echo "== importing it =="
|
||||
echo "\$ java -jar $JAR \\"
|
||||
echo " --spring.config.import=configtree:$TREE/,configtree:$SECRET/"
|
||||
echo
|
||||
start_app "--spring.config.import=configtree:$TREE/,configtree:$SECRET/" > /dev/null
|
||||
report demo.greeting
|
||||
echo
|
||||
echo " demo.pool-size = $(curl -s "http://127.0.0.1:${APP_PORT}/precedence?name=demo.pool-size" | python3 -c 'import json,sys; print(json.load(sys.stdin)["effectiveValue"])')"
|
||||
echo " demo.nested.value = $(curl -s "http://127.0.0.1:${APP_PORT}/precedence?name=demo.nested.value" | python3 -c 'import json,sys; print(json.load(sys.stdin)["effectiveValue"])')"
|
||||
echo " demo.api-key = $(curl -s "http://127.0.0.1:${APP_PORT}/precedence?name=demo.api-key" | python3 -c 'import json,sys; print(json.load(sys.stdin)["effectiveValue"])')"
|
||||
echo
|
||||
echo "A directory under the mount becomes a nested property: demo/nested/value is"
|
||||
echo "demo.nested.value. That is how a ConfigMap with slashes in its keys arrives."
|
||||
echo
|
||||
echo "== the part that surprises people =="
|
||||
echo "An imported config tree outranks application.yaml, but it is still config data,"
|
||||
echo "so it still loses to an environment variable:"
|
||||
echo
|
||||
APP_ENV="DEMO_GREETING=from-environment-variable" \
|
||||
start_app "--spring.config.import=configtree:$TREE/" > /dev/null
|
||||
report demo.greeting
|
||||
echo
|
||||
echo "There is also no such thing as a profile-specific config tree. There is no"
|
||||
echo "<mount>-prod directory convention; a per-environment ConfigMap is a different mount"
|
||||
echo "chosen by the deployment, not by spring.profiles.active."
|
||||
stop_app
|
||||
} > docs/output/03-config-tree.txt 2>&1
|
||||
cat docs/output/03-config-tree.txt
|
||||
56
profiles-and-config/scripts/demo-import-and-multidoc.sh
Executable file
56
profiles-and-config/scripts/demo-import-and-multidoc.sh
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
# spring.config.import ordering, multi-document activation, and the activation Boot refuses.
|
||||
set -euo pipefail
|
||||
set +m
|
||||
cd "$(dirname "$0")/.."
|
||||
source scripts/env.sh
|
||||
|
||||
{
|
||||
echo "== spring.config.import: which document wins? =="
|
||||
echo
|
||||
echo "application-import.yaml imports imported.yaml. Both set demo.greeting."
|
||||
echo "\$ java -jar $JAR --spring.profiles.active=import"
|
||||
echo
|
||||
start_app --spring.profiles.active=import > /dev/null
|
||||
report demo.greeting
|
||||
echo
|
||||
echo " demo.imported-only = $(curl -s "http://127.0.0.1:${APP_PORT}/precedence?name=demo.imported-only" | python3 -c 'import json,sys; print(json.load(sys.stdin)["effectiveValue"])')"
|
||||
echo
|
||||
echo "The imported file WON. spring.config.import does not behave like #include, and it"
|
||||
echo "does not behave like a default either: the imported document is processed AFTER the"
|
||||
echo "document that declared the import, so it outranks the file that pulled it in."
|
||||
echo "If you import a shared baseline expecting your own file to override it, every key"
|
||||
echo "the baseline sets will quietly beat yours."
|
||||
echo
|
||||
|
||||
echo "== one file, several documents, activated by condition =="
|
||||
for profile in "" staging prod; do
|
||||
label="${profile:-<none>}"
|
||||
echo "--- spring.profiles.active=$label (with the multidoc profile) ---"
|
||||
if [ -z "$profile" ]; then
|
||||
start_app --spring.profiles.active=multidoc > /dev/null
|
||||
else
|
||||
start_app --spring.profiles.active="multidoc,$profile" > /dev/null
|
||||
fi
|
||||
report demo.greeting
|
||||
echo
|
||||
done
|
||||
echo "Later documents in the same file win over earlier ones, so the unconditional first"
|
||||
echo "document acts as the default and each conditional document overrides it."
|
||||
echo
|
||||
|
||||
echo "== the activation Spring Boot refuses =="
|
||||
echo "application-badactivation.yaml tries to set spring.profiles.active from a document"
|
||||
echo "that is itself conditional on a profile."
|
||||
echo "\$ java -jar $JAR --spring.profiles.active=badactivation,staging"
|
||||
echo
|
||||
stop_app
|
||||
java -jar "$JAR" --spring.profiles.active=badactivation,staging 2>&1 | clean \
|
||||
| grep -E 'InvalidConfigDataPropertyException' | head -2 | fold -s -w 96
|
||||
echo
|
||||
echo
|
||||
echo "InvalidConfigDataPropertyException, naming the file and the line. Boot refuses"
|
||||
echo "rather than half-applying it: a profile that activates itself would change which"
|
||||
echo "files are loaded after those files had already been chosen."
|
||||
} > docs/output/04-import-and-multidoc.txt 2>&1
|
||||
cat docs/output/04-import-and-multidoc.txt
|
||||
42
profiles-and-config/scripts/demo-precedence.sh
Executable file
42
profiles-and-config/scripts/demo-precedence.sh
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# The whole precedence question, asked once with every source set at the same time.
|
||||
#
|
||||
# demo.greeting is set by application.yaml, by application-prod.yaml, by an environment
|
||||
# variable, by a system property and by a command-line argument -- simultaneously. The
|
||||
# endpoint reports all of them in order, so the winner is not a matter of opinion.
|
||||
set -euo pipefail
|
||||
set +m # no job-control notices ("Killed") in the captured transcript
|
||||
cd "$(dirname "$0")/.."
|
||||
source scripts/env.sh
|
||||
|
||||
{
|
||||
echo "== every source sets demo.greeting at once =="
|
||||
echo
|
||||
echo "\$ DEMO_GREETING=from-environment-variable \\"
|
||||
echo " java -Ddemo.greeting=from-system-property \\"
|
||||
echo " -jar $JAR --spring.profiles.active=prod \\"
|
||||
echo " --demo.greeting=from-command-line-argument"
|
||||
echo
|
||||
|
||||
scripts/stop.sh
|
||||
DEMO_GREETING=from-environment-variable setsid nohup java \
|
||||
-Ddemo.greeting=from-system-property -jar "$JAR" \
|
||||
--spring.profiles.active=prod --demo.greeting=from-command-line-argument \
|
||||
> /tmp/profiles-precedence.log 2>&1 < /dev/null &
|
||||
echo $! > target/app.pid
|
||||
for _ in $(seq 1 60); do
|
||||
curl -s -o /dev/null "http://127.0.0.1:${APP_PORT}/precedence" && break; sleep 1; done
|
||||
|
||||
curl -s "http://127.0.0.1:${APP_PORT}/precedence?name=demo.greeting" | python3 -m json.tool
|
||||
echo
|
||||
echo "== and with the environment variable removed, nothing else changed =="
|
||||
scripts/stop.sh
|
||||
setsid nohup java -Ddemo.greeting=from-system-property -jar "$JAR" \
|
||||
--spring.profiles.active=prod > /tmp/profiles-precedence2.log 2>&1 < /dev/null &
|
||||
echo $! > target/app.pid
|
||||
for _ in $(seq 1 60); do
|
||||
curl -s -o /dev/null "http://127.0.0.1:${APP_PORT}/precedence" && break; sleep 1; done
|
||||
curl -s "http://127.0.0.1:${APP_PORT}/precedence?name=demo.greeting" | python3 -m json.tool
|
||||
scripts/stop.sh
|
||||
} > docs/output/01-precedence.txt 2>&1
|
||||
cat docs/output/01-precedence.txt
|
||||
44
profiles-and-config/scripts/demo-profile-file-loses.sh
Executable file
44
profiles-and-config/scripts/demo-profile-file-loses.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# The article's title question: why did application-prod.yaml have no effect?
|
||||
#
|
||||
# Because an environment variable was set. Profile-specific files beat non-profile files,
|
||||
# but the whole config-data group sits BELOW environment variables in the documented
|
||||
# precedence list, so a profile file never outranks one.
|
||||
set -euo pipefail
|
||||
set +m
|
||||
cd "$(dirname "$0")/.."
|
||||
source scripts/env.sh
|
||||
|
||||
{
|
||||
echo "== does application-prod.yaml win? =="
|
||||
echo
|
||||
echo "demo.datasource-url is set in application.yaml and again in application-prod.yaml."
|
||||
echo
|
||||
|
||||
echo "--- 1. prod profile active, no environment variable ---"
|
||||
echo "\$ java -jar $JAR --spring.profiles.active=prod"
|
||||
start_app --spring.profiles.active=prod > /dev/null
|
||||
report demo.datasource-url
|
||||
echo
|
||||
|
||||
echo "--- 2. identical, plus one leftover environment variable ---"
|
||||
echo "\$ DEMO_DATASOURCE_URL=jdbc:postgresql://leftover:5432/orders \\"
|
||||
echo " java -jar $JAR --spring.profiles.active=prod"
|
||||
APP_ENV="DEMO_DATASOURCE_URL=jdbc:postgresql://leftover:5432/orders" \
|
||||
start_app --spring.profiles.active=prod > /dev/null
|
||||
report demo.datasource-url
|
||||
echo
|
||||
|
||||
echo "The profile-specific file is still loaded and still holds its value -- it is listed,"
|
||||
echo "and it lost. Config data is item 3 in the documented precedence list; OS environment"
|
||||
echo "variables are item 5, and later items win."
|
||||
echo
|
||||
echo "== the full property-source stack, in order =="
|
||||
echo "\$ curl -s localhost:8080/sources"
|
||||
curl -s "http://127.0.0.1:${APP_PORT}/sources" | python3 -c '
|
||||
import json,sys
|
||||
for r in json.load(sys.stdin):
|
||||
print(" %2d. %-34s %s" % (r["rank"], r["type"], r["name"][:110]))'
|
||||
stop_app
|
||||
} > docs/output/02-profile-file-loses.txt 2>&1
|
||||
cat docs/output/02-profile-file-loses.txt
|
||||
12
profiles-and-config/scripts/demo-versions.sh
Executable file
12
profiles-and-config/scripts/demo-versions.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
set +m # no job-control notices ("Killed") in the captured transcript
|
||||
cd "$(dirname "$0")/.."
|
||||
source scripts/env.sh
|
||||
{
|
||||
echo "== versions =="
|
||||
java -version 2>&1 | clean
|
||||
echo
|
||||
echo "spring-boot-starter-parent: $(grep -A2 '<artifactId>spring-boot-starter-parent' pom.xml | grep '<version>' | sed 's/.*<version>\(.*\)<\/version>.*/\1/')"
|
||||
} > docs/output/00-versions.txt 2>&1
|
||||
cat docs/output/00-versions.txt
|
||||
76
profiles-and-config/scripts/env.sh
Executable file
76
profiles-and-config/scripts/env.sh
Executable file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared environment. Point JAVA_HOME at a JDK 25 (or newer) installation.
|
||||
: "${JAVA_HOME:?set JAVA_HOME to a JDK 25+ installation}"
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
MVN="${MVN:-mvn}"
|
||||
JAR="target/profiles-and-config-1.0.0.jar"
|
||||
APP_MAIN="com.ankurm.profiles.ProfilesApplication"
|
||||
APP_PORT="${APP_PORT:-8080}"
|
||||
|
||||
# Strip environment noise that is an artefact of the machine, not of Spring:
|
||||
# the JVM prints a JAVA_TOOL_OPTIONS banner to stderr on every launch when a proxy
|
||||
# truststore is configured, and it would otherwise end up in every committed transcript.
|
||||
clean() { grep -v "Picked up JAVA_TOOL_OPTIONS" | grep -v "^OpenJDK 64-Bit Server VM warning"; }
|
||||
|
||||
# Start the demo jar detached, record its PID, and block until it answers.
|
||||
# Extra arguments are passed to the application. Environment variables for the run are
|
||||
# passed by setting them on the call: `APP_ENV="A=1 B=2" start_app --spring.profiles.active=x`
|
||||
start_app() {
|
||||
stop_app
|
||||
mkdir -p target
|
||||
# Deliberately NOT setsid: setsid forks when it is not already a process-group leader,
|
||||
# so $! would be the PID of a process that exits immediately and the JVM would survive
|
||||
# every later stop_app. A surviving JVM keeps the port, the next scenario fails to bind,
|
||||
# and curl answers from the previous scenario -- which reads exactly like the
|
||||
# configuration change under test having had no effect. Three wrong findings in this
|
||||
# repository came from that before it was tracked down.
|
||||
if [ -n "${APP_ENV:-}" ]; then
|
||||
# shellcheck disable=SC2086
|
||||
env $APP_ENV nohup java -jar "$JAR" "$@" > /tmp/profiles-demo.log 2>&1 < /dev/null &
|
||||
else
|
||||
nohup java -jar "$JAR" "$@" > /tmp/profiles-demo.log 2>&1 < /dev/null &
|
||||
fi
|
||||
echo $! > target/app.pid
|
||||
for _ in $(seq 1 60); do
|
||||
curl -s -o /dev/null "http://127.0.0.1:${APP_PORT}/precedence" 2>/dev/null && return 0
|
||||
kill -0 "$(cat target/app.pid)" 2>/dev/null || { echo "JVM exited during startup:"
|
||||
tail -20 /tmp/profiles-demo.log; return 1; }
|
||||
sleep 1
|
||||
done
|
||||
echo "application did not answer"; tail -20 /tmp/profiles-demo.log; return 1
|
||||
}
|
||||
|
||||
# Stop it by recorded PID. Never by pattern: `ps | grep <jar name>` also matches the shell
|
||||
# running the script, because the jar name is on that shell's own command line.
|
||||
stop_app() {
|
||||
if [ -f target/app.pid ]; then
|
||||
pid=$(cat target/app.pid)
|
||||
if [ -n "$pid" ] && grep -qa "profiles-and-config" "/proc/$pid/cmdline" 2>/dev/null; then
|
||||
kill -9 "$pid" 2>/dev/null || true
|
||||
wait "$pid" 2>/dev/null || true # reap, so bash prints no "Killed" notice
|
||||
fi
|
||||
rm -f target/app.pid
|
||||
fi
|
||||
for _ in $(seq 1 40); do
|
||||
if ! (exec 3<>/dev/tcp/127.0.0.1/"${APP_PORT:-8080}") 2>/dev/null; then break; fi
|
||||
sleep 0.25
|
||||
done
|
||||
exec 3<&- 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Print the precedence report for one property, compactly.
|
||||
report() {
|
||||
curl -s "http://127.0.0.1:${APP_PORT}/precedence?name=$1" | python3 -c '
|
||||
import json,sys
|
||||
d=json.load(sys.stdin)
|
||||
print(" active profiles :", ", ".join(d["activeProfiles"]) or "(none)")
|
||||
print(" effective value :", d["effectiveValue"])
|
||||
for h in d["holders"]:
|
||||
src=h["source"]
|
||||
for noisy,short in (("Config resource \x27class path resource [","file "),
|
||||
("\x27 via location \x27optional:classpath:/\x27}","")):
|
||||
src=src.replace(noisy,short)
|
||||
src=src.replace("OriginTrackedMapPropertySource {name=","").replace("]","")
|
||||
print(" %d. %-34s <- %s" % (h["rank"], h["value"], src.strip()))
|
||||
print(" holders that lost:", d["shadowedCount"])'
|
||||
}
|
||||
16
profiles-and-config/scripts/run-all.sh
Executable file
16
profiles-and-config/scripts/run-all.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regenerate every transcript under docs/output/.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
source scripts/env.sh
|
||||
|
||||
"$MVN" -B -q package -DskipTests
|
||||
|
||||
for demo in versions precedence profile-file-loses config-tree import-and-multidoc; do
|
||||
echo "=== $demo ==="
|
||||
"scripts/demo-$demo.sh" > /dev/null
|
||||
done
|
||||
stop_app
|
||||
echo
|
||||
echo "regenerated:"
|
||||
ls -1 docs/output/
|
||||
34
profiles-and-config/scripts/run.sh
Executable file
34
profiles-and-config/scripts/run.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# Start the application and block until it answers. Extra arguments are passed to the app,
|
||||
# so a scenario can add --demo.mail.recipients=a,b,c without a new profile.
|
||||
# ./scripts/run.sh # defaults
|
||||
# ./scripts/run.sh csvlist # a profile
|
||||
# ./scripts/run.sh "" --demo.x=y # no profile, one override
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
source scripts/env.sh
|
||||
PROFILES="${1:-}"; shift || true
|
||||
LOG="${LOG:-/tmp/configprops-demo.log}"
|
||||
PIDFILE="${PIDFILE:-target/app.pid}"
|
||||
|
||||
scripts/stop.sh
|
||||
|
||||
ARGS=(-jar "$JAR")
|
||||
[ -n "$PROFILES" ] && ARGS+=("--spring.profiles.active=$PROFILES")
|
||||
ARGS+=("$@")
|
||||
|
||||
setsid nohup java "${ARGS[@]}" > "$LOG" 2>&1 < /dev/null &
|
||||
echo $! > "$PIDFILE"
|
||||
|
||||
for _ in $(seq 1 60); do
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${APP_PORT}/precedence" || true)
|
||||
[ "$code" = "200" ] && exit 0
|
||||
# If the JVM died -- most often because the port was still held -- fail fast and loudly
|
||||
# instead of letting curl answer from a process started by an earlier scenario.
|
||||
kill -0 "$(cat "$PIDFILE")" 2>/dev/null || { echo "JVM exited during startup:" >&2
|
||||
tail -25 "$LOG" >&2; exit 1; }
|
||||
sleep 1
|
||||
done
|
||||
echo "application did not answer; tail of $LOG:" >&2
|
||||
tail -40 "$LOG" >&2
|
||||
exit 1
|
||||
28
profiles-and-config/scripts/stop.sh
Executable file
28
profiles-and-config/scripts/stop.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# Stop the demo application.
|
||||
#
|
||||
# This uses a PID file rather than a pattern match, deliberately. `pkill -f spring-boot`
|
||||
# matches the shell that is running the script and takes the terminal with it. Even a
|
||||
# careful-looking `ps | grep '[c]onfiguration-properties'` matches the shell's own command
|
||||
# line whenever that string appears in the command you just typed -- which it does, because
|
||||
# you typed the jar name. Killing a recorded PID cannot misfire.
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
PIDFILE="${PIDFILE:-target/app.pid}"
|
||||
|
||||
if [ -f "$PIDFILE" ]; then
|
||||
pid=$(cat "$PIDFILE")
|
||||
# Confirm the PID is still ours before signalling it: PIDs are reused.
|
||||
if [ -n "$pid" ] && grep -qa "profiles-and-config" "/proc/$pid/cmdline" 2>/dev/null; then
|
||||
kill -9 "$pid" 2>/dev/null || true
|
||||
fi
|
||||
rm -f "$PIDFILE"
|
||||
fi
|
||||
|
||||
# Killing the process is not the same as the socket closing, and a stale listener looks
|
||||
# exactly like your configuration change having had no effect.
|
||||
for _ in $(seq 1 40); do
|
||||
if ! (exec 3<>/dev/tcp/127.0.0.1/"${APP_PORT:-8080}") 2>/dev/null; then break; fi
|
||||
sleep 0.25
|
||||
done
|
||||
exec 3<&- 2>/dev/null || true
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.ankurm.profiles;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* Companion application for the ankurm.com article
|
||||
* "Spring Boot Profiles Done Right: Config Import, Config Trees and Kubernetes ConfigMaps".
|
||||
*
|
||||
* <p>Every scenario in {@code scripts/} starts this same application with a different
|
||||
* combination of profiles, imported locations and environment variables, and asks it one
|
||||
* question: which source won, and which sources were present and lost.
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class ProfilesApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ProfilesApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.ankurm.profiles.web;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
|
||||
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
|
||||
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.EnumerablePropertySource;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* The endpoint the whole article is built on: for one property, every source that holds a
|
||||
* value for it, in precedence order, with the winner first.
|
||||
*
|
||||
* <p>"I set it in {@code application-prod.yaml} and it had no effect" is not a mystery once
|
||||
* you can see that four sources hold the property and yours is third. Spring Boot knows the
|
||||
* answer; it just never volunteers it.
|
||||
*
|
||||
* <p>Documented in {@code docs/03-seeing-precedence.md}. Delete it before shipping: it will
|
||||
* print whatever a mounted secret contains.
|
||||
*/
|
||||
@RestController
|
||||
public class PrecedenceEndpoint {
|
||||
|
||||
private final ConfigurableEnvironment environment;
|
||||
|
||||
public PrecedenceEndpoint(ConfigurableEnvironment environment) {
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
/** Every source holding {@code name}, highest precedence first. */
|
||||
@GetMapping("/precedence")
|
||||
public Map<String, Object> precedence(
|
||||
@RequestParam(defaultValue = "demo.greeting") String name) {
|
||||
|
||||
ConfigurationPropertyName propertyName = ConfigurationPropertyName.of(name);
|
||||
List<Map<String, Object>> holders = new ArrayList<>();
|
||||
|
||||
for (ConfigurationPropertySource source : ConfigurationPropertySources.get(environment)) {
|
||||
var property = source.getConfigurationProperty(propertyName);
|
||||
if (property == null) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("rank", holders.size() + 1);
|
||||
row.put("source", shortName(source.getUnderlyingSource()));
|
||||
row.put("value", String.valueOf(property.getValue()));
|
||||
row.put("origin", String.valueOf(property.getOrigin()));
|
||||
holders.add(row);
|
||||
}
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("property", name);
|
||||
result.put("effectiveValue", environment.getProperty(name));
|
||||
result.put("activeProfiles", List.of(environment.getActiveProfiles()));
|
||||
result.put("holders", holders);
|
||||
result.put("shadowedCount", Math.max(0, holders.size() - 1));
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The environment's property sources in order, so the article can show the real stack. */
|
||||
@GetMapping("/sources")
|
||||
public List<Map<String, Object>> sources() {
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
int rank = 1;
|
||||
for (PropertySource<?> source : environment.getPropertySources()) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("rank", rank++);
|
||||
row.put("name", source.getName());
|
||||
row.put("type", source.getClass().getSimpleName());
|
||||
if (source instanceof EnumerablePropertySource<?> enumerable) {
|
||||
row.put("propertyCount", enumerable.getPropertyNames().length);
|
||||
}
|
||||
rows.add(row);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private String shortName(Object underlying) {
|
||||
String text = String.valueOf(underlying);
|
||||
return text.length() > 150 ? text.substring(0, 150) + "..." : text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# spring.profiles.active cannot be set from a document that is itself profile-specific.
|
||||
# Spring Boot refuses this rather than silently half-applying it. The exact exception is
|
||||
# captured in docs/output/05-invalid-activation.txt.
|
||||
demo:
|
||||
greeting: from-badactivation
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: staging
|
||||
profiles:
|
||||
active: sneaky
|
||||
@@ -0,0 +1,11 @@
|
||||
# Demonstrates spring.config.import ordering.
|
||||
#
|
||||
# The imported document is processed as though it appeared immediately AFTER this one, which
|
||||
# means the importing file wins on any key both of them set. That is the opposite of the
|
||||
# intuition most people bring from #include, and it is the subject of docs/05-config-import.md.
|
||||
spring:
|
||||
config:
|
||||
import: "optional:classpath:/imported.yaml"
|
||||
|
||||
demo:
|
||||
greeting: from-application-import-yaml
|
||||
@@ -0,0 +1,20 @@
|
||||
# One file, three documents, activated by condition rather than by filename.
|
||||
#
|
||||
# spring.config.activate.on-profile is the mechanism behind profile-specific behaviour when
|
||||
# you would rather keep everything in one file. Later documents win over earlier ones.
|
||||
demo:
|
||||
greeting: from-multidoc-default-document
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: staging
|
||||
demo:
|
||||
greeting: from-multidoc-staging-document
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: prod
|
||||
demo:
|
||||
greeting: from-multidoc-prod-document
|
||||
@@ -0,0 +1,3 @@
|
||||
# Activated as part of the "prod" profile group declared in application.yaml.
|
||||
demo:
|
||||
pool-size: 40
|
||||
@@ -0,0 +1,3 @@
|
||||
# The second member of the "prod" group.
|
||||
demo:
|
||||
metrics-enabled: true
|
||||
@@ -0,0 +1,5 @@
|
||||
# Profile-specific configuration. This file always beats application.yaml -- and still loses
|
||||
# to an environment variable, which is the point of docs/04-why-your-profile-file-lost.md.
|
||||
demo:
|
||||
greeting: from-application-prod-yaml
|
||||
datasource-url: jdbc:postgresql://prod-db:5432/orders
|
||||
22
profiles-and-config/src/main/resources/application.yaml
Normal file
22
profiles-and-config/src/main/resources/application.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
spring:
|
||||
application:
|
||||
name: profiles-and-config
|
||||
profiles:
|
||||
# A profile group: activating "prod" activates all three. Groups are resolved before
|
||||
# config data is processed, which is why a group can be declared here and still affect
|
||||
# which application-<profile>.yaml files are loaded.
|
||||
group:
|
||||
prod: prod-db,prod-metrics
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: WARN
|
||||
|
||||
demo:
|
||||
# The property every scenario asks about. Each source below sets it to a string naming
|
||||
# itself, so the winner is self-identifying in the transcript.
|
||||
greeting: from-application-yaml
|
||||
datasource-url: jdbc:h2:mem:default
|
||||
4
profiles-and-config/src/main/resources/imported.yaml
Normal file
4
profiles-and-config/src/main/resources/imported.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
# Imported by application-import.yaml. Sets the same key, and loses.
|
||||
demo:
|
||||
greeting: from-imported-yaml
|
||||
imported-only: yes-this-file-was-read
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.ankurm.profiles;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Pins the precedence claims the article makes, so a future Spring Boot upgrade that changes
|
||||
* any of them fails the build rather than quietly making the article wrong.
|
||||
*/
|
||||
class PrecedenceContractTests {
|
||||
|
||||
private ConfigurableApplicationContext run(String... args) {
|
||||
SpringApplication application = new SpringApplication(ProfilesApplication.class);
|
||||
application.setWebApplicationType(WebApplicationType.NONE);
|
||||
return application.run(args);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a profile-specific file beats application.yaml")
|
||||
void profileFileBeatsBaseFile() {
|
||||
try (var context = run("--spring.profiles.active=prod")) {
|
||||
assertThat(context.getEnvironment().getProperty("demo.greeting"))
|
||||
.isEqualTo("from-application-prod-yaml");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a profile group activates every profile it names")
|
||||
void profileGroupExpands() {
|
||||
try (var context = run("--spring.profiles.active=prod")) {
|
||||
assertThat(context.getEnvironment().getActiveProfiles())
|
||||
.containsExactlyInAnyOrder("prod", "prod-db", "prod-metrics");
|
||||
assertThat(context.getEnvironment().getProperty("demo.pool-size")).isEqualTo("40");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a command-line argument beats every config file")
|
||||
void commandLineBeatsConfigData() {
|
||||
try (var context = run("--spring.profiles.active=prod",
|
||||
"--demo.greeting=from-command-line")) {
|
||||
assertThat(context.getEnvironment().getProperty("demo.greeting"))
|
||||
.isEqualTo("from-command-line");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The counterintuitive one, and the reason the article has a callout about it: an
|
||||
* imported document outranks the document that imported it.
|
||||
*/
|
||||
@Test
|
||||
@DisplayName("spring.config.import: the IMPORTED file wins over the importing file")
|
||||
void importedFileWins() {
|
||||
try (var context = run("--spring.profiles.active=import")) {
|
||||
ConfigurableEnvironment environment = context.getEnvironment();
|
||||
assertThat(environment.getProperty("demo.imported-only"))
|
||||
.as("the import was processed at all")
|
||||
.isEqualTo("yes-this-file-was-read");
|
||||
assertThat(environment.getProperty("demo.greeting"))
|
||||
.as("and it beat application-import.yaml, which declared the import")
|
||||
.isEqualTo("from-imported-yaml");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("later documents in a multi-document file win over earlier ones")
|
||||
void multiDocumentOrdering() {
|
||||
try (var context = run("--spring.profiles.active=multidoc")) {
|
||||
assertThat(context.getEnvironment().getProperty("demo.greeting"))
|
||||
.isEqualTo("from-multidoc-default-document");
|
||||
}
|
||||
try (var context = run("--spring.profiles.active=multidoc,staging")) {
|
||||
assertThat(context.getEnvironment().getProperty("demo.greeting"))
|
||||
.isEqualTo("from-multidoc-staging-document");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a config tree beats application.yaml but still loses to nothing above it")
|
||||
void configTreeIsConfigData(@org.junit.jupiter.api.io.TempDir java.nio.file.Path mount)
|
||||
throws Exception {
|
||||
java.nio.file.Files.writeString(mount.resolve("demo.greeting"), "from-config-tree");
|
||||
|
||||
try (var context = run("--spring.config.import=configtree:" + mount + "/")) {
|
||||
assertThat(context.getEnvironment().getProperty("demo.greeting"))
|
||||
.isEqualTo("from-config-tree");
|
||||
}
|
||||
// A command-line argument still outranks it: a config tree is config data.
|
||||
try (var context = run("--spring.config.import=configtree:" + mount + "/",
|
||||
"--demo.greeting=from-command-line")) {
|
||||
assertThat(context.getEnvironment().getProperty("demo.greeting"))
|
||||
.isEqualTo("from-command-line");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user