Processing interface annotations by maven plugin - java

I have an annotation-processor java app that is looking into the target folder of the external maven project for the class files. This app uses URLClassLoader. The target folder is the absolute path app parameter. I have a simple junit test which is loading classes and processing the annotations successfully. Now I wrote the maven plugin that is just a wrapper for annotation-processor app. When running this maven plugin, annotation-processor loads the classes but it does not see any annotations in those. How can I access class/interface annotations from the maven plugin?
maven-plugin-plugin:3.6.4
java 11.0.15
private static Class<?> getClass(String path, String className, String packageName) {
File file = new File(path);
try {
URL[] urls = new URL[]{file.toURI().toURL()};
ClassLoader cl = new URLClassLoader(urls);
return cl.loadClass(packageName + "." + className));
} catch (MalformedURLException | ClassNotFoundException e) {
log.warn("Cannot find class: " + packageName + "." + className, e);
}
return null;
}
Example call:
Class clazz = getClass("/home/user/java/my-project/target/classes", "MyClass", "com.my.example.package");
assert clazz.getAnnotations() > 0;

There are two possible reasons:
your annotations do not have proper #Retention
JVM silently drops information about annotations while loading the class - that may happen if classloader does know nothing about annotation classes - that is very likely your case because you are instantiating new classloader without specifying parent

Related

Gradle Plugin Task Reflection Scans Plugin's Project Class Files Instead of Current Project Files

I've created a Gradle plugin which scans the class files in my project and find which ones have an annotation.
I got it to work and compiled it. Now when I run the task in my project, instead of scanning the classes of the project that I'm in and runs the task on it, it scans the class files of the plugin project itself. What am I doing wrong?
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
try {
ClassPath classpath = ClassPath.from(loader); // scans the class path used by classloader
log.info("classPath = {}", classpath);
for (ClassPath.ClassInfo classInfo : classpath.getTopLevelClassesRecursive(packageName)) {
log.info("classInfo={}", classInfo);
Class<?> clazz = classInfo.load();
if (clazz.isAnnotationPresent(RestController.class)) {
Method[] methods = clazz.getMethods();
for (Method method : methods) {
doSomething(clazz, method);
}
}
}
} catch (IOException | NoSuchFieldException | IllegalAccessException ex) {
String errorMsg = new StringBuilder("Unable to generate ").append(propertiesFileName).append(" file.").toString();
log.error(errorMsg, ex);
throw ex;
}
This cannot work the way you expect:
Your project class files are, by default, not loaded by Gradle as part of building your project. They will be produced by compilation, loaded by test execution, etc ... but normally not as part of Gradle executing tasks.
When your plugin reaches out to classloaders, it is in the managed Gradle world, which has its own tricks with classloaders. So you would have to understand which tricks are pulled ... but given 1, that would still not work.
Given that what you are attempting looks a lot like annotation processing, you should investigate that way of handling your needs.
It took a lot of digging in Gradle forums, and still, I'm experiencing many issues with that, but this is the best solution I could found:
URL[] urls;
List<URL> listOfURL = new ArrayList<>();
SourceSetContainer ssc = getProject().getConvention().getPlugin(JavaPluginConvention.class).getSourceSets();
File classesDir = ssc.getByName("main").getOutput().getClassesDir();
listOfURL.add(classesDir.toURI().toURL());
urls = listOfURL.toArray(new URL[0]);
final ClassLoader loader = new URLClassLoader(urls);

Loading of imported classes of loaded classes using Reflection in Java

I am trying to load classes from a jar file. Basically, I want to call a method in a particular class in a package of that jar. The problem I am facing here is that after the class is successfully loaded from the jar and when I try to instantiate I get exception : ClassNotFound for classes imported in my class.
Here is the class which loads the class:
inputs: D:\Myjar.jar , com.vendor.epbroker.VNFLCMCommunicator
public Class<?> loadClass(String libPath, String pkgName) {
LogManager.getLogger().info("Adding Class");
File jarFile = null;
try {
jarFile = new File(libPath);
URL fileURL = jarFile.toURI().toURL();
String jarURL = "jar:" + fileURL + "!/";
URL urls[] = { new URL(jarURL) };
URLClassLoader ucl = new URLClassLoader(urls);
Class<?> beanClass = ucl.loadClass(pkgName);
ucl.close();
return beanClass;
} catch (Exception ex) {
LogManager.getLogger().error("Given Library: " + libPath + " or Class name: " + pkgName + " is not Valid");
LogManager.getLogger().error("Exception occurred : ", ex);
}
LogManager.getLogger().error("Class loading Error: Returning NULL");
return null;
}
The code snippet which receives this Class:
Object instance = classToLoad.newInstance();
// To get the list of methods exist in the Class
Method[] listOfMethods = classToLoad.getMethods();
The following error is encountered:
SEVERE: Servlet.service() for servlet [spring] in context with path [/vnflcm] threw exception [Handler processing failed; nested exception is java.lang.NoClassDefFoundError: com/vendor/epbroker/exception/EPBrokerException] with root cause
java.lang.ClassNotFoundException: com.vendor.epbroker.exception.EPBrokerException
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.Class.getDeclaredConstructors0(Native Method)
at java.lang.Class.privateGetDeclaredConstructors(Class.java:2671)
at java.lang.Class.getConstructor0(Class.java:3075)
Any help would be appreciated?
Have a look at the following lines:
URLClassLoader ucl = new URLClassLoader(urls);
Class<?> beanClass = ucl.loadClass(pkgName);
ucl.close();
and consider the documentation of URLClassLoader.close():
Closes this URLClassLoader, so that it can no longer be used to load new classes or resources that are defined by this loader.
In other words, you should only close a class loader if you are really done using the classes of that loader. Even if all required classes had already been loaded at this point, there was still the possibility that a required resource needs to be accessed. Note that some frameworks have there own reflection library requiring access to the byte code of the classes, which will be accesses like a resource.
In your specific case, it’s even simpler. You have just loaded one class, which only triggered resolving of the required minimum set of classes (e.g. the direct super class), but no other dependencies. Then you close the class loader, preventing subsequent loading of any other class from your jar file, which hits you when resolving the constructors needs resolving more referenced classes.
There are a few considerations you have to take when trying to use Reflection.
Your URLClassLoader must contain the URL of the jar that you want to reflect into.
If the desired jar depends on any other jars, you must load the URLs for those jars as well.
As #VGR pointed out, you cannot simply use the file path for the jar to use as a URL. One thing you can do is :
File myJar = new File("path/to/myJar.jar");
URL myJarUrl = myJar.toURI().toURL();
A simple example to demonstrate the issue:
Let's call your jar myToolProject. And let's say while developing this tool you created a class, call it JsonMaker, that converts a POJO to a JSON and you accomplish this via the gson.jar. When you build your jar, let's call it myjar.jar, you mention that in the manifest that it depends on gson.
When trying to reflect on myjar, you reflect on each class in your jar, until you reach JsonMaker.class. When trying to reflect here we notice that there is com.google.Gson type object here. The URLClassLoader looks through the URLS in its array and tries to find com.google.Gson in some class. If it cannot find any com.google.Gson class, it cannot reflect on that class, and throws a ClassNotFoundException.

Instantiating classes from a jar that implement a common interface, and then assigning the instance to the interface causes ClassCastException

This is a classloader issue that I am struggling with. I understand the root cause of the issue (different classloaders), but I'm not sure about the best way to fix it.
I have project with some common interfaces; let's call it api. I have two other projects called runner and module that both use api as a dependency.
The job of runner is to dynamically load a module artifact (from a jar; it's a fat one that includes its dependencies) and then execute it. runner expects module to provide certain concrete implementations from api. To make sure that classes from different versions of module.jar don't clobber each other, I create a new classloader with a URL to module.jar, and set the parent classloader to the classloader of the class that loads and processes module.jar. This works without any issues.
The problem arose when I used runner as a dependency inside a webapp (a spring boot app to be specific), and quickly found that I couldn't load some classes from module.jar because they conflict with classes that already exist in the current classpath (from other dependencies in the webapp).
Since module.jar really only needs the classes from api, I thought that I could create a new URLClassLoader (without a parent) that only has classes from api.jar, and then use that as the parent classloader when I load up the module. This is where I started running into trouble:
CommonInterface commonInterface = null;
Class<CommonInterface> commonInterfaceClass = null;
ClassLoader myClassLoader = URLClassLoader.newInstance(moduleJarURL, apiClassesClassLoader);
//...
//...
//clazz is a concrete implementation from module.jar
if(myClassLoader.loadClass(CommonInterface.class.getName()).isAssignableFrom(clazz)) {
commonInterfaceClass = clazz;
}
commonInterface = commonInterfaceClass.newInstance(); //ClassCastException
I understand that my original problem is due to the fact that the classloader first checks to see if the class has already been loaded before attempting to load it, which meant that when it was resolved using the name from module.jar, it was linking against an incompatible version of the class.
What's a good way to deal with this issue? Instead of creating a URL classloader that only has classes from api, does it make sense to create my own implementation that delegates to the parent only if the requested class is one from api?
You have loaded CommonInterface from two different class loaders. Classes with the same name but different class loaders are different classes to the JVM. (Even if the classes are 100% identical in the .class file - the problem is not incompatibility but the fact that they're from different class loaders)
If you do a
System.out.println(CommonInterface.class == myClassLoader.loadClass(CommonInterface.class.getName()));
You'll find that this prints false.
The way your create your classloader:
ClassLoader myClassLoader = URLClassLoader.newInstance(moduleJarURL, apiClassesClassLoader);
.. would only work if apiClassesClassLoader is also a parent class loader of the class that contains this code.
You could try:
ClassLoader myClassLoader = URLClassLoader.newInstance(moduleJarURL,
getClass().getClassLoader());
But from your description (it's a "fat" jar that contains its own dependencies) and the intricacies of the web classloader (child first) this may not solve your problem.
In that case, the only solution is to make your module jar "lean" to ensure that you only load each class once with one class loader only.
I forgot to update this question with my solution. I was able to solve this issue by creating a custom class-loader that extends URLClassLoader. This classloader does not have a parent.
I then overrode loadClass to control how classes were being loaded. I first check to see if the class exists in module.jar. If so, I load it from there. Otherwise, I load it using the current classloader. Since my custom classloader doesn't have a parent, it can load classes from module.jar even if they were already loaded by the main classloader, because they do not exist in my custom classloader's hierarchy.
The basic approach was like this:
public class MyClassLoader extends URLClassLoader {
private final ClassLoader mainClassLoader = MyClassLoader.class.getClassLoader();
private final Set<String> moduleClasses;
private MyClassLoader(URL url) {
super(new URL[]{ url });
try {
JarURLConnection connection = (JarURLConnection) url.openConnection();
this.moduleClasses = connection.getJarFile().stream()
.map(JarEntry::getName)
.filter(name -> name.endsWith(".class"))
.map(name -> name.replace(".class", "").replaceAll("/", "."))
.collect(Collectors.toSet());
} catch(IOException e) {
throw new IllegalArgumentException(String.format("Unexpected error while reading module jar: %s", e.getMessage()));
}
}
public static MyClassLoader newInstance(JarFile libraryJar) {
try {
return new MyClassLoader(new URL(String.format("jar:file:%s!/", libraryJar.getName())));
} catch(MalformedURLException e) {
throw new IllegalArgumentException(String.format("Path to module jar could not be converted into proper URL: %s", e.getMessage()));
}
}
#Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if(moduleClasses.contains(name)) {
Class<?> clazz = findLoadedClass(name);
if(clazz != null) {
return clazz;
} else {
return findClass(name);
}
} else {
return mainClassLoader.loadClass(name);
}
}
}

Spring webapp loading plugins: ClassNotFoundException

I am writing a Spring web application, which loads plugin files at runtime. These plugins are classes that implement IPlugin. However, when loading the plugin at runtime I get a ClassNotFoundException(IPlugin cannot be found).
The interface IPlugin is located in a package in my web app. In order to build a plugin I exported the interface to a jar file and included it in the plugin's build path.
In my web app, the plugin is loaded using a URLClassLoader:
URL fileUrl = jar.toURI().toURL();
String jarUrl = "jar: " + fileUrl + "!/";
URL[] urls = new URL[] {new URL(jarUrl)};
URLClassLoader loader = new URLClassLoader(urls);
IPlugin plugin = (IPlugin) Class.forName(clazz, true, loader).newInstance();
How do I make the interface available at runtime?
Edit:
It does work if I load the jar containing IPlugin.class together with the plugin. But is that really necessary?
I don't think you can use IPlugin without importing on the top
Simply bring the IPlugin.java to your code with the same package name, nothing wrong in this
You can directly use full file path to load a JAR via URLClassLoader
Try the following code
URL[] classLoaderUrls = new URL[]{new URL("file:///<your directory path>/<your filename>.jar")};
URLClassLoader urlClassLoader = new URLClassLoader(classLoaderUrls);
Class<?> beanClass = urlClassLoader.loadClass("com.my.MyImplementation");
Constructor<?> constructor = beanClass.getConstructor();
Object beanObj = constructor.newInstance();
Try to type cast, if you have IPlugin.java in your import on top this will work
IPlugin plugin = (IPlugin)beanObj;
Otherwise you need to use Reflection
Method method = beanClass.getMethod("myMethod");
method.invoke(beanObj);
I found the solution, I had to set the parent class loader like this: new URLClassLoader(URLs, getClass().getClassLoader()). It is then sufficient to have the IPlugin interface present in the webapp, no additional jar is needed in WEB-INF/lib.

TrueZip class could not be instantiated

There's a little program which was running well with TrueZip 6. Recently I've updated TrueZip jars to version 7.7.9 by adding 6 packages to the project's classpath: truezip-driver-file, truezip-driver-tar, truezip-driver-zip, truezip-file, truezip-kernel and truezip-swing with all necessary dependencies (xz 1.5 etc).
There's no error during compilation, however, when I try to run in the main method:
TConfig.get().setArchiveDetector(
new TArchiveDetector(TArchiveDetector.NULL, new Object[][] {
{ "tar", new TarDriver(IOPoolLocator.SINGLETON) },
{ "tgz|tar.gz", new TarGZipDriver(IOPoolLocator.SINGLETON) },
{ "zip|alt|alib", new ZipDrive(IOPoolLocator.SINGLETON) } }));
It shows de.schlichtherle.truezip.socket.sl.IOPoolLocator$Boot could not be instantiated in IOPoolLocator
Boot is an inner and static final class
http://grepcode.com/file/repo1.maven.org/maven2/de.schlichtherle.truezip/truezip-kernel/7.7.9/de/schlichtherle/truezip/socket/sl/IOPoolLocator.java#IOPoolLocator
I found few references but unfortunately not very helpful.
I had the same issue and I guess you're adding these TrueZip classpath entries in separated lines ?
In this case, my solution is : add them in one single line with paths separated by comma ","
Try to go deep and debug the real error from the first instantiation of class Boot in JVM :
static final IOPool<?> pool;
static {
final Class<?> clazz = IOPoolLocator.class;
final Logger logger = Logger.getLogger(clazz.getName(), clazz.getName());
final ServiceLocator locator = new ServiceLocator(clazz.getClassLoader());
pool = decorate((IOPool) create(locator, logger), locator, logger);
}
You'll see that finally it goes to a line which is the source of later exceptions :
this.l1 = null != loader ? loader : ClassLoader.getSystemClassLoader();
Basically it's using a ServiceLoader or ClassLoader. Now perform a test in the main method :
aClassLoader.getResourceAsStream("/META-INF/services/de.schlichtherle.truezip.socket.spi.IOPoolService")
using different classes contained in each of your 6 jar files, you should see that only classes in truezip-kernel.jar can find IOPoolService, because all jar files are loaded by different loaders (not the same object id).

Categories

Resources