2013-06-29

Gradle: ServiceLoader support

At one point, one of my projects used the JDK's ServiceLoader support. In essence, this works as follows:

  • you create an interface in your code, say com.foo.MyInterface;
  • you create implementations of this interface;
  • you create a file in META-INF/services, named after your interface (therefore, in this example, META-INF/services/com.foo.MyInterface;
  • in this file, you add implementations of your interfaces, one per line.
The problem is that you basically have to generate these files by hand; if you forget to add, or remove, lines as you change implementations, you will be greeted with various unchecked exceptions. Not good...

The ideal solution is to generate them at compile time/packaging time. But you then stumble upon another problem: your IDE may not generate them; crashes again!

As to build systems, Maven has a plugin available; but for Gradle, nothing... So, I had to "write" it. I took the aforementioned plugin as a reference and came up with the below code, which generates a task called generateServiceFiles.

Now, beware that this code reflects my Groovy/Gradle experience: not even a week! Seasoned Groovy developers in particular will certainly balk at the number of semicolons ;) But it works... Feel free to pick it up and make a plugin out of it!


/*
 * List to fill with your interfaces to be implemented
 */
project.ext {
    serviceClasses = [
        "com.foo.MyInterface",
        "org.bar.OtherInterface"
    ];
};

project.ext {
    dotClass = ".class";
    classpathURI = sourceSets.main.output.classesDir.canonicalFile.toURI();
    serviceMap = new HashMap<Class<?>, List<String>>();
    tree = fileTree(classpathURI.path)
        .filter({ it.isFile() && it.name.endsWith(dotClass); }); // FileTree
    resourceURI = sourceSets.main.output.resourcesDir.canonicalFile.toURI()
        .resolve("META-INF/services/"); // Ending '/' is critical!
}

task generateServiceFiles(dependsOn: compileJava) << {
    if (!project.hasProperty("serviceClasses"))
        return;
    if (serviceClasses.empty)
        return;
    project.ext({
        runtimeURLs = sourceSets.main.runtimeClasspath.collect({
            it.toURI().toURL()
        }) as URL[];
        classLoader = URLClassLoader.newInstance(runtimeURLs);
    });
    serviceClasses.each() {
        serviceMap.put(classLoader.loadClass(it), new ArrayList<String>());
    };
    tree.each() {
        File candidate ->
            serviceMap.each() {
                key, value ->
                    final String className = toClassName(candidate);
                    if (isImplementationOf(key, className))
                        value.add(className);
            }
    };
    createServicesDirectory();
    serviceMap.each() {
        name, list ->
            if (list.empty)
                return;
            final String path = resourceURI.resolve(name.canonicalName)
                .getPath();
            new File(path).withWriter {
                out -> list.each() { out.writeLine(it); }
            };
    };
}

processResources {
    dependsOn(generateServiceFiles);
}

/*
 * Support methods for the generateServiceFiles task
 */

void createServicesDirectory()
{
    final File file = new File(resourceURI.getPath());
    if (file.exists()) {
        if (!file.directory)
            throw new IOException("file " + file + " exists but is not a directory");
        return;
    }
    if (!file.mkdirs())
        throw new IOException("failed to create META-INF/services directory");
}

String toClassName(final File file)
{
    final URI uri = file.canonicalFile.toURI();
    final String path = classpathURI.relativize(uri).getPath();
    return path.substring(0, path.length() - dotClass.length())
        .replace("/", ".");
}

boolean isImplementationOf(final Class<?> baseClass, final String className)
{
    final Class<?> c = classLoader.loadClass(className);
    final int modifiers = c.modifiers;
    if (c.anonymousClass)
        return false;
    if (c.interface)
        return false;
    if (c.enum)
        return false;
    if (Modifier.isAbstract(modifiers))
        return false;
    return Modifier.isPublic(modifiers) && baseClass.isAssignableFrom(c);
}

2013-06-22

Mimicking Java 7's try-with-resources with Java 6 -- and even further than that

Introduction

Java 7 has introduced the try-with-resources statement which allows you to do such things as:

try (
    final InputStream in = someNewInputStream();
    final OutputStream out = someNewOutputStream();
) {
    // do something with "in" and "out"
}

// That's it

Now, why it allows that is because InputStream and OutputStream (and therefore all implementations of these interfaces) implement the AutoCloseable interface. In this situation, the JVM itself guarantees that your resources which you initialize within () will be closed.

But I am using Java 6...

In Java 6, there is no such thing as try-with-resources. The recommended practice, given one I/O resource, is to do as follows:

in = someInputStream();
try {
    // do something with "in";
} finally {
    in.close();
}

But if you have more than one resource to deal with, in a same method, this quickly becomes cumbersome. Basically, this technique means one try/finally per I/O resource. Not practical, and, more importantly, not sustainable.

And this is where Guava (starting with version 14.0) has come with a very interesting class: Closer.

Here we will go further than that: we will also account for Flushable as well.

Here is a pure Java 6 class handling all possible input sources implementing Closeable and, optionally, Flushable). This class offers a good number of guarantees, mentioned below:

public final class CloserFlusher
    implements Closeable, Flushable
{
    /*
     * Implicit public constructor: not declared.
     */

    /*
     * Our lists of closeables and flushables
     */
    private final List closeables = new ArrayList();
    private final List flushables = new ArrayList();

    /*
     * Add a Closeable to the closeables list.
     *
     */
    public  C register(final C c)
        throws IOException
    {
        closeables.add(c);
        return c;
    }

    /*
     * Add a Closeable which also implements Flushable
     */
    public  registerFlushable(final CF cf)
    {
        closeables.add(cf);
        flushables.add(cf);
        return cf;
    }

    /*
     * Implementation of the Closeable interface.
     *
     * All registered resources to this class will see an attempt at
     * being closed, in the _reverse_ order in which they have been
     * registered.
     * 
     * The thrown exception will be the one of the first resource which has
     * failed to close properly.
     */
    @Override
    public void close()
        throws IOException
    {
        final int csize = closeables.size();
        IOException thrown = null;

        /*
         * Could be improved: for instance, a Logger could be used so as to
         * throw each and every exception occurring.
         */
        for (int i = csize - 1; i >= 0; i--)
            try {
                closeables.get(i).close();
            } catch (IOException e) {
                if (thrown == null)
                    thrown = e;
            }

        if (thrown != null)
            throw thrown;
    }

    /*
     * Implementation of the Flushable interface.
     *
     * Here, the list of Flushables is walked in registration order.
     * The first one to fail with an exception "wins" as to exception
     * throwing contest.
     */
    @Override
    public void flush()
        throws IOException
    {
        final int fsize = flushables.size();
        IOException thrown = null;

        for (int i = 0; i < fsize; i++)
            try {
                flushables.get(i).flush();
            } catch (IOException e) {
                if (thrown == null)
                    thrown = e;
            }

        if (thrown != null)
            throw  thrown;
    }

    public void closeQuietly()
    {
        try {
            close();
        } catch (IOException ignored) {
        }
    }

    public void flushQuietly()
    {
        try {
            flush();
        } catch (IOException ignored) {
        }
    }
}

Should you use this class in a method which throws IOException, you can do that:

final CloserFlusher cf = new CloserFlusher();
final InputStream in;
final OutputStream out;

try {
    in = cf.register(someInputStream());
    out = cf.registerFlushable(someOutputStream());
    // do work with in and out
    cf.flush();
} finally {
    cf.close();
}

The advantages are numerous:

  • no descriptor leaks: all I/O resources submitted to this class ultimately try to close; what is more, closing is done in reverse order;
  • resource initalization: quite a few implementations of InputStream or OutputputStream throw an IOException when they cannot be initialized: when this class is used as recommended above, the risk of dangling and/or unreclaimable I/O resources is minimized;
  • controlled flush: only the resources you register using .addFlushable() will be affected by this class' .flush().

Should you choose to use only the quiet methods of it... I do not recommend it ;) But in all events, correctly used, this class guarantees that you have a "best effort" attempt at flushing and closing your resources.

And note that it could be improved! For instance, all exceptions could be logged if you chose to use java.util.logger.