How to load ResourceBundles within loaded JAR - java

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.

Related

Trying to laod dependencys in java runtime

I just want to load .jar libraries in my running programm. Therefore i created a "libs" folder in my programm directory.
In the main in call the function loadDependencies() to load all the .jar files in the libs directory to use them in a plugin extension system.
Now the problem, it does not work :)
Here the code i tried so far:
public class DependencyLoader {
private static final Class<?>[] parameters = new Class[]{URL.class};
public static void addFile(String s) throws IOException {
File f = new File(s);
addFile(f);
}
public static void addFile(File f) throws IOException {
addURL(f.toURI().toURL());
}
public static void addURL(URL u) throws IOException {
URLClassLoader sysloader = (URLClassLoader)ClassLoader.getSystemClassLoader();
Class<?> sysclass = URLClassLoader.class;
try {
Method method = sysclass.getDeclaredMethod("addURL",parameters);
method.setAccessible(true);
method.invoke(sysloader,new Object[]{ u });
} catch (Throwable t) {
t.printStackTrace();
throw new IOException("Error, could not add URL to system classloader");
}
}
public static void loadDependencies(){
File libsDir = new File("/home/admin/network/lobby/libs");
if(!libsDir.exists() && !libsDir.mkdirs() && !libsDir.isDirectory()){
System.out.println("could not find lib directory!");
System.exit(-1);
}
for(File file : libsDir.listFiles()){
if(file.getName().endsWith(".jar")){
System.out.println("loading dependency "+file.getName());
try {
addFile(file);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
The libraries are found . But not loaded correctly. The result is a noclassdef error.
Hope someone can help me.
Regards!

Loading Class from Path

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.

User URLClassLoader to load jar file "on the fly"

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.

Issue with loading class from jar file represented as byte array

I'm trying to create an instance of a class from jar file loaded on a byte array.
I'm receiving two args:
1. byte[] which represents jar file with required class
2. Qualified class name
When I'm testing it locally it works as expected, but when I upload exactly the same jar file with the same qualified class name remotely (using web application implemented with Spring MVC for back and AngularJS for front end deployed in Tomcat server) It can't find the required class:
java.lang.ClassNotFoundException
When I was debugging, it turned out, that classloader is properly invoked but no one class is loaded from jar.
I would be grateful if anyone can tell what can be the reason of that difference or how can I implement this functionality in other ways.
A method which loads class and returns an instance of it:
public static <T> T getInstanceOfLoadedClass(byte[] jarFileBytes, String qualifiedClassName) throws ClassFromJarInstantiationException {
LOGGER.info("Getting instance of class from loaded jar file. Class name: " + qualifiedClassName);
try {
return (T) Class.forName(qualifiedClassName, true, new ByteClassLoader(jarFileBytes)).newInstance();
} catch (InstantiationException | IllegalAccessException | IOException | ClassNotFoundException | NoSuchFieldException e) {
LOGGER.error("Exception was thrown while reading jar file for " + qualifiedClassName + "class.", e);
throw new ClassFromJarInstantiationException(e);
}
}
Custom ByteClassLoader:
public class ByteClassLoader extends ClassLoader {
private static final Logger LOGGER = Logger.getLogger(ByteClassLoader.class);
private final byte[] jarBytes;
private final Set<String> names;
public ByteClassLoader(byte[] jarBytes) throws IOException {
this.jarBytes = jarBytes;
this.names = loadNames(jarBytes);
}
private Set<String> loadNames(byte[] jarBytes) throws IOException {
Set<String> set = new HashSet<>();
try (ZipInputStream jis = new ZipInputStream(new ByteArrayInputStream(jarBytes))) {
ZipEntry entry;
while ((entry = jis.getNextEntry()) != null) {
set.add(entry.getName());
}
}
return Collections.unmodifiableSet(set);
}
#Override
public InputStream getResourceAsStream(String resourceName) {
if (!names.contains(resourceName)) {
return null;
}
boolean found = false;
ZipInputStream zipInputStream = null;
try {
zipInputStream = new ZipInputStream(new ByteArrayInputStream(jarBytes));
ZipEntry entry;
while ((entry = zipInputStream.getNextEntry()) != null) {
if (entry.getName().equals(resourceName)) {
found = true;
return zipInputStream;
}
}
} catch (IOException e) {
LOGGER.error("ByteClassLoader threw exception while reading jar byte stream for resource: "+resourceName, e);
e.printStackTrace();
} finally {
if (zipInputStream != null && !found) {
try {
zipInputStream.close();
} catch (IOException e) {
LOGGER.error("ByteClassLoader threw exception while closing jar byte stream for resource: "+resourceName, e);
e.printStackTrace();
}
}
}
return null;
} }
The problem was that the class required to be loaded was in a range of classloader while it was tested.
Hope it helps someone in solving this problem because it is really easy to miss.

How can i do to get all class of a given package with guava

I am working on a maven project and I added to my pom.xml file the guava dependency and the dependency of an other project. I want to get all class of a given package of that project which I have added to my pom as a dependency.
So I tried with this :
public void getClassOfPackage() {
final ClassLoader loader = Thread.currentThread()
.getContextClassLoader();
try {
for (final ClassPath.ClassInfo info : ClassPath.from(loader)
.getTopLevelClasses()) {
System.out.println(info.getSimpleName());
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new TestMain().getClassOfPackage();
}
And I know that is wrong because it gives that in console:
HierarchicalConfigurationConverter
HierarchicalConfigurationXMLReader
HierarchicalINIConfiguration
SubsetConfiguration
SystemConfiguration...
I don't know where I can specify the project where I am talking about and how I can just pass the package name and it gives me all class which it contains.
I can show all the classes whitch are in the same project but for an other project added as a dependency in pom.xml not yet :
The solution for what we i have in the same project:
public void getClassOfPackage() {
final ClassLoader loader = Thread.currentThread()
.getContextClassLoader();
try {
ClassPath classpath = ClassPath.from(loader); // scans the class path used by classloader
for (ClassPath.ClassInfo classInfo : classpath.getTopLevelClasses("org.mypackage")) {
System.out.println(classInfo.getSimpleName()+" <==> "+classInfo.getPackageName());
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new TestMain().getClassOfPackage();
}
I found tow solutions :
With guava :
public void getClassOfPackage(String packagenom) {
final ClassLoader loader = Thread.currentThread()
.getContextClassLoader();
try {
ClassPath classpath = ClassPath.from(loader); // scans the class path used by classloader
for (ClassPath.ClassInfo classInfo : classpath.getTopLevelClasses(packagenom)) {
if(!classInfo.getSimpleName().endsWith("_")){
System.out.println(classInfo.getSimpleName());
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new TestMain().getClassOfPackage("org.myproject");
}
With Java.util:
public List<Class> getClassOfPackage(String packageName)
throws ClassNotFoundException, IOException {
ClassLoader classLoader = Thread.currentThread()
.getContextClassLoader();
assert classLoader != null;
String path = packageName.replace('.', '/');
Enumeration<URL> resources = classLoader.getResources(path);
List<File> dirs = new ArrayList<File>();
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
dirs.add(new File(resource.getFile()));
}
ArrayList<Class> classes = new ArrayList<Class>();
for (File directory : dirs) {
classes.addAll(findClasses(directory, packageName));
}
return classes;
}
private static List<Class> findClasses(File directory, String packageName)
throws ClassNotFoundException {
List<Class> classes = new ArrayList<Class>();
if (!directory.exists()) {
return classes;
}
File[] files = directory.listFiles();
for (File file : files) {
if (file.isDirectory()) {
assert !file.getName().contains(".");
classes.addAll(findClasses(file,
packageName + "." + file.getName()));
} else if (file.getName().endsWith(".class")) {
classes.add(Class.forName(packageName
+ '.'
+ file.getName().substring(0,
file.getName().length() - 6)));
}
}
return classes;
}
public static void main(String[] args) throws ClassNotFoundException,
IOException {
for (Class className : new ClassOfPackage()
.getClassOfPackage("org.mypackage")) {
if (!className.getSimpleName().endsWith("_")) {
System.out.println(className.getSimpleName());
}
}
}
This is why you're seeing it print out sun.misc.Launcher$AppClassLoader:
} else {
System.out.println(loader.getClass().getName());
}
Every class on your classpath that isn't in your package is causing your program to print out the name of the ClassLoader implementation class, which you probably don't want.
Also, while info.getName().startsWith("org.mypackage") should work, it might be preferable to write info.getPackageName().equals("org.mypackage") (or startsWith if you want to include subpackages too).
It is easier to solve this problem with the help of Google reflections.
Please check the following question for the correct answer - note that the checked answer does not provide a working solution, the second one (Aleksander Blomskøld) does!

Categories