Profils avec Spring Boot

La notion de profile permet d’activer ou désactiver des beans en fonction du contexte de déploiement.

Un exemple courant est la spécification d’une datasource qui peut être différente entre les environnements de développement, de test et de déploiement.

Fonctionnement de base

@Profile

Un bean annoté avec @Profile ne sera valable que si le profil spécifié est actif dans le contexte Spring.

@Component
@Profile("dev")
public class DevOnlyBean implements SomeBean {
  ...
}
@Component
@Profile("prod")
public class ProdOnlyBean implements SomeBean {
  ...
}

Active profiles

Il y a plusieurs façons d’activer des profils.

  • par propriété système

~# java -jar -Dspring.profiles.active=prod app.jar
  • par paramètre de programme

~# java -jar app.jar --spring.profiles.active=prod

Par le passé, il était aussi possible de spécifier des profils additionnels avec la propriété ou le paramètre spring.profiles.include. Cette possibilité a disparu avec Spring Boot 2.4, au profit de la notion de groupe de profils.

Default profiles

Si aucun profil n’est spécifié au démarrage, ce sont les beans et configurations marquée en "default" qui sont utilisés. Il est aussi possible de changer ça avec la propriété ou le paramètre spring.profiles.default.

~# java -jar -Dspring.profiles.default=none app.jar

Je ne sais pas trop à quoi ça peut servir, mais je le note au cas où…​

Configuration

Avec Spring Boot, la configuration est dans un fichier application.properties ou application.yml.

Ajout de profil

Il est possible de spécifier les profils dans le fichier de configuration.

application.properties
spring.profiles.active=cloud
...

Si des profils ont déjà été spécifié via la propriété spring.profiles.active, ceux du fichier de configuration viennent s’ajouter.

Configuration spécifique

En plus de rattacher des beans à un profil, on peut aussi faire une configuration spécifique. Celle-ci se fait dans un fichier nommé application-{profile}.properties ou application-{profile}.yml.

Par exemple, on peut configurer une datasource uniquement pour le développement.

application-dev.yml
spring:
  datasource:
    jdbc-url: jdbc:postgresql://localhost:5432/postgres
    username: 'postgres'
    password: 'pgpwd'

On ne peut pas activer d’autres profils dans un fichier de configuration dédié à un profil.

Configuration multi-profils

Depuis Spring Boot 2.4, il est aussi possible de regrouper les configurations de plusieurs profils dans le même fichier. Pour ça, on utilise la notion de fichiers multi-documents de YAML. Et chaque document du fichier indique quel profil il configure.

application.yml
...
---
spring:
  config:
    activate:
      on-profile: dev
  datasource:
    jdbc-url: jdbc:postgresql://localhost:5432/postgres
    username: postgres
    password: pgpwd

Spring Boot supporte aussi ça avec des fichiers properties, avec le séparateur #---.

application.properties
...
#---
spring.config.activate.on-profile=dev
spring.datasource.jdbc-url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=postgres
spring.datasource.password=pgpwd

API

On peut lire la liste des profils actifs via le bean d’environnement.

@Component
public class SomeBean {

  private final Environment environment;

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

  @PostConstruct
  public void init() {
    var defaultProfiles = environment.getDefaultProfiles();
    var activeProfiles = environment.getActiveProfiles();
  }

}

On peut aussi modifier la liste de profils actifs par programmation, mais uniquement avant le démarrage du contexte Spring.

  • Démarrage d’une application Spring Boot

@SpringBootApplication
public class SpringExampleApplication {

  public static void main(String[] args) {
    new SpringApplicationBuilder()
        .sources(SpringExampleApplication.class)
        .profiles("profile-a", "profile-b")
        .build()
        .run(args);
  }

}
  • Initialisation du contexte Spring (à déclarer au démarrage de l’application Spring Boot, dans web.xml ou dans la classe de test)

public class CommonProfileInitializer
    implements ApplicationContextInitializer<ConfigurableApplicationContext> {

  @Override
  public void initialize(ConfigurableApplicationContext context) {
    context.getEnvironment()
           .addActiveProfile("common");
  }

}
  • Post-processeur
    (à déclarer dans le fichier META-INF/spring.factories)

public class CommonProfilePostProcessor
    implements EnvironmentPostProcessor {

  @Override
  public void postProcessEnvironment(
      ConfigurableEnvironment environment,
      SpringApplication application) {
    environment.addActiveProfile("common");
  }

}
META-INF/spring.factories
org.springframework.boot.env.EnvironmentPostProcessor=info.jtips.spring.CommonProfilePostProcessor

Tests automatisés

Utiliser les profils pour les tests JUnit passe évidemment par l’utilisation d’un profil "test" !

@ActiveProfiles

Pour choisir les profils actifs au démarrage d’un test, on utilise habituellement l’annotation @ActiveProfiles.

@SpringBootTest
@ActiveProfiles("test")
class MainBeanTest {
  ...
}

Le fonctionnement de cette annotation est un peu particulier puisque les profils spécifiés ici remplacent tous les autres. Les propriétés système sont ignorées, ainsi que les profils activés dans la configuration application.properties ou application.yml. Ce comportement est définit dans l'ActiveProfilesResolver par défaut.

ActiveProfilesResolver

Pour qu’une autre source de profils soit prise en compte avec l’annotation @ActiveProfiles, il faut adopter un ActiveProfilesResolver personnalisé.

@SpringBootTest
@ActiveProfiles(profiles="test", resolver=EnhancedActiveProfileResolver.class)
class MainBeanTest {
  ...
}

Pour ce test, c’est dans la classe EnhancedActiveProfileResolver qu’on définit les sources de profils. Dans l’implémentation ci-dessous, on combine les profils de l’annotation @ActiveProfiles avec ceux de la propriété système spring.profiles.active.

public class EnhancedActiveProfileResolver
        implements ActiveProfilesResolver {

    public static final String PROPERTY_KEY = "spring.profiles.active";

    private final DefaultActiveProfilesResolver defaultActiveProfilesResolver
                = new DefaultActiveProfilesResolver();

    @Override
    public String[] resolve(Class<?> testClass) {
        return Stream
            .concat(
                Stream.of(defaultActiveProfilesResolver.resolve(testClass)),
                Stream.of(this.getPropertyProfiles())
            )
            .toArray(String[]::new);
    }

    private String[] getPropertyProfiles() {
        return System.getProperties().containsKey(PROPERTY_KEY)
                ? System.getProperty(PROPERTY_KEY).split("\\s*,\\s*")
                : new String[0];
    }
}

Cette solution a encore des défauts. En effet, cette classe n’a aucune information de contexte Spring, elle ne peut donc pas récupérer les profils qui seraient activés dans le fichier de configuration application.properties ou application.yml.

ApplicationContextInitializer

On a vu la possibilité ci-dessus la possibilité d’ajouter un profil dans une classe d’initialisation du contexte. Cette solution a l’avantage de conserver tous les autres profils.

Il est possible de déclarer cette initialisation dans le test.

@ContextConfiguration(initializers = ProfileInitializer.class)
class MainBeanWithInitializerTest {
  //...
}

EnvironmentPostProcessor

On a aussi vu la possibilité d’ajouter un profil dans une classe post-processeur. Cette solution conserve aussi tous les autres profils.

Pour activer le post-processeur, il faut l’activer dans un fichier META-INF/spring.factories. Pour ne l’activer qu’en test, il suffit de le déclarer dans le META-INF/spring.factories de test.

ContextCustomizerFactory

Une autre solution passe par le développement d’un ContextCustomizerFactory. Cette solution a aussi l’avantage de conserver tous les autres profils. Elle a l’autre avantage d’être globale, et évite d’ajouter une annotation à chaque test.

public class CustomContextCustomizerFactory
        implements ContextCustomizerFactory {

  @Override
  public ContextCustomizer createContextCustomizer(
                Class<?> testClass,
                List<ContextConfigurationAttributes> configAttributes) {
    return (context, mergedConfig) -> {
      context.getEnvironment().addActiveProfile("test");
    };
  }
}

Cette fabrique doit être déclarée dans META-INF/spring.factories.

META-INF/spring.factories
org.springframework.test.context.ContextCustomizerFactory=info.jtips.spring.profiles.CustomContextCustomizerFactory

Si toutefois on continue d’utiliser l’annotation @ActiveProfiles, le personnalisateur de contexte ajoute son profil à ceux de l’annotation. Et si c’est un profil identique, il n’a pas d’effet, puisque les doublons sont éliminés.

Synthèse

Dans cette page, nous avons vu les cas d’usage suivants :

  • profils pour un processus : spring.profiles.active en propriété système ou paramètre du processus,

  • profils pour tous les processus (hors tests) : springApplicationBuilder.profiles(…​),

  • profils pour les tests : @ActiveProfiles ou CustomContextCustomizerFactory,

  • profils pour tous les processus (hors tests) : application.yml et tests avec @ActiveProfiles,

  • profils pour tous les processus (tests compris) : application.yml et tests avec CustomContextCustomizerFactory.

Spring Boot

Parmi les techniques citées, les suivantes sont spécifiques à Spring Boot :

  • paramètre du processus

  • springApplicationBuilder.profiles(…​)

  • application.yml

Références