Reading Resources from JAR Files
One interesting challenge I encountered is the need or ability to make an Java application extensible by providing additional classes and configuation. Ideally extension should happen by dropping a properly crafted JAR file into a specified location and restard the server. Along the line I learned about Java's classpath. This is what is to be shared here.
Act one: onto the classpath
When you start off with Java, you would expect, that you simply can set the classpath varible either using an environment variable or the java -cp
parameter. Then you learn the hard way, that java -jar
and java -cp
are mutually exclusive. After a short flirt with fatJAR, you end up with a directory structure like this:
The secreingredient to make this work is the manifest file inside the myApp.jar
. It needs to be told to put all jar files in libs
onto the classpath too. In maven, it looks like this:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>${maven.jar.plugin.version}</version>
<configuration>
<archive>
<manifest>
<mainClass>com.hcl.domino.keep.Launch</mainClass>
</manifest>
<manifestEntries>
<Class-Path>.</Class-Path>
<Class-Path>libs/*</Class-Path>
</manifestEntries>
</archive>
</configuration>
</plugin>
Now, that all JARS are successfully availble on the classspath, we can try to retrieve them.
Act two: find a resource
Reliably opening resources in an Java app needs to be done using an InputStream. It is the only way to happily ignore if the resource is inside a JAR or happens to live on the file system. Why is that important? Well, in a Docker deployment I usually don't package my applications as Jars, since they live inside a container already. We don't want to play Inception.
There are two method returning a resource as an InputStream:
On the surface they do the same, but on a closer look slightly different. Class.getResourceAsStream
looks for resources at the path location of the class itself, so when a resource is at the root of the jar, you need a leading /
, e.g. /config.json
. ClassLoader.getResourceAsStream
on the other hand would return null
when the resource name is prefixed with /
, while without it works as expected e.g. config.json
.
I use Google Guava's I/O system to read a resource into a String:
static Optional<String> stream2String(final InputStream in) {
try {
ByteSource byteSource = new ByteSource() {
@Override
public InputStream openStream() {
return in;
}
};
return Optional.of(byteSource.asCharSource(StandardCharsets.UTF_8).read());
} catch (IOException e) {
e.printStackTrace();
return Optional.empty();
}
}
Getting one file is only half the Story, we want all of them
Act three: find all resources
The method we need for this is ClassLoader.getResources
. It returns an Enumeration of URLs. You might be tempted to use URL.getFile()
to get hands on the file, but you will fail when the resource is inside a jar, indicated by the URL starting with jar:file:/
. The solution is the same as for single resources: use the InputStream provided by URL.openStream()
. I use this helper:
String readFromURL(final URL source) {
try (InputStream in = source.openStream()) {
return stream2String(in).orElseThrow(() -> new Exception("Can't read " + source));
} catch (Exception e) {
return e.getMessage();
}
}
You can test this with a quick code snippet, it will provide you with 3 resources:
void allTheFileContent() throws IOException {
Enumeration<URL> r = this.getClass().getClassLoader().getResources("settings.json");
Collections.list(r).stream()
.map(this::readFromURL)
.forEach(System.out::println);
}
Act four: merge
In my use case, I need a combined settings.json
file. Luckily the Eclipse vert.x framework provides a JsonObject.mergeIn()
method that does the heavy lifting. Add a Collector
and you get:
JsonObject merge(final String resourceName) throws IOException {
Enumeration<URL> r = this.getClass().getClassLoader().getResources(resourceName);
return Collections.list(r).stream()
.map(this::readFromURL)
.map(JsonObject::new)
.collect(Collector.of(() -> new JsonObject(),
JsonObject::mergeIn,
JsonObject::mergeIn));
}
Mission accomplished: all settings merged. As usual YMMV
Posted by Stephan H Wissel on 29 April 2021 | Comments (0) | categories: Java Maven