3 minuto(s) de lectura

Ahí estaba yo, con mi application.properties prolijo, mi clase con @ConfigurationProperties que parecía sacada de un tutorial, y cuando ejecuto la app… nada…. mas desconcertado que Bruce Willis en Sexto Sentido.

La escena del crimen

Imaginate esta situación: tenés múltiples datasources que necesitás configurar dinámicamente. Idealmente, no deberías tener múltiples datasources, pero puede suceder.

app.datasources.primary.url=jdbc:h2:mem:primary
app.datasources.primary.username=primary_user
app.datasources.secondary.url=jdbc:h2:mem:secondary
app.datasources.secondary.username=secondary_user

Y naturalmente, pensás: “Listo, uso un Map para esto”, lo combino con la annotation y sale con fritas:

@ConfigurationProperties(prefix = "app.datasources")
public class DataSourceConfig {
    private Map<String, DataSourceProperties> connections = new HashMap<>();
    
    // getters y setters...
}

Resultado: Map vacío… y vos como el 2 de oro.

No importa si reiniciás la app, limpiás el cache, hacés un rebuild completo, te cebás un mate, insultás a Spring… nada. Vacío.

¿Por Qué Pasa Esto?

Spring Boot 3.x cambió las reglas del juego en la vinculación de propiedades. Especialmente cuando mezclás:

  1. Maps con claves dinámicas - primary, secondary no son propiedades fijas en tu clase
  2. Objetos anidados complejos - Spring se confunde tratando de instanciar objetos dentro de Maps
  3. Cambios en la vinculación relajada - Las reglas se volvieron más estrictas que antes
  4. Borrado de tipos genéricos - Map<String, MiClase> puede causar problemas de tipos

Básicamente, Spring mira tu Map, mira las propiedades, y dice: “Ni idea cómo matchear esto. Chau.” Eso si, no te avisa.

Las soluciones que funcionan

1. Records al rescate

Una buena solución es usar records con @ConstructorBinding`:

@ConfigurationProperties(prefix = "app.datasources")
public record DataSourceConfig(
    Map<String, DataSourceProperties> connections
) {
    @ConstructorBinding
    public DataSourceConfig(Map<String, DataSourceProperties> connections) {
        this.connections = connections != null ? Map.copyOf(connections) : Map.of();
    }
    
    public record DataSourceProperties(
        String url, String username, String password, String driverClassName
    ) {}
}

2. Estructura explícita (Cuando podés predecir las claves)

Si sabés de antemano qué claves vas a tener, olvidate del Map:

@ConfigurationProperties(prefix = "app.apis")
public record ApiConfig(
    ExternalApis external,
    InternalApis internal
) {
    public record ExternalApis(ApiProperties payment, ApiProperties notification) {}
    public record InternalApis(ApiProperties userService) {}
}

Más verboso, pero funciona siempre.

3. Lo Clásico Nunca Falla

@Component
@ConfigurationProperties(prefix = "app.features")
public class FeatureConfig {
    private Authentication authentication = new Authentication();
    private Caching caching = new Caching();
    
    // getters y setters tradicionales
}

Lo viejo funciona, Juan. A veces es la mejor opción.

Plan B: Las alternativas

Si @ConfigurationProperties definitivamente no quiere cooperar:

@Value (El Martillo)

@Value("${app.datasources.primary.url}")
private String primaryUrl;

@Value("${app.datasources.secondary.url}")  
private String secondaryUrl;

No es elegante, pero funciona. Ojo que se rumora que Spring podría deprecar @Value en futuras versiones.

Environment API (Control Total)

@Autowired
private Environment environment;

public String getPrimaryUrl() {
    return environment.getProperty("app.datasources.primary.url");
}

Más control, más código. Como en los viejos tiempos.

Binder API (Para Masoquistas)

@Autowired
private Environment environment;

public Map<String, DataSourceProperties> getDataSources() {
    Binder binder = Binder.get(environment);
    return binder.bind("app.datasources", 
            Bindable.mapOf(String.class, DataSourceProperties.class))
            .orElse(new HashMap<>());
}

Si querés hacer todo manual, esta es tu opción.

Probalo Vos Mismo

Armé un proyecto completo con ejemplos de todo esto: spring-config-fails

Tiene endpoints que te muestran:

  • El problema en acción (Maps vacíos)
  • Las soluciones funcionando
  • Las alternativas para casos extremos

Clonalo, correlo, y vas a ver exactamente qué pasa:

git clone https://github.com/rcantore/spring-config-fails
cd spring-config-fails
mvn spring-boot:run

Después visitá los endpoints que están documentados en el README del proyecto:

  • /api/problematic/datasources - Para ver el problema
  • /api/working/datasources - Para ver las soluciones
  • /api/alternatives/value-based - Para ver las alternativas

Para Cerrar

@ConfigurationProperties es una herramienta genial, pero tiene sus mañas en Spring Boot 3.x. Mi recomendación:

  1. Usá Records siempre que puedas - Después de todo son una solución piola
  2. Evitá Maps con claves dinámicas si podés definir la estructura de antemano
  3. Tené alternativas preparadas para casos complejos
  4. Probá siempre que tu configuración se esté leyendo correctamente

Y por favor, no te quedes dos horas debuggeando como hice yo la primera vez. A veces la solución es cambiar el approach, no insistir con el mismo código.

El repositorio tiene todo el código funcional, las explicaciones detalladas, y hasta instrucciones para troubleshooting. Porque cuando algo no funciona, lo último que querés es más frustración.


Te pasó algo similar con Spring Boot? Encontraste otras soluciones que funcionen? Los comentarios están abiertos para compartir experiencias (y frustraciones).

Deja un comentario