Search code examples
javaurlclassloader

URL to load resources from the classpath in Java


In Java, you can load all kinds of resources using the same API but with different URL protocols:

file:///tmp.txt
http://127.0.0.1:8080/a.properties
jar:http://www.foo.com/bar/baz.jar!/COM/foo/Quux.class

This nicely decouples the actual loading of the resource from the application that needs the resource, and since a URL is just a String, resource loading is also very easily configurable.

Is there a protocol to load resources using the current classloader? This is similar to the Jar protocol, except that I do not need to know which jar file or class folder the resource is coming from.

I can do that using Class.getResourceAsStream("a.xml"), of course, but that would require me to use a different API, and hence changes to existing code. I want to be able to use this in all places where I can specify a URL for the resource already, by just updating a property file.


Solution

  • Intro and basic Implementation

    First up, you're going to need at least a URLStreamHandler. This will actually open the connection to a given URL. Notice that this is simply called Handler; this allows you to specify java -Djava.protocol.handler.pkgs=org.my.protocols and it will automatically be picked up, using the "simple" package name as the supported protocol (in this case "classpath").

    Usage

    new URL("classpath:org/my/package/resource.extension").openConnection();
    

    Code

    package org.my.protocols.classpath;
    
    import java.io.IOException;
    import java.net.URL;
    import java.net.URLConnection;
    import java.net.URLStreamHandler;
    
    /** A {@link URLStreamHandler} that handles resources on the classpath. */
    public class Handler extends URLStreamHandler {
        /** The classloader to find resources from. */
        private final ClassLoader classLoader;
    
        public Handler() {
            this.classLoader = getClass().getClassLoader();
        }
    
        public Handler(ClassLoader classLoader) {
            this.classLoader = classLoader;
        }
    
        @Override
        protected URLConnection openConnection(URL u) throws IOException {
            final URL resourceUrl = classLoader.getResource(u.getPath());
            return resourceUrl.openConnection();
        }
    }
    

    Launch issues

    If you're anything like me, you don't want to rely on a property being set in the launch to get you somewhere (in my case, I like to keep my options open like Java WebStart - which is why I need all this).

    Workarounds/Enhancements

    Manual code Handler specification

    If you control the code, you can do

    new URL(null, "classpath:some/package/resource.extension", new org.my.protocols.classpath.Handler(ClassLoader.getSystemClassLoader()))
    

    and this will use your handler to open the connection.

    But again, this is less than satisfactory, as you don't need a URL to do this - you want to do this because some lib you can't (or don't want to) control wants urls...

    JVM Handler registration

    The ultimate option is to register a URLStreamHandlerFactory that will handle all urls across the jvm:

    package my.org.url;
    
    import java.net.URLStreamHandler;
    import java.net.URLStreamHandlerFactory;
    import java.util.HashMap;
    import java.util.Map;
    
    class ConfigurableStreamHandlerFactory implements URLStreamHandlerFactory {
        private final Map<String, URLStreamHandler> protocolHandlers;
    
        public ConfigurableStreamHandlerFactory(String protocol, URLStreamHandler urlHandler) {
            protocolHandlers = new HashMap<String, URLStreamHandler>();
            addHandler(protocol, urlHandler);
        }
    
        public void addHandler(String protocol, URLStreamHandler urlHandler) {
            protocolHandlers.put(protocol, urlHandler);
        }
    
        public URLStreamHandler createURLStreamHandler(String protocol) {
            return protocolHandlers.get(protocol);
        }
    }
    

    To register the handler, call URL.setURLStreamHandlerFactory() with your configured factory. Then do new URL("classpath:org/my/package/resource.extension") like the first example and away you go.

    JVM Handler Registration Issue

    Note that this method may only be called once per JVM, and note well that Tomcat will use this method to register a JNDI handler (AFAIK). Try Jetty (I will be); at worst, you can use the method first and then it has to work around you!

    License

    I release this to the public domain, and ask that if you wish to modify that you start a OSS project somewhere and comment here with the details. A better implementation would be to have a URLStreamHandlerFactory that uses ThreadLocals to store URLStreamHandlers for each Thread.currentThread().getContextClassLoader(). I'll even give you my modifications and test classes.