Spicy Spring: Create your own ResourceLoader
As a Spring developer we know how to load a resource in a Service. The prefixes of classpath:
and file:
are commonly used. But what if we want to load resources from a different location like a database table RESOURCE, with our own prefix db:
? The Spring Framework provides some default built-in resource implementations, which can be found in the chapter Resources in the Spring Framework Reference Guide. The URL is prefixed and handled by the corresponding ResourceLoader
(JavaDoc). If we want to load a resource from a database table RESOURCE
we have to create our own ResourceLoader which triggers on the prefix db:
.
DatabaseResourceDao
Let's create an interface which describes the action of getting a resource based on the resourceName.
import java.io.InputStream;
import org.springframework.core.io.Resource;
public interface DatabaseResourceDao {
Resource getResource(String resourceName);
}
In this example our filename is equal to our resourcename. So if we execute getResource("db:myResource.txt")
the name of the resource would be myResource.txt
.
Database table
Next we create a database table which contains our binary resources. It has two columns: one for the name and one with the binary data.
//HSQLDB script
CREATE TABLE RESOURCE
(
NAME VARCHAR(256) NOT NULL PRIMARY KEY,
DATA BLOB NOT NULL
);
DatabaseResourceDao implementation
After we have created our database table it is time to write a DatabaseResourceDao
implementation to retrieve the Resource
data from our database table.
import java.io.InputStream;
import java.sql.ResultSet;
import java.util.Collections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.support.lob.DefaultLobHandler;
import org.springframework.stereotype.Repository;
@Repository
public class DatabaseResourceDaoImpl implements DatabaseResourceDao {
private final NamedParameterJdbcTemplate namedJdbcTemplate;
@Autowired
public DatabaseResourceDaoImpl(
NamedParameterJdbcTemplate namedJdbcTemplate) {
this.namedJdbcTemplate = namedJdbcTemplate;
}
@Override
public Resource getResource(final String resourceName) {
byte[] byteArray = namedJdbcTemplate.queryForObject(
"SELECT data FROM resource WHERE name = :name",
Collections.singletonMap("name", resourceName),
(ResultSet rs, int rowNum) ->
new DefaultLobHandler().getBlobAsBytes(rs, "data")
);
return new ByteArrayResource(byteArray);
}
}
A Custom ResourceLoader
We our able to retrieve a Resource
with the created bean DatabaseResourceDaoImpl
. This bean needs to be injected into a custom ResourceLoader
, a standard Spring interface for loading a Resource
. Our DatabaseResourceLoader
is an implementation that looks for "db:". If the string parameter location
contains the prefix db:
, we know we need to load the resource by making a call to the DatabaseResourceDao
implementation and returning the result. When the location
parameter does not contain the "db:" prefix, the DatabaseResourceLoader will delegate to the delegate
, which will be the original ResourceLoader
. NB. Our DatabaseResourceLoader
isn't ComponentScanned. In the next step we will construct it manually.
import org.apache.commons.lang.StringUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
public class DatabaseResourceLoader implements ResourceLoader {
private static final String DB_URL_PREFIX = "db:";
private final ApplicationContext applicationContext;
private final ResourceLoader delegate;
public DatabaseResourceLoader(
ApplicationContext applicationContext,
ResourceLoader delegate) {
this.applicationContext = applicationContext;
this.delegate = delegate;
}
@Override
public Resource getResource(String location) {
if (location.startsWith(DB_URL_PREFIX)) {
DatabaseResourceDao databaseResourceDao =
this.applicationContext.getBean(DatabaseResourceDao.class);
String resourceName =
StringUtils.removeStart(location, DB_URL_PREFIX);
return databaseResourceDao.getResource(resourceName);
}
return this.delegate.getResource(location);
}
@Override
public ClassLoader getClassLoader() {
return this.delegate.getClassLoader();
}
}
Activate the ResourceLoader
At last we need to construct and bind the DatabaseResourceLoader
and inject the original ResourceLoader
. By defining a bean that implements the BeanFactoryPostProcessor
and the ResourceLoaderAware
interface we get the original ResourceLoader
injected and register the new one by calling beanFactory.registerResolvableDependency(..)
. We also need to implement ApplicationContextAware
to be able to inject the ApplicationContext
into the DatabaseResourceLoader
. The BeanPostProcessor
interface is implemented to make sure that every other bean that implements ResourceLoaderAware
gets the correct ResourceLoader
injected.
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.Ordered;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Component;
@Component
public class ResourceLoaderBeanPostProcessor
implements BeanPostProcessor, BeanFactoryPostProcessor, Ordered,
ResourceLoaderAware, ApplicationContextAware {
private ResourceLoader resourceLoader;
private ApplicationContext applicationContext;
@Override
public Object postProcessBeforeInitialization(
Object bean, String beanName) {
if (bean instanceof ResourceLoaderAware) {
((ResourceLoaderAware)bean).
setResourceLoader(this.resourceLoader);
}
return bean;
}
@Override
public Object postProcessAfterInitialization(
Object bean,
String beanName) {
return bean;
}
@Override
public void postProcessBeanFactory(
ConfigurableListableBeanFactory beanFactory) {
this.resourceLoader =
new DatabaseResourceLoader(
this.applicationContext,
this.resourceLoader);
beanFactory.registerResolvableDependency(
ResourceLoader.class, this.resourceLoader);
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
@Override
public void setResourceLoader(
ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override
public void setApplicationContext(
ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
}
Use in our Service
Finally we are able to load a resource with db:
as prefix.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Service;
@Service
public class MyService {
@Autowired
public MyService(
ResourceLoader resourceLoader) {
//I should have myDbResource now!
resourceLoader.getResource("db:myResource.txt");
}
}