Create Custom Resource Loader for Spring Framework

18 March, 2022 |  Vladimir Djurovic 
img/files-custom-resource.png

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:

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:

  1. inject resource loader and load the resource programatically

  2. 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.