Abstracción de Recursos en Spring: Resource y ResourceLoader
Trabajar con archivos en Java es un dolor de cabeza innecesario. Un archivo del sistema de archivos, un recurso del classpath y una URL remota requieren APIs distintas (File, ClassLoader.getResource, URL.openStream). Spring resuelve esto con Resource y ResourceLoader: una sola interfaz para todo.
La interfaz Resource
org.springframework.core.io.Resource reemplaza a java.net.URL con una API más capaz. Sobre todo porque URL no maneja bien el classpath ni los recursos relativos a ServletContext, que es justo lo que necesitas el 90% del tiempo en una aplicación web.
Los métodos que vas a usar: exists() para verificar que el recurso existe físicamente, getInputStream() para abrirlo (te devuelve un stream nuevo cada vez), getDescription() para mensajes de error legibles, getURL() y getFile() para acceder directamente cuando el recurso es un archivo físico. Y isOpen() para saber si ya tienes un handle abierto que no debes cerrar tú.
Implementaciones que importa conocer
Spring tiene varias implementaciones de Resource para distintos orígenes de datos. UrlResource cubre URLs estándar (http:, https:, file:). ClassPathResource accede a recursos empaquetados en el JAR, con prefijo classpath:. FileSystemResource trabaja con archivos del sistema, con prefijo file: o sin prefijo. ServletContextResource accede a recursos dentro de aplicaciones web (sin prefijo). Y ByteArrayResource para datos en memoria cuando necesitas la interfaz pero no tienes un archivo.
La regla práctica: si el recurso va a vivir dentro del JAR (plantillas, configuración), usa ClassPathResource. Si es un archivo externo que cambia sin redeploy, usa FileSystemResource o UrlResource.
ResourceLoader: cargar recursos desde un String
La interfaz ResourceLoader toma un String y devuelve un Resource. Todos los ApplicationContext implementan esta interfaz, así que puedes inyectar el contexto como ResourceLoader directamente.
Las reglas de resolución son simples. Si el String no tiene prefijo, el tipo de Resource depende del ApplicationContext (en una app web sería ServletContextResource). Si lleva prefijo (classpath:, file:, http:), se fuerza el tipo correspondiente.
@Component
public class ResourceExample {
private final ResourceLoader resourceLoader;
public ResourceExample(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
public void loadResources() throws IOException {
// Resolución dependiente del contexto
Resource template1 = resourceLoader.getResource("config/template.txt");
// Forzar ClassPathResource
Resource config = resourceLoader.getResource("classpath:app.properties");
// Forzar UrlResource (sistema de archivos)
Resource fileLog = resourceLoader.getResource("file:/var/log/app.log");
}
}Cargar múltiples recursos con wildcards
Cuando necesitas buscar recursos que coincidan con un patrón, ResourcePatternResolver es tu amigo. Acepta wildcards con classpath*: (que escanea todos los JARs del classpath) y * para nombres de archivo:
@Service
public class ConfigurationService {
private final ResourcePatternResolver resolver;
public ConfigurationService(ResourcePatternResolver resolver) {
this.resolver = resolver;
}
public void loadAllConfigurations() throws IOException {
// Cargar todos los XML del classpath
Resource[] configs = resolver.getResources("classpath*:META-INF/*.xml");
// Cargar todos los properties de un directorio
Resource[] properties = resolver.getResources("file:/config/*.properties");
for (Resource config : configs) {
// Procesar cada configuración
try (InputStream is = config.getInputStream()) {
// Leer y procesar el archivo
}
}
}
}Esto es útil cuando tienes módulos que registran su propia configuración y no quieres mantener un listado manual.
Inyección con @Value
La forma más cómoda de inyectar recursos es con @Value. Puedes poner un valor por defecto con : y el prefijo correspondiente:
@Service
public class TemplateService {
private final Resource emailTemplate;
private final Resource logoImage;
public TemplateService(
@Value("${app.email.template:classpath:templates/default.html}") Resource emailTemplate,
@Value("classpath:static/images/logo.png") Resource logoImage) {
this.emailTemplate = emailTemplate;
this.logoImage = logoImage;
}
public void processTemplate() throws IOException {
if (emailTemplate.exists()) {
try (InputStream is = emailTemplate.getInputStream()) {
// Procesar la plantilla
String content = StreamUtils.copyToString(is, StandardCharsets.UTF_8);
System.out.println("Plantilla cargada desde: " + emailTemplate.getDescription());
}
}
}
}Lo bueno de @Value con placeholders es que la ubicación se vuelve configurable sin tocar código. En desarrollo usas el classpath, en producción un volumen montado.
Configuración modular
@Configuration
public class ModuleConfiguration {
@Bean
public Properties moduleProperties(ResourceLoader resourceLoader) throws IOException {
Properties props = new Properties();
// Cargar todas las propiedades de módulos del classpath
Resource[] moduleResources = resourceLoader.getResources("classpath*:modules/*.properties");
for (Resource resource : moduleResources) {
try (InputStream is = resource.getInputStream()) {
Properties moduleProps = new Properties();
moduleProps.load(is);
// Combinar con propiedades principales
props.putAll(moduleProps);
}
}
return props;
}
}Recursos con validación
Antes de abrir un recurso conviene validar que existe, que es legible y que no excede el tamaño esperado:
@Component
public class ResourceValidator {
public void validateResource(Resource resource) throws IOException {
if (!resource.exists()) {
throw new IllegalArgumentException("El recurso no existe: " + resource.getDescription());
}
if (!resource.isReadable()) {
throw new IllegalArgumentException("El recurso no es legible: " + resource.getDescription());
}
// Validar tamaño para archivos
if (resource.isFile()) {
File file = resource.getFile();
if (file.length() > 10 * 1024 * 1024) { // 10MB
throw new IllegalArgumentException("El archivo es demasiado grande: " + file.getName());
}
}
}
}Caching de recursos
Si accedes al mismo recurso repetidamente, un caché simple con ConcurrentHashMap evita resolver la ubicación cada vez:
@Service
public class CachedResourceService {
private final Map<String, Resource> resourceCache = new ConcurrentHashMap<>();
private final ResourceLoader resourceLoader;
public CachedResourceService(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
public Resource getResource(String location) {
return resourceCache.computeIfAbsent(location, loc -> {
Resource resource = resourceLoader.getResource(loc);
// Validar antes de cachear
try {
if (resource.exists()) {
return resource;
}
} catch (IOException e) {
throw new RuntimeException("Error al validar recurso: " + loc, e);
}
throw new IllegalArgumentException("Recurso no encontrado: " + loc);
});
}
}Prácticas que te ahorran dolores de cabeza
Prefiere classpath: sobre file: cuando puedas. Un ClassPathResource funciona igual en desarrollo (IDE) y en producción (JAR dentro de un contenedor). Un file:src/main/resources/... solo funciona en desarrollo y explota en el primer deploy.
Usa placeholders para la configuración que cambia entre entornos: @Value("${app.template.location:classpath:templates/default.html}"). Así dejas un default razonable y overrides sin tocar código.
Cierra los streams siempre con try-with-resources. getInputStream() abre un handle nuevo en cada llamada y no se cierra solo:
public void processResource(Resource resource) {
try (InputStream is = resource.getInputStream()) {
// Procesar recurso
// El stream se cierra automáticamente
} catch (IOException e) {
throw new RuntimeException("Error procesando recurso: " + resource.getDescription(), e);
}
}Y valida recursos críticos al arrancar, no cuando ya tienes peticiones entrando:
@PostConstruct
public void validateResources() {
if (!requiredResource.exists()) {
throw new IllegalStateException("Recurso requerido no encontrado: " + requiredResource.getDescription());
}
}Integración con Spring Boot
Spring Boot permite externalizar la configuración de recursos con @ConfigurationProperties:
@ConfigurationProperties(prefix = "app.resources")
@Component
public class ResourceProperties {
private String templatesLocation = "classpath:templates/";
private String staticLocation = "classpath:static/";
private String externalLocation = "file:/var/app/resources/";
// getters y setters
public Resource getTemplate(String name) {
return new PathMatchingResourcePatternResolver()
.getResource(templatesLocation + name);
}
}Rendimiento: qué usar cuándo
ClassPathResource es lo más rápido para recursos dentro del JAR. FileSystemResource gana con archivos grandes o cuando necesitas acceso aleatorio (RandomAccessFile). Si accedes al mismo recurso en cada petición, cachéalo. Y si un recurso es caro de cargar pero no siempre se usa, carga diferido.
Mi recomendación después de usar esto en producción: usa siempre @Value con prefijos explícitos (classpath:, file:). Sin prefijo la resolución depende del contexto de la aplicación, y eso es justo el tipo de magia implícita que te muerde a las 3 AM en producción.