Todos os artigos

// Knowledge.log — 技術記事

Configuração externa no Spring Boot 4.1: descubra quem venceu

Veja por que uma variável, um YAML ou a CLI venceu no Spring Boot 4.1 e descubra a origem do valor com um aplicativo mínimo.

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.

  1. Propriedades padrão definidas com SpringApplication.setDefaultProperties(Map).
  2. @PropertySource em classes @Configuration. Essa fonte só entra durante o refresh do contexto, tarde demais para configurar propriedades como logging.* e spring.main.*.
  3. Config Data, incluindo application.properties e arquivos YAML.
  4. RandomValuePropertySource, usado apenas por propriedades random.*.
  5. Variáveis de ambiente do sistema operacional.
  6. Propriedades de sistema Java, como as passadas com -D.
  7. Atributos JNDI em java:comp/env.
  8. Parâmetros de inicialização de ServletContext.
  9. Parâmetros de inicialização de ServletConfig.
  10. SPRING_APPLICATION_JSON, fornecido como variável de ambiente ou propriedade de sistema.
  11. Argumentos de linha de comando.
  12. O atributo properties em @SpringBootTest e testes slice.
  13. @DynamicPropertySource em testes.
  14. @TestPropertySource em testes.
  15. 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:

  1. application.properties ou YAML empacotado no jar.
  2. Arquivo empacotado específico do profile, como application-prod.yml.
  3. application.properties ou YAML fora do jar.
  4. 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.

RecursoOnde declararO que faz
spring.config.locationAmbiente, -D ou CLISubstitui os locais de busca padrão
spring.config.additional-locationAmbiente, -D ou CLIAcrescenta locais cujos arquivos podem sobrescrever os padrões
spring.config.importDentro de Config DataImporta documentos adicionais no ponto da declaração; o importado vence o documento importador
spring.config.activate.on-profileEm um documento de configuraçãoAtiva 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:

  1. Confirme o nome relaxado. Para spring.datasource.url, use SPRING_DATASOURCE_URL, não SPRING_DATA_SOURCE_URL.
  2. Procure uma fonte posterior: --demo.who-won, SPRING_APPLICATION_JSON ou -Ddemo.who-won vencem a variável do sistema operacional.
  3. 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.
  4. Reinicie a aplicação depois da mudança. Alterar o ambiente ou um Secret não modifica o Environment de uma JVM que já está em execução.
  5. Em injeções com @Value, use o nome canônico em kebab-case, como ${demo.who-won}, ou prefira @ConfigurationProperties para 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.

javaspring-boot

// Continue.training — 次のステップ

Conhecimento só conta quando vira prática.

Volte ao artigo, execute os exemplos e compartilhe o que aprendeu.

Explorar mais artigos