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.ymlwird nur gelesen, wenn dasdev-Profil aktiv ist. - Welche Beans werden erstellt: Eine Bean mit
@Profile("prod")wird nur registriert, wenn dasprod-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: devbedeutet, dass Spring dasdev-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 erstapplication.yml, dannapplication-prod.yml. Werte in der profilspezifischen Datei überschreiben die gemeinsamen Werte. Daher wirdapp.mail.hostzum Wert der UmgebungsvariableMAIL_HOST, währendapp.mail.fromweiterhinno-reply@shop.vnist. server.port: 0intestlä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ßerprod.dev | test: erstelle wenndevodertestaktiv 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 dastest-Profil für den Context dieser Klasse, unabhängig vonspring.profiles.default.- Der erste Test bestätigt, dass Spring
InMemoryNotificationServicestatt 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.hostausapplication-test.ymlden Wertlocalhostausapplication.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.