CodeGym /Cursos /Spring Core /Lectura segura de Resource

Lectura segura de Resource

Spring Core
Nivel 15, Lección 4
Disponible

1. Leer Resource como parte de la fiabilidad de la aplicación

Los recursos ya están distribuidos en carpetas y se han integrado como dependencias normales. Ahora queda la parte más mundana: leerlos de forma que la aplicación o bien obtenga un texto normal, o bien falle de inmediato y de forma clara.

Para las plantillas obligatorias de ContextFlow, un buen escenario es uno: el recurso se encuentra, se lee y proporciona un texto válido. La opción "no se pudo – enviaremos una cadena vacía" es peor que cualquier error ruidoso, porque la avería se adentra profundamente en el escenario de negocio.

Para apreciar el contraste, recordemos la opción más frágil, que a menudo "funciona por casualidad" en el IDE:

import java.nio.file.Files;
import java.nio.file.Path;

// ATENCIÓN: la ruta relativa depende del directorio de trabajo actual (working dir).
// "Funciona" en el IDE – y se rompe después de compilar en un jar o al ejecutarse desde otra ubicación.
String text = Files.readString(Path.of(
        "src/main/resources/templates/notifications/order-created.txt"
)); // "funciona" mientras se ejecute desde la raíz del proyecto

El problema no está en Files.readString() como tal. El problema es que ese código está ligado a la estructura de los fuentes y al working directory. Por eso, a continuación construimos otro contrato: una plantilla obligatoria o se carga con éxito al arrancar el contenedor, o la aplicación se detiene honestamente con una causa clara.

2. Fail-fast al cargar plantillas

Dado que los recursos ya se han convertido en dependencias, es lógico resolver también el momento de su lectura a nivel del contenedor. En temas de infraestructura, casi siempre es más ventajoso el enfoque "fail-fast": si un recurso crítico falta, no fingimos que todo está bien, sino que detenemos honestamente la construcción del contexto. Suena estricto, pero en la práctica ahorra horas de tiempo. Y sí, esto es muy propio de Spring: el contenedor es capaz de fallar al arrancar precisamente porque es más útil que detectar problemas más tarde en un lugar aleatorio.

En nuestro proyecto, esto se relaciona naturalmente con el tema del ciclo de vida que ya hemos visto. @PostConstruct es el punto ideal para verificar la existencia de las plantillas y cargarlas en memoria. Así, el error se manifestará en el momento de arrancar el ApplicationContext, y no cuando el usuario ya ha "creado un pedido" y está esperando una notificación.

Un pequeño esquema de lo que buscamos:

flowchart TD
    A[Spring crea el bean NotificationTemplateCatalog] --> B["@PostConstruct loadTemplates()"]
    B --> C{"Resource exists()?"}
    C -- no --> D["IllegalStateException con getDescription()"]
    C -- sí --> E["getInputStream + readAllBytes"]
    E --> F[Plantilla guardada en campo/caché]
    F --> G[El servicio usa la cadena lista sin I/O]

Y aquí está el "esqueleto" mínimo de la idea en código (aún sin detalles de lectura):

import jakarta.annotation.PostConstruct;

@PostConstruct
void loadTemplates() {
    // Se invoca una vez al arrancar el contexto — lugar ideal para la comprobación fail-fast.
    // 1) verificar que los recursos existen
    // 2) leerlos en cadenas
    // 3) guardarlos en campos (caché)
}

Un punto metodológico importante: no realizamos la lectura de la plantilla en cada envío de notificación. La plantilla es parte de la configuración de la aplicación, no "datos de usuario", por lo que es lógico cargarla una vez y usarla muchas veces. Esto es más rápido y más predecible: no dependemos de I/O en el momento de ejecutar un escenario de negocio.

3. Métodos de Resource para diagnóstico

Cuando estás empezando, es fácil perderse en la API: Resource tiene muchos métodos, y cada implementación tiene aún más matices. Ahora necesitamos un pequeño "conjunto de diagnóstico" que ayude a tomar decisiones y escribir mensajes de error útiles. En un proyecto de aprendizaje, es mejor dominar 4-5 métodos y usarlos de forma consistente, que conocer 20 métodos y tener que elegir al azar cada vez.

A continuación, una tabla compacta de lo que más nos servirá para nuestra tarea (plantillas y textos pequeños):

Método de Resource Para qué sirve Cómo lo usamos en ContextFlow
exists() Verificar si el recurso se encuentra Antes de leer una plantilla obligatoria
isReadable() A veces el recurso existe pero no se puede leer Protección adicional, especialmente para file:
getInputStream() Forma universal de leer el contenido Teaching default para lectura de texto
getDescription() Descripción legible del recurso Siempre lo añadimos al texto del error
getFilename() Nombre del archivo (si lo tiene) A veces útil para logs/mensajes, pero no es base de la lógica

Sintamos de inmediato la utilidad de getDescription(). Es un caso donde Spring ayuda directamente a escribir buenos errores:

import org.springframework.core.io.Resource;

String describe(Resource r) {
    // Muy adecuado para excepciones y logs: Spring mismo forma una descripción clara.
    return r.getDescription(); // p.ej.: "class path resource [templates/.../order-created.txt]"
}

Si luego ves una excepción con esa cadena, comprendes casi al instante qué se buscaba y dónde. Esto es mucho mejor que "File not found" sin contexto, donde luego pasas 20 minutos preguntándote: "¿y qué archivo era exactamente?".

4. getInputStream en lugar de getFile

En este punto, muchos principiantes (y, sinceramente, algunos desarrolladores adultos después de una semana dura) cometen el mismo error: intentan convertir un Resource en un File y seguir como antes. Mentalmente piensan: "bueno, si es un recurso, entonces debe tener un archivo". Pero un recurso de classpath no es necesariamente un archivo, especialmente cuando la aplicación está empaquetada en un jar.

Aquí hay un ejemplo de código que parece plausible pero puede romperse inesperadamente:

import java.nio.file.Files;
import org.springframework.core.io.Resource;

String readViaFile(Resource resource) throws Exception {
    // Peligroso: para un recurso de classpath dentro de un jar puede no existir un archivo físico.
    return Files.readString(resource.getFile().toPath()); // puede fallar para classpath dentro de un jar
}

¿Por qué? Porque para un recurso "dentro de un jar" puede no existir un archivo físico en el sistema de archivos. Hay un "trozo de archivo", no una ruta en el disco. Por eso, para una lectura universal, Spring nos proporciona una API mucho más honesta: getInputStream().

Aquí tienes una versión corta y correcta para leer texto (para plantillas pequeñas es un excelente teaching default):

import java.nio.charset.StandardCharsets;
import org.springframework.core.io.Resource;

String readUtf8(Resource resource) throws Exception {
    // Universal: funciona tanto para classpath como para file: y para recursos URL.
    try (var in = resource.getInputStream()) { // try-with-resources garantiza el cierre del flujo
        // Para plantillas pequeñas, readAllBytes() se lee de forma simple y transparente.
        return new String(in.readAllBytes(), StandardCharsets.UTF_8);
    }
}

Aquí hay dos beneficios importantes. Primero, no estamos atados al sistema de archivos: el flujo se puede abrir tanto desde classpath como desde URL. Segundo, try-with-resources garantiza el cierre del flujo. Un flujo es como un grifo de agua: si lo abres y no lo cierras, las "fugas" pueden no ser visibles de inmediato, pero con el tiempo empezarán a molestar (especialmente cuando hay muchas lecturas de este tipo).

5. Helper ResourceTextReader

Si en dos o tres clases empiezas a escribir manualmente exists(), try-with-resources, new String(...), catch(IOException e) y mensajes de error, rápidamente aparecerá código copiado. Y el código copiado en infraestructura es desagradable porque multiplica "diferentes estándares de comportamiento": una clase falla, otra calla, una tercera devuelve una cadena vacía, una cuarta registra en System.out.println (sí, eso también es un pecado, solo que más leve).

Por lo tanto, un paso razonable es extraer la lectura en un pequeño helper que se convierta en la forma unificada de trabajar con recursos de texto en ContextFlow. No debe ser un "motor universal de recursos", no estamos construyendo una nave espacial. Necesitamos un ladrillo ordenado, comprensible y reutilizable.

Empecemos con la comprobación fail-fast:

import org.springframework.core.io.Resource;

public void requireExists(Resource resource) {
    // Fail-fast: si un recurso obligatorio no existe, es mejor fallar al arranque con una causa clara.
    if (!resource.exists()) {
        throw new IllegalStateException(
                "Resource not found: " + resource.getDescription()
        );
    }
}

Ahora añadimos la lectura y una envoltura adecuada de IOException en una excepción runtime, para no arrastrar excepciones checked por todos los servicios:

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.springframework.core.io.Resource;

public String readRequiredUtf8(Resource resource) {
    requireExists(resource);

    try (var in = resource.getInputStream()) {
        // Fijamos explícitamente la codificación para que el comportamiento no dependa de la plataforma.
        return new String(in.readAllBytes(), StandardCharsets.UTF_8);
    } catch (IOException e) {
        // Punto importante: no tragamos el error, sino que añadimos contexto y lo elevamos como excepción runtime.
        throw new IllegalStateException(
                "Cannot read resource: " + resource.getDescription(), e
        );
    }
}

Observa: no "tragamos" el error. Lo elevamos añadiendo contexto. Eso es un diagnóstico adecuado: el error no se oculta, pero se vuelve más comprensible.

En un proyecto real, es conveniente hacer de este helper un bean de Spring, para poder inyectarlo en catálogos de plantillas y otros componentes de infraestructura:

import org.springframework.stereotype.Component;

@Component
public class ResourceTextReader {
    // métodos requireExists(...) y readRequiredUtf8(...)
}

6. Plantillas de notificaciones en ContextFlow

Ahora armemos una versión funcional de ContextFlow, donde el catálogo de plantillas haga tres cosas a la vez: mantenga referencias a los recursos, los lea con fail-fast al arrancar y proporcione a los servicios de negocio métodos listos render...().

Supongamos que en src/main/resources tenemos estos archivos:

src/main/resources/templates/notifications
├─ order-created.txt
└─ order-cancelled.txt

Para no introducir un lenguaje de plantillas separado, mantenemos las plantillas lo más simples posible: texto normal más %s, que luego sustituimos mediante String.formatted(...).

# order-created.txt
ORDER CREATED: id=%s, customer=%s
# order-cancelled.txt
ORDER CANCELLED: id=%s, reason=%s

Ahora creemos un catálogo que inyecte dos Resource y los lea en @PostConstruct. Para no complicar el wiring, podemos inyectar los recursos a través de @Value directamente con classpath:. Esto no es "hardcode malo" si los consideras parte del suministro de la aplicación.

Fragmento del constructor:

import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;

@Component
public class NotificationTemplateCatalog {

    public NotificationTemplateCatalog(
            // Le decimos explícitamente a Spring que es un recurso de classpath (no un "archivo en disco").
            @Value("classpath:templates/notifications/order-created.txt") Resource created,
            @Value("classpath:templates/notifications/order-cancelled.txt") Resource cancelled) {
        // ...
    }
}

Ahora, la pieza clave: @PostConstruct y la lectura a través de nuestro helper. Lo dividimos en pequeñas partes para que el código no parezca una sábana.

Campos:

import org.springframework.core.io.Resource;

private final Resource createdTemplate;
private final Resource cancelledTemplate;
private final ResourceTextReader reader;

private String createdText;
private String cancelledText;

Carga con @PostConstruct:

import jakarta.annotation.PostConstruct;

@PostConstruct
void load() {
    createdText = reader.readRequiredUtf8(createdTemplate);
    cancelledText = reader.readRequiredUtf8(cancelledTemplate);
}

Y los métodos que devuelven el texto ya preparado. Podemos hacer el "motor de plantillas" más simple a nivel de String.formatted() — sin librerías ni magia:

public String renderCreated(String orderId, String customerName) {
    return createdText.formatted(orderId, customerName);
}

public String renderCancelled(String orderId, String reason) {
    return cancelledText.formatted(orderId, reason);
}

Esta forma de catálogo es útil aquí: la clase depende honestamente de los recursos, los lee una vez y luego ofrece a los servicios una API simple "genera el texto de la notificación". Los servicios de negocio ya no saben dónde están los archivos ni cómo se leen. Este es un excelente ejemplo de una aislamiento saludable.

Diagnóstico de errores y "rol" del recurso

El mensaje de error no es un lugar para la creatividad, pero es un lugar para el respeto hacia tu yo futuro. El "tú del futuro" suele estar cansado, con una fecha límite encima, y no quiere descifrar un acertijo sobre "qué se rompió exactamente". Por eso buscamos errores que respondan a tres preguntas: qué se buscaba, dónde se buscaba y por qué falló.

Comparemos dos mensajes:

Malo (formalmente "verdad", pero inútil):

IllegalStateException: Cannot read template

Bueno (ya tiene concreción):

IllegalStateException: Cannot read resource: class path resource [templates/notifications/order-created.txt]

Para mejorar aún más el diagnóstico, a veces es útil añadir un "rol" al recurso. No solo "no se pudo leer el recurso", sino "no se pudo leer la plantilla de creación de pedido". Así, en tu cabeza el problema se asocia inmediatamente con el escenario de negocio.

Aquí hay una versión mínima del método que acepta un rol y lo añade a la excepción:

import org.springframework.core.io.Resource;

public String readRequiredUtf8(Resource resource, String role) {
    if (!resource.exists()) {
        throw new IllegalStateException(
                "Missing resource (" + role + "): " + resource.getDescription()
        );
    }
    return readRequiredUtf8(resource); // usamos el método base
}

Y la llamada se vuelve autodocumentada:

createdText = reader.readRequiredUtf8(createdTemplate, "order-created template");

Este truco es muy "didácticamente útil": muestra que el diagnóstico es parte del diseño. No solo leemos un archivo, sino que construimos el soporte de la aplicación. Y sí, el soporte comienza con excepciones normales, no con una depuración heroica un viernes por la noche.

NotificationDispatchService usa el catálogo

Para confirmar que realmente hemos mejorado la arquitectura, veamos cómo se ve esto en el servicio de envío de notificaciones. Antes, fácilmente podría haber surgido la tentación de "vamos a leer el archivo aquí mismo". Ahora simplemente pide al catálogo que genere el texto.

Versión mini del servicio:

import org.springframework.stereotype.Service;

@Service
public class NotificationDispatchService {

    private final NotificationTemplateCatalog templates;
    private final NotificationSender sender;

    public NotificationDispatchService(NotificationTemplateCatalog templates,
                                       NotificationSender sender) {
        this.templates = templates;
        this.sender = sender;
    }
}

Y el método que envía la notificación de creación de pedido:

public void notifyOrderCreated(String orderId, String customerName) {
    String text = templates.renderCreated(orderId, customerName);
    sender.send(text); // los detalles del envío están en la implementación de NotificationSender
}

Si en algún lugar del escenario (por ejemplo, en ScenarioRunner) llamas a esto, obtendrás un resultado comprensible, y la lectura de los recursos ocurrirá exactamente una vez al arrancar. En una buena versión, el código del escenario no debería tener I/O relacionado con las plantillas: se ocupa de las acciones de negocio, y la infraestructura está preparada de antemano.

Si quieres verlo con tus ojos, puedes usar temporalmente un ConsoleNotificationSender y ver algo como:

System.out.println(text); // ORDER CREATED: id=ORD-1, customer=Alice

Y un hecho agradable: si accidentalmente renombraste order-created.txt o lo pusiste en otra carpeta, lo sabrás al arrancar el contexto, no "dentro" de notifyOrderCreated().

Aquí trabajamos con el texto como un recurso físico concreto: encontramos el archivo, lo leímos, sustituimos valores. Cuando hay muchos textos y ya no es necesario buscarlos por ruta sino por clave, locale y reglas de fallback, sobre esta misma base de recursos aparece MessageSource.

7. Errores típicos al leer Resource

Error n.º 1: llamar directamente a getFile en un recurso de classpath.
Este error surge de un viejo hábito: "si algo se parece a un archivo, entonces es un archivo". Pero un recurso de classpath puede vivir dentro de un jar, y puede que no exista un archivo físico. Por eso getFile a veces funciona en el IDE y falla inesperadamente después del empaquetado. El teaching default para la lectura es getInputStream().

Error n.º 2: olvidar cerrar el flujo.
resource.getInputStream() devuelve un InputStream, y un flujo es un recurso en el sentido literal. Hay que cerrarlo. La forma más sencilla y correcta es try-with-resources. Si lees recursos "manualmente" y no los cierras, puedes tener problemas extraños en una aplicación de larga duración y síntomas muy desagradables que cuesta relacionar con la causa original.

Error n.º 3: tragarse el IOException y continuar con una plantilla vacía.
A veces uno quiere hacer "como en la UI": si algo no se cargó, mostramos vacío. Pero una plantilla de notificación o un encabezado de informe es parte de la configuración de la aplicación, no un elemento opcional. Si no está, es mejor fallar de inmediato y explicar el motivo. De lo contrario, obtienes una "avería silenciosa": los escenarios siguen ejecutándose, pero las notificaciones se vuelven sin sentido, y la depuración se convierte en una cacería de fantasmas.

Error n.º 4: escribir un mensaje de error sin getDescription().
Un mensaje "Resource not found" sin indicar qué recurso se buscaba apenas ayuda. getDescription() en Spring suele contener información sobre el tipo de recurso y su "dirección" (classpath/file/url). Es diagnóstico gratuito: pecado no usarlo.

Error n.º 5: dispersar la lectura de recursos por los servicios de negocio.
Si OrderPlacementService o NotificationDispatchService empiezan a juntar rutas y hacer I/O, aparece una "infraestructura oculta" dentro del código de negocio. Ese código es difícil de probar, difícil de cambiar y difícil de leer. Es mucho mejor mantener la lógica de recursos en un único bean de infraestructura/helper (por ejemplo, NotificationTemplateCatalog + ResourceTextReader) y ofrecer a la capa de negocio una API lista "dame el texto".

1
Tarea
Spring Core, nivel 15, lección 4
Bloqueada
Helper para leer el recurso obligatorio
Helper para leer el recurso obligatorio
1
Tarea
Spring Core, nivel 15, lección 4
Bloqueada
Carga fail-fast de plantilla faltante
Carga fail-fast de plantilla faltante
1
Cuestionario/control
Recursos Spring, nivel 15, lección 4
No disponible
Recursos Spring
Trabajo con recursos Spring
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION