ServiceLoader does not load implementation jars - java

I am trying to implement a simple modular application with a self written plugin loader. For that I created a gradle multiproject build. The main project is going to be the started application. In the subproject spi is a module containing the Plugin interface, while in the subproject simple-plugin I wanted to create a plugin to test the plugin loading.
The Plugin interface is very simple:
package io.github.zeroneca.orgascreen.spi;
public interface Plugin {
String getId();
}
module-info.java:
module io.github.zeroneca.orgascreen.spi {
exports io.github.zeroneca.orgascreen.spi;
}
SimplePlugin.java:
package io.github.zeroneca.orgascreen.plugins.simple;
import io.github.zeroneca.orgascreen.spi.Plugin;
public class SimplePlugin implements Plugin {
private final String id = "SimplePlugin";
#Override
public String getId() { return id; }
}
module-info.java
module io.github.zeroneca.orgascreen.plugins.simple {
requires io.github.zeroneca.orgascreen.spi;
provides io.github.zeroneca.orgascreen.spi.Plugin with io.github.zeroneca.orgascreen.plugins.simple.SimplePlugin;
}
Now in the class PluginLoader I'm loading all jars from a directory and want to add all Plugins to a List:
package io.github.zeroneca.orgascreen;
// several imports
public class PluginLoader {
private static final List<Plugin> PLUGINS = new ArrayList<>();
public static void load() {
URL[] pluginURLs = getPluginURLs(); // returns all URLs of jar files in a directory
URLClassLoader urlClassLoader = URLClassLoader.newInstance(pluginURLs);
ServiceLoader<Plugin> serviceLoader = ServiceLoader.load(Plugin.class, urlClassLoader);
for (Plugin plugin : serviceLoader) {
PLUGINS.add(plugin);
}
}
// some more methods hidden here
}
module-info.java
module io.github.zeroneca.orgascreen {
requires io.github.zeroneca.orgascreen.spi;
uses io.github.zeroneca.orgascreen.spi.Plugin;
}
Now after building with gradle and copying the resulting jar simple-plugin-0.1.jar into the directory, I can see, that the URL of the file is in pluginJars but the List PLUGINS remains empty. I'm not sure what I'm doing wrong, online searches didn't help, I cannot see a difference to working solution, but the use of gradle.
gradle 6.5.1 is in use and modularity.inferModulePath is set to true.
Help is greatly appreciated!
Note: when adding META-INF/services/io.github.zeroneca.orgascreen.spi.Plugin to the jar of SimplePlugin it works, so something must be wrong with the module declarations. But I just can't see what.

Related

Getting the resource path of the java project from a custom gradle plugin

Trying to create a custom gradle plugin in java, how do i get the resources path from inside the task class?
public class MyCustomPlugin implements Plugin<Project> {
#Override
public void apply(Project project) {
project.getTasks().register("doStuff", CustomTask.class);
}
}
public class CustomTask extends DefaultTask {
// How do I get java project resources dir from here?
#Inject
public CustomTask(ProjectLayout projectLayout) {
directoryProperty = projectLayout.getBuildDirectory();
}
#TaskAction
public void execute() {
...
}
}
I would recommend to not get the directory inside the task, because the plugin that provides it might not be applied. Instead I would do it from within your plugin that registers the task, this way you can also ensure that the necessary plugin is actually applied. Gradle will display an error if the task is used without a value being assigned to the input that explains that nothing was assigned.
With the kotlin-dsl:
#CacheableTask
abstract class CustomTask : DefaultTask() {
#get:InputFiles
abstract val resources: FileCollection
//...
}
I cannot answer if #InputFiles is the right annotation for your use case, because I don't know what you want to do with the resource. Refer to the Gradle documentation for more information on the available annotations, and what they do.
plugins {
java
}
tasks.register<CustomTask>("customTask") {
resources.set(sourceSets.main.map { it.resources })
}
Notice the map {} which ensures that our task has a dependency on the processResources task, this is done automatically for us because we stick to the provider API of Gradle for everything.
Note that the resources are by default in one directory, but they don't have to be. This is why the resources are defined as SourceDirectorySet and not as Provider<Directory>. The same is true for anything that originates from the SourceSetContainer. It is easier to explain with Java source code: imagine you have Java and Kotlin, then you will have src/main/java and src/main/kotlin, hence, 2 directories. The former will have a **/*.java include filter, whereas the latter has a **/*.kt includes filter. If we just want to get all sources then we use sourceSets.main.map { it.java.sourceDirectories }, and if we want to get one of both it gets complicated. 😝
First, you'd have to ensure this is a Java project: either applying the "java" plugin from your plugin (project.getPluginManager().apply("java")), or only registering the task when the "java" plugin has been applied by the user (project.getPluginManager().withPlugin("java", ignored -> { project.getTasks().register(…); });).
You could then get the resources from the main source set:
SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class);
// Use named() instead of get() if you prefer/need to use providers
SourceSet mainSourceSet = sourceSets.get(SourceSet.MAIN_SOURCE_SET_NAME);
SourceDirectorySet resources = mainSourceSet.getResources();
BTW, the best practice is to have tasks only declare their inputs and outputs (e.g. I need a set of directories, or files, as inputs, and my outputs will be one single file, or in one single directory) and have the actual wiring with default values be done by the plugin.
You could have the plugin unconditionally register the task, then conditionally when the "java" plugin is applied configure its inputs to the project resources; or conditionally register the task or unconditionally apply the "java" plugin, as I showed above.
You can access the sources through the project.sourceSets.
#Inject
public CustomTask(Project project) {
directoryProperty = project.projectLayout.getBuildDirectory();
sourceSet = project.sourceSets.main
}
See also the reference documentation here: https://docs.gradle.org/current/userguide/java_plugin.html#sec:java_project_layout

Load classpath of project gradle plugin is applied to

I am writing a custom gradle plugin, which will generate some code for me, based on the code it finds in the project it is applied to.
For this I need to find all classes that extend a specific class.
The problem is that the class, that is extended, is not loaded in the classpath, since it is a dependency of the other project.
Currently I got this for my custom Task
public class GenerateCodeTask extends DefaultTask {
#TaskAction
public void generateCode() throws MalformedURLException, ClassNotFoundException {
File buildDir = new File(getProject().getBuildDir(), "classes/main");
File root = new File(getProject().getProjectDir(), "src/main/generated");
URLClassLoader classLoader = new URLClassLoader(new URL[]{buildDir.toURL()});
Class itemClass = classLoader.loadClass("net.minecraft.item.Item");
Reflections reflections = new Reflections(classLoader);
Set<Class<?>> items = reflections.getSubTypesOf(itemClass);
}
}
And this for the plugin
public class EasymodsPlugin implements Plugin<Project> {
#Override
public void apply(Project p) {
Task t = p.getTasks().create("generateCode", GenerateCodeTask.class);
t.dependsOn(p.getTasks().getByPath("compileJava"));
}
}
This is the error I am getting
java.lang.ClassNotFoundException: net.minecraft.item.Item
I know that the problem is that the library containing the class is not loaded, and that causes the exception.
What I want is to be able to load all dependencies of my project into the classloader, so I can use reflections to find all "items" in the project (which I need to generate code)
Greetings Failender
I think you almost got it.
You need the compileClasspath property. I pass it as an input parameter to my task, and build the Class Loader from it:
In plugin:
Set<File> ccp = project.getConfigurations().getByName("compileClasspath").getFiles();
task.classpath = ccp;
In task:
#InputFiles
Iterable<File> classpath;
What this property is adding is:
the path to the classes on projects it depends on that are generated under project_id/build dir. ( So no need to build the path manually )
All library deps for your project. That i think you were missing.
So, in the case your class is of type 1:
It is better if you have different subprojects: the one that has the original classes and the other with the generated ones.
So the second depends on the first:
dependencies {
compile project(':my_project_with_classes_to_extend')
}
In the case is the second one, you can just add the library as a dep to your project and it will find the class.
And rewire the tasks so you are sure the fist project is built before calling your task (just on root level of your 2nd project build.gradle):
I think that´s the part that wasn't really working for you, apparently, compileJava and build are not exactly the same. Or at least, compileJava wasnt working for me either.
myGeneratorTask.dependsOn( ":my_project_with_classes_to_extend:build" )
compileJava.dependsOn( "myGeneratorTask" )

SPI with Guice error

So I've been making some kind of plugins API for a Java project (to load JAR files externally) and well, I wanted to be able to add any Guice module inside any plugin to my project's dependency graph.
What I did was have a PluginsModule and in the configure method scan for other modules in plugins and install them using Java's ServiceLoader.
I made a test plugin and made a module for it, I confirmed it did get installed. No problems at this point. The problems appear when I do anything inside that module, for example I bound some interface to an implementation in that plugin (just to clear this up, I did the same thing without the plugin and it worked so it's not a binding problem) and tried to inject it, configuration errors saying there was no implementation for that interface appear.
public enum StandardGuiceModuleScanningStrategy implements GuiceModuleScanningStrategy {
INSTANCE;
#Override
public Set<Module> scan(Path directory) throws IOException {
File directoryAsFile = directory.toFile();
File[] childrenFiles = directoryAsFile.listFiles();
if (!directoryAsFile.isDirectory()
|| childrenFiles == null
|| childrenFiles.length == 0) {
return Collections.emptySet();
}
Set<Module> modules = new HashSet<>();
for (File childrenFile : childrenFiles) {
ClassLoader directoryClassLoader = new URLClassLoader(
new URL[]{childrenFile.toURI().toURL()});
ServiceLoader<Module> moduleServiceLoader = ServiceLoader.load(
Module.class, directoryClassLoader);
moduleServiceLoader.forEach(modules::add);
}
return modules;
}
In that implementation of my GuiceModuleScanningStrategy, as I mentioned before, I did use ServiceLoader. Anyways, I also tried other stuff, like scanning the JAR file and checking for a Module, and seeing if it has a specific annotation.
All Guice Modules annotated with #GuiceModule, will be installed into a child Injector. All classes annotated with #AutoBind will be bound to all inherited interfaces. You can also name it, which would lead to a named binding and overwrite the interfaces, which should be used. And if you don't want to use all Features, just overwrite the StartupModule and bind only the Features you want or your own.

AspectJ annotated based pointcuts not being invoked

I am attempting to create an android library that checks if internet is available before executing a method that has a custom annotation I have defined. I'm using AspectJ to accomplish this.
My annotation is as follows :
#Target({METHOD}) #Retention(RUNTIME)
public #interface InternetRequired {
}
Now for my aspect:
#Aspect
public class CilantroAspect
{
private static final String POINTCUT_METHOD = "execution(#com.cilantro.service.InternetRequired * *(..))";
private static final String POINTCUT_METHOD2 ="#annotation(com.cilantro.service.InternetRequired)";
;
#Pointcut(POINTCUT_METHOD2)
public void internetAnnotatedMethod() {
}
#Around("internetAnnotatedMethod()")
public void checkInternetConnectivity(ProceedingJoinPoint joinPoint) throws Throwable {
Log.v("Aspect","advice being triggered");
if (Cilantro.isConnected()) {
joinPoint.proceed();
} else {
Cilantro.Toast("Internet not available");
}
}
}
Snippet of my activity with the annotated method.
....
Cilantro.init(this);
test();
}
#InternetRequired
public void test()
{
Toast.makeText(this,"Test method",Toast.LENGTH_LONG).show();
}
When I run my android app the around advice is not being triggered. I tried using POINTCUT_METHOD and POINTCUT_METHOD2. Still no luck.
My android app is configured to use Aspect J so I know that's not the problem because if I make errors within the pointcut definitions it's detected..but just to be sure let me share.
Main build script
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.2.3'
classpath 'com.uphyca.gradle:gradle-android-aspectj-plugin:0.9.+'
}
....
module containing Aspects
apply plugin: 'com.android.library'
apply plugin: 'android-aspectj'
....
The around advice wasn't being triggered because I was using annotations related to the Aspect from the app level while the aspect was contained in the library. For the aspects to be weaved into the app module at compile time I had to simply publish the library to maven (local for testing and maven central for distribution) and then include the library as a project dependency in a gradle plugin which contains the AspectJ weaving tasks.The plugin is then applied to the app's module.
Here's a snippet of my plugin that's written in groovy. I add my library that contains my Aspects and then run the weaving tasks on the app's module.
project.dependencies {
compile 'com.github.jd-alexander:flender-runtime:1.0'
// TODO this should come transitively
compile 'org.aspectj:aspectjrt:1.8.5'
}
variants.all { variant ->
variant.dex.doFirst {
String[] args = [
"-showWeaveInfo",
"-1.5",
"-inpath", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-d", javaCompile.destinationDir.toString(),
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)
]
log.debug "ajc args: " + Arrays.toString(args)
MessageHandler handler = new MessageHandler(true);
new Main().run(args, handler);
For a complete example of how this is done you can check my library out on github https://github.com/jd-alexander/flender

scanning java classpath in the maven plugin

What I'm trying to do is to write some Maven plugin which scans application classes looking for implementation of a particular interface (it might be classes with some annotation as well) and basis on the result generating some code. I've successfully implemented plugin running in the generate-sources phase and writing source code to the generated-sources directory.
The problem is with scanning classpath for the particular interface implementations/classes with some annotation.
I am using the Reflections library to scan classes in the following way:
private Set<Class< ? extends MyInterface >> scan(final String packageName) {
final Reflections reflections = new Reflections(packageName);
return reflections.getSubTypesOf(MyInterface.class);
}
Unfortunately, this method returns empty set. When I print my classpath in the class extending org.apache.maven.plugin.AbstractMojo (the same within which I'm using Reflections) I get the following result:
/home/pd5108/apache-maven-2.2.1/boot/classworlds-1.1.jar
The classes I want to find using Reflections exists in dependend JARs as well as in the module within which plugin is configured. Looking at the classpath printed out it seems that at this point (generate-sources phase) dependencies defined in maven all not available on classpath yet - probably they are added in the next phases. Is that true? Is there any other approach I can use?
Here is the way how classpath is printed out:
URL[] urls = ((URLClassLoader)sysClassLoader).getURLs();
for(int i=0; i< urls.length; i++) {
System.out.println(urls[i].getFile());
}
Required MOJO class fields:
/**
* The project currently being built.
*
* #parameter expression="${project}"
* #readonly
* #required
*/
private MavenProject project;
/** #parameter expression="${localRepository}" */
protected ArtifactRepository m_localRepository;
/**#parameter default-value="${localRepository}" */
private org.apache.maven.artifact.repository.ArtifactRepository
localRepository;
/** #parameter default-value="${project.remoteArtifactRepositories}" */
private java.util.List remoteRepositories;
/** #component */
private org.apache.maven.artifact.factory.ArtifactFactory artifactFactory;
/** #component */
private org.apache.maven.artifact.resolver.ArtifactResolver resolver;
Resolution of all dependencies JARs:
final List<Dependency> dependencies = project.getDependencies();
for (Dependency d : dependencies) {
final Artifact artifact =
artifactFactory.createArtifactWithClassifier(d.getGroupId(),
d.getArtifactId(), d.getVersion(), d.getType(),
d.getClassifier());
try {
resolver.resolve(artifact, remoteRepositories,
localRepository );
} catch (ArtifactResolutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ArtifactNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
File artifactFile = artifact.getFile();
System.out.println(artifactFile.getAbsolutePath());
}
And now we need to scan these JARs using reflection API looking for the appropriate classes.
At this point I think that there's no other way, since I work in generate-sources phase and artifact values for the next phases are not computed yet.
There are artifact dependencies defined in <dependencies> section and plugin dependencies defined under <plugin><dependencies>.
Plugin dependencies are added to the classpath while I am not sure about the artifact dependencies. Did you try to add your plugin dependencies under the <plugin><dependencies>?
Might be quicker/safer/easier to just have the plugin config list the classes you want to generate code based on. Then you just add it to the pom and you're done. No reflection needed, and it'd certainly speed up the running of the plugin.

Categories