Você definiu demo.who-won em application.yml, mudou o valor em application-prod.yml e ainda passou DEMO_WHOWON no ambiente. A aplicação iniciou, mas não com o valor que você esperava.
O resultado que queremos é menos misterioso: olhar para a saída do processo e encontrar tanto o valor vencedor quanto sua origem. Antes disso, precisamos separar duas ordens que costumam ser misturadas: a precedência geral dos PropertySource e a ordem dos arquivos dentro de Config Data.
Um ponto importante desde já: uma variável de ambiente válida vence qualquer application-prod.yml. Se a aplicação parece contrariar isso, a investigação deve procurar um nome incorreto, uma fonte ainda mais forte ou uma variável que nunca chegou ao processo — não uma regra secreta de profile.
Versões e pré-requisitos
Os exemplos usam Spring Boot 4.1.1 com Java 25 LTS. O Boot 4.1.1 requer Java 17 e aceita versões até Java 26, conforme os requisitos oficiais do sistema. Assim, Java 25 é a LTS mais nova dentro do intervalo suportado.
Você precisa de Maven e de um JDK 25 disponível no PATH. O pom.xml mínimo fica assim:
<?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>academy.devdojo</groupId>
<artifactId>config-winner</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>25</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
As 15 camadas de PropertySource
A documentação de configuração externa do Spring Boot 4.1 define 15 camadas. A regra é simples: fontes posteriores podem sobrescrever as anteriores.
- Propriedades padrão definidas com
SpringApplication.setDefaultProperties(Map). @PropertySourceem classes@Configuration. Essa fonte só entra durante o refresh do contexto, tarde demais para configurar propriedades comologging.*espring.main.*.- Config Data, incluindo
application.propertiese arquivos YAML. RandomValuePropertySource, usado apenas por propriedadesrandom.*.- Variáveis de ambiente do sistema operacional.
- Propriedades de sistema Java, como as passadas com
-D. - Atributos JNDI em
java:comp/env. - Parâmetros de inicialização de
ServletContext. - Parâmetros de inicialização de
ServletConfig. SPRING_APPLICATION_JSON, fornecido como variável de ambiente ou propriedade de sistema.- Argumentos de linha de comando.
- O atributo
propertiesem@SpringBootTeste testes slice. @DynamicPropertySourceem testes.@TestPropertySourceem testes.- Configurações globais do Devtools em
$HOME/.config/spring-boot, quando o Devtools está ativo.
Para uma aplicação comum em produção, o trecho mais útil dessa ordem é:
Config Data < variável de ambiente < -D < SPRING_APPLICATION_JSON < CLI
Portanto, DEMO_WHOWON=from-env vence o valor de application-prod.yml, mas perde para -Ddemo.who-won=from-system, para JSON inline e para --demo.who-won=from-cli.
A ordem de quatro arquivos dentro de Config Data
A terceira camada tem sua própria ordem. Também aqui, o item posterior vence:
application.propertiesou YAML empacotado no jar.- Arquivo empacotado específico do profile, como
application-prod.yml. application.propertiesou YAML fora do jar.- Arquivo externo específico do profile, como
application-prod.yml.
Essa lista compara arquivos de configuração entre si; ela não eleva um YAML de profile acima de uma variável de ambiente. Misturar as duas listas é uma forma bastante eficiente de passar meia hora discutindo uma precedência que não existe.
Se houver .properties e YAML no mesmo local, .properties tem precedência. Escolher um formato por local evita mais uma dimensão desnecessária no diagnóstico.
location, additional-location, import e ativação de documento
Os quatro recursos participam da configuração externa, mas resolvem problemas diferentes.
| Recurso | Onde declarar | O que faz |
|---|---|---|
spring.config.location | Ambiente, -D ou CLI | Substitui os locais de busca padrão |
spring.config.additional-location | Ambiente, -D ou CLI | Acrescenta locais cujos arquivos podem sobrescrever os padrões |
spring.config.import | Dentro de Config Data | Importa documentos adicionais no ponto da declaração; o importado vence o documento importador |
spring.config.activate.on-profile | Em um documento de configuração | Ativa aquele documento quando a expressão de profile corresponde |
spring.config.location e spring.config.additional-location são lidos muito cedo. Colocá-los apenas dentro de application.yml é tarde demais, porque o Boot já precisou decidir quais arquivos carregar. É o override que nunca chegou à própria reunião.
Use location quando quiser controlar todo o conjunto pesquisado:
java -jar target/config-winner-0.0.1-SNAPSHOT.jar \
--spring.config.location=optional:classpath:/custom-config/,optional:file:./custom-config/
Nesse caso, os locais padrão deixam de participar. Já additional-location preserva os padrões e acrescenta outro local com precedência sobre eles:
java -jar target/config-winner-0.0.1-SNAPSHOT.jar \
--spring.config.additional-location=optional:file:./config-extra/
A mesma opção pode vir do sistema operacional ou de uma propriedade Java:
SPRING_CONFIG_ADDITIONALLOCATION=optional:file:/etc/config-winner/ \
java -jar target/config-winner-0.0.1-SNAPSHOT.jar
java -Dspring.config.additional-location=optional:file:/etc/config-winner/ \
-jar target/config-winner-0.0.1-SNAPSHOT.jar
Para diretórios, mantenha a / final. O prefixo optional: evita que a aplicação falhe quando o local não existir.
Prefira file: para configuração externa ao artefato. Um classpath: adicional ainda aponta para algo empacotado com a aplicação; trocar o nome da opção não teletransporta o segredo para fora do jar.
spring.config.import, por outro lado, pertence ao arquivo já carregado:
spring:
config:
import: optional:file:./local-overrides.yml
O documento importado é inserido logo abaixo do importador e seus valores têm precedência sobre os valores do arquivo que fez o import. Cada recurso importado é carregado uma única vez.
Para segredos montados como arquivos em Kubernetes, a alternativa documentada é uma config tree:
spring:
config:
import: optional:configtree:/run/secrets/
Nesse formato, o nome de cada arquivo vira a chave e o conteúdo vira o valor.
Por fim, spring.config.activate.on-profile ativa um documento, não define uma convenção de nome de arquivo:
demo:
who-won: from-base-document
---
spring:
config:
activate:
on-profile: prod
demo:
who-won: from-prod-document
Isso é diferente de criar application-prod.yml, embora ambos possam reagir ao profile prod. Com vários profiles ativos, a ordem declarada importa: em prod,live, o último profile vence quando os dois definem a mesma chave.
Um aplicativo mínimo que mostra o vencedor
Crie src/main/java/academy/devdojo/configwinner/ConfigWinnerApplication.java:
package academy.devdojo.configwinner;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
@SpringBootApplication
public class ConfigWinnerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigWinnerApplication.class, args);
}
@Bean
CommandLineRunner printConfigurationWinner(Environment environment) {
return args -> {
var name = ConfigurationPropertyName.of("demo.who-won");
for (var source : ConfigurationPropertySources.get(environment)) {
var property = source.getConfigurationProperty(name);
if (property != null) {
System.out.printf("value=%s%n", property.getValue());
System.out.printf("origin=%s%n", property.getOrigin());
return;
}
}
System.out.println("demo.who-won não foi definida");
};
}
}
A combinação de ConfigurationPropertyName com ConfigurationPropertySources.get(environment) respeita o modelo de nomes do Boot e permite obter o ConfigurationProperty. O método getOrigin() mostra a origem rastreada quando ela está disponível. O primeiro resultado encontrado é o vencedor.
Não percorra os PropertySource brutos procurando literalmente por demo.who-won: uma variável como DEMO_WHOWON não tem essa grafia. Também não é necessário transformar /actuator/env na rota de diagnóstico do tutorial e, por distração, ampliar a superfície que pode revelar configuração sensível.
Prove a precedência na tela
Em src/main/resources/application.yml, coloque:
demo:
who-won: from-jar
Em src/main/resources/application-prod.yml, coloque:
demo:
who-won: from-prod-file
Empacote a aplicação:
mvn clean package
Agora execute os cenários abaixo e observe as linhas value= e origin=.
Primeiro, ative prod sem outra fonte:
java -jar target/config-winner-0.0.1-SNAPSHOT.jar \
--spring.profiles.active=prod
A saída relevante terá value=from-prod-file, pois o arquivo específico do profile vence o arquivo comum dentro de Config Data.
Depois, passe a variável de ambiente correta:
DEMO_WHOWON=from-env \
java -jar target/config-winner-0.0.1-SNAPSHOT.jar \
--spring.profiles.active=prod
Agora o resultado será value=from-env. A variável de ambiente está depois de Config Data na ordem oficial.
Por último, acrescente um argumento de linha de comando:
DEMO_WHOWON=from-env \
java -jar target/config-winner-0.0.1-SNAPSHOT.jar \
--spring.profiles.active=prod \
--demo.who-won=from-cli
O resultado será value=from-cli, porque argumentos de linha de comando estão depois das variáveis de ambiente.
Também podemos testar um arquivo fora do jar. Crie override.yml no diretório atual:
demo:
who-won: from-file-outside-jar
Execute:
java -jar target/config-winner-0.0.1-SNAPSHOT.jar \
--spring.config.additional-location=optional:file:./override.yml
O arquivo externo vence o arquivo empacotado. Ainda assim, perderia para DEMO_WHOWON ou --demo.who-won, pois continua dentro da camada Config Data.
Por que a variável parece não ter sido aplicada
Para converter um nome canônico em variável de ambiente, troque pontos por _, remova hífens e use maiúsculas. Assim:
demo.who-won -> DEMO_WHOWON
spring.datasource.url -> SPRING_DATASOURCE_URL
DEMO_WHO_WON tem um _ extra e não representa demo.who-won. O ambiente é rigoroso desse jeito: flexível no binding, mas não telepático.
Se a saída ainda não mostrar o valor esperado, verifique estes casos na ordem:
- Confirme o nome relaxado. Para
spring.datasource.url, useSPRING_DATASOURCE_URL, nãoSPRING_DATA_SOURCE_URL. - Procure uma fonte posterior:
--demo.who-won,SPRING_APPLICATION_JSONou-Ddemo.who-wonvencem a variável do sistema operacional. - Inspecione o ambiente recebido pelo processo, não apenas o shell em que alguém acredita ter definido a variável. systemd, Docker Compose e Kubernetes podem iniciar o processo sem ela.
- Reinicie a aplicação depois da mudança. Alterar o ambiente ou um Secret não modifica o
Environmentde uma JVM que já está em execução. - Em injeções com
@Value, use o nome canônico em kebab-case, como${demo.who-won}, ou prefira@ConfigurationPropertiespara configuração agrupada.
SPRING_APPLICATION_JSON merece atenção especial: ele fica acima das variáveis de ambiente. Além disso, um valor JSON null é tratado como ausente e não apaga um valor vindo de uma fonte inferior.
Próximo passo
Adicione temporariamente o CommandLineRunner ao serviço que apresenta o conflito, execute-o no mesmo ambiente do deploy e compare value= com origin=. Remova o diagnóstico depois de identificar a fonte, principalmente se a chave real puder carregar um segredo.
Se o valor precisa variar por ambiente sem ser empacotado, a recomendação da DevDojo é firme: use spring.config.additional-location com um caminho file: fora do jar; em Kubernetes, prefira optional:configtree: quando o segredo já estiver montado como arquivo. Só use spring.config.location quando a intenção explícita for substituir toda a busca padrão.