How to insatiate instance of class from external jar in Tomcat Webapp - java

Basically our code allows people to pass in a custom implementation of our IRepository interface if they want to define their own repository for the files we generate. I keep getting the following error:
ClassCastException: S3RepositoryPlugin.S3Repository cannot be cast to package_name.IRepository
This is how I define the S3Repository:
public class S3Repository implements IRepository
And this is how im trying to insatiate it:
URL[] urls = { new URL("jar:file:" + assembly +"!/") };
URLClassLoader cl = URLClassLoader.newInstance(urls);
Class classToLoad = Class.forName(className, true, cl);
IRepository externalRepo = (IRepository) classToLoad.newInstance();
Where assembly is the full path to the jar, and className is "S3RepositoryPlugin.S3Repository".
Any idea as to why this is happening? From the stuff Im seeing online it seems casting to interfaces works differently than casting to classes but im not sure if the code im using is necessarily affected by that.
NOTE:
Using Tomcat9 and java8

I had to add the following code to the URLClassLoader to make it work:
URLClassLoader cl = URLClassLoader.newInstance(urls, getClass.getClassLoader());

Related

Gradle Custom Plugins: Read Compiled Java Class

In a custom plugin (or task) I would like to read all compiled classes (preferrably those that have changed from last compilation) with a classloader so that I'll be able to use reflection on them.
Is that possible?
1) It would be great to have a cook right after a Java class was compiled so that I could read it, but I found no way to do this.
2) I'm thinking of something like this ...
compileJava.doLast {
ClassLoader parent = getClass().getClassLoader();
GroovyClassLoader loader = new GroovyClassLoader(parent);
// retrieve all class files
// for each class file, loader.parseClass(classFile)
}
In a gradle script getClass().getClassloader() will get you the classloader of the gradle script. This will NOT contain the compiled classes or compile/runtime jars. I think you want to do something similar to:
Collection<URL> urls = sourceSets.main.runtimeClasspath.files.collect { it.toURI().toURL() }
Classloader parent = new URLClassLoader(urls.toArray());
If you want to only act on the classes that have changed you are best to do do that in an incremental task

Load a class from a jar having dependency on another jar

My project structure is the following (very simplified of course):
So under lib-ext i download on a daily basis from a Jenkins server 2 jar files 'jar1 and jar2' to be checked by my program, i need one file from 'jar1' lets call it: "Class2Bloaded".
The issue is that this file implements an interface that is to be found in 'jar2', lets call this 'Dependency'
What i would like to do is, from my class under src "ClassThatLoads.java", load 'Class2Bloaded.class' and tell the class loader to look into 'jar2' to search for the implementing interface "Dependency.class"
My code so far (omitting exceptions handling):
//Create the URL pointing to Jar1
private URL getJarUrl(JarFile jarFile)
{
return new File(jarFile.getName()).toURI().toURL();
}
URL jar1Url = getJarUrl(jar1);
ClassLoader jar1classLoader = new URLClassLoader(new URL[] { jar1Url });
Class<?> Class2Bloaded = Class.forName(fullClassName, false, jar1classLoader );
So the problem happens within the Class.forName invocation, because the class i want to load implements an interface that is in jar 2.
Exception in thread "main" java.lang.NoClassDefFoundError: com/packagewithinJar2/Dependency
So eventually i have prepared another class loader that points to 'jar2', and i have even got the actual Interface i need:
URL jar2Url = getJarUrl(jar2);
ClassLoader jar2classLoader = new URLClassLoader(new URL[] { jar2Url });
Class<?> Interface2Bloaded = Class.forName(fullClassName, false, jar2classLoader );
Where 'fullClassName' in the second case is the fully qualified name of the interface from which 'Class2Bloaded' depends on.
Is just that i cant find anything in the javadocs of ClassLoader that allows me to 'inject' an additional class loader for the dependencies.
I hope my explanation is clear.
The first thing to do would be to add jar2 to the list of jars your URLClassLoader reads:
ClassLoader jarclassLoader = new URLClassLoader(new URL[] { jar1Url, jar2Url });
BUT the normal thing to do would be to add jar1 and jar2 on your classpath from the beginning.
To do so you would use the -cp parameter of the java executable.
for example, if you compile your classes into the bin directory:
java -cp libext/jar1.jar:libext/jar2.jar:bin ClassThatLoads
That way, you could use the classes seamless in your own java source and get rid of the cumbersome loading part :
public class ClassThatLoads {
public static void main(String[] args) {
Class2Bloaded stuff = new Class2Bloaded();
//use stuff from here...
}
}

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.

Java - how to load different versions of the same class?

I have read a lot about Java classloaders, but so far I have failed to find an answer for this simple question:
I have two versions of com.abc.Hello.class in jars v1.jar and v2.jar. I want to use both in my application. What is the simplest way of doing this ?
I don't expect to be that simple, but something along these lines would be awesome :
Classloader myClassLoader = [magic that includes v1.jar and ignores v2.jar]
Hello hello = myclassLoader.load[com.abc.Hello]
And in a different class :
Classloader myClassLoader = [magic that includes v2.jar and ignores v1.jar]
Hello hello = myclassLoader.load[com.abc.Hello]
I would like to avoid using OSGi.
You're going the right way. You must take some things into account.
The normal thing is classes that exist in parent classloaders are used. So if you want two versions those classes must not be there.
But if you want to interact you can use reflection, or even better a common interface. So I'll do this:
common.jar:
BaseInterface
v1.jar:
SomeImplementation implements BaseInterface
v2.jar:
OtherImplementation implements BaseInterface
command-line:
java -classpath common.jar YourMainClass
// you don't put v1 nor v2 into the parent classloader classpath
Then in your program:
loader1 = new URLClassLoader(new URL[] {new File("v1.jar").toURL()}, Thread.currentThread().getContextClassLoader());
loader2 = new URLClassLoader(new URL[] {new File("v2.jar").toURL()}, Thread.currentThread().getContextClassLoader());
Class<?> c1 = loader1.loadClass("com.abc.Hello");
Class<?> c2 = loader2.loadClass("com.abc.Hello");
BaseInterface i1 = (BaseInterface) c1.newInstance();
BaseInterface i2 = (BaseInterface) c2.newInstance();
You have almost written the solution.
I hope the following code fragment will help.
ClassLoader cl = new URLClassLoader(new URL[] {new File("v1.jar").toURL()}, Thread.currentThread().getContextClassLoader());
Class<?> clazz = cl.loadClass("Hello");
Replace v1.jar by v2.jar and this code will load version #2.
For a sample implementation of the accepted answer by #helios checkout github.com/atulsm/ElasticsearchClassLoader
it depends on what are u going to do with both versions and how, but in general you can at least load 2 version of class in different classloaders, and then set the Thread.contextClassloader() and play...
see http://www.javaworld.com/javaqa/2003-06/01-qa-0606-load.html and http://docs.oracle.com/javase/jndi/tutorial/beyond/misc/classloader.html

Categories

Resources