Profile in Spring Boot: dev-, test- und prod-Konfiguration

1. Übersicht

Eine Anwendung läuft typischerweise in mehreren Umgebungen. Auf Ihrem Rechner nutzen Sie H2-Datenbank. Der Test-Server nutzt PostgreSQL. Production nutzt einen echten Cluster mit echtem SMTP. Wenn Sie die Konfigurationsdatei bei jedem Deploy manuell ändern, werden Sie früher oder später versehentlich Dev-Einstellungen auf Production deployen.

Spring Boot löst das mit Profilen. Nach diesem Artikel wissen Sie, wie Sie Konfigurationsdateien nach Profile aufteilen, Beans mit @Profile aktivieren/deaktivieren, Profile auf verschiedene Arten aktivieren, Tests mit @ActiveProfiles schreiben und typische Fehler vermeiden.

2. Was ist ein Profil?

Ein Profil ist ein benanntes Label, wie dev, test oder prod. Spring nutzt dieses Label, um zwei Dinge zu entscheiden:

  • Welche Konfiguration wird geladen: application-dev.yml wird nur gelesen, wenn das dev-Profil aktiv ist.
  • Welche Beans werden erstellt: Eine Bean mit @Profile("prod") wird nur registriert, wenn das prod-Profil aktiv ist.

Wenn kein Profil aktiviert ist, nutzt Spring ein Standard-Profil namens default. Beans ohne @Profile werden immer erstellt, unabhängig davon, welches Profil aktiv ist.

Sie brauchen nur die Basis-Web-Abhängigkeit für die Beispiele in diesem Artikel:

<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. Konfigurationsdateien nach Profil

3.1. In mehrere Dateien aufteilen

Die Namenskonvention ist application-{profile}.yml (oder .properties). Die Datei application.yml enthält gemeinsame Konfiguration, die immer geladen wird:

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

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

Die profilspezifischen Dateien enthalten nur die Unterschiede:

# 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}

Erklärung:

  • spring.profiles.default: dev bedeutet, dass Spring das dev-Profil nutzt, wenn Sie kein Profil explizit aktivieren. Auf Ihrer Dev-Maschine läuft die App sofort ohne Parameter.
  • Wenn das prod-Profil aktiv ist, lädt Spring erst application.yml, dann application-prod.yml. Werte in der profilspezifischen Datei überschreiben die gemeinsamen Werte. Daher wird app.mail.host zum Wert der Umgebungsvariable MAIL_HOST, während app.mail.from weiterhin no-reply@shop.vn ist.
  • server.port: 0 in test lässt Spring einen zufälligen Port wählen, um Konflikte beim parallelen Ausführen zu vermeiden.

3.2. In eine YAML-Datei kombinieren

Wenn Sie weniger Dateien mögen, können Sie mehrere YAML-Dokumente nutzen, getrennt durch ---:

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

Das zweite Dokument ist nur aktiv, wenn das prod-Profil aktiv ist. Das ist kompakt, wenn jedes Profil nur wenige Unterschiede hat. Bei längeren Konfigurationen ist das Aufteilen in separate Dateien einfacher zu lesen und zu reviewen.

Hinweis: In .properties-Dateien nutzen Sie die Zeile #---, um Dokumente zu trennen, statt ---.

4. Profile aktivieren

Es gibt mehrere Wege. Die Priorität folgt der allgemeinen Spring-Boot-Regel: Kommandozeilenparameter > Umgebungsvariablen > Konfigurationsdatei.

Kommandozeilenparameter:

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

JVM System-Property:

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

Umgebungsvariable, der häufigste Weg auf Docker und Kubernetes:

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

Mit Maven-Plugin:

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

Im Code, nützlich wenn Sie immer ein bestimmtes Profil hinzufügen möchten:

@SpringBootApplication
public class ShopApplication {

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

setAdditionalProfiles fügt Profile hinzu, ersetzt nicht die extern aktivierten Profile. Mit --spring.profiles.active=prod sind sowohl prod als auch metrics aktiv.

Sie können mehrere Profile gleichzeitig mit Komma aktivieren: --spring.profiles.active=prod,eu. Wenn zwei Dateien denselben Schlüssel definieren, gewinnt das später stehende Profil. In diesem Beispiel überschreibt application-eu.yml die Werte aus application-prod.yml.

Beim Start zeigt das Log, welche Profile aktiv sind:

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

Wenn kein Profil aktiv ist und Sie auch spring.profiles.default nicht setzen, sehen Sie:

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

Schauen Sie nach jedem Deploy auf diese Log-Zeile. Das ist der schnellste Weg, um zu erkennen, ob der Server mit dem falschen Profil läuft.

5. Beans mit @Profile aktivieren/deaktivieren

Unterschiedliche Konfiguration ist nur die halbe Lösung. Manchmal brauchen Sie völlig unterschiedliche Implementierungen. Beispiel: Dev gibt E-Mails nur auf der Konsole aus, Test speichert sie im Speicher zum Prüfen, Prod sendet sie per SMTP.

Definieren Sie ein gemeinsames Interface:

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

Implementierung für dev:

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

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

Implementierung für test, speichert Nachrichten zum Überprüfen:

@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);
    }
}

Implementierung für 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) {
        // In der Praxis würde man JavaMailSender nutzen; hier nur zur Illustration
        System.out.println("[SMTP " + host + "] Nachricht an " + to);
    }
}

Die verbrauchende Klasse muss nicht wissen, in welcher Umgebung sie läuft:

@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, "Bestellung #" + orderId + " wurde erstellt");
    }
}

Zu jedem Zeitpunkt wird nur eine der drei NotificationService-Beans erstellt. Spring injiziert die richtige Bean in OrderService. Mit Profil dev und dem Aufruf placeOrder("an@shop.vn", 42) erhalten Sie:

[DEV] Nachricht an an@shop.vn: Bestellung #42 wurde erstellt

5.1. Profil-Ausdrücke

@Profile unterstützt logische Ausdrücke:

@Configuration
public class SecurityConfig {

    @Bean
    @Profile("!prod")
    public CommandLineRunner seedDemoData() {
        return args -> System.out.println("Lade Demo-Daten...");
    }

    @Bean
    @Profile("dev | test")
    public CommandLineRunner relaxedCors() {
        return args -> System.out.println("CORS für alle Origins offen");
    }
}
  • !prod: erstelle die Bean in jedem Profil außer prod.
  • dev | test: erstelle wenn dev oder test aktiv ist. Es gibt auch & für “und”, z.B. prod & eu.

@Profile kann auf einer Klasse mit @Component/@Configuration oder auf einer @Bean-Methode stehen. Wenn Sie es auf @Configuration setzen, unterliegen alle Beans darin der gleichen Bedingung.

5.2. Profil im Code prüfen

Manchmal müssen Sie in der Logik verzweigen. Nutzen Sie dafür 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("Läuft in PRODUCTION");
        }
    }
}

Nutzen Sie das sparsam. Wenn Ihr Code voll von if (prod) ist, sollten Sie besser getrennte Beans mit @Profile verwenden.

6. Profil-Gruppen

Wenn das System wächst, könnten Sie Profile wie proddb für die Datenbank und prodmq für den Message Broker haben. Profil-Gruppen lassen Sie mehrere Profile mit einem Namen aktivieren:

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

Jetzt aktiviert --spring.profiles.active=prod die Profile prod, proddb und prodmq. Spring lädt nacheinander application-prod.yml, application-proddb.yml, application-prodmq.yml. Der Operator muss nur einen Namen merken.

7. Tests mit @ActiveProfiles schreiben

In Tests wählen Sie das Profil mit @ActiveProfiles. Der folgende Test überprüft zwei Dinge: dass die richtige Bean injiziert wird, und dass die Konfiguration aus application-test.yml wirksam ist.

@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|Bestellung #7 wurde erstellt");
    }

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

Erklärung:

  • @ActiveProfiles("test") aktiviert das test-Profil für den Context dieser Klasse, unabhängig von spring.profiles.default.
  • Der erste Test bestätigt, dass Spring InMemoryNotificationService statt echtem Emailversand nutzt.
  • Der zweite Test nutzt die In-Memory-Bean, um den Nachrichteninhalt zu prüfen, ohne zu mocken.
  • Der dritte Test beweist, dass app.mail.host aus application-test.yml den Wert localhost aus application.yml überschreibt.

Hinweis: Jede unterschiedliche Profil-Kombination erstellt einen neuen Spring-Context. Wenn jede Test-Klasse ein anderes Profil-Set nutzt, wird der Context-Cache nicht wiederverwendet und die Test-Suite wird viel langsamer. Halten Sie die Anzahl der Profil-Kombinationen in Tests minimal.

8. Häufige Fehler

spring.profiles.active in profilspezifischen Dateien setzen. Seit Spring Boot 2.4 führt die Angabe von spring.profiles.active oder spring.profiles.include in application-dev.yml (oder in einem Dokument mit on-profile) zu einem Startup-Fehler:

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

Nutzen Sie stattdessen Profil-Gruppen (Abschnitt 6).

spring.profiles.active=dev in application.yml hardcodieren. Diese Datei ist in der jar enthalten. Wenn jemand die Umgebungsvariable auf dem Server vergisst, läuft Production mit Dev-Konfiguration. Nutzen Sie spring.profiles.default für die lokale Umgebung und aktivieren Sie Profile auf dem Server immer explizit.

Geheimnisse in application-prod.yml speichern. Datenbank-Passwörter oder API-Keys gehören nicht ins Git. Nutzen Sie Platzhalter wie ${DB_PASSWORD} und liefern Sie Werte über Umgebungsvariablen oder einen Secret-Manager.

Keine Bean passt zum Profil. Wenn Sie mit Profil staging laufen, aber nur Implementierungen für dev, test, prod haben, schlägt die Anwendung fehl:

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

Erwägen Sie eine Fallback-Bean mit @Profile("!prod") oder fügen Sie eine Implementierung für das neue Profil hinzu.

Profile für Feature-Flags missbrauchen. Profile eignen sich für die Beschreibung der Umgebung. Wenn Sie eine Funktion unabhängig von der Umgebung aktivieren/deaktivieren möchten, ist @ConditionalOnProperty meist klarer:

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

Wenn Sie Profile wie prod-with-loyalty oder dev-no-cache haben, sollten Sie auf Properties wechseln.

9. Fazit

Profile helfen Ihnen, Konfiguration und Verhalten nach Umgebung zu trennen, ohne Code zu ändern. Sie haben gesehen, wie Sie application-{profile}.yml nutzen, um Konfiguration zu überschreiben, @Profile nutzen, um Implementierungen zu wählen, Profil-Gruppen nutzen, um mehrere Profile zu bündeln, und @ActiveProfiles für Tests nutzen.

Ein paar Regeln zum Merken: Gemeinsame Konfiguration in application.yml, nur Unterschiede in profilspezifischen Dateien. Profile auf dem Server über Umgebungsvariablen oder Kommandozeilenparameter aktivieren. Keine Geheimnisse ins Git. Und überprüfen Sie nach jedem Deploy die Log-Zeile The following profiles are active.

spring-bootprofileconfigurationtesting