Profiles no Spring Boot: configuração dev, test, prod

1. Visão geral

Uma aplicação geralmente roda em múltiplos ambientes. Sua máquina usa banco de dados H2. O servidor de teste usa PostgreSQL separado. Production usa um cluster real com SMTP real. Se você for editar o arquivo de configuração manualmente cada vez que faz deploy, mais cedo ou mais tarde vai acabar mandando configuração de dev para prod.

Spring Boot resolve isso com profiles. Depois de ler este artigo, você saberá como separar arquivos de configuração por profile, ligar/desligar beans com @Profile, ativar profiles de várias formas, escrever testes com @ActiveProfiles e evitar os erros mais comuns.

2. O que é profile?

Profile é um rótulo com nome, por exemplo dev, test, prod. Spring usa esse rótulo para tomar duas decisões:

  • Qual configuração é carregada: application-dev.yml só é lido quando o profile dev está ativo.
  • Qual bean é criado: um bean marcado com @Profile("prod") só é registrado quando o profile prod está ativo.

Se nenhum profile for ativado, Spring usa o profile padrão chamado default. Beans sem @Profile são sempre criados, não importa qual profile está ativo.

Você precisa apenas da dependência web básica para os exemplos deste artigo:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

3. Arquivos de configuração por profile

3.1. Separar em múltiplos arquivos

A convenção é nomear como application-{profile}.yml (ou .properties). O arquivo application.yml contém configurações comuns, sempre carregadas:

# application.yml
spring:
  application:
    name: shop-service
  profiles:
    default: dev

app:
  mail:
    from: no-reply@shop.vn
    host: localhost

Arquivos específicos de profile contêm apenas as diferenças:

# application-dev.yml
server:
  port: 8080
logging:
  level:
    root: DEBUG
# application-test.yml
server:
  port: 0
app:
  mail:
    host: fake-smtp
# application-prod.yml
server:
  port: 80
logging:
  level:
    root: WARN
app:
  mail:
    host: ${MAIL_HOST}

Explicação:

  • spring.profiles.default: dev significa que quando você não ativa nenhum profile, Spring usa dev em vez de default. Sua máquina de desenvolvimento roda imediatamente sem precisar de parâmetros.
  • Quando o profile prod está ativo, Spring carrega application.yml primeiro e depois application-prod.yml. Valores no arquivo de profile sobrescrevem valores comuns. Por isso app.mail.host se torna o valor da variável de ambiente MAIL_HOST, mas app.mail.from permanece no-reply@shop.vn.
  • server.port: 0 em test faz Spring escolher uma porta aleatória, evitando conflitos ao rodar em paralelo.

3.2. Consolidar em um único arquivo YAML

Se quiser menos arquivos, você pode usar YAML com múltiplos documentos, separados por ---:

# application.yml
app:
  mail:
    host: localhost
---
spring:
  config:
    activate:
      on-profile: prod
app:
  mail:
    host: smtp.shop.vn

O segundo documento só é válido quando prod está ativo. Essa abordagem é compacta quando cada profile tem poucas diferenças. Para configurações longas, separar em arquivos ainda é mais fácil de ler e revisar.

Nota: Em arquivos .properties, use a linha #--- para separar documentos em vez de ---.

4. Ativar profiles

Há várias formas. A ordem de prioridade segue a regra geral do Spring Boot: argumentos da linha de comando > variáveis de ambiente > arquivo de configuração.

Argumentos da linha de comando:

java -jar shop-service.jar --spring.profiles.active=prod

Propriedade do sistema JVM:

java -Dspring.profiles.active=prod -jar shop-service.jar

Variável de ambiente, o jeito mais comum em Docker e Kubernetes:

export SPRING_PROFILES_ACTIVE=prod
java -jar shop-service.jar

Ao rodar com o plugin Maven:

./mvnw spring-boot:run -Dspring-boot.run.profiles=dev

Via código, útil quando você sempre quer adicionar um profile específico:

@SpringBootApplication
public class ShopApplication {

    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(ShopApplication.class);
        app.setAdditionalProfiles("metrics");
        app.run(args);
    }
}

setAdditionalProfiles adiciona profiles à lista, não substitui profiles já ativados de fora. Se rodar com --spring.profiles.active=prod, tanto prod quanto metrics estarão ativos.

Você pode ativar múltiplos profiles de uma vez com vírgulas: --spring.profiles.active=prod,eu. Se dois arquivos definem a mesma chave, o profile que vem depois vence. Nesse exemplo, application-eu.yml sobrescreve application-prod.yml.

Quando a aplicação inicia, o log diz qual profile está rodando:

INFO  c.e.shop.ShopApplication : The following 1 profile is active: "prod"

Se nenhum profile estiver ativo e você também não definir spring.profiles.default, você verá:

INFO  c.e.shop.ShopApplication : No active profile set, falling back to 1 default profile: "default"

Sempre dê uma olhada nessa linha de log após fazer deploy. É o jeito mais rápido de descobrir se o servidor está rodando com o profile errado.

5. Ligar/desligar beans com @Profile

Configuração diferente é apenas metade do problema. Às vezes você precisa de implementações completamente diferentes. Por exemplo: dev apenas imprime e-mail no console, test salva em memória para verificar, prod envia via SMTP.

Defina uma interface comum:

public interface NotificationService {
    void send(String to, String message);
}

Implementação para dev:

@Service
@Profile("dev")
public class ConsoleNotificationService implements NotificationService {

    @Override
    public void send(String to, String message) {
        System.out.println("[DEV] Enviando para " + to + ": " + message);
    }
}

Implementação para test, que guarda mensagens para assertions:

@Service
@Profile("test")
public class InMemoryNotificationService implements NotificationService {

    private final List<String> sentMessages = new CopyOnWriteArrayList<>();

    @Override
    public void send(String to, String message) {
        sentMessages.add(to + "|" + message);
    }

    public List<String> getSentMessages() {
        return List.copyOf(sentMessages);
    }
}

Implementação para prod:

@Service
@Profile("prod")
public class SmtpNotificationService implements NotificationService {

    private final String host;

    public SmtpNotificationService(@Value("${app.mail.host}") String host) {
        this.host = host;
    }

    @Override
    public void send(String to, String message) {
        // Na prática usaria JavaMailSender; aqui é só para ilustrar
        System.out.println("[SMTP " + host + "] Enviando para " + to);
    }
}

A classe que usa não precisa saber em qual ambiente está:

@Service
public class OrderService {

    private final NotificationService notificationService;

    public OrderService(NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    public void placeOrder(String customerEmail, long orderId) {
        notificationService.send(customerEmail, "Pedido #" + orderId + " foi criado");
    }
}

A qualquer momento, apenas um dos três beans NotificationService é criado. Spring injeta o bean correto em OrderService. Rodando com profile dev e chamando placeOrder("an@shop.vn", 42), você recebe:

[DEV] Enviando para an@shop.vn: Pedido #42 foi criado

5.1. Expressões de profile

@Profile suporta expressões lógicas:

@Configuration
public class SecurityConfig {

    @Bean
    @Profile("!prod")
    public CommandLineRunner seedDemoData() {
        return args -> System.out.println("Carregando dados de demo...");
    }

    @Bean
    @Profile("dev | test")
    public CommandLineRunner relaxedCors() {
        return args -> System.out.println("CORS aberto para todos os origins");
    }
}
  • !prod: cria o bean em qualquer profile exceto prod.
  • dev | test: cria quando dev ou test está ativo. Também tem & para “e”, por exemplo prod & eu.

@Profile pode ser colocado em classes @Component/@Configuration ou em métodos @Bean individuais. Quando colocado em @Configuration, todos os beans dentro compartilham a mesma condição.

5.2. Verificar profile no código

Às vezes você precisa criar lógica condicional. Use Environment:

@Component
public class StartupBanner implements CommandLineRunner {

    private final Environment environment;

    public StartupBanner(Environment environment) {
        this.environment = environment;
    }

    @Override
    public void run(String... args) {
        if (environment.acceptsProfiles(Profiles.of("prod"))) {
            System.out.println("Rodando em PRODUCTION");
        }
    }
}

Use essa abordagem com moderação. Se o código está cheio de if (prod), você deveria separar em beans distintos com @Profile.

6. Grupos de profiles

Conforme o sistema cresce, você pode querer dividir configurações em partes menores, por exemplo proddb para banco de dados e prodmq para message broker. Grupos de profiles permitem ativar um conjunto inteiro com um único nome:

# application.yml
spring:
  profiles:
    group:
      prod: proddb, prodmq
      dev: devdb

Agora rodar --spring.profiles.active=prod ativa prod, proddb e prodmq. Spring carrega em sequência application-prod.yml, application-proddb.yml, application-prodmq.yml. O operador só precisa lembrar um nome.

7. Escrever testes com @ActiveProfiles

Em testes, você seleciona o profile com @ActiveProfiles. O teste abaixo verifica duas coisas: o bean correto é injetado e a configuração de application-test.yml está em efeito.

@SpringBootTest
@ActiveProfiles("test")
class OrderServiceProfileTest {

    @Autowired
    private OrderService orderService;

    @Autowired
    private NotificationService notificationService;

    @Value("${app.mail.host}")
    private String mailHost;

    @Test
    void shouldUseInMemoryNotificationInTestProfile() {
        assertThat(notificationService).isInstanceOf(InMemoryNotificationService.class);
    }

    @Test
    void shouldRecordNotificationWhenOrderPlaced() {
        orderService.placeOrder("binh@shop.vn", 7);

        var inMemory = (InMemoryNotificationService) notificationService;
        assertThat(inMemory.getSentMessages())
                .contains("binh@shop.vn|Pedido #7 foi criado");
    }

    @Test
    void shouldOverrideMailHostFromTestProfile() {
        assertThat(mailHost).isEqualTo("fake-smtp");
    }
}

Explicação:

  • @ActiveProfiles("test") ativa o profile test para o contexto dessa classe, não importa o que spring.profiles.default seja.
  • O primeiro teste confirma que Spring escolheu InMemoryNotificationService em vez de enviar e-mail de verdade.
  • O segundo teste usa o bean em-memory para verificar o conteúdo da mensagem sem precisar fazer mock.
  • O terceiro teste prova que app.mail.host de application-test.yml sobrescreveu o valor localhost em application.yml.

Nota: Cada combinação diferente de profiles cria um contexto Spring separado. Se cada classe de teste usa um conjunto diferente de profiles, o cache de contexto não reutiliza e a suite de testes fica lenta. Mantenha o número de combinações de profiles em testes no mínimo.

8. Erros comuns

Definir spring.profiles.active em arquivo de profile. A partir do Spring Boot 2.4, declarar spring.profiles.active ou spring.profiles.include em application-dev.yml (ou em um documento com on-profile) causa erro na inicialização:

InvalidConfigDataPropertyException: Property 'spring.profiles.active' imported from
location 'class path resource [application-dev.yml]' is invalid in a profile specific resource

Use grupos de profiles (seção 6) em vez disso.

Hard-code spring.profiles.active=dev em application.yml. Esse arquivo é empacotado no jar. Se alguém esquecer de passar a variável de ambiente no servidor, prod rodará com configuração de dev. Use spring.profiles.default para o ambiente local e sempre ative profiles explicitamente no servidor.

Colocar segredos em application-prod.yml. Senhas de banco de dados e chaves de API não devem estar no Git. Use placeholders como ${DB_PASSWORD} e forneça o valor via variável de ambiente ou gerenciador de segredos.

Nenhum bean correspondendo ao profile. Se você rodar com profile staging mas só tem implementações para dev, test, prod, a aplicação reportará erro:

Parameter 0 of constructor in OrderService required a bean of type
'NotificationService' that could not be found.

Considere um bean fallback com @Profile("!prod") ou adicione uma implementação para o novo profile.

Abusar de profiles para feature flags. Profiles são bons para descrever ambientes. Se você quer ligar/desligar um recurso independentemente do ambiente, @ConditionalOnProperty geralmente é mais claro:

@Bean
@ConditionalOnProperty(name = "app.feature.loyalty-points", havingValue = "true")
public LoyaltyService loyaltyService() {
    return new LoyaltyService();
}

Quando você tem profiles como prod-with-loyalty ou dev-no-cache, é sinal de que deveria migrar para properties.

9. Conclusão

Profiles permitem separar configurações e comportamento por ambiente sem modificar código. Você viu como usar application-{profile}.yml para sobrescrever configurações, @Profile para escolher implementações, grupos de profiles para reunir múltiplos profiles, e @ActiveProfiles para testes.

Alguns princípios para lembrar: coloque configurações comuns em application.yml, escreva apenas as diferenças nos arquivos de profile. Ative profiles no servidor via variável de ambiente ou argumentos da linha de comando. Nunca coloque segredos no Git. E sempre verifique a linha de log The following profiles are active após cada deploy.

spring-bootprofileconfigurationtesting