Create Custom Resource Loader for Spring Framework
Spring Framework is the most popular Java framework for web application development. It’s defining feature is dependency injection support, and it’s the first thing that comes to mind when Spring is mentioned. But, in addition to dependency injection, Spring provides a lot other features that simplify application development,
In this post, I will talk about resource handling in Spring Framework. We will implement custom resource loading mechanism and integrate it into sample application.
Complete source code is provided, and you can find it Github repo.
Table Of Contents
Spring resource handling
The main resource abstraction in Spring Framework is Resource interface. This interface provides methods common to all types of resources, whether they are simple files, streams, URLs or any other type. The benefit of this approach is that it provides unified approach to handling resources regardless of their type.
In addition to this interface, Spring also offer some built-in implementation which covers most common resource types:
UrlResource - used for any type of resource which can be accessed by URL
ClassPathResource - used to access resources which are available in application classpath
FileSystemResource - used to access file in the file system
PathResource - similar to
FileSystemResource
but used forjava.nio.file.Path
handlesServletContextResource - used to access resources relative to web application root directory
InputStreamResource - wraps given
InputStream
in Spring resourceByteArrayResource - wraps a byte array into Spring resource
Loading resources in Spring Framework
In order to unify resource loading, Spring provides ResourceLoader interface. Classes implementing this interface load resources based on specific logic appropriate for the resource type.
Spring also provides default implementation of this interface, conveniently called DefaultResourceLoader.
In addition to ResourceLoader
, this class also implements ProtocolResolver interface. This interface defines resolution strategy to determine which resource loader should be used to load the requested resource type. This is approach we will use in this example to load our custom resource.
Final piece of puzzle is ResourceLoaderAware interface. Classes implementing this interface expect to get a ResourceLoader
instance so they can operate on it.
Custom resource loading implementation
In this example, we will implement loading resources located in .zip files. This will allow us to load specific entries from zip archive into our application.
Implementing this solution, we’ll be able to load specific zip file entry using a path like this:
zip:///path/to/zipfile.zip!/pat/to/zip/entry
This URL starts with zip://
prefix, followed by absolute path to relevant zip file. Then we have an !
mark, which denotes that part after it marks the path to requested entry.
Resource definition
First order of business is to define the class which will return the resource. We will not implement Resource
interface, but rather we’ll use ByteArrayResource
as a way to represent zip entry data.
public class ZipEntryResource {
public Resource getResource(String zipFilePath, String entryName) {
try (var zipFile = new ZipFile(zipFilePath)) {
var entry = zipFile.getEntry(entryName);
var in = zipFile.getInputStream(entry);
var resource = new ByteArrayResource(in.readAllBytes());
in.close();
return resource;
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}
We pass path to zip file and to the requested zip entry to the getResource()
method. This method will first open the zip file represented by the first argument. it will then load the entry denoted by second argument, and wrap it’s input stream into an instance of ByteArrayResource
. We finally return the resource instance.
Resource loader definition
Next part of the puzzle is implementing ResourceLoader
interface to load our custom resource. The following class represents this:
public class ZipResourceLoader implements ResourceLoader {
public static final String ZIP_PREFIX = "zip://";
private final ResourceLoader delegate;
public ZipResourceLoader(ResourceLoader delegate) {
this.delegate = delegate;
}
@Override
public Resource getResource(String location) {
if(location.startsWith(ZIP_PREFIX)) {
var path = location.substring(ZIP_PREFIX.length());
int entryIndex = path.lastIndexOf('!');
String zipFilePath = path.substring(0, entryIndex);
String entryPath = path.substring(entryIndex + 1);
var zipEntry = new ZipEntryResource();
return zipEntry.getResource(zipFilePath, entryPath);
}
return delegate.getResource(location);
}
@Override
public ClassLoader getClassLoader() {
return this.delegate.getClassLoader();
}
}
Field ZIP_PREFIX
marks the protocol for our custom resource. Fiel delegate
represents default resource loader registered in the application. We will use this resource loader as a fallback, if requested resource is not the one supported by this loader.
Method getResource()
accepts the path to the resource. This method will determine the path to zip file and requested entry, based on description of our custom URL. It will finally return the resource represented by ZipEntryResource
class.
Register custom resource loader
Finally, we need to register our cusomt resource loader with the application in order to use it. We will create custom component which implements ResourceLoaderAware
and ProtocolResolver
interface.
@Component
public class CustomResourceLoaderProcessor implements ResourceLoaderAware, ProtocolResolver {
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
if(DefaultResourceLoader.class.isAssignableFrom(resourceLoader.getClass())) {
((DefaultResourceLoader)resourceLoader).addProtocolResolver(this);
} else {
System.out.println("Could not assign protocol loader.");
}
}
@Override
public Resource resolve(String location, ResourceLoader resourceLoader) {
if(location.startsWith(ZipResourceLoader.ZIP_PREFIX)) {
var loader = new ZipResourceLoader(resourceLoader);
return loader.getResource(location);
}
return resourceLoader.getResource(location);
}
}
Method setResourceLoader()
is a part of ResourceLoaderAware
interface. It will check if the passed resource loader is an instance of DefaultResourceLoader
. If so, it will add itself as a protocol resolver.
Method resolve()
will return the requested resource. It will first check if requested location starts with zip://
, which we defiend as prefix for our custom resource path. If it does, it will return our custom resource. Otherwise, it will offload resource loading to default loader.
Testing custom resource loader
For testting our resource loader, we will create a custom service which loads the resource. We will use two aproaches for testing:
inject resource loader and load the resource programatically
inject the resource directly using annotation
Here’s the service code:
@Service
public class ZipService {
@Autowired
private ResourceLoader resourceLoader;
@Value("zip://./archive.zip!file2.txt")
private Resource customResource;
public void loadResource(String resourceUrl) throws IOException {
var resource = resourceLoader.getResource(resourceUrl);
var txt = new String(resource.getInputStream().readAllBytes());
System.out.println("File content: " + txt);
}
public void getCustomResource() throws IOException {
var txt = new String(customResource.getInputStream().readAllBytes());
System.out.println("Resource from property: " + txt);
}
}
Method loadResource
will use injected ResourceLoader
to load the requested resource. Under the hood, this method uses protocol resolver to determine which resource loader to use.
Field customResource
is annotated with @Value
annotation, whose value is path to our custom resource. Spring will automatically inject requested resource using our custom resource loader. Just as with the previous method, this method uses our custom resource loader to load the resource.
Finally, we need a main method to invoke the service and get the result.
public class SpringCustomResourceLoading
{
public static void main( String[] args ) throws Exception {
var ctx = new AnnotationConfigApplicationContext(AppConfig.class);
var svc = ctx.getBean(ZipService.class);
svc.loadResource("zip://./archive.zip!file1.txt");
svc.getCustomResource();
}
}
This method creates application context and fetches the service bean. There is s test zip file provided with the application source code. In this test, we load the resources using service bean method and using the annotation.
This is the expected output:
File content: this is file 1.
Resource from property: This is file 2.
Final thoughts
So, this is it. We developed our own custom resoure loader which allows us to load zip file entries directly into the application. You can use this approach as the base for developing your own resource loaders for other resource types.
As always, I would love to hear your thouhts about this. Feel free to post any comments using the form bellow.