Profiles en Spring Boot: configuración dev, test, prod
1. Descripción general
Una aplicación típicamente se ejecuta en múltiples entornos. Tu máquina usa base de datos H2. El servidor de test usa PostgreSQL separado. Producción usa un cluster real con SMTP real. Si modificas manualmente el archivo de configuración cada vez que despliegas, tarde o temprano terminarás subiendo la configuración de dev a producción.
Spring Boot resuelve esto usando profiles. Después de leer este artículo, sabrás cómo separar archivos de configuración por profile, habilitar/deshabilitar beans con @Profile, activar profiles de múltiples formas, escribir tests con @ActiveProfiles y evitar errores comunes.
2. ¿Qué es un Profile?
Un profile es una etiqueta con nombre, por ejemplo dev, test, prod. Spring usa esta etiqueta para decidir dos cosas:
- Qué configuración se carga:
application-dev.ymlsolo se lee cuando el profiledevestá activo. - Qué beans se crean: un bean anotado con
@Profile("prod")solo se registra cuando el profileprodestá activo.
Si ningún profile está activado, Spring usa el profile por defecto llamado default. Un bean sin anotación @Profile siempre se crea, independientemente de qué profile esté activo.
Solo necesitas las dependencias web básicas para los ejemplos en este artículo:
<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. Archivos de configuración por profile
3.1. Separar en múltiples archivos
La convención es nombrar archivos como application-{profile}.yml (o .properties). El archivo application.yml contiene la configuración común que siempre se carga:
# application.yml
spring:
application:
name: shop-service
profiles:
default: dev
app:
mail:
from: no-reply@shop.vn
host: localhost
Los archivos según el profile solo incluyen las diferencias:
# 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}
Explicación:
spring.profiles.default: devsignifica que cuando no activas ningún profile, Spring usadeven lugar dedefault. Tu máquina de desarrollo funciona de inmediato sin parámetros adicionales.- Cuando el profile
prodestá activo, Spring carga primeroapplication.ymly luegoapplication-prod.yml. Los valores en el archivo del profile sobrescriben los valores comunes. Por lo tanto,app.mail.hostse convierte en el valor de la variable de entornoMAIL_HOST, mientras queapp.mail.fromconservano-reply@shop.vn. server.port: 0entestpermite que Spring elija un puerto aleatorio, evitando conflictos al ejecutar tests en paralelo.
3.2. Combinar en un único archivo YAML
Si quieres menos archivos, puedes usar YAML multi-documento, separado por ---:
# application.yml
app:
mail:
host: localhost
---
spring:
config:
activate:
on-profile: prod
app:
mail:
host: smtp.shop.vn
El segundo documento solo tiene efecto cuando prod está activo. Este método es más compacto cuando cada profile tiene pocas diferencias. Cuando la configuración es larga, separar en archivos sigue siendo más fácil de leer y revisar.
Nota: En archivos
.properties, usa#---para separar documentos en lugar de---.
4. Activar profiles
Hay varias formas. El orden de precedencia sigue la regla general de Spring Boot: parámetros de línea de comandos > variables de entorno > archivos de configuración.
Parámetros de línea de comandos:
java -jar shop-service.jar --spring.profiles.active=prod
Propiedades del sistema JVM:
java -Dspring.profiles.active=prod -jar shop-service.jar
Variables de entorno, la forma más común en Docker y Kubernetes:
export SPRING_PROFILES_ACTIVE=prod
java -jar shop-service.jar
Al ejecutar con el plugin Maven:
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
Mediante código, útil cuando quieres siempre agregar un 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 agrega profiles a la lista, no reemplaza los profiles activados desde fuera. Al ejecutar con --spring.profiles.active=prod, tanto prod como metrics están activos.
Puedes activar múltiples profiles a la vez usando comas: --spring.profiles.active=prod,eu. Si dos archivos definen la misma clave, el profile que viene después gana. En este ejemplo, application-eu.yml sobrescribe application-prod.yml.
Cuando la aplicación inicia, el log te dice qué profiles están activos:
INFO c.e.shop.ShopApplication : The following 1 profile is active: "prod"
Si no hay profiles activos y tampoco has establecido spring.profiles.default, verás:
INFO c.e.shop.ShopApplication : No active profile set, falling back to 1 default profile: "default"
Siempre revisa esta línea de log cuando despliegas. Es la forma más rápida de detectar si el servidor está ejecutando el profile equivocado.
5. Habilitar/deshabilitar beans con @Profile
Diferentes configuraciones son solo la mitad del problema. A veces necesitas implementaciones completamente diferentes. Por ejemplo: dev solo imprime emails en la consola, test los guarda en memoria para verificar, prod los envía por SMTP.
Define una interfaz común:
public interface NotificationService {
void send(String to, String message);
}
Implementación para dev:
@Service
@Profile("dev")
public class ConsoleNotificationService implements NotificationService {
@Override
public void send(String to, String message) {
System.out.println("[DEV] Enviando a " + to + ": " + message);
}
}
Implementación para test, guarda mensajes para verificaciones:
@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);
}
}
Implementación 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) {
// En realidad usarías JavaMailSender; aquí es solo una demostración
System.out.println("[SMTP " + host + "] Enviando a " + to);
}
}
La clase que utiliza el servicio no necesita saber en qué entorno se ejecuta:
@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 + " ha sido creado");
}
}
En cualquier momento, solo uno de los tres beans NotificationService se crea. Spring inyecta el bean correcto en OrderService. Al ejecutar con el profile dev y llamar a placeOrder("an@shop.vn", 42), obtienes:
[DEV] Enviando a an@shop.vn: Pedido #42 ha sido creado
5.1. Expresiones de profile
@Profile soporta expresiones lógicas:
@Configuration
public class SecurityConfig {
@Bean
@Profile("!prod")
public CommandLineRunner seedDemoData() {
return args -> System.out.println("Cargando datos de demostración...");
}
@Bean
@Profile("dev | test")
public CommandLineRunner relaxedCors() {
return args -> System.out.println("CORS abierto para cualquier origen");
}
}
!prod: crea el bean en todos los profiles exceptoprod.dev | test: crea cuandodevotestestá activo. También hay¶ “y”, por ejemploprod & eu.
@Profile se puede poner en clases @Component/@Configuration o en métodos @Bean individuales. Si se coloca en @Configuration, todos los beans dentro están sujetos a esa condición.
5.2. Comprobar el profile en código
A veces necesitas bifurcar la lógica. Usa 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("Ejecutándose en PRODUCCIÓN");
}
}
}
Usa este enfoque con moderación. Si tu código está lleno de if (prod), deberías separar en beans diferentes con @Profile.
6. Grupos de profiles
A medida que el sistema crece, podrías querer dividir la configuración, por ejemplo proddb para base de datos y prodmq para message broker. Los grupos de profiles te permiten activar un conjunto completo con un solo nombre:
# application.yml
spring:
profiles:
group:
prod: proddb, prodmq
dev: devdb
Ahora ejecutar --spring.profiles.active=prod activa prod, proddb y prodmq. Spring carga secuencialmente application-prod.yml, application-proddb.yml, application-prodmq.yml. El operador solo necesita recordar un nombre.
7. Escribir tests con @ActiveProfiles
En los tests, seleccionas el profile usando @ActiveProfiles. El test siguiente verifica dos cosas: que se inyecta el bean correcto y que la configuración de application-test.yml está en vigor.
@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 ha sido creado");
}
@Test
void shouldOverrideMailHostFromTestProfile() {
assertThat(mailHost).isEqualTo("fake-smtp");
}
}
Explicación:
@ActiveProfiles("test")activa el profiletestpara el contexto de esta clase, independientemente de cuál seaspring.profiles.default.- El primer test confirma que Spring elige
InMemoryNotificationServiceen lugar de enviar emails reales. - El segundo test usa el bean in-memory para verificar el contenido del mensaje sin necesidad de mock.
- El tercer test demuestra que
app.mail.hostdeapplication-test.ymlha sobrescrito el valorlocalhostenapplication.yml.
Nota: Cada combinación de profiles diferente crea un contexto de Spring separado. Si cada clase de test usa un conjunto de profiles diferente, la caché de contexto no se puede reutilizar y la suite de tests se ejecutará mucho más lentamente. Mantén el número de combinaciones de profiles en los tests al mínimo.
8. Errores comunes
Establecer spring.profiles.active en un archivo según el profile. Desde Spring Boot 2.4, declarar spring.profiles.active o spring.profiles.include en application-dev.yml (o en un documento con on-profile) hará que la aplicación no se inicie:
InvalidConfigDataPropertyException: Property 'spring.profiles.active' imported from
location 'class path resource [application-dev.yml]' is invalid in a profile specific resource
Usa grupos de profiles (sección 6) en su lugar.
Hardcodear spring.profiles.active=dev en application.yml. Este archivo se empaqueta en el jar. Si alguien olvida pasar la variable de entorno en el servidor, prod se ejecutará con la configuración de dev. Usa spring.profiles.default para el entorno local y siempre activa el profile explícitamente en el servidor.
Dejar secretos en application-prod.yml. Las contraseñas de base de datos o claves API no deben estar en Git. Usa placeholders como ${DB_PASSWORD} y proporciona los valores a través de variables de entorno o un gestor de secretos.
Ningún bean coincide con el profile. Si ejecutas con el profile staging pero solo tienes implementaciones para dev, test, prod, la aplicación reportará un error:
Parameter 0 of constructor in OrderService required a bean of type
'NotificationService' that could not be found.
Considera un bean de respaldo con @Profile("!prod") o agrega una implementación para el nuevo profile.
Abusar de profiles para feature flags. Los profiles son apropiados para describir entornos. Si quieres habilitar/deshabilitar una característica independientemente del entorno, @ConditionalOnProperty es generalmente más claro:
@Bean
@ConditionalOnProperty(name = "app.feature.loyalty-points", havingValue = "true")
public LoyaltyService loyaltyService() {
return new LoyaltyService();
}
Cuando tienes profiles como prod-with-loyalty o dev-no-cache, es señal de que deberías cambiar a propiedades.
9. Conclusión
Los profiles te ayudan a separar la configuración y el comportamiento según cada entorno sin modificar el código. Has visto cómo usar application-{profile}.yml para sobrescribir configuración, @Profile para elegir implementaciones, grupos de profiles para combinar varios profiles, y @ActiveProfiles para tests.
Algunos principios a recordar: coloca la configuración común en application.yml, escribe solo las diferencias en archivos de profile. Activa profiles en el servidor a través de variables de entorno o parámetros de línea de comandos. No pongas secretos en Git. Y siempre verifica la línea de log The following profiles are active después de cada despliegue.