- 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 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); }