I'm working on a university java project that requires me to implement a simple plugin architecture. A main application in Project A would have to load plugins from a specified directory located in another project B. After having done some research on the topic (including stackoverflow), I decided to go with URLClassLoader to load the classes which I might then instatiate. Project B references Project A and all plugins extend a common plugin class (might as well be an interface but should not make any difference). This is what I have got so far:
The source for the plugin class I try to load:
package plugin;
import de.uks.student.pluginpattern.model.EditorPlugin;
public class Circle extends EditorPlugin
{
#Override
public void init()
{
}
}
and the code that is supposed to load the class:
public void init(String[] args)
{
editorPane = new EditorPane().withWidth(500).withHeight(500);
toolBar = new ToolBar();
plugins = new EditorPluginSet();
// load plugins
File pluginDir = new File(PLUGIN_PATH);
if (!pluginDir.exists())
{
System.err.println("Plugin path not found!!");
return;
}
String[] plugins = pluginDir.list();
if (plugins == null)
{
System.err.println("Plugin path points to a file!!");
return;
}
for (String pluginString : plugins)
{
for (String argString : args)
{
if (pluginString.contains(argString))
{
System.out.println("Loading plugin from " + pluginString);
File pluginFile = new File(PLUGIN_PATH + "/");
// as getAbsolutePath embeds the relative path ... /../pluginProject ...
// which may not be processed correctly by the OS (Windows at least),
// we correct the path manually (just to be on the safe side)
String absolutePath = pluginFile.getAbsolutePath();
String[] split = absolutePath.split("\\\\");
List<String> splitsimpleList = Arrays.asList(split);
ArrayList<String> splitList = new ArrayList<>(splitsimpleList);
for (int i = 0; i < splitList.size() - 1; ++i)
{
if (splitList.get(i + 1).equals(".."))
{
splitList.remove(i);
splitList.remove(i);
break;
}
}
StringBuilder b = new StringBuilder();
for (String string : splitList)
{
b.append(string);
b.append("/");
}
File pluginFileForRealThisTime = new File(b.toString());
URL pluginURL = null;
try
{
pluginURL = pluginFileForRealThisTime.toURI().toURL();
} catch (MalformedURLException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
URL[] urls = {pluginURL};
ClassLoader parentClassLoader = this.getClass().getClassLoader();
URLClassLoader uLoader = new URLClassLoader(urls, parentClassLoader);
Class<?> pluginClass = null;
try
{
String className = "plugin." + argString;
pluginClass = uLoader.loadClass(className);
} catch (ClassNotFoundException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
Now I always end up with a ClassNotFoundException:
java.lang.ClassNotFoundException: plugin.Circle at
java.net.URLClassLoader$1.run(Unknown Source) at
java.net.URLClassLoader$1.run(Unknown Source) at
java.security.AccessController.doPrivileged(Native Method) at
java.net.URLClassLoader.findClass(Unknown Source) at
java.lang.ClassLoader.loadClass(Unknown Source) at
java.lang.ClassLoader.loadClass(Unknown Source) at
de.uks.student.pluginpattern.model.EditorSystem.init(EditorSystem.java:412)
at
de.uks.student.pluginpattern.EditorSystem.main(EditorSystem.java:13)
I've already checked that
the generated URL can be correctly resolved by any web browser and Windows Explorer.
Circle.class has been built and is present in the directory referenced by the used URL
className resolves to "plugin.Circle" which should be the correct binary name to use
removing inheritance from plugin.Circle does not make any difference.
I've pretty much run out of ideas on what else to try. What am I doing wrong?
I had to point the ClassLoader to the bin folder instead of the bin/plugins folder
Related
I have a problem with loading resource bundles in loaded jars. The main program is loading jars from a folder with a plugin manager. When an object of the main class of a jar is initialized by the plugin manager, resource bundles of this jar can be loaded. By this, I mean in a static block or in a constructor. Otherwise, an MissingResourceException is thrown. Like when you call a method on that object, that tries to load an existing resource-bundle
Currently, I use a static block at the beginning of the main class of a jar to load all resource bundles of the plugin with possible locales. Because of this, the resource bundles will be cached for some time. Also, my current way seems to work out for sub-loaded jars the same way as for the loaded jar
public class PluginMain implements PluginInterface {
static {
for (Locale availableLocale : getAvailableLocales()) {
try {
ResourceBundle resourceBundle = ResourceBundle.getBundle(BUNDLE_PATH, availableLocale);
} catch (MissingResourceException e) {
e.printStackTrace();
}
}
}
...
}
I think it's about the classLoader that is loading the resource-bundle. Still i cannot find a good solution.
I already tried to find some solutions. The best i could find fitting is Loading with ResourceBundle from inside a jar, but that did not work out.
Edit: I load my jars like this
public class PluginManagerImpl implements PluginManager {
private final List<PluginInterface> loadedPlugins = new ArrayList<>();
private final String path;
public PluginManagerImpl(String path) {
File pluginsDir = new File(path, "plugins");
this.path = pluginsDir.getPath();
if (pluginsDir.exists()) {
//pluginsfolder exists
File[] files = pluginsDir.listFiles();
if (files != null) {
for (File f : files)
if (!f.isDirectory()) {
loadPlugin(f);
}
}
} else {
//pluginsfolder does not exist
if (pluginsDir.mkdir()) {
Output.WriteLine("Dictionary created: " + pluginsDir.getPath());
}
}
}
#Override
public void loadPlugin(File file) {
URL urlFile;
//trying to load file, convert it first to URI and then to URL
try {
urlFile = file.toURI().toURL();
} catch (MalformedURLException e) {
Output.WriteLineProblem(e.getMessage(), 4);
return;
}
//trying to create JAR-file from file
try (
//use JarFIle and URLClassLoader as auto-closable
JarFile jarFile = new JarFile(file);
//use classloader of this class as parent classLoader
URLClassLoader classLoader = new URLClassLoader(new URL[]{urlFile}, this.getClass().getClassLoader())
) {
//load manifest
Manifest manifest = jarFile.getManifest();
//read attributes from manifest
Attributes attributes = manifest.getMainAttributes();
//get main class from attributes
String main = attributes.getValue(Attributes.Name.MAIN_CLASS);
if (main == null) {
Output.WriteLineProblem(file.getName() + " has no main specified");
return;
}
String title = attributes.getValue(Attributes.Name.IMPLEMENTATION_TITLE);
if (title == null) {
//https://maven.apache.org/shared/maven-archiver/index.html
Output.WriteLineProblem(file.getName() + " has no implementation title specified");
return;
}
//https://javapapers.com/core-java/java-class-loader/
//load class with classLoader of jarFile
Class<?> cl = classLoader.loadClass(main);
//get implemented interfaces of class
Class<?>[] interfaces = cl.getInterfaces();
//iterate over interfaces and check for PluginInterface.class
boolean isPlugin = false;
for (Class<?> anInterface : interfaces) {
if (anInterface.equals(PluginInterface.class)) {
isPlugin = true;
break;
}
}
if (isPlugin) {
//load all classes in jar file
loadClassesOfjarFile(jarFile, cl.getClassLoader());
//add the pluginfile
PluginInterface plugin = (PluginInterface) cl.getConstructor().newInstance();
plugin.calledAfterInstancing(new File(path, title).getPath());
Output.WriteLine("Loaded Plugin " + title);
loadedPlugins.add(plugin);
}
} catch (Exception e) {
Output.WriteLineProblem("Error on checking " + file.getName() + " for plugin");
e.printStackTrace();
}
}
public static void loadClassesOfjarFile(JarFile jarFile, ClassLoader classLoader) {
jarFile.entries().asIterator().forEachRemaining(jarEntry -> {
String jarEntryName = jarEntry.getName();
if ((jarEntryName.endsWith(".class"))) {
String className = jarEntry.getName().replaceAll("/", "\\.");
String myClass = className.substring(0, className.lastIndexOf('.'));
try {
Class<?> clazz = classLoader.loadClass(myClass);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} else if (jarEntryName.endsWith(".xml")) {
String resourceName = jarEntry.getName().replaceAll("/", "\\.");
classLoader.getResourceAsStream(jarEntry.getName());
}
});
}
}
Edit 2: Here a sample project to test
The resource bundles are contained in the the resource folder of the plugin.
Hierarchy of the project
Sample for the main program:
package main;
public class Main {
public static final String DEFAULT_PATH = FileSystems.getDefault().getPath("").toAbsolutePath().toString();
public static void main(String[] args) {
PluginManager plugins = new PluginManager(DEFAULT_PATH);
List<PluginInterface> loadedPlugins = plugins.getLoadedplugins();
for (PluginInterface loadedPlugin : loadedPlugins) {
loadedPlugin.loadResourceBundle(Locale.ENGLISH);
}
}
}
Sample for plugin:
package plugin;
public class Main implements PluginInterface {
static {
Locale locale = Locale.ENGLISH;
ResourceBundle main = ResourceBundle.getBundle("mainLoadedInStatic", locale);
//only uncomment to check, that it would work if loaded in static
// ResourceBundle mainNotLoadedInStatic = ResourceBundle.getBundle("mainNotLoadedInStatic", locale);
}
#Override
public void loadResourceBundle(Locale locale) {
ResourceBundle mainLoadedInStatic = ResourceBundle.getBundle("mainLoadedInStatic", locale);
ResourceBundle mainNotLoadedInStatic = ResourceBundle.getBundle("mainNotLoadedInStatic", locale);
}
}
The error should be:
Exception in thread "main" java.util.MissingResourceException: Can't find bundle for base name mainNotLoadedInStatic, locale en
at java.base/java.util.ResourceBundle.throwMissingResourceException(ResourceBundle.java:2045)
at java.base/java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:1683)
at java.base/java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:1586)
at java.base/java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:1549)
at java.base/java.util.ResourceBundle.getBundle(ResourceBundle.java:932)
at plugin.Main.loadResourceBundle(Main.java:19)
at main.Main.main(Main.java:18)
I discovered that closing the URLClassLoader (as autocloseable) in loadPlugin of PluginManagerImpl was causing the Problem.
The Resources are tried to be loaded with that URLClassLoader and if it is closed, it will fail.
Which effect would occur, if the URLClassLoader doesn't get closed at all? As far as i understand this could have a negativ effect because of an unclosed JarFile.
I want to create 'modules' which extend a 'Module' interface, these would be separate jars that get loaded on runtime - essentially adding extra features. I want to have these loaded as the 'Module' object (from the interface).
I've tried the below code, and while my code prints out the Found module, it doesn't do anything after that.
try {
Collection<URL> urlList = new ArrayList<>();
Path pluginsDir = Paths.get(Common.getPlugin().getDataFolder().getPath(), "modules");
try (DirectoryStream<Path> jars = Files.newDirectoryStream(pluginsDir, "*.jar")) {
for (Path jar : jars) {
System.out.println("Found module (" + jar.toFile().getName() + ").");
urlList.add(jar.toUri().toURL());
}
}
URL[] urls = urlList.toArray(new URL[0]);
ClassLoader pluginClassLoader = new URLClassLoader(urls, this.getClass().getClassLoader());
ServiceLoader<Module> loader = ServiceLoader.load(Module.class, pluginClassLoader);
for (Module module : loader) {
System.out.println("module.getName() = " + module.getName());
}
} catch (IOException ex) {
ex.printStackTrace();
}
I'd expect it to print out the modules name (as an interface method) but it doesn't loop.
For example, a module would do;
public class ExampleModule implements Module {
#Override
public String getName() {
return "Example";
}
#Override
public void load() {
Bukkit.getLogger().info("Hello!");
}
}
Thanks.
Ok, basically, I try to use the method described here JarFileLoader to load a jar containing a class that will be used the same as if it was on the classpath (the class name will be dynamic so that we can just add any jar with any class and the program will load it through parsing a text file, in the main line).
Problem is that when I debug and check the URLClassLoader object
protected Class<?> findClass(final String name)
Line :
Resource res = ucp.getResource(path, false);
the getResource() does not find the class name in parameter.
Does someone already try loading a jar file this way ?
Thanks.
Loader :
public class JarFileLoader extends URLClassLoader {
public JarFileLoader() {
super(new URL[] {});
}
public JarFileLoader withFile(String jarFile) {
return withFile(new File(jarFile));
}
public JarFileLoader withFile(File jarFile) {
try {
if (jarFile.exists())
addURL(new URL("file://" + jarFile.getAbsolutePath() + "!/"));
} catch (MalformedURLException e) {
throw new IllegalArgumentException(e);
}
return this;
}
public JarFileLoader withLibDir(String path) {
Stream.of(new File(path).listFiles(f -> f.getName().endsWith(".jar"))).forEach(this::withFile);
return this;
}
}
Main :
public static void main(String[] args) {
new Initializer();
JarFileLoader cl = new JarFileLoader();
cl = cl.withFile(new File("libs/dpr-common.jar"));
try {
cl.loadClass("com.*****.atm.dpr.common.util.DPRConfigurationLoader");
System.out.println("Success!");
} catch (ClassNotFoundException e) {
System.out.println("Failed.");
e.printStackTrace();
} finally {
try {
cl.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Here the test class I used. When I debug URLClassLoader I can see in the third loop the path of the jar file(loop on the classpath and the URL you add here), but still does not find ressource (and cannot debug the class URLClassPath so do not know what getRessource does exactly).
Ok I take the answer from this question : How to load all the jars from a directory dynamically?
And changing the URL part at the beginning with the way it is done in the long part it works.
So an example could be :
String path = "libs/dpr-common.jar";
if (new File(path).exists()) {
URL myJarFile = new File(path).toURI().toURL();
URL[] urls = { myJarFile };
URLClassLoader child = new URLClassLoader(urls);
Class DPRConfLoad = Class.forName("com.thales.atm.dpr.common.util.DPRConfigurationLoader", true, child);
Method method = DPRConfLoad.getDeclaredMethod("getInstance");
final Object dprConf = method.invoke(DPRConfLoad);
}
All my time wasted in search while it was the example which was wrong... Still does not understand why they use a stupid URL like "jar:file..." etc.
Thanks everyone.
I have a PluginManager class that watches a ./plugins directory for file creation using a WatchServiceand then utilizes a static method loadPlugin(File) from a PluginLoader class to load newly added jars at runtime. All jars in the folder are also loaded and started at application start up, in that case everything goes well, even with a bunch of plugins.
But when I drop the plugins into the folder one by one I get a weird behavior:
the first jar is loaded fine in 98% of the time
the second only in about 5%
the third only in a very rare case, but it happens
What I get is this:
java.io.FileNotFoundException: .\plugins\test2.jar (Der Prozess kann nicht auf die Datei zugreifen, da sie von einem anderen Prozess verwendet wird)
at java.util.zip.ZipFile.open(Native Method)
at java.util.zip.ZipFile.<init>(ZipFile.java:220)
at java.util.zip.ZipFile.<init>(ZipFile.java:150)
at java.util.jar.JarFile.<init>(JarFile.java:166)
at java.util.jar.JarFile.<init>(JarFile.java:130)
at PluginLoader.loadPlugin(PluginLoader.java:34)
at PluginManager$WatchQueueReader.run(PluginManager.java:118)
at java.lang.Thread.run(Thread.java:745)
Exception in thread "FileWatcher" java.lang.NullPointerException
at PluginLoader.loadPlugin(PluginLoader.java:41)
at PluginManager$WatchQueueReader.run(PluginManager.java:118)
at java.lang.Thread.run(Thread.java:745)
It is saying that the file is used in another process.
PluginLoader:
public static BasePlugin loadPlugin(File pluginJar) {
Attributes attrib = null;
JarFile file = null;
try {
file = new JarFile(pluginJar);
Manifest manifest = file.getManifest();
attrib = manifest.getMainAttributes();
} catch (IOException e) {
e.printStackTrace();
}
String main = attrib.getValue(Attributes.Name.MAIN_CLASS);
String name = attrib.getValue("Plugin-Name");
if (main == null || name == null) {
System.out.println("Not a valid manifest: " + pluginJar.getName());
return null;
}
URLClassLoader loader = null;
try {
loader = URLClassLoader.newInstance(new URL[] { pluginJar.toURI().toURL() }, PluginLoader.class.getClassLoader());
} catch (MalformedURLException e2) {
e2.printStackTrace();
}
Class<?> cl = null;
try {
cl = Class.forName(main, true, loader);
} catch (ClassNotFoundException e1) {
e1.printStackTrace();
} finally {
try {
loader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
try {
Class<? extends BasePlugin> c = cl.asSubclass(BasePlugin.class);
Constructor<? extends BasePlugin> ctr = c.getConstructor(String.class, SystemManager.class);
return ctr.newInstance(name, this.sm);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
WatchService inside PluginManager:
private static class WatchQueueReader implements Runnable {
private WatchService watcher;
private PluginManager pm;
public WatchQueueReader(PluginManager pm, WatchService watcher) {
this.pm = pm;
this.watcher = watcher;
}
#Override
public void run() {
try {
WatchKey key = watcher.take();
while (key != null) {
for (WatchEvent<?> event : key.pollEvents()) {
switch (event.kind().toString()) {
case "ENTRY_CREATE":
Path dir = (Path) key.watchable();
Path fullPath = dir.resolve(event.context().toString());
BasePlugin plugin = PluginLoader.loadPlugin(fullPath.toFile());
if (plugin != null) {
this.pm.startPlugin(plugin);
}
break;
case "ENTRY_MODIFY":
break;
case "ENTRY_DELETE":
this.pm.stopPlugin(event.context().toString()); // TODO wrong name (.jar)
break;
default:
break;
}
}
key.reset();
key = watcher.take();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
BasePlugin is an abstract class that plugins extend.
The file is still being copied, the WatchService simply finds it too soon.
In fact, it will report the file as existing as soon as it['s inode] is created, but not yet filled with information (the bytes, you know).
My first tip is that if you encounter this exception, wait a few seconds and try again. Just after like 10-30 seconds of futile attempts you can give up, as it have been deleted maybe. But this part needs fine tuning, as maybe the copy operation is slow.
I am very new to StackOverflow and I've done my best to fix this problem before posting this question here. I'm faced with the problem of getResource() returning null. I have a hunch that this is because I'm on a mac and the pathing is different here than on a PC (where this code seems to work fine). This is the code:
public class SampleClass
{
static String imgpath = "/theimage.png";
public static void main(String[] args)
{
System.out.println(imgpath);
System.out.println(SampleClass.class.getResource(imgpath));
try
{
BufferedImage image = ImageIO.read(SampleClass.class.getResource(imgpath));
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
src, res and bin are all in the same directory and theimage.png is inside of res.
System.out.println(SampleClass.class.getResource("imgpath")); gives me null.
I had the same issue on my mac using spring boot :
the file is located on properties/report/example.jasper
when the path was : "report/example.jasper" i got nullPointerException
So i changed to : "./report/example.jasper" and It works fine without any bug.
InputStream inStream = null;
try {
inStream = ExportController.class.getClassLoader().getResourceAsStream(path);
final JasperReport jasperReport = (JasperReport) JRLoader.loadObject(inStream);
jasperReport.setWhenNoDataType(WhenNoDataTypeEnum.ALL_SECTIONS_NO_DETAIL);
jasperPrint = JasperFillManager.fillReport(jasperReport, parameters, dataSource);
} catch (final JRException jre) {
throw new TechnicalException("Error when export jasper");
} finally {
if (inStream != null) {
inStream.close();
}
}
you get nullpointer exception because there is no image named imgpath in that folder
public class SampleClass
{
static String imgpath = "/theimage.png";
public static void main(String[] args)
{
System.out.println(imgpath);
System.out.println(SampleClass.class.getResource(imgpath));
try
{
BufferedImage image = ImageIO.read(SampleClass.class.getResource(imgpath));
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
I faced the same issue on Mac. Here how I now get files from resources. For example, I have a common Maven project with resource folder in src/main. In resource folder I have a file "test.txt".
To get a path to the file:
public class Utils {
public static String getFilePathInResources() {
URL url = Utils.class.getClassLoader().getResource("test.txt");
return url.getPath();
}
}
Here the filename is hardcored just for clearity, of course, really it is a parameter in the method.
If set a filename as "/test.txt" with "/" - this will give null.
URL url = Utils.class.getClassLoader().getResource("/test.txt"); // url == null