Profile dans Spring Boot : configuration dev, test, prod

1. Vue d’ensemble

Une application s’exécute généralement dans plusieurs environnements. Votre machine utilise une base de données H2. Le serveur de test utilise PostgreSQL dédié. La production utilise un vrai cluster avec SMTP réel. Si vous modifiez manuellement le fichier de configuration à chaque déploiement, tôt ou tard vous allez accidentellement pousser une configuration dev en production.

Spring Boot résout ce problème avec les profiles. Après avoir lu cet article, vous saurez comment séparer les fichiers de configuration par profile, activer/désactiver les beans avec @Profile, activer les profiles de plusieurs façons, écrire des tests avec @ActiveProfiles et éviter les erreurs courantes.

2. Qu’est-ce qu’un profile ?

Un profile est un label nommé, par exemple dev, test, prod. Spring utilise ce label pour décider de deux choses :

  • Quelle configuration est chargée : application-dev.yml n’est lue que quand le profile dev est actif.
  • Quels beans sont créés : un bean avec @Profile("prod") n’est enregistré que quand le profile prod est actif.

Si aucun profile n’est activé, Spring utilise le profile par défaut nommé default. Les beans sans @Profile sont toujours créés, quel que soit le profile actif.

Vous avez besoin uniquement des dépendances web de base pour les exemples de cet article :

<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. Fichiers de configuration par profile

3.1. Séparation en plusieurs fichiers

La convention de nommage est application-{profile}.yml (ou .properties). Le fichier application.yml contient la configuration commune, toujours chargée :

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

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

Les fichiers par profile ne contiennent que les différences :

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

Explication :

  • spring.profiles.default: dev signifie que quand vous n’activez aucun profile, Spring utilise dev à la place de default. Votre machine de dev s’exécute immédiatement sans nécessiter de paramètres.
  • Quand le profile prod est actif, Spring charge d’abord application.yml puis application-prod.yml. Les valeurs dans le fichier de profile écrasent la configuration commune. Donc app.mail.host prend la valeur de la variable d’environnement MAIL_HOST, tandis que app.mail.from reste no-reply@shop.vn.
  • server.port: 0 dans test laisse Spring choisir un port aléatoire, évitant les conflits lors de l’exécution parallèle.

3.2. Fusion en un seul fichier YAML

Pour avoir moins de fichiers, vous pouvez utiliser plusieurs documents YAML, séparés par --- :

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

Le second document n’a d’effet que quand prod est actif. Cette approche est plus compacte quand chaque profile a peu de différences. Pour une configuration longue, la séparation en fichiers reste plus lisible et plus facile à relire.

Note : Dans les fichiers .properties, vous utilisez #--- pour séparer les documents à la place de ---.

4. Activation du profile

Il y a plusieurs façons. L’ordre de priorité suit la règle commune de Spring Boot : arguments ligne de commande > variables d’environnement > fichier de configuration.

Arguments ligne de commande :

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

Propriété système JVM :

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

Variable d’environnement, la plus courante sur Docker et Kubernetes :

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

Lors de l’exécution avec le plugin Maven :

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

Via le code, utile quand vous voulez toujours ajouter un profile spécifique :

@SpringBootApplication
public class ShopApplication {

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

setAdditionalProfiles ajoute un profile à la liste, il ne remplace pas les profiles déjà activés depuis l’extérieur. Exécuter avec --spring.profiles.active=prod activera à la fois prod et metrics.

Vous pouvez activer plusieurs profiles à la fois avec une virgule : --spring.profiles.active=prod,eu. Si deux fichiers définissent la même clé, le profile qui vient après l’emporte. Dans cet exemple, application-eu.yml écrase application-prod.yml.

Quand l’application démarre, le log vous indique quel profile est actif :

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

Si aucun profile n’est actif et que spring.profiles.default n’est pas défini, vous verrez :

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

Jetez toujours un coup d’œil à cette ligne de log après chaque déploiement. C’est le moyen le plus rapide de détecter si le serveur utilise le mauvais profile.

5. Activation/désactivation des beans avec @Profile

La configuration différente n’est que la moitié du problème. Parfois vous avez besoin d’une implémentation entièrement différente. Par exemple : dev affiche juste les emails sur la console, test les stocke en mémoire pour vérification, prod les envoie via SMTP.

Définissez une interface commune :

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

Implémentation pour dev :

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

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

Implémentation pour test, stocke les messages pour les 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);
    }
}

Implémentation pour 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) {
        // En réalité, on utiliserait JavaMailSender ; ici c'est juste une démonstration
        System.out.println("[SMTP " + host + "] Envoi à " + to);
    }
}

La classe qui l’utilise n’a pas besoin de savoir dans quel environnement elle 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, "Commande #" + orderId + " a été créée");
    }
}

À un moment donné, un seul des trois beans NotificationService est créé. Spring injecte le bon bean dans OrderService. Exécuter avec le profile dev et appeler placeOrder("an@shop.vn", 42), vous obtenez :

[DEV] Envoi à an@shop.vn: Commande #42 a été créée

5.1. Expressions de profile

@Profile supporte les expressions logiques :

@Configuration
public class SecurityConfig {

    @Bean
    @Profile("!prod")
    public CommandLineRunner seedDemoData() {
        return args -> System.out.println("Chargement des données de démonstration...");
    }

    @Bean
    @Profile("dev | test")
    public CommandLineRunner relaxedCors() {
        return args -> System.out.println("CORS ouvert pour toutes les origines");
    }
}
  • !prod : crée le bean sur tous les profiles sauf prod.
  • dev | test : crée quand dev ou test est actif. Il y a aussi & pour “et”, par exemple prod & eu.

@Profile peut être mis sur une classe @Component/@Configuration ou sur chaque method @Bean. Sur @Configuration, tous les beans à l’intérieur subissent cette condition.

5.2. Vérifier le profile dans le code

Parfois vous avez besoin de faire une branche dans la logique. Utilisez 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("En cours d'exécution en PRODUCTION");
        }
    }
}

Utilisez cela avec modération. Si votre code est rempli de if (prod), vous devriez plutôt séparer en beans distincts avec @Profile.

6. Groupes de profiles

Au fur et à mesure que le système grandit, vous voudrez peut-être diviser la configuration, par exemple proddb pour la base de données et prodmq pour le courtier de messages. Les groupes de profiles permettent d’activer un ensemble avec un seul nom :

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

Maintenant, exécuter --spring.profiles.active=prod activera prod, proddb et prodmq. Spring charge successivement application-prod.yml, application-proddb.yml, application-prodmq.yml. L’opérateur n’a besoin de mémoriser qu’un seul nom.

7. Tests avec @ActiveProfiles

Dans les tests, vous sélectionnez le profile avec @ActiveProfiles. Le test ci-dessous vérifie deux choses : le bon bean est injecté, et la configuration de application-test.yml est en vigueur.

@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|Commande #7 a été créée");
    }

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

Explication :

  • @ActiveProfiles("test") active le profile test pour le contexte de cette classe, peu importe ce qu’est spring.profiles.default.
  • Le premier test confirme que Spring choisit InMemoryNotificationService au lieu d’envoyer de vrais emails.
  • Le second test utilise le bean en mémoire pour vérifier le contenu du message sans avoir besoin de mock.
  • Le troisième test prouve que app.mail.host de application-test.yml a écrasé la valeur localhost dans application.yml.

Note : Chaque combinaison de profiles différente crée son propre contexte Spring. Si chaque classe de test utilise un ensemble de profiles différent, le cache de contexte ne peut pas être réutilisé et la suite de tests s’exécute beaucoup plus lentement. Gardez le nombre de combinaisons de profiles dans les tests au minimum.

8. Erreurs courantes

Définir spring.profiles.active dans un fichier par profile. À partir de Spring Boot 2.4, déclarer spring.profiles.active ou spring.profiles.include dans application-dev.yml (ou dans un document avec on-profile) empêchera l’application de démarrer :

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

Utilisez plutôt les groupes de profiles (section 6).

Hard-coder spring.profiles.active=dev dans application.yml. Ce fichier est intégré au jar. Si quelqu’un oublie de passer la variable d’environnement sur le serveur, la production s’exécutera avec la configuration de dev. Utilisez spring.profiles.default pour l’environnement local, et activez toujours le profile explicitement sur le serveur.

Mettre des secrets dans application-prod.yml. Les mots de passe de base de données ou les clés API ne doivent pas être dans Git. Utilisez des placeholders comme ${DB_PASSWORD} et fournissez la valeur via une variable d’environnement ou un gestionnaire de secrets.

Aucun bean ne correspond au profile. Si vous exécutez avec le profile staging mais que vous n’avez une implémentation que pour dev, test, prod, l’application signalera une erreur :

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

Envisagez un bean de secours avec @Profile("!prod") ou ajoutez une implémentation pour le nouveau profile.

Abuser des profiles pour les feature flags. Les profiles conviennent pour décrire l’environnement. Si vous voulez activer/désactiver une fonctionnalité indépendamment de l’environnement, @ConditionalOnProperty est généralement plus clair :

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

Si vous avez des profiles du type prod-with-loyalty ou dev-no-cache, c’est un signe que vous devriez passer à des propriétés.

9. Conclusion

Les profiles vous permettent de séparer la configuration et le comportement par environnement sans modifier le code. Vous avez vu comment utiliser application-{profile}.yml pour écraser la configuration, @Profile pour choisir une implémentation, les groupes de profiles pour regrouper plusieurs profiles, et @ActiveProfiles pour les tests.

Quelques principes à retenir : mettez la configuration commune dans application.yml, écrivez uniquement les différences dans les fichiers de profile. Activez le profile sur le serveur via une variable d’environnement ou un argument ligne de commande. Ne mettez pas de secrets dans Git. Et vérifiez toujours la ligne de log The following profiles are active après chaque déploiement.

spring-bootprofileconfigurationtesting