CodeGym /Cursos /Spring Core /@PropertySource y propiedades en Spring

@PropertySource y propiedades en Spring

Spring Core
Nivel 12, Lección 2
Disponible

1. Archivo de configuración y classpath

Antes de escribir una sola anotación, es necesario entender dónde vive realmente el archivo de configuración. En una aplicación Java, «el archivo junto al código» no siempre significa «el archivo está disponible en tiempo de ejecución». Especialmente cuando Gradle empaqueta un jar. Ahora entenderemos qué es src/main/resources, qué va al classpath y por qué a Spring le gusta el prefijo classpath:.

En un proyecto Gradle hay una «constitución» no escrita: todo lo que está en src/main/java se convierte en .class, y todo lo que está en src/main/resources se convierte en «recursos de la aplicación». Gradle copia estos recursos al directorio de compilación y luego los empaqueta en el jar (si se compila un jar). Es decir, contextflow.properties no debe estar «en algún lugar cercano», «en la raíz del disco» o «en Descargas porque es más cómodo». Debe estar donde la compilación lo pueda recoger garantizadamente.

Lo muestro en forma de árbol (para que el cerebro no intente adivinar dónde Spring busca el archivo):

spring-core-contextflow/
├─ src/
│  └─ main/
│     ├─ java/
│     │  └─ com/example/contextflow/...
│     └─ resources/
│        └─ contextflow.properties   <-- aquí
└─ build/
   └─ resources/
      └─ main/
         └─ contextflow.properties   <-- aquí lo copia Gradle al compilar/ejecutar

Cuando ejecutas la aplicación desde el IDE o a través de Gradle, es el contenido de build/resources/main y build/classes/java/main lo que normalmente va al classpath. Y Spring, cuando ve classpath:contextflow.properties, hace una búsqueda «lógica» más o menos así: «¿hay un recurso con esa ruta en el classpath?».

Si no se entiende esto, aparecen situaciones clásicas del tipo: «seguro que creé el archivo, de verdad... ¿por qué Spring dice que no existe?». Y en el 90% de los casos la respuesta es trivial: el archivo no está en src/main/resources, sino en la «carpeta del proyecto» o en algún lugar cercano, y Gradle no lo copia, por lo que el classpath ni siquiera se entera de su existencia.

2. Archivo contextflow.properties

Un archivo de propiedades no es un vertedero de cualquier cosa, sino un pequeño acuerdo entre «quien escribe el código» y «quien ejecuta la aplicación». Si las claves son caóticas, en una semana olvidarás qué significan mode=1 y value2=yes. Vamos a crear contextflow.properties y acordemos inmediatamente nombres claros.

Empezamos con el archivo en sí. Es simple: líneas de la forma clave=valor, comentarios mediante #, nada de JSON, nada de YAML, pura «contabilidad» para la configuración.

Archivo: src/main/resources/contextflow.properties

# Configuración básica de ContextFlow
contextflow.app-name=ContextFlow

# Dónde guardar los resultados (solo en build/, para que el proyecto sea reproducible)
contextflow.report.output-dir=build/reports

# El resto de valores son aún cadenas: llegaremos a la tipificación más adelante
contextflow.notifications.default-channel=console
contextflow.audit.mode=console
contextflow.locale.default=ru

Para valores operativos como console y file es conveniente mantenerlos en minúsculas: así las configuraciones se leen como parámetros de ejecución, no como constantes de Java.

Nótese la disciplina en los nombres: todas las claves comienzan con contextflow.. Esto no es «amor por los puntos», es una forma de protegerse del caos. Si las claves empiezan a llamarse simplemente name, mode, format, muy pronto te encontrarás con el conflicto de «¿de quién es el nombre?» y «¿qué modo?». El prefijo hace explícito el espacio de nombres.

Para que sea aún más visual, puedes tener en mente una pequeña tabla (no es obligatoria para el código, pero ayuda mucho al leer el proyecto):

Clave Ejemplo de valor Para qué sirve
contextflow.app-name ContextFlow Nombre técnico de la aplicación (para salida, diagnóstico, registro comprensible)
contextflow.report.output-dir build/reports Directorio base para los archivos de informes
contextflow.notifications.default-channel console Canal de notificaciones por defecto (por ahora como cadena)
contextflow.audit.mode console Modo de auditoría (por ahora como cadena)
contextflow.locale.default ru Locale por defecto (por ahora como cadena)

Nótese un punto importante: en esta lección deliberadamente no hacemos que los valores sean tipificados (no convertimos console en un enum). Por ahora son cadenas. Nuestro objetivo hoy es enseñar a Spring a «ver» el archivo y enseñar al contenedor a sustituir valores.

3. Conexión mediante @PropertySource

Ahora que el archivo existe, es lógico esperar que Spring lo encuentre automáticamente, lo lea y comience a usarlo. Pero en Spring puro no hay «telepatía»: el contenedor debe recibir una instrucción explícita de que este recurso debe añadirse a la lista de fuentes de configuración. Esa instrucción será @PropertySource en la clase de configuración.

La forma más directa es colocar @PropertySource en su clase de configuración (la que se pasa a AnnotationConfigApplicationContext). En nuestro ContextFlow, normalmente actúa como «punto de ensamblaje de la aplicación».

Ejemplo de configuración mínima:

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

@Configuration
@ComponentScan("com.example.contextflow") // Le decimos a Spring dónde buscar @Component/@Service, etc.
@PropertySource(
        value = "classpath:contextflow.properties", // Tomamos el archivo del classpath (es decir, de resources en compilación)
        encoding = "UTF-8" // Fijamos explícitamente la codificación para no encontrarnos con caracteres extraños
)
public class AppConfig {
    // La configuración puede estar vacía: lo importante es que las anotaciones ya definen el comportamiento del contenedor
}

Aquí hay varios «detalles» que en la práctica resultan no serlo tanto.

classpath:contextflow.properties indica: «busca el archivo en el classpath». Esto es ideal para un proyecto de aprendizaje y para muchas aplicaciones del mundo real donde la configuración predeterminada está dentro del jar.

encoding = "UTF-8" no siempre es obligatorio, pero es muy útil si en algún momento quieres poner letras del español en los valores (por ejemplo, el nombre de la aplicación «ContextFlow para demo»). Sin la codificación, podrías obtener caracteres extraños de repente, y depurar caracteres extraños es un deporte aparte.

Después de esto, el archivo se convierte en un PropertySource dentro del Environment. Y esto se puede comprobar sin necesidad de @Value: simplemente preguntar al Environment si ve la clave.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ContextFlowApplication {
    public static void main(String[] args) {
        // try-with-resources garantiza el cierre correcto del contexto (y la liberación de recursos)
        try (var ctx = new AnnotationConfigApplicationContext(AppConfig.class)) {
            // Verificamos que Environment realmente ve la clave de contextflow.properties
            String appName = ctx.getEnvironment().getProperty("contextflow.app-name");
            System.out.println("appName = " + appName); // appName = ContextFlow
        }
    }
}

Si ves ContextFlow, significa que @PropertySource funcionó: el archivo está conectado, Environment lee los valores.

En esta etapa, AppConfig ya actúa como punto superior de ensamblaje: es precisamente el que levantas a través de AnnotationConfigApplicationContext, y las configuraciones más específicas se pueden mantener como módulos del mismo ensamblaje.

Observación sobre file:

A veces se quiere decir: «Quiero un archivo externo junto al jar, para poder cambiarlo sin recompilar». Para eso existe realmente el prefijo file::

@PropertySource("file:./contextflow.properties") // ./ es el directorio de trabajo actual del proceso

Pero este enfoque tiene un matiz: ./ es el «directorio de trabajo actual». Y este depende de desde dónde se ejecute la aplicación (IDE, Gradle, terminal). Para un proyecto de aprendizaje y para la estabilidad, es mejor empezar con classpath:. Cuando ya tengas claro qué es Environment y PropertySource, la transición al file: externo será más fácil y sin misterios innecesarios.

4. ${...} y PropertySourcesPlaceholderConfigurer

Después de @PropertySource, el Environment ya sabe leer las claves del archivo. La siguiente pregunta práctica es cómo ese mismo valor llega al ${...} en @Value. En un AnnotationConfigApplicationContext simple, un PropertySourcesPlaceholderConfigurer independiente no siempre es necesario como primer paso, pero es útil conocerlo de todas formas: es un mecanismo de infraestructura explícito que hace visible la capa de resolución de placeholders y proporciona un punto donde se puede configurar por separado.

Es útil tener en mente dos ideas diferentes: conectar el archivo al contexto y separar explícitamente la mecánica de ${...} no son lo mismo. La mayoría de las veces basta con la primera, pero la segunda opción muestra bien a través de qué capa el contenedor sustituye los valores en las cadenas.

Primero veamos un @Value normal:

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class AppNamePrinter {
    public AppNamePrinter(@Value("${contextflow.app-name}") String appName) {
        // @Value intenta resolver el placeholder ${...} mediante el mecanismo de placeholders
        System.out.println("AppNamePrinter: " + appName);
    }
}

Si el placeholder por alguna razón no se resuelve, el error será más o menos así:

Could not resolve placeholder 'contextflow.app-name' ...

Es decir, el problema aquí no está en la clave en sí, sino en que la capa de sustitución ${...} no funcionó como esperabas.

A continuación se muestra una variante de infraestructura explícita, que es útil tener a mano para entender la mecánica o cuando se quiere configurar esta capa por separado:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;

@Configuration
public class PlaceholderConfig {
    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        // Separamos explícitamente la capa de resolución de placeholders.
        // static se suele elegir para que el configurador aparezca lo antes posible,
        // antes incluso de crear los beans normales donde se encuentra @Value("${...}").
        return new PropertySourcesPlaceholderConfigurer();
    }
}

Aquí la palabra clave es static. Para esta variante explícita, normalmente se hace static para que el configurador aparezca lo antes posible, antes de que Spring comience a crear los beans comunes con @Value.

Si se quiere una analogía, es como la seguridad en la entrada de una sala de conciertos. Debe llegar antes que el público. Si la seguridad llega después del público, por supuesto que también «funcionará», pero será demasiado tarde y demasiado divertido.

Deliberadamente no profundizo ahora en los detalles de qué tipo de bean de infraestructura es y en qué fase actúa. Nos basta con dos conclusiones prácticas: PropertySourcesPlaceholderConfigurer es una forma explícita de separar la resolución de placeholders en una capa independiente, y si lo defines como un método @Bean separado, normalmente se hace static.

Es útil fijar en la mente un pequeño esquema de qué se conecta con qué:

flowchart TD
    A["System properties"] --> ENV["Environment"]
    B["OS env vars"] --> ENV
    C["@PropertySource: contextflow.properties"] --> ENV

    ENV --> PSC["PropertySourcesPlaceholderConfigurer"]
    PSC --> VAL["@Value: ${...} resolution"]

Es decir, el archivo de configuración se convierte en una de las fuentes del Environment, y PropertySourcesPlaceholderConfigurer hace que @Value pueda obtener valores de ese Environment.

Comprobación en un contexto vivo

La teoría está bien, pero en Spring hay una práctica excelente: «comprobar que realmente arranca». Si se quiere separar explícitamente la capa de placeholders en un fragmento de infraestructura independiente, el contexto se puede levantar con dos configuraciones: AppConfig (donde está @PropertySource) y PlaceholderConfig (donde esta capa está separada).

Primero, asegurémonos de que AppNamePrinter se crea e imprime el nombre de la aplicación.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ContextFlowApplication {
    public static void main(String[] args) {
        try (var ctx = new AnnotationConfigApplicationContext(
                AppConfig.class,
                PlaceholderConfig.class // Conectamos la capa explícita de sustitución de ${...}
        )) {
            String appName = ctx.getEnvironment().getProperty("contextflow.app-name");
            System.out.println("Environment says: " + appName); // Environment says: ContextFlow
        }
    }
}

Y ahora la propia «impresora» del nombre (que ya tenemos):

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class AppNamePrinter {
    public AppNamePrinter(@Value("${contextflow.app-name}") String appName) {
        // Aquí es importante que appName llegue ya sustituido, no como una cadena del tipo "${contextflow.app-name}"
        System.out.println("AppNamePrinter: " + appName); // AppNamePrinter: ContextFlow
    }
}

Si todo está conectado correctamente, verás ambas salidas. El orden de las líneas puede variar un poco (Spring crea los beans en su propio orden), pero el significado es el mismo: tanto Environment como @Value llegaron al mismo valor.

Prueba de sobrescritura sin modificar el archivo

Para sentir el verdadero poder de «varias fuentes de configuración», se puede añadir una propiedad del sistema en main(). Normalmente tiene alta prioridad.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ContextFlowApplication {
    public static void main(String[] args) {
        // Sobrescribimos el valor a nivel de System properties (normalmente alta prioridad)
        System.setProperty("contextflow.app-name", "ContextFlow-Demo");

        try (var ctx = new AnnotationConfigApplicationContext(AppConfig.class, PlaceholderConfig.class)) {
            System.out.println(ctx.getEnvironment().getProperty("contextflow.app-name"));
            // ContextFlow-Demo
        }
    }
}

Y ya has cambiado el comportamiento de la ejecución sin modificar contextflow.properties y sin cambiar las clases Java. Sí, es un ejemplo pequeño, pero es exactamente la puerta a través de la cual la aplicación deja de estar «eternamente atada a un único entorno».

5. Ensamblaje de la configuración en el proyecto

Cuando el mecanismo funciona, surge un pensamiento peligroso: «¡Genial! Ahora voy a esparcir @PropertySource y ${...} por todo el proyecto». Y aquí es importante detenerse y dar un paso cuidadoso y maduro: ensamblar la configuración de manera que dentro de un mes puedas entender de dónde vienen las configuraciones, sin convertir el proyecto en una excavación arqueológica. Ahora haremos precisamente ese pequeño pero correcto incremento.

Hay dos reglas prácticas que mantienen el proyecto en orden.

Primera: @PropertySource debe vivir en la capa de configuración (en config.*), no en los servicios de negocio. Un servicio de negocio debe «recibir el valor listo», no «encargarse de buscar el archivo».

Segunda: PropertySourcesPlaceholderConfigurer es infraestructura, y es mejor que también viva cerca de la configuración, no «accidentalmente en algún lugar de support».

En el ContextFlow de aprendizaje se puede mantener esto en dos archivos (AppConfig y PlaceholderConfig), si se quiere ver la capa de infraestructura por separado. Pero es más cómodo tener en mente una única raíz superior — AppConfig — y considerar el resto de configuraciones como sus módulos. Si se prefiere algo más compacto, se puede colocar el static @Bean directamente en AppConfig. No es «mejor» ni «peor», es simplemente otra forma de organización. Lo importante es que en el proyecto haya un acuerdo claro.

Por ejemplo, así (una sola clase, pero aún legible):

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;

@Configuration
@ComponentScan("com.example.contextflow") // Escaneamos los componentes de la aplicación
@PropertySource(value = "classpath:contextflow.properties", encoding = "UTF-8") // Conectamos el archivo de propiedades desde los recursos
public class AppConfig {

    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        // Mostramos explícitamente la capa de sustituciones ${...} para @Value
        return new PropertySourcesPlaceholderConfigurer();
    }
}

Precisamente este AppConfig es conveniente considerarlo como el punto superior de ensamblaje de ContextFlow: es el que pasas a AnnotationConfigApplicationContext, y el resto de piezas de configuración viven como sus módulos si es necesario.

Ahora el punto de entrada vuelve a ser «un solo botón»:

new AnnotationConfigApplicationContext(AppConfig.class);

Y esto es agradable. Pero, de nuevo, no es «obligatorio», sino «cómodo».

Y una idea muy importante que te será útil desde hoy: si una propiedad es realmente obligatoria, y la inyectas como ${contextflow.app-name} sin valor por defecto, entonces si falta la clave, Spring debe fallar al iniciar. Esto no es crueldad, es preocupación por el futuro. Fail-fast es cuando la aplicación se rompe donde la causa es visible de inmediato, no media hora después en medio de un escenario de negocio. Todavía hablaremos de valores por defecto en @Value, pero ya es útil aceptar la idea: la ausencia de una clave obligatoria es un error de ensamblaje de la aplicación, no «bueno, no pasa nada».

6. Errores típicos de configuración

Error n.º 1: colocar contextflow.properties fuera de src/main/resources.
Esto es un clásico del género. El archivo está junto a build.gradle.kts, lo ves en el proyecto, pero Spring no. Síntoma: @PropertySource se queja de que no se encuentra el recurso, o Environment devuelve null. Se soluciona fácilmente: el archivo debe estar en src/main/resources, entonces Gradle lo colocará garantizadamente en el classpath.

Error n.º 2: confundir classpath: con la ruta al archivo.
A veces se escribe @PropertySource("classpath:/src/main/resources/contextflow.properties"). Esto parece lógico desde el punto de vista humano, pero no desde el de Java. En classpath: la ruta se especifica relativa a la raíz del classpath, no a las carpetas de código fuente. Lo correcto es: @PropertySource("classpath:contextflow.properties"), si el archivo está directamente en src/main/resources.

Error n.º 3: no distinguir entre conectar un PropertySource y la capa de resolución de placeholders alrededor de ${...}.
Síntoma: Environment.getProperty() devuelve el valor, pero ${...} en @Value no se comporta como esperabas, o la infraestructura es difícil de depurar. Una de las formas explícitas de hacer esta capa comprensible es sacar PropertySourcesPlaceholderConfigurer a un static @Bean independiente. En Spring Boot parte de esta infraestructura viene automáticamente, pero en Spring puro es útil verla al menos una vez de forma explícita.

Error n.º 4: si ya sacaste PropertySourcesPlaceholderConfigurer a un @Bean independiente, lo declaraste sin static.
A veces la aplicación parece arrancar, pero a veces empieza a comportarse de forma extraña al complicar la configuración, o te encuentras con problemas de inicialización temprana. Para la variante explícita de infraestructura, es mejor aprender el patrón de inmediato: public static PropertySourcesPlaceholderConfigurer .... Es uno de esos casos en los que «más corto» no significa «mejor».

Error n.º 5: poner un «valor por defecto sin sentido» en una propiedad obligatoria para que la aplicación no falle.
Por ejemplo: ${contextflow.app-name:asd}. La aplicación arranca, pero tú mismo has barrido el problema debajo de la alfombra. En una semana ejecutarás la aplicación, verás «asd» en la salida y empezarás a depurar «por qué audit no escribe donde debe», cuando el problema real era que el archivo de propiedades no se conectó. Si la clave es obligatoria, deja que el fallo sea honesto y temprano.

1
Tarea
Spring Core, nivel 12, lección 2
Bloqueada
Conexión del archivo properties en Environment
Conexión del archivo properties en Environment
1
Tarea
Spring Core, nivel 12, lección 2
Bloqueada
Sustitución de $${...} en bean mediante PropertySourcesPlaceholderConfigurer
Sustitución de ${...} en bean mediante PropertySourcesPlaceholderConfigurer
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION