So I'm trying to make a plugin system like the "Bukkit" plugin system. See right now on my project I have some classes extending my base class "Plugin" and then I add them to my list. How would I make it so I can make it so it automatically loads jars from a "mods" folder that are extending my "Plugin" class and automatically add them to the arraylist? Thank you VERY much for the help, I'm trying to make a mod loader.
There might be better ways to do this, but this is what I have done in the past:
private static final String JAVA_CLASS_PATH_PROPERTY = "java.class.path";
private static final String CUSTOM_CLASS_PATH_PROPERTY = "custom.class.path";
public static void addPath(String s) throws Exception {
File f = new File(s);
URL u = f.toURI().toURL();
URLClassLoader urlClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
Class<URLClassLoader> urlClass = URLClassLoader.class;
Method method = urlClass.getDeclaredMethod("addURL", new Class[] { URL.class });
method.setAccessible(true);
method.invoke(urlClassLoader, new Object[] { u });
if (System.getProperties().containsKey(CUSTOM_CLASS_PATH_PROPERTY)) {
StringBuilder sb = new StringBuilder();
sb.append(System.getProperty(CUSTOM_CLASS_PATH_PROPERTY));
sb.append(File.pathSeparatorChar);
sb.append(s);
System.setProperty(CUSTOM_CLASS_PATH_PROPERTY, sb.toString());
}
else {
StringBuilder sb = new StringBuilder();
sb.append(System.getProperty(JAVA_CLASS_PATH_PROPERTY));
sb.append(File.pathSeparatorChar);
sb.append(s);
System.setProperty(JAVA_CLASS_PATH_PROPERTY, sb.toString());
}
}
This adds the path to the jar (the s param) to the system class loaders list of URLS, then appends the path to the jar to the end of the custom class path if it exists, or the java class path otherwise. This should allow any other classes whose class loaders can reach the system class loader to use the newly loaded class.
You can load a class from a JAR but using a ClassLoader
URL jarUrl = ...;
URLClassLoader loader = new URLClassLoader(new URL[] { jarUrl });
Class myClass = Class.forName("myjar.mypackage.MyClass", true, loader);
MyPluginInterface myPlugin = myClass.asSubClass(MyPluginInterface.class).newInstance();
The myClass will be a class from the jar. Most likely you will want a JAR of interfaces you share with the plugin. By using these interfaces you can deal with instances which implement those interfaces easily. i.e. MyClass should implement MyPluginInterface which is an interface you provide.
Note: using a ClassLoader for each plugin allows you to unload the ClassLoader/JAR and load a new version of it, should it change.
Related
I am trying to load in text files on the fly and compile them.
File file = new File("Files/"+fileName+".java");
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
compiler.run(null, null, errStream, file.getAbsolutePath());
I then will load the compiled .class files later:
public Class loadStrategyClass(File strategyClassFile) throws IOException
{
FileChannel roChannel = new RandomAccessFile(strategyClassFile, "r").getChannel();
ByteBuffer buffer = roChannel.map(FileChannel.MapMode.READ_ONLY, 0, (int)roChannel.size());
return defineClass(strategyClassFile.getName(), buffer, (ProtectionDomain)null);
}
I am currently running into two issues:
The first is if the .java files I load in contain anonymous classes. It doesn't appear that the JavaCompiler class will compile these.
Exception in thread "main" java.lang.IllegalAccessException: Class Loader.ClassLoader can not access a member of class Files.myname.myclass$1 with modifiers ""
The second:
Is that sometimes I will get errors for NoClassDefFoundError:
Exception in thread "main" java.lang.NoClassDefFoundError: Files/myname/myclass
Despite the fact that other classes will load correctly and the .class file is in that path.
Apparently, your loadStrategyClass is defined within a custom ClassLoader. The problem is that it is not enough to call defineClass once for the class you’re interested in, your class loader must be able to resolve classes on demand, usually by implementing findClass, so the JVM can resolve dependencies, like the inner classes.
You didn’t specify, how you get the strategyClassFile argument for the loadStrategyClass method. Since you ran the compiler without any options, I suppose you simply looked up the file relative to the source file. To resolve other dependencies, the actual root of the class directory needs to be known. It becomes much easier when you define where to store the class files, e.g.
// customize these, if you want, null triggers default behavior
DiagnosticListener<JavaFileObject> diagnosticListener = null;
Locale locale = null;
JavaCompiler c = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fm
= c.getStandardFileManager(diagnosticListener, locale, Charset.defaultCharset());
// define where to store compiled class files - use a temporary directory
Path binaryDirectory = Files.createTempDirectory("compile-test");
fm.setLocation(StandardLocation.CLASS_OUTPUT,
Collections.singleton(binaryDirectory.toFile()));
JavaCompiler.CompilationTask task = c.getTask(null, fm,
diagnosticListener, Collections.emptySet(), Collections.emptySet(),
// to make this a stand-alone example, I use embedded source code
Collections.singleton(new SimpleJavaFileObject(
URI.create("string:///Class1.java"), Kind.SOURCE) {
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return "package test;\npublic class Class1 { public class Inner {} }";
}
}));
if(task.call()) try {
URLClassLoader cl = new URLClassLoader(new URL[]{ binaryDirectory.toUri().toURL() });
Class<?> loadedClass = cl.loadClass("test.Class1");
System.out.println("loaded "+loadedClass);
System.out.println("inner classes: "+Arrays.toString(loadedClass.getClasses()));
} catch(ClassNotFoundException ex) {
ex.printStackTrace();
}
In the example above, we know the root of the class directory, because we have defined it. This allows to simply use the existing URLClassLoader rather than implementing a new type of class loader. Of course, using a custom file manager, we also could use an in-memory storage for rather than a temporary directory.
You may use this API to discover what has been generated, which enables you to use the resulting class without knowing beforehand, which package or inner class declarations exist in the source file you’re going to compile.
public static Class<?> compile(
DiagnosticListener<JavaFileObject> diagnosticListener,
Locale locale, String sourceFile) throws IOException, ClassNotFoundException {
JavaCompiler c = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fm
= c.getStandardFileManager(diagnosticListener, locale, Charset.defaultCharset());
// define where to store compiled class files - use a temporary directory
Path binaryDirectory = Files.createTempDirectory("compile-test");
fm.setLocation(StandardLocation.CLASS_OUTPUT,
Collections.singleton(binaryDirectory.toFile()));
JavaCompiler.CompilationTask task = c.getTask(null, fm,
diagnosticListener, Collections.emptySet(), Collections.emptySet(),
fm.getJavaFileObjects(new File(sourceFile)));
if(task.call()) {
Class<?> clazz = null;
URLClassLoader cl = new URLClassLoader(new URL[]{binaryDirectory.toUri().toURL()});
for(JavaFileObject o: fm.list(
StandardLocation.CLASS_OUTPUT, "", Collections.singleton(Kind.CLASS), true)) {
String s = binaryDirectory.toUri().relativize(o.toUri()).toString();
s = s.substring(0, s.length()-6).replace('/', '.');
clazz = cl.loadClass(s);
while(clazz.getDeclaringClass() != null) clazz = clazz.getDeclaringClass();
if(Modifier.isPublic(clazz.getModifiers())) break;
}
if(clazz != null) return clazz;
throw new ClassNotFoundException(null,
new NoSuchElementException("no top level class generated"));
}
throw new ClassNotFoundException(null,
new NoSuchElementException("compilation failed"));
}
If you use this to dynamically bind plugins or modules, you may extend the search to look for a result class which implements a particular interface or has a certain annotation.
I've got a classloader problem with Java 9.
This code worked with previous Java versions:
private static void addNewURL(URL u) throws IOException {
final Class[] newParameters = new Class[]{URL.class};
URLClassLoader urlClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
Class newClass = URLClassLoader.class;
try {
Method method = newClass.getDeclaredMethod("addNewURL", newParameters );
method.setAccessible(true);
method.invoke(urlClassLoader, new Object[]{u});
} catch (Throwable t) {
throw new IOException("Error, could not add URL to system classloader");
}
}
From this thread I learned that this has to be replaced by something like this:
Class.forName(classpath, true, loader);
loader = URLClassLoader.newInstance(
new URL[]{u},
MyClass.class.getClassLoader()
MyClass is the class I'm trying to implement the Class.forName() method in.
u = file:/C:/Users/SomeUser/Projects/MyTool/plugins/myNodes/myOwn-nodes-1.6.jar
String classpath = URLClassLoader.getSystemResource("plugins/myNodes/myOwn-nodes-1.6.jar").toString();
For some reason - I really can't figure out, why - I get a ClassNotFoundException when running Class.forName(classpath, true, loader);
Does someone know what I'm doing wrong?
From the documentation of the Class.forName(String name, boolean initialize, ClassLoader loader) :-
throws ClassNotFoundException - if the class cannot be located by the specified class loader
Also, note the arguments used for the API includes the name of the class using which the classloader returns the object of the class.
Given the fully qualified name for a class or interface (in the same format returned by getName) this method attempts to locate, load, and link the class or interface.
In your sample code, this can be redressed to something like :
// Constructing a URL form the path to JAR
URL u = new URL("file:/C:/Users/SomeUser/Projects/MyTool/plugins/myNodes/myOwn-nodes-1.6.jar");
// Creating an instance of URLClassloader using the above URL and parent classloader
ClassLoader loader = URLClassLoader.newInstance(new URL[]{u}, MyClass.class.getClassLoader());
// Returns the class object
Class<?> yourMainClass = Class.forName("MainClassOfJar", true, loader);
where MainClassOfJar in the above code shall be replaced by the main class of the JAR myOwn-nodes-1.6.jar.
In my code I want to dynamically load Module class implementations from Jar files.
In my directory I have 3 files: A.jar, B.jar, C.jar
Each jar has one class called Main which extends Module class
A.jar code example:
public class Main extends Module {
private static String name = "A";
public Main() {
super(name);
}
}
(B and C files are the same but with "B" and "C" instead of "A" in the name property).
My Module class code is:
public abstract class Module{
private StringProperty nameProperty;
public Module(String name){
this.nameProperty = new SimpleStringProperty(name);
}
public StringProperty nameProperty(){
return nameProperty;
}
}
This is the code that I use to dynamically load the three classes:
for (File moduleFile : Data.modulesDir.listFiles()) {
try {
URL url = moduleFile.toURI().toURL();
Class[] parameters = new Class[] { URL.class };
URLClassLoader sysLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
Class<URLClassLoader> sysClass = URLClassLoader.class;
Method method = sysClass.getDeclaredMethod("addURL", parameters);
method.setAccessible(true);
method.invoke(sysLoader, new Object[] { url });
Constructor<?> cs = ClassLoader.getSystemClassLoader().loadClass("com.ehzlab.webreaper.module.Main")
.getConstructor();
Module instance = (Module) cs.newInstance();
System.out.println(instance.nameProperty.get());
} catch (Exception e) {
e.printStackTrace();
}
}
I expect this ouput:
A
B
C
but I get this instead:
A
A
A
It seems like that loads the same jar at each file list iteration. But debugging I noted that the URL changes every time.
I also tried inverting the order, for example, placing B.jar before the other jar, and the output is:
B
B
B
Why?
Simply because you are using same classloader each time, which doesn't reload underlying classes:
ClassLoader.getSystemClassLoader()...
In order to get access to specific classes, you have to use appropriate classloader used for loading particular jar file (may be it is sysLoader, not sure, as I didn't check):
Constructor<?> cs = sysLoader.loadClass("com.ehzlab.webreaper.module.Main")
.getConstructor();
Look at this question as well: How should I load Jars dynamically at runtime?
Combining the information from many posts on this site and many others, I got the following code to dynamically add (at run time) a directory containing classes to the classpath and load a class within that directory.
I'm using OSGi bundles and running from eclipse an "Eclipse Application" (a kind of Run Configuration).
This is the code I'm using:
CASE 1: (both cases are different things I've tried to do the same thing.)
File file = new File("/Users/alek/fastFIX/myJPass/");
URL url = file.toURI().toURL();
URL[] urls = new URL[]{url};
ClassLoader cl = new URLClassLoader(urls);
Class cls = cl.loadClass("GuiLauncher"); //the file GuiLauncher.class is in the /Users/alek/fastFIX/myJPass/ directory
Class[] argTypes = new Class[] { String[].class };
Method main = cls.getDeclaredMethod("main", argTypes); //trying to run the main class
main.invoke(null, (Object) args);
I don't get any error, and nothing happens.
I've also tryied the following, as I actually need the loaded class to interact with other (already loaded) classes.
CASE 2:
ClassLoader currentThreadClassLoader = Thread.currentThread().getContextClassLoader();
URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { new File("/Users/alek/fastFIX/myJPass/").toURL() }, currentThreadClassLoader);
Thread.currentThread().setContextClassLoader(urlClassLoader);
then i load like this:
Class<?> c = Class.forName("GuiLauncher");
or like this:
Class<?> c = Thread.currentThread().getContextClassLoader().loadClass("GuiLauncher");
and try to invoke the main function like this:
Class[] argTypes = new Class[] { String[].class };
Method main = cls.getDeclaredMethod("main", argTypes); //trying to run the main class
main.invoke(null, (Object) args);
here also nothing happens.
Any clue of what could be happening? I've read all related posts here and many places else with no luck.
In OSGI framework it is necessary to add the OSGI class loader as a parent, something like this:
...
ClassLoader cl = new URLClassLoader(new URL[]{file.toURI().toURL()}, this.getClass().getClassLoader());
...
In case 1, I suspect that the GuiLauncher class is already on the classpath, so may get loaded by the default classloader. Try doing Class.forName() before setting up the dynamic classloader, to confirm that there's no class available. If you are in Eclipse, you need to be careful that the class is not included on the Eclipse classpath, which is what would normally happen. You might need to compile it once then move the .java and .class files elsewhere to hide them from Eclipse!
In case 2:
Class.forName("GuiLauncher");
will not work as you expect, because this will use the system classloader. This should fail, hence my suspicion above. You need use the other version of this method that specifies your dynamic classloader:
Class.forName("GuiLauncher", true, urlClassLoader)
The following code works for me.
import java.net.*;
import java.lang.reflect.*;
import java.io.File;
public class Main{
public static void main(String[] args)
{
try{
Class cls = Class.forName("Plugin");
}
catch(Exception e){
System.out.println("Nothing there!");
}
try{
File file = new File("plugin");
ClassLoader cl = new URLClassLoader(new URL[]{file.toURI().toURL()});
Class cls = Class.forName("Plugin", true, cl);
Method main = cls.getDeclaredMethod("main", new Class[] { String[].class });
main.invoke(null, (Object) args);
}
catch(Exception e){
e.printStackTrace();
}
}
}
The Plugin class is compiled in the plugin subfolder, so it's not on the classpath used to run Main, as shown by the first Class.forName().
public class Plugin{
public static void main(String[] args)
{
System.out.println("Plugin was invoked!");
}
}
and prints out:
Nothing there!
Plugin was invoked!
I'm trying to load dynamically a class contained in a .jar file. I know the whole class name and I know for sure that the class implements the interface AlgorithmClass.
My code looks like this:
addURLToSystemClassLoader(dir.toURI().toURL());
Class cl = Class.forName(algorithm.getClassName());
AlgorithmClass algorithmClass = (AlgorithmClass)cl.newInstance();
Where dir is the File object of the .jar file and addURLToSystemClassLoader(URL) looks like this:
private void addURLToSystemClassLoader(URL url) throws IntrospectionException {
URLClassLoader systemClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
Class<URLClassLoader> classLoaderClass = URLClassLoader.class;
try {
Method method = classLoaderClass.getDeclaredMethod("addURL", new Class[]{URL.class});
method.setAccessible(true);
method.invoke(systemClassLoader, new Object[]{url});
} catch (Throwable t) {
t.printStackTrace();
throw new IntrospectionException("Error when adding url to system ClassLoader ");
}
}
I checked and the URL is being added to the class loader.
When I try to get the Class object I get the error:
SEVERE: javax.servlet.ServletException: java.lang.ClassNotFoundException: id3.Algorithm
(id3.Algorithm is the full name of the class I'm trying to load)
I've tried creating a new ClassLoader like below:
ClassLoader cload = new URLClassLoader(new URL[]{dir.toURI().toURL()}, ClassLoader.getSystemClassLoader());
Class cl = Class.forName(algorithm.getClassName(), false, cload);
AlgorithmClass algorithmClass = (AlgorithmClass)cl.newInstance();
But then I get the error:
java.lang.NoClassDefFoundError: lib/algorithm/AlgorithmClass
I've tried creating a new URLClassLoader with all the URLs that the system class loader has but the effect was the same.
The "worst" part of this is that both ways are working perfectly fine on the jUnit test that I have for testing this part of my code.
I'm using Glassfish 3.1.1 as my app server.
dir shouldn't contain 'lib'.
Try this:
ClassLoader cload = new URLClassLoader(new URL[]{dir.toURI().toURL()}, Thread.currentThread().getContextClassLoader());
Class cl = Class.forName(algorithm.getClassName(), true, cload);
AlgorithmClass algorithmClass = (AlgorithmClass)cl.newInstance();
You have class-loading issue. You shoud be aware that your addURLToSystemClassLoader() is actually the heck...
Put your jar to the classpath. Use Class.forName() idiom. If it fails use version that receives ClassLoader as parammeter namely
public static Class<?> forName(String name, boolean initialize,
ClassLoader loader)
and path Thread.currentThread().getContextClassLoader() as ClassLoader parameter.
See also my another answer below.