CicloUrbana está llena de números escritos a fuego: 0.50 de desbloqueo, 0.12 por minuto, 15 minutos gratuitos para estudiantes, 8 anclajes mínimos por estación. Cambiar cualquiera de ellos exige hoy recompilar y volver a desplegar. Eso no es aceptable: el ayuntamiento de Ribalta cambia sus tarifas cada temporada y el puerto del servidor no es el mismo en tu portátil que en el servidor de producción. La solución es externalizar la configuración, y Spring Boot tiene para eso uno de los mecanismos más completos —y más incomprendidos— del ecosistema Java. En esta lección veremos qué es el Environment y de dónde saca sus valores, el orden exacto de precedencia entre las fuentes, las diferencias reales entre .properties y .yaml, cómo Spring relaja los nombres de las propiedades para que CICLOURBANA_TARIFA_BASE y ciclourbana.tarifa-base sean lo mismo, cómo leer valores con @Value, y por qué una credencial no puede estar nunca en el repositorio.
Contenido
- El
Environmenty losPropertySource - El orden de precedencia de las fuentes
- Demostrar la precedencia en la práctica
.propertiesfrente a.yaml- Relajación de nombres (relaxed binding)
- Leer propiedades con
@Value - SpEL y valores por defecto
- Las propiedades que usa CicloUrbana
- Configuración externa:
spring.config.importyspring.config.location - Secretos y credenciales
- Errores Comunes y Consejos
- Ejercicios
- El
Environment y los PropertySource
Environment y los PropertySourceEn la lección 01-05 vimos que una de las primeras fases del arranque es "preparar el Environment". Ahora podemos concretar qué significa.
El Environment es un bean de Spring que responde a dos preguntas: ¿qué valor tiene esta propiedad? y ¿qué perfiles están activos? (los perfiles se estudian en la lección 07-02). Internamente no guarda ningún valor: mantiene una lista ordenada de PropertySource y, cuando le preguntas por una clave, los recorre en orden y devuelve el primero que la tenga.
flowchart TD
E["Environment.getProperty('server.port')"] --> PS["MutablePropertySources<br/>(lista ORDENADA)"]
PS --> P1["1. commandLineArgs<br/>--server.port=9090"]
P1 -->|no la tiene| P2["2. systemEnvironment<br/>SERVER_PORT"]
P2 -->|no la tiene| P3["3. systemProperties<br/>-Dserver.port"]
P3 -->|no la tiene| P4["4. applicationConfig:<br/>application.properties"]
P4 -->|la tiene: 8080| R["Devuelve 8080<br/>y deja de buscar"]
La consecuencia es fundamental: el primero que responde gana. Toda la lógica de precedencia de Spring Boot se reduce a en qué orden se colocan los PropertySource en esa lista.
Puedes inspeccionar la lista completa en tu propia aplicación:
package com.ciclourbana.comun;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.stereotype.Component;
/**
* Herramienta de diagnóstico: vuelca las fuentes de propiedades
* en el orden real de precedencia y resuelve algunas claves clave.
*/
@Component
public class InspectorConfiguracion implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(InspectorConfiguracion.class);
private final ConfigurableEnvironment entorno;
public InspectorConfiguracion(ConfigurableEnvironment entorno) {
this.entorno = entorno;
}
@Override
public void run(String... args) {
log.info("=== Fuentes de propiedades, de mayor a menor prioridad ===");
int posicion = 1;
for (var fuente : entorno.getPropertySources()) {
log.info(" {}. {}", posicion++, fuente.getName());
}
log.info("server.port resuelto a: {}", entorno.getProperty("server.port"));
log.info("spring.application.name resuelto a: {}",
entorno.getProperty("spring.application.name"));
}
}Salida típica en CicloUrbana:
=== Fuentes de propiedades, de mayor a menor prioridad ===
1. configurationProperties
2. commandLineArgs
3. servletConfigInitParams
4. servletContextInitParams
5. systemProperties
6. systemEnvironment
7. random
8. Config resource 'class path resource [application.properties]'
server.port resuelto a: 8080
spring.application.name resuelto a: ciclourbanaEste InspectorConfiguracion es una herramienta que merece la pena tener a mano: cuando una propiedad "no se lee", lo primero es ver qué fuente la está ganando.
- El orden de precedencia de las fuentes
Spring Boot define un orden completo y documentado. De mayor a menor prioridad, y quedándonos con lo que importa en la práctica:
| # | Fuente | Ejemplo | Uso habitual |
|---|---|---|---|
| 1 | Propiedades de DevTools (~/.config/spring-boot) |
— | Desarrollo local |
| 2 | @TestPropertySource y properties de @SpringBootTest |
@SpringBootTest(properties = "server.port=0") |
Pruebas (módulo 6) |
| 3 | Argumentos de línea de comandos | --server.port=9090 |
Arranque puntual, contenedores |
| 4 | SPRING_APPLICATION_JSON |
SPRING_APPLICATION_JSON='{"server":{"port":9090}}' |
Plataformas cloud |
| 5 | Parámetros del ServletContext/ServletConfig |
— | Despliegue en WAR |
| 6 | Atributos JNDI | — | Servidores de aplicaciones clásicos |
| 7 | Propiedades de sistema Java | -Dserver.port=9090 |
Scripts de arranque |
| 8 | Variables de entorno | SERVER_PORT=9090 |
Docker, Kubernetes, CI |
| 9 | application-{perfil}.properties fuera del jar |
./config/application-prod.properties |
Configuración por entorno |
| 10 | application-{perfil}.properties dentro del jar |
application-dev.properties |
Perfiles (lección 07-02) |
| 11 | application.properties fuera del jar |
./config/application.properties |
Ajustes del operador |
| 12 | application.properties dentro del jar |
src/main/resources/application.properties |
Valores por defecto del proyecto |
| 13 | @PropertySource en clases @Configuration |
@PropertySource("classpath:tarifas.properties") |
Ficheros adicionales |
| 14 | Valores por defecto (SpringApplication.setDefaultProperties) |
— | Último recurso |
Tres reglas que resumen la tabla y que conviene memorizar:
- Lo más externo gana. Cuanto más cerca del momento de arranque se especifica un valor, más prioridad tiene. Un
--server.port=9090en la línea de comandos vence a cualquier fichero. - Fuera del jar gana a dentro del jar. Es lo que permite empaquetar valores por defecto razonables y que el operador los ajuste sin reconstruir nada.
- Con perfil gana a sin perfil.
application-prod.propertiessobrescribeapplication.properties.
Las tres filas que usarás el 95 % del tiempo son la 3 (argumentos), la 8 (variables de entorno) y la 12 (application.properties del proyecto). El resto conviene conocerlo para no sorprenderse.
- Demostrar la precedencia en la práctica
Nada convence como verlo funcionar. Empecemos con el fichero del proyecto:
# src/main/resources/application.properties
spring.application.name=ciclourbana
server.port=8080
ciclourbana.ciudad=RibaltaArranca normalmente:
Ahora sobrescribe con una variable de entorno (fila 8):
Ahora con una propiedad de sistema (fila 7, que gana a la variable de entorno):
Y finalmente con un argumento de línea de comandos (fila 3, el que gana a todos):
SERVER_PORT=8081 java -Dserver.port=8082 -jar target/ciclourbana-0.0.1-SNAPSHOT.jar --server.port=8083Fíjate en la diferencia sintáctica, que confunde a mucha gente:
| Sintaxis | Qué es | Posición |
|---|---|---|
-Dclave=valor |
Propiedad de sistema de la JVM | Antes de -jar |
--clave=valor |
Argumento de la aplicación | Después del jar |
CLAVE=valor (delante del comando) |
Variable de entorno | Antes de todo |
Poner --server.port=9090 antes de -jar no funciona: la JVM lo interpretaría como una opción suya y fallaría. Y -Dserver.port después del jar llega como un argumento más de la aplicación, que Spring no reconoce como propiedad.
Con Maven, para pasar argumentos hay que usar la propiedad del plugin:
.properties frente a .yaml
.properties frente a .yamlSpring Boot admite ambos formatos, con las mismas capacidades. La elección es de estilo... hasta que la estructura crece.
La misma configuración de CicloUrbana en los dos formatos:
# src/main/resources/application.properties
spring.application.name=ciclourbana
server.port=8080
server.servlet.context-path=/
ciclourbana.ciudad=Ribalta
ciclourbana.tarifa.desbloqueo=0.50
ciclourbana.tarifa.precio-minuto=0.12
ciclourbana.red.capacidad-minima=8
ciclourbana.red.umbral-bateria=20
ciclourbana.estaciones-destacadas[0]=Plaza Mayor
ciclourbana.estaciones-destacadas[1]=Universidad
ciclourbana.tarifas-por-usuario.estandar=0.12
ciclourbana.tarifas-por-usuario.estudiante=0.08
ciclourbana.tarifas-por-usuario.jubilado=0.05
logging.level.com.ciclourbana=DEBUG
logging.level.org.springframework.web=INFO# src/main/resources/application.yaml
spring:
application:
name: ciclourbana
server:
port: 8080
servlet:
context-path: /
ciclourbana:
ciudad: Ribalta
tarifa:
desbloqueo: 0.50
precio-minuto: 0.12
red:
capacidad-minima: 8
umbral-bateria: 20
estaciones-destacadas:
- Plaza Mayor
- Universidad
tarifas-por-usuario:
estandar: 0.12
estudiante: 0.08
jubilado: 0.05
logging:
level:
com.ciclourbana: DEBUG
org.springframework.web: INFOComparados:
| Criterio | .properties |
.yaml |
|---|---|---|
| Jerarquía | Repetitiva: cada línea completa | Anidada, sin repetición |
| Listas | clave[0], clave[1]... |
- elemento (natural) |
| Mapas | clave.subclave=valor |
Anidado (natural) |
| Sensible a la indentación | No | Sí, y es su gran problema |
| Tabuladores | Irrelevantes | Prohibidos: rompen el fichero |
| Comentarios | # |
# |
Búsqueda de una clave con grep |
Trivial: la clave completa está en la línea | Difícil: la clave está repartida |
| Varios documentos en un fichero | No (se usa #---) |
Sí, con --- |
| Valores multilínea | Con \ al final |
Sí, con | y > |
| Precedencia si existen ambos | Gana .properties |
— |
Recomendación práctica: elige uno y sé coherente. Si tu configuración es plana y corta, .properties es más difícil de romper. En cuanto aparecen listas, mapas y tres niveles de anidamiento —el caso de CicloUrbana en cuanto lleguemos a la lección 02-05—, YAML es claramente más legible. En este curso usaremos YAML a partir de aquí.
Nunca tengas los dos ficheros a la vez: application.properties gana, y pasarás una tarde entera preguntándote por qué tu YAML no se lee.
Los errores de indentación de YAML
Son el peaje del formato, y todos son silenciosos: el fichero se lee, pero las propiedades quedan en otro sitio.
# MAL: 'port' cuelga de la raíz, no de 'server'
server:
port: 8080
# MAL: tabulador en lugar de espacios (aquí no se ve, pero rompe el arranque)
server:
port: 8080
# MAL: 'context-path' con menos indentación de la que necesita
server:
servlet:
context-path: /api # 3 espacios donde el bloque usa 2 o 4: inconsistente
# BIEN
server:
port: 8080
servlet:
context-path: /El primer caso produce un error de arranque claro (server no admite un valor escalar), pero variantes más sutiles simplemente dejan la propiedad huérfana y la aplicación arranca con el valor por defecto. Regla: dos espacios por nivel, nunca tabuladores, y activa en tu IDE la visualización de caracteres invisibles.
- Relajación de nombres (relaxed binding)
Spring Boot no exige que el nombre de la propiedad se escriba exactamente igual en todas partes. Aplica un algoritmo de relajación que considera equivalentes varias formas:
| Forma | Ejemplo | Dónde se usa |
|---|---|---|
| kebab-case | ciclourbana.tarifa-base |
Recomendada en ficheros |
| camelCase | ciclourbana.tarifaBase |
Admitida en ficheros |
| snake_case | ciclourbana.tarifa_base |
Admitida |
| MAYÚSCULAS con guion bajo | CICLOURBANA_TARIFABASE |
Variables de entorno |
Las cuatro se resuelven a la misma propiedad. Esto es lo que permite que en un docker-compose.yml o en un Deployment de Kubernetes escribas:
environment:
CICLOURBANA_TARIFA_DESBLOQUEO: "0.60"
CICLOURBANA_RED_CAPACIDADMINIMA: "10"
SERVER_PORT: "8080"y esos valores lleguen a ciclourbana.tarifa.desbloqueo, ciclourbana.red.capacidadMinima y server.port.
Las reglas para traducir una propiedad a variable de entorno son tres:
- Los puntos (
.) se convierten en guiones bajos (_). - Los guiones (
-) se eliminan. - Todo en mayúsculas.
Así, ciclourbana.red.capacidad-minima → CICLOURBANA_RED_CAPACIDADMINIMA. Ese segundo punto es el que más quebraderos de cabeza da: mucha gente escribe CICLOURBANA_RED_CAPACIDAD_MINIMA y no funciona, porque ese nombre correspondería a ciclourbana.red.capacidad.minima, con un punto de más.
Dos limitaciones importantes de la relajación:
- Solo se aplica al enlazado de
@ConfigurationProperties(lección 02-05) y a las propiedades del propio Spring Boot. Con@Valuela coincidencia es exacta:@Value("${ciclourbana.tarifa-base}")no encuentra una propiedad escritaciclourbana.tarifaBase. - Las claves de un
Mapno se relajan: si definesciclourbana.tarifas-por-usuario.estudiante-becado, la clave del mapa será literalmenteestudiante-becado.
Convención recomendada: escribe siempre en kebab-case en los ficheros. Es la forma canónica, la que aparece en la documentación de Spring Boot y la que evita ambigüedades.
- Leer propiedades con
@Value
@Value@Value inyecta el valor de una propiedad en un campo o parámetro. Vamos a sacar por fin de las constantes los precios de TarifaEstandar:
package com.ciclourbana.alquileres;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Duration;
@Component
@Primary
public class TarifaEstandar implements CalculadoraTarifa {
private final BigDecimal desbloqueo;
private final BigDecimal porMinuto;
// @Value en parámetros del constructor: mantiene los campos final
public TarifaEstandar(
@Value("${ciclourbana.tarifa.desbloqueo}") BigDecimal desbloqueo,
@Value("${ciclourbana.tarifa.precio-minuto}") BigDecimal porMinuto) {
this.desbloqueo = desbloqueo;
this.porMinuto = porMinuto;
}
@Override
public BigDecimal calcular(Duration duracion) {
BigDecimal minutos = BigDecimal.valueOf(Math.max(1, duracion.toMinutes()));
return desbloqueo
.add(porMinuto.multiply(minutos))
.setScale(2, RoundingMode.HALF_UP);
}
@Override
public String nombre() {
return "estandar";
}
}Observa que @Value va en los parámetros del constructor, no en los campos. Así los campos siguen siendo final y la clase sigue siendo construible en un test con new TarifaEstandar(new BigDecimal("0.50"), new BigDecimal("0.12")), sin Spring de por medio. Es la misma lógica de la lección 02-02 aplicada a la configuración.
Spring convierte automáticamente el texto al tipo del parámetro:
@Value("${server.port}") int puerto; // 8080
@Value("${ciclourbana.ciudad}") String ciudad; // "Ribalta"
@Value("${ciclourbana.tarifa.desbloqueo}") BigDecimal desbloqueo; // 0.50
@Value("${ciclourbana.red.mantenimiento}") boolean enMantenimiento; // true/false
@Value("${ciclourbana.estaciones-destacadas}") List<String> destacadas; // separadas por comas
@Value("${ciclourbana.sesion.duracion}") Duration duracion; // "30m" -> PT30MPara que List<String> funcione con @Value, el valor debe ser una cadena separada por comas, no una lista YAML:
ciclourbana:
estaciones-destacadas: # NO sirve para @Value
- Plaza Mayor # sí sirve para @ConfigurationProperties
- UniversidadEsta es ya la primera grieta de @Value, y no será la última.
- SpEL y valores por defecto
Valores por defecto
Si una propiedad no existe, @Value falla en el arranque:
Could not resolve placeholder 'ciclourbana.tarifa.desbloqueo' in value
"${ciclourbana.tarifa.desbloqueo}"Se evita con la sintaxis ${clave:valorPorDefecto}:
@Value("${ciclourbana.tarifa.desbloqueo:0.50}") BigDecimal desbloqueo; // 0.50 si falta
@Value("${ciclourbana.red.umbral-bateria:20}") int umbralBateria;
@Value("${ciclourbana.mensaje-mantenimiento:}") String mensaje; // cadena vacía
@Value("${ciclourbana.contacto:#{null}}") String contacto; // null explícitoCuidado con un caso especial: si el valor por defecto contiene : (una URL, por ejemplo), hay que tener presente que solo cuenta el primer : como separador, así que @Value("${url:http://localhost:8080}") funciona correctamente y el valor por defecto es la URL completa.
SpEL: el lenguaje de expresiones de Spring
@Value admite también expresiones SpEL con la sintaxis #{...} (almohadilla, no dólar):
// Aritmética sobre una propiedad
@Value("#{${ciclourbana.tarifa.precio-minuto} * 60}")
BigDecimal precioHora;
// Llamar a un método de otro bean
@Value("#{selectorTarifa.disponibles().size()}")
int numeroDeTarifas;
// Leer del Environment con lógica
@Value("#{environment['ciclourbana.ciudad'] ?: 'desconocida'}")
String ciudad;
// Convertir una cadena separada por comas en lista (útil de verdad)
@Value("#{'${ciclourbana.estaciones-destacadas}'.split(',')}")
List<String> destacadas;
// Propiedades del sistema
@Value("#{systemProperties['user.timezone']}")
String zonaHoraria;Y una que sí es realmente práctica: valores aleatorios, para puertos o identificadores en pruebas.
@Value("${random.int(1000,9999)}") int codigoSesion;
@Value("${random.uuid}") String identificadorArranque;| Sintaxis | Nombre | Qué hace |
|---|---|---|
${...} |
Marcador de propiedad | Sustituye por el valor de la propiedad |
#{...} |
Expresión SpEL | Evalúa una expresión (puede contener ${...} dentro) |
Las limitaciones de @Value
@Value es cómodo para uno o dos valores sueltos, pero se queda corto en cuanto la configuración crece. Sus problemas:
| Limitación | Consecuencia |
|---|---|
| Sin relajación de nombres | La clave debe escribirse exactamente igual. |
| Sin validación | Un valor absurdo (-5 de capacidad) se acepta sin protestar. |
| Sin estructuras | No enlaza listas YAML ni mapas de forma natural. |
| Sin metadatos | El IDE no autocompleta ni documenta las propiedades. |
| Errores dispersos | Si faltan cinco propiedades, el arranque falla por la primera, una a una. |
| Configuración desperdigada | Las claves aparecen en veinte clases: nadie sabe qué configura la aplicación. |
| Difícil de agrupar y reutilizar | No hay un objeto que represente "la configuración de tarifas". |
Todas ellas las resuelve @ConfigurationProperties, que es el tema de la lección siguiente. La regla que aplicaremos en CicloUrbana: @Value para valores aislados y ocasionales; @ConfigurationProperties para todo lo demás.
- Las propiedades que usa CicloUrbana
Esta es la configuración base del proyecto, con explicación de cada bloque:
# src/main/resources/application.yaml
spring:
application:
name: ciclourbana # aparece en los logs, en Actuator y en trazas (módulo 9)
server:
port: 8080 # 0 = puerto aleatorio libre (muy útil en pruebas)
servlet:
context-path: / # prefijo de TODAS las rutas
shutdown: graceful # apagado ordenado, visto en la lección 01-05
logging:
level:
root: INFO
com.ciclourbana: DEBUG # nuestro código, con detalle
org.springframework.web: INFO
org.springframework.beans.factory: INFO # a DEBUG para depurar el ciclo de vida
pattern:
console: "%d{HH:mm:ss.SSS} %-5level [%logger{20}] - %msg%n"
ciclourbana:
ciudad: Ribalta
tarifa:
desbloqueo: 0.50
precio-minuto: 0.12
red:
capacidad-minima: 8
umbral-bateria: 20Las propiedades del servidor y de la aplicación más útiles:
| Propiedad | Valor típico | Para qué sirve |
|---|---|---|
server.port |
8080, 0 |
Puerto HTTP. 0 asigna uno libre. |
server.servlet.context-path |
/, /ciclourbana |
Prefijo de todas las rutas. |
server.shutdown |
graceful |
Espera a que terminen las peticiones en curso. |
server.error.include-message |
always |
Incluye el mensaje de error en la respuesta (módulo 3). |
server.compression.enabled |
true |
Comprime las respuestas grandes. |
spring.application.name |
ciclourbana |
Identifica la app en logs, métricas y trazas. |
spring.main.banner-mode |
off, console |
Controla el banner (lección 01-05). |
spring.main.web-application-type |
servlet, none |
Fuerza el tipo de aplicación. |
logging.level.<paquete> |
DEBUG |
Nivel de log por paquete. Se estudia a fondo en 09-05. |
logging.file.name |
logs/ciclourbana.log |
Escribe el log también a fichero. |
Un aviso sobre server.servlet.context-path: si lo pones a /ciclourbana, la URL de nuestro endpoint pasa a ser http://localhost:8080/ciclourbana/api/v1/estaciones. Es un cambio que rompe todos los clientes y todos los ficheros .http de la lección 01-02, así que en CicloUrbana lo dejaremos en /.
Verifica que la configuración se aplica:
- Configuración externa:
spring.config.import y spring.config.location
spring.config.import y spring.config.locationTodo lo anterior vive dentro del jar. En un despliegue real hace falta configuración que no se empaquete.
Ficheros externos por convención
Spring Boot busca application.yaml automáticamente, y por este orden de prioridad, en:
./config/(subdirectorioconfigjunto al jar)./(directorio actual)classpath:/config/classpath:/(dentro del jar)
Es decir, basta con dejar un fichero junto al jar para sobrescribir los valores empaquetados:
target/
├── ciclourbana-0.0.1-SNAPSHOT.jar
└── config/
└── application.yaml # sobrescribe lo que traiga el jar# target/config/application.yaml — configuración del operador
server:
port: 9090
ciclourbana:
tarifa:
desbloqueo: 0.60 # el ayuntamiento subió el desbloqueospring.config.import
Permite incluir otros ficheros desde la configuración principal. Es la forma moderna y preferida frente a @PropertySource:
# src/main/resources/application.yaml
spring:
config:
import:
- optional:file:./config/tarifas-ribalta.yaml # opcional: no falla si no existe
- optional:file:/etc/ciclourbana/secretos.yaml # secretos del servidorEl prefijo optional: es clave: sin él, si el fichero no existe el arranque falla. Esto tiene su lógica —quieres saber si falta un fichero imprescindible— pero en desarrollo es incómodo, y por eso los ficheros propios de un entorno concreto se marcan casi siempre como opcionales.
spring.config.import admite también otros orígenes:
spring:
config:
import:
- optional:file:./config/ # un directorio entero
- optional:configtree:/run/secrets/ # secretos de Docker/Kubernetes
- optional:classpath:tarifas-por-defecto.yaml # otro fichero del jarEl formato configtree: merece una nota: en Kubernetes y en Docker Swarm los secretos se montan como ficheros, uno por clave, dentro de un directorio. Con configtree: Spring Boot lee ese árbol y convierte cada fichero en una propiedad. Es el mecanismo idiomático para secretos en contenedores, y lo retomaremos en la lección 07-04.
spring.config.location
Sustituye por completo las ubicaciones por defecto, en lugar de añadirse a ellas:
Y spring.config.additional-location añade ubicaciones conservando las por defecto, que suele ser lo que realmente quieres:
| Opción | Efecto |
|---|---|
spring.config.location |
Reemplaza las ubicaciones por defecto. Control total, riesgo de perder valores. |
spring.config.additional-location |
Añade ubicaciones con más prioridad que las por defecto. Más seguro. |
spring.config.import |
Incluye ficheros concretos desde la propia configuración. Declarativo. |
- Secretos y credenciales
Este apartado no es opcional. Es la parte de la lección con consecuencias más graves si se ignora.
Nunca, bajo ninguna circunstancia, escribas contraseñas, claves de API, tokens o certificados en un fichero que vaya al repositorio de código.
Lo que no debes hacer, aunque lo veas en tutoriales:
# MAL: esto acabará en Git y en el historial para siempre
spring:
datasource:
url: jdbc:postgresql://bd.ribalta.example:5432/ciclourbana
username: ciclourbana_app
password: Sup3rS3cr3t0! # ← catástrofe
ciclourbana:
pasarela-pago:
api-key: sk_live_9f3a2b1c8d7e # ← catástrofePor qué es tan grave, más allá de lo evidente:
- Git no olvida. Borrar la línea en un commit posterior no elimina el secreto: sigue en el historial, accesible con
git log -p. Rotar la credencial es obligatorio, no opcional. - El repositorio se copia. Forks, clones en portátiles, copias de seguridad, integraciones de CI, el ordenador de un becario. Un secreto en Git está, en la práctica, en muchos más sitios de los que crees.
- Los repositorios cambian de visibilidad. Un repositorio privado que se hace público filtra todo su historial de golpe. Es un accidente sorprendentemente frecuente.
- Los robots rastrean. Existen bots que escanean GitHub buscando patrones de claves; una clave de un proveedor cloud publicada por error se explota en minutos.
Qué hacer en su lugar:
1. Variables de entorno con marcador y sin valor por defecto. El fichero versionado declara qué necesita, no cuánto vale:
# src/main/resources/application.yaml — sí va al repositorio
spring:
datasource:
url: ${CICLOURBANA_BD_URL:jdbc:h2:mem:ribalta}
username: ${CICLOURBANA_BD_USUARIO:sa}
password: ${CICLOURBANA_BD_PASSWORD:} # vacío en local, obligatorio fuera
ciclourbana:
pasarela-pago:
api-key: ${CICLOURBANA_PASARELA_API_KEY} # sin defecto: falla si no estáFíjate en el detalle: la clave de la pasarela no tiene valor por defecto. Si alguien despliega sin definirla, el arranque falla inmediatamente con un mensaje claro. Eso es mucho mejor que arrancar y fallar en el primer pago.
export CICLOURBANA_BD_PASSWORD='la-de-verdad'
export CICLOURBANA_PASARELA_API_KEY='sk_live_...'
java -jar ciclourbana.jar2. Fichero externo fuera del árbol del proyecto, con permisos restringidos:
sudo mkdir -p /etc/ciclourbana
sudo tee /etc/ciclourbana/secretos.yaml > /dev/null <<'EOF'
spring:
datasource:
password: la-de-verdad
EOF
sudo chmod 600 /etc/ciclourbana/secretos.yaml
sudo chown ciclourbana:ciclourbana /etc/ciclourbana/secretos.yaml3. Un gestor de secretos, que es lo correcto en producción seria: HashiCorp Vault, AWS Secrets Manager, Azure Key Vault o los Secret de Kubernetes montados como configtree:. Se tratan en el módulo 8.
4. Protege el repositorio. Añade al .gitignore cualquier fichero local de secretos y considera un hook de pre-commit que detecte patrones sospechosos:
5. Si un secreto se filtra, rótalo. No lo borres del fichero y sigas. Invalida la credencial y emite una nueva. Es la única acción que realmente cierra el agujero.
Una última nota: en la lección 02-05 veremos cómo evitar además que un secreto acabe accidentalmente en los logs, y en el módulo 7, cómo Actuator oculta los valores sensibles en el endpoint /actuator/env.
Errores Comunes y Consejos
Tener a la vez application.properties y application.yaml. Gana el .properties y el YAML parece ignorado. Borra uno.
Escribir mal el nombre de la variable de entorno. CICLOURBANA_RED_CAPACIDAD_MINIMA no es ciclourbana.red.capacidad-minima: los guiones se eliminan, no se convierten en guion bajo. El nombre correcto es CICLOURBANA_RED_CAPACIDADMINIMA.
Usar tabuladores en YAML. El fichero se rechaza con un error de análisis que no siempre señala la línea correcta. Configura tu editor para insertar espacios.
Poner --server.port antes del -jar. La JVM no lo entiende. Los -- van después del jar; los -D van antes.
Esperar relajación de nombres con @Value. No la hay. Con @Value la clave debe coincidir carácter a carácter.
Confundir ${...} con #{...}. El primero resuelve propiedades; el segundo evalúa SpEL. @Value("#{ciclourbana.ciudad}") intenta evaluar ciclourbana.ciudad como expresión y falla; lo correcto es ${ciclourbana.ciudad}.
Poner una contraseña en application.yaml. Repetido a propósito: es el error más caro de esta lección.
Consejo: nombra tus propiedades con un prefijo propio. Todo lo de CicloUrbana empieza por ciclourbana.. Así nunca colisionas con una propiedad de Spring Boot o de una librería, y grep -r "ciclourbana\." src/ te dice de un vistazo qué configura la aplicación.
Consejo: usa server.port=0 en las pruebas. Asigna un puerto libre y evita fallos cuando dos pruebas se ejecutan a la vez. Volveremos a ello en el módulo 6.
Consejo: documenta cada propiedad con un comentario. Quien despliegue tu aplicación dentro de un año lo agradecerá, y no necesitará leer el código para saber si umbral-bateria va en porcentaje o en voltios.
Consejo: en desarrollo, no dependas de la línea de comandos. Un application-dev.yaml con perfil (lección 07-02) es más reproducible que un comando largo que solo está en tu memoria.
Ejercicios
Ejercicio 1: demostrar la precedencia
Define ciclourbana.ciudad=Ribalta en application.yaml. Escribe un componente AvisoCiudad que registre su valor al arrancar. Después arranca la aplicación cuatro veces —normal, con variable de entorno, con propiedad de sistema y con argumento de línea de comandos— dando un valor distinto en cada una, y anota qué gana. Añade al componente el volcado de la fuente que aportó el valor.
Ejercicio 2: externalizar la tarifa de estudiante
TarifaEstudiante tiene todavía sus constantes escritas a fuego. Externalízalas a ciclourbana.tarifa.estudiante.precio-minuto y ciclourbana.tarifa.estudiante.minutos-gratis, con valores por defecto en el propio @Value para que la aplicación arranque aunque falten. Verifica que el importe de un alquiler de 45 minutos cambia al sobrescribir las propiedades por línea de comandos.
Ejercicio 3: configuración de operador con fichero externo
Prepara un despliegue realista: empaqueta el jar, crea un directorio config/ junto a él con un application.yaml que cambie el puerto a 9090, suba el desbloqueo a 0,60 € y lea la contraseña de la base de datos de una variable de entorno sin valor por defecto. Comprueba que la aplicación falla al arrancar si la variable no está definida, y que arranca correctamente cuando sí lo está.
Soluciones
Solución 1
package com.ciclourbana.comun;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.stereotype.Component;
@Component
public class AvisoCiudad implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(AvisoCiudad.class);
private static final String CLAVE = "ciclourbana.ciudad";
private final String ciudad;
private final ConfigurableEnvironment entorno;
public AvisoCiudad(@Value("${ciclourbana.ciudad}") String ciudad,
ConfigurableEnvironment entorno) {
this.ciudad = ciudad;
this.entorno = entorno;
}
@Override
public void run(String... args) {
log.info("Ciudad de la red: {}", ciudad);
log.info("Aportada por la fuente: {}", fuenteQueGana());
}
/** Recorre las fuentes en orden y devuelve la primera que tiene la clave. */
private String fuenteQueGana() {
for (var fuente : entorno.getPropertySources()) {
if (fuente instanceof EnumerablePropertySource<?> enumerable
&& enumerable.containsProperty(CLAVE)) {
return fuente.getName() + " -> " + enumerable.getProperty(CLAVE);
}
}
return "ninguna (valor por defecto)";
}
}Las cuatro ejecuciones:
# 1. Solo el fichero
java -jar target/ciclourbana-0.0.1-SNAPSHOT.jar
# Ciudad de la red: Ribalta
# Aportada por la fuente: Config resource 'class path resource [application.yaml]' -> Ribalta
# 2. Variable de entorno
CICLOURBANA_CIUDAD=Ribalta-Norte java -jar target/ciclourbana-0.0.1-SNAPSHOT.jar
# Ciudad de la red: Ribalta-Norte
# Aportada por la fuente: systemEnvironment -> Ribalta-Norte
# 3. Propiedad de sistema (gana a la variable de entorno)
CICLOURBANA_CIUDAD=Ribalta-Norte \
java -Dciclourbana.ciudad=Ribalta-Sur -jar target/ciclourbana-0.0.1-SNAPSHOT.jar
# Ciudad de la red: Ribalta-Sur
# Aportada por la fuente: systemProperties -> Ribalta-Sur
# 4. Argumento de línea de comandos (gana a todos)
CICLOURBANA_CIUDAD=Ribalta-Norte \
java -Dciclourbana.ciudad=Ribalta-Sur -jar target/ciclourbana-0.0.1-SNAPSHOT.jar \
--ciclourbana.ciudad=Ribalta-Centro
# Ciudad de la red: Ribalta-Centro
# Aportada por la fuente: commandLineArgs -> Ribalta-CentroComentario: el método fuenteQueGana() implementa literalmente el algoritmo del Environment descrito en el apartado 1 —recorrer la lista ordenada y quedarse con la primera coincidencia— y por eso su resultado coincide siempre con el valor inyectado. Es un buen diagnóstico para tener a mano.
Nota sobre la fuente configurationProperties que aparece la primera en el listado: es una fuente sintética que Spring Boot usa internamente para el enlazado; no aporta valores propios.
Solución 2
# src/main/resources/application.yaml
ciclourbana:
tarifa:
desbloqueo: 0.50
precio-minuto: 0.12
estudiante:
precio-minuto: 0.08
minutos-gratis: 15package com.ciclourbana.alquileres;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Duration;
@Component("tarifaEstudiante")
public class TarifaEstudiante implements CalculadoraTarifa {
private final BigDecimal porMinuto;
private final long minutosGratis;
public TarifaEstudiante(
// Valor por defecto tras los dos puntos: la app arranca aunque falten
@Value("${ciclourbana.tarifa.estudiante.precio-minuto:0.08}") BigDecimal porMinuto,
@Value("${ciclourbana.tarifa.estudiante.minutos-gratis:15}") long minutosGratis) {
this.porMinuto = porMinuto;
this.minutosGratis = minutosGratis;
}
@Override
public BigDecimal calcular(Duration duracion) {
long facturables = Math.max(0, duracion.toMinutes() - minutosGratis);
return porMinuto
.multiply(BigDecimal.valueOf(facturables))
.setScale(2, RoundingMode.HALF_UP);
}
@Override
public String nombre() {
return "estudiante";
}
}Verificación con el DemostracionTarifas de la lección 02-02:
# Con los valores del fichero: (45-15) * 0,08 = 2,40 €
java -jar target/ciclourbana-0.0.1-SNAPSHOT.jar
# Alquiler de 45 min con tarifa 'estudiante': 2.40 €
# Convenio ampliado: 30 minutos gratis y 0,06 €/min -> (45-30) * 0,06 = 0,90 €
java -jar target/ciclourbana-0.0.1-SNAPSHOT.jar \
--ciclourbana.tarifa.estudiante.minutos-gratis=30 \
--ciclourbana.tarifa.estudiante.precio-minuto=0.06
# Alquiler de 45 min con tarifa 'estudiante': 0.90 €Comentario y error frecuente: si escribes @Value("${ciclourbana.tarifa.estudiante.minutosGratis:15}") —en camelCase— no enlazará con la propiedad minutos-gratis del fichero: tomará siempre el valor por defecto 15, en silencio y sin ningún error. Es exactamente la ausencia de relajación de nombres del apartado 5, y es una de las razones de peso para migrar a @ConfigurationProperties en la próxima lección.
Consejo: fíjate en que los precios se declaran como BigDecimal, no como double. En dinero, double produce errores de redondeo inaceptables (0.1 + 0.2 no es 0.3). Es una regla que no debe romperse nunca en un dominio con importes.
Solución 3
# src/main/resources/application.yaml — versionado, sin secretos
spring:
application:
name: ciclourbana
datasource:
url: ${CICLOURBANA_BD_URL:jdbc:h2:mem:ribalta}
username: ${CICLOURBANA_BD_USUARIO:sa}
# Sin valor por defecto: si falta la variable, el arranque falla
password: ${CICLOURBANA_BD_PASSWORD}
server:
port: 8080
ciclourbana:
ciudad: Ribalta
tarifa:
desbloqueo: 0.50
precio-minuto: 0.12# Empaquetar
./mvnw clean package -DskipTests
# Preparar la configuración del operador junto al jar
mkdir -p target/config
cat > target/config/application.yaml <<'EOF'
# Configuración de producción de la red de Ribalta.
# Este fichero NO está en el repositorio: lo gestiona el operador.
server:
port: 9090
ciclourbana:
tarifa:
desbloqueo: 0.60 # tarifa de temporada alta aprobada por el ayuntamiento
EOFPrimer intento, sin la variable de entorno:
***************************
APPLICATION FAILED TO START
***************************
Description:
Could not resolve placeholder 'CICLOURBANA_BD_PASSWORD' in value
"${CICLOURBANA_BD_PASSWORD}"Segundo intento, con ella definida:
Comentario: el fallo del primer intento es deseable. Un marcador sin valor por defecto convierte un olvido de despliegue en un error inmediato y evidente, en lugar de un fallo silencioso a las tres de la madrugada cuando alguien intente pagar. Es la misma filosofía de "fallar pronto y en voz alta" que vimos con la creación anticipada de singletons en la lección 02-03.
Consejo de despliegue: en un servidor real, la contraseña no se escribe en la línea de comandos —quedaría visible en ps aux y en el historial de la shell— sino en el fichero de unidad de systemd, en el EnvironmentFile correspondiente con permisos 600, o en el gestor de secretos de la plataforma. En Docker se pasa por variable de entorno del contenedor o, mejor, por secreto montado como fichero y leído con configtree: (lección 07-04).
Conclusión
La configuración de CicloUrbana ha salido del código. Sabes que el Environment no guarda valores sino una lista ordenada de PropertySource, y que toda la lógica de precedencia se reduce a en qué orden está esa lista: lo más externo gana, fuera del jar gana a dentro, y con perfil gana a sin perfil. Lo has comprobado tú mismo arrancando la misma aplicación con fichero, variable de entorno, propiedad de sistema y argumento de línea de comandos, y sabes distinguir la sintaxis de cada uno. Conoces las diferencias reales entre .properties y YAML —y por qué a partir de aquí el curso usa YAML— junto con la trampa de la indentación. Entiendes la relajación de nombres y las tres reglas que traducen ciclourbana.red.capacidad-minima a CICLOURBANA_RED_CAPACIDADMINIMA, incluida la eliminación de los guiones que tantos despliegues rompe. Sabes leer propiedades con @Value, darles valores por defecto, usar SpEL cuando aporta algo... y también conoces las siete limitaciones que hacen de @Value una herramienta de uso puntual. Sabes importar configuración externa con spring.config.import y spring.config.additional-location. Y, sobre todo, sabes que una credencial nunca va al repositorio, por qué el daño es permanente y cuáles son las alternativas reales.
Los precios de la red de Ribalta ya se pueden cambiar sin recompilar. Pero la solución tiene grietas evidentes: las claves están desperdigadas por varias clases, un camelCase mal escrito falla en silencio, nadie valida que el precio por minuto sea positivo y el IDE no ayuda a escribir las propiedades. La siguiente lección, Propiedades de Spring Boot, resuelve las cuatro cosas con @ConfigurationProperties: configuración tipada, agrupada en objetos inmutables, validada con Bean Validation, con conversión automática de Duration y DataSize, y con autocompletado en el IDE. Convertiremos toda la configuración de tarifas y de la red de Ribalta a esa forma, que es la que el proyecto mantendrá hasta el final del curso.
Curso de Spring Boot
Módulo 1: Introducción a Spring Boot
- ¿Qué es Spring Boot?
- Configuración de tu Entorno de Desarrollo
- Creando tu Primera Aplicación Spring Boot
- Entendiendo la Estructura del Proyecto
- El Arranque y el Ciclo de Vida de la Aplicación
Módulo 2: Conceptos Básicos de Spring Boot
- Anotaciones de Spring Boot
- Inyección de Dependencias en Spring Boot
- Ámbito y Ciclo de Vida de los Beans
- Configuración de Spring Boot
- Propiedades de Spring Boot
- Autoconfiguración y Starters por Dentro
Módulo 3: Construyendo Servicios Web RESTful
- Introducción a los Servicios Web RESTful
- Creando Controladores REST
- Manejo de Métodos HTTP
- Validación de Datos de Entrada
- DTOs y Mapeo entre Capas
- Manejo de Excepciones en REST
- Documentar la API con OpenAPI
Módulo 4: Acceso a Datos con Spring Boot
- Introducción a Spring Data JPA
- Configuración de Fuentes de Datos
- Creación de Entidades JPA
- Relaciones entre Entidades
- Uso de Repositorios de Spring Data
- Métodos de Consulta en Spring Data JPA
- Transacciones y Gestión de la Persistencia
- Migraciones de Esquema con Flyway
Módulo 5: Seguridad en Spring Boot
- Introducción a Spring Security
- Configuración de Spring Security
- Autenticación y Autorización de Usuarios
- Implementación de Autenticación JWT
- Seguridad a Nivel de Método y Endurecimiento de la API
Módulo 6: Pruebas en Spring Boot
- Introducción a las Pruebas
- Pruebas Unitarias con JUnit
- Simulación con Mockito
- Pruebas de Integración
- Pruebas con Testcontainers
Módulo 7: Funciones Avanzadas de Spring Boot
- Spring Boot Actuator
- Perfiles de Spring Boot
- Tareas Programadas y Ejecución Asíncrona
- Spring Boot con Docker
- Spring Boot y Microservicios
- Comunicación entre Servicios y Tolerancia a Fallos
Módulo 8: Despliegue de Aplicaciones Spring Boot
- Introducción al Despliegue
- Desplegando en Heroku
- Desplegando en AWS
- Desplegando en Kubernetes
- Integración y Entrega Continua
Módulo 9: Rendimiento y Monitoreo
- Ajuste de Rendimiento
- Caché con Spring Cache
- Monitoreo con Spring Boot Actuator
- Uso de Prometheus y Grafana
- Gestión de Registros y Logs
- Trazabilidad Distribuida
