I currently develop an open-source project where people may add their own .jar to extend the included features. However, I'm stuck with how to load the classes in the jar.
I have an abstract class which has an abstract method onEnable() and some getter that provides some objects to work with the application. The plugin needs the subclass my plugin-class BasePlugin. The jar should be added to /plugins and thus I want all *.jar files in the /plugins folder to be loaded when the application starts.
The problem I'm running to now is that, of all the approaches I found, I need to declare a classpath of the classes in the jar file, which I do not know. Neither do I know the name of the jar file. Thus, I need to scan the /plugins folder for any *.jar file and load the corresponding class inside the jar which implements BasePlugin and invoke the onEnable() method.
The basic idea is too...
Read all the files in a specific directory
Convert the File reference to a URL for each result
Use a URLClassLoader, seeded with the URL results to load each result
Use URLClassLoader#findResources to find all the match resources with a specific name
Iterate over the matching resources and load each one, which should give, at least, the "entry point" class name
Load the class specified by the "entry point" property
For example...
public List<PluginClass> loadPlugins() throws MalformedURLException, IOException, ClassNotFoundException {
File plugins[] = new File("./Plugins").listFiles(new FileFilter() {
#Override
public boolean accept(File file) {
return file.getName().endsWith(".jar");
}
});
List<URL> plugInURLs = new ArrayList<>(plugins.length);
for (File plugin : plugins) {
plugInURLs.add(plugin.toURI().toURL());
}
URLClassLoader loader = new URLClassLoader(plugInURLs.toArray(new URL[0]));
Enumeration<URL> resources = loader.findResources("/META-INFO/Plugin.properties");
List<PluginClass> classes = new ArrayList<>(plugInURLs.size());
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
Properties properties = new Properties();
try (InputStream is = resource.openStream()) {
properties.load(is);
String className = properties.getProperty("enrty-point");
PluginClass pluginClass = loader.loadClass(className);
classes.add(pluginClass);
}
}
return classes
}
nb: I've not run this, but this is the "basic
SpigotMC uses JAR files as plugins as well, inside the jar is a plugin.yaml file that stores extra information about the plugin including the classpath. You don't need to use a YAML file, instead you could use something like JSON or even a plain text file.
The YAML file is inside the jar and can be accessed by using some of the methods explained here. You can then get the classpath property and then load the jar using the methods explained here. Extra information can be stored about the plugin such as the name, version, and dependencies.
Java already has a class for this: ServiceLoader.
The ServiceLoader class was introduced with Java 6, but the “SPI jar” concept is actually as old as Java 1.3. The idea is that each jar contains a short text file that describes its implementations of a particular service provider interface.
For instance, if a .jar file contains two subclasses of BasePlugin named FooPlugin and BarPlugin, the .jar file would also contain the following entry:
META-INF/services/com.example.BasePlugin
And that jar entry would be a text file, containing the following lines:
com.myplugins.FooPlugin
com.myplugins.BarPlugin
Your project would scan for the plugins by creating a ClassLoader that reads from the plugins directory:
Collection<URL> urlList = new ArrayList<>();
Path pluginsDir = Paths.get(
System.getProperty("user.home"), "plugins");
try (DirectoryStream<Path> jars =
Files.newDirectoryStream(pluginsDir, "*.jar")) {
for (Path jar : jars) {
urlList.add(jar.toUri().toURL());
}
}
URL[] urls = urlList.toArray(new URL[0]);
ClassLoader pluginClassLoader = new URLClassLoader(urls,
BasePlugin.class.getClassLoader());
ServiceLoader<BasePlugin> plugins =
ServiceLoader.load(BasePlugin.class, pluginClassLoader);
for (BasePlugin plugin : plugins) {
plugin.onEnable();
// etc.
}
An additional advantage of using ServiceLoader is that your code will be capable of working with modules, a more complete form of code encapsulation introduced with Java 9 which offers increased security.
There is an example here it may be helpful. Also, you should take a look at OSGi.
Related
A lot has been discussed already here about getting a resource.
If there is already a solution - please point me to it because I couldn't find.
I have a program which uses several jars.
To one of the jars I added a properties file under main/resources folder.
I've added the following method to the jar project in order to to read it:
public void loadAppPropertiesFile() {
try {
Properties prop = new Properties();
ClassLoader loader = Thread.currentThread().getContextClassLoader();
String resourcePath = this.getClass().getClassLoader().getResource("").getPath();
InputStream stream = loader.getResourceAsStream(resourcePath + "\\entities.properties");
prop.load(stream);
String default_ssl = prop.getProperty("default_ssl");
}catch (Exception e){
}
}
The problem (?) is that resourcePath gives me a path to the target\test-clasess but under the calling application directory although the loading code exists in the jar!
This the jar content:
The jar is added to the main project by maven dependency.
How can I overcome this state and read the jar resource file?
Thanks!
I would suggest using the classloader used to load the class, not the context classloader.
Then, you have two options to get at a resource at the root of the jar file:
Use Class.getResourceAsStream, passing in an absolute path (leading /)
Use ClassLoader.getResourceAsStream, passing in a relative path (just "entities.properties")
So either of:
InputStream stream = getClass().getResourceAsStream("/entities.properties");
InputStream stream = getClass().getClassLoader().getResourceAsStream("entities.properties");
Personally I'd use the first option as it's briefer and just as clear.
Can you try this:
InputStream stream = getClass().getClassLoader().getResourceAsStream("entities.properties")
I have a Maven project, and I am packaging it as a single fat jar using the One-Jar Maven plugin. According to this page on the One-Jar site documentation resource loading must be done in a special manner with One-Jar packaged apps, however that documentation does not cover how to list the resources in a manner which does not depend on their custom class loader.
My question is this: how to I list the contents of the root of my classpath within an inner jar in a packaging agnostic manner, meaning not assuming it will always be in such special packaging? As covered in this SO question, Spring's PathMatchingResourcePatternResolver will not work because Spring's DefaultResourceLoader ignores the classloader.
NOTE: The closest SO question I could find was this question, but this does not address the listing of classpath resources from nested jars.
It can be done in a FAT jar. Your own answer stating it's not possible has a link to a page that shows it's possible! It's not that insane of a solution. Here's a slightly modified version:
private List<String> getResourceFiles(String path) throws IOException
{
List<String> files = new ArrayList<>();
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
URL dirURL = classLoader.getResource(path);
if (dirURL.getProtocol().equals("jar"))
{
String jarPath = dirURL.getPath().substring(5, dirURL.getPath().indexOf("!"));
try (final JarFile jar = new JarFile(URLDecoder.decode(jarPath, "UTF-8")))
{
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements())
{
String name = entries.nextElement().getName();
if (name.startsWith(path + "/") && !name.endsWith("/"))
{
files.add(name);
}
}
}
return files;
}
// return some other way when not a JAR
}
This version will return the full resource path of any file under the given resource path, recursively. For example, an input of config/rest would return files under that resource folder as:
config/rest/options.properties
config/rest/samples/data.json
With those values, one can get the resource with ClassLoader::getResource, or get the contents of the resource with ClassLoader::getResourceAsStream.
After reviewing many articles, and blogs posts, web pages, site and the Spring source code, I had concluded this is just a weakness in Java and is not possible in any sane manner.
I am trying to access a directory inside my jar file. I want to go through every of the files inside the directory itself. I tried using the following:
File[] files = new File("ressources").listFiles();
for (File file : files) {
XMLParser parser = new XMLParser(file.getAbsolutePath());
// some work
}
If I test this, it works well. But once I put the contents into the jar, it doesn't because of several reasons. If I use this code, the URL always points outside the jar.
structure of my project :
src
controllers
models
class that containt traitement
views
ressources
See this:
How do I list the files inside a JAR file?
Basically, you just use a ZipInputStream to find a list of files (a .jar is the same as a .zip)
Once you know the names of the files, you can use getClass().getResource(String path) to get the URL to the file.
I presume this jar is on your classpath.
You can list all the files in a directory using the ClassLoader.
First you can get a list of the file names then you can get URLs from the ClassLoader for individual files:
public static void main(String[] args) throws Exception {
final String base = "/path/to/folder/inside/jar";
final List<URL> urls = new LinkedList<>();
try (final Scanner s = new Scanner(MyClass.class.getResourceAsStream(base))) {
while (s.hasNext()) {
urls.add(MyClass.class.getResource(base + "/" + s.nextLine()));
}
}
System.out.println(urls);
}
You can do whatever you want with the URL - either read and InputStream into memory or copy the InputStream into a File on your hard disc.
Note that this definitely works with the URLClassLoader which is the default, if you are using an applet or a custom ClassLoader then this approach may not work.
NB:
You have a typo - its resources not ressources.
You should use reverse domain name notation for your project, this is the convention.
I have a use case where I need to export this specific piece of code as a java library (which will be a JAR eventually) but the problem is that it needs to use some piece of information stored in physical files on the file system.
I have 2 questions here:
1) Where should I put these files on the filesystem (One option that I could think of was in the resources directory of the Java module containing the library: Have a doubt though that the resources directory also gets compiled into the jar?)
2) When I am using this library from an external Java application, how would the library be able to locate the files? Would they still be in the classpath?
You have two options, first one is to place the files inside the package structure, so that they will be packed inside the jar. You would get them from the code like this:
getClass().getResourceAsStream("/path/to/your/resource/resource.ext");
If you would call it from a static method of class named A then you should write like this:
A.class.getResourceAsStream("/path/to/your/resource/resource.ext");
The "/path" part of the path is the topmost package, and the resource.ext is your file name.
The other option is to put them outside the jar package, but then the jar needs to know their location:
provide it as an argument to the program (java -jar program.jar system/path/to/file)
hardcode the location from which you would read the file with paths
The way I undestood your queastion and answered it, it has nothing to do with classpath:
The CLASSPATH variable is one way to tell applications, including the JDK tools, where to look for user classes. (Classes that are part of the JRE, JDK platform, and extensions should be defined through other means, such as the bootstrap class path or the extensions directory.)
EDIT:
but you can nevertheless, put it there and get it from code like this:
System.getProperty("java.class.path");
It would however require some logic to parse it out.
You can pass the location of the files in a property file or some technique like this.
Where should I put these files on the filesystem
That is up to you to decide, though it would be a good idea to make this configurable. It would also be a good idea to try to fit into the conventions of the host operating system / distro, though these vary ... and depend on the nature of your application.
When I am using this library from an external Java application, how would the library be able to locate the files?
You would typically use a configuration property or initialization parameter to hold/pass the location. If you were writing an application rather that a library, you could use the Java Preferences APIs, though this probably a poor choice for a library.
Would they still be in the classpath?
Only if you put the location on the classpath ... and that is going to make configuration more tricky. Given that these files are required to be stored in the file system, I'd recommend using FileInputStream or similar.
Using Eclipse, I always create a package 'resources' where I put the files the jar needs. I access the files (from pretty much anywhere) through
this.getClass().getClassLoader().getResources("/resources/file.ext");
With export->runnable jar all those files are included in the .jar. I'm not sure this is the correct way of doing it though. Also, I'm not 100% sure about the "/" before resources, maybe it should be omitted.
I found a relevant answer as a part of another question : How to load a folder from a .jar?
I am able to successfully retrieve the files using the following code:
/**
* List directory contents for a resource folder. Not recursive.
* This is basically a brute-force implementation.
* Works for regular files and also JARs.
*
* #author Greg Briggs
* #param clazz Any java class that lives in the same place as the resources you want.
* #param path Should end with "/", but not start with one.
* #return Just the name of each member item, not the full paths.
* #throws URISyntaxException
* #throws IOException
*/
String[] getResourceListing(Class clazz, String path) throws URISyntaxException, IOException {
URL dirURL = clazz.getClassLoader().getResource(path);
if (dirURL != null && dirURL.getProtocol().equals("file")) {
/* A file path: easy enough */
return new File(dirURL.toURI()).list();
}
if (dirURL == null) {
/*
* In case of a jar file, we can't actually find a directory.
* Have to assume the same jar as clazz.
*/
String me = clazz.getName().replace(".", "/")+".class";
dirURL = clazz.getClassLoader().getResource(me);
}
if (dirURL.getProtocol().equals("jar")) {
/* A JAR path */
String jarPath = dirURL.getPath().substring(5, dirURL.getPath().indexOf("!")); //strip out only the JAR file
JarFile jar = new JarFile(URLDecoder.decode(jarPath, "UTF-8"));
Enumeration<JarEntry> entries = jar.entries(); //gives ALL entries in jar
Set<String> result = new HashSet<String>(); //avoid duplicates in case it is a subdirectory
while(entries.hasMoreElements()) {
String name = entries.nextElement().getName();
if (name.startsWith(path)) { //filter according to the path
String entry = name.substring(path.length());
int checkSubdir = entry.indexOf("/");
if (checkSubdir >= 0) {
// if it is a subdirectory, we just return the directory name
entry = entry.substring(0, checkSubdir);
}
result.add(entry);
}
}
return result.toArray(new String[result.size()]);
}
throw new UnsupportedOperationException("Cannot list files for URL "+dirURL);
}
I have an I18n helper class that can find out the available Locales by looking at the name of the files inside the application's Jar.
private static void addLocalesFromJar(List<Locale> locales) throws IOException {
ProtectionDomain domain = I18n.class.getProtectionDomain();
CodeSource src = domain.getCodeSource();
URL url = src.getLocation();
JarInputStream jar = new JarInputStream(url.openStream());
while (true) {
JarEntry entry = jar.getNextJarEntry();
if (entry == null) {
break;
}
String name = entry.getName();
// ...
}
}
Currently, this isn't working - jar.getNextJarEntry() seems to always return null. I have no idea why that's happening, all I know is that url is set to rsrc:./. I have never seen that protocol, and couldn't find anything about it.
Curiously, this works:
class Main {
public static void main(String[] args) {
URL url = Main.class.getProtectionDomain().getCodeSource().getLocation();
JarInputStream jar = new JarInputStream(url.openStream());
while (true) {
JarEntry entry = jar.getNextJarEntry();
if (entry == null) {
break;
}
System.out.println(entry.getName());
}
}
}
In this version, even though there is practically no difference between them, the url is correctly set to the path of the Jar file.
Why doesn't the first version work, and what is breaking it?
UPDATE:
The working example really only works if I don't use Eclipse to export it. It worked just fine in NetBeans, but in the Eclipse version the URL got set to rsrc:./ too.
Since I exported it with Package required libraries into generated JAR library handling, Eclipse put its jarinjarloader in my Jar so I can have all dependencies inside it. It works fine with the other settings, but is there any way to make this work independently of them?
Another question
At the moment, that class is part of my application, but I plan to put it in a separate library. In that case, how can I make sure it will work with separate Jars?
The problem is the jarinjarloader ClassLoader that is being used by Eclipse. Apparently it is using its own custom rsrc: URL scheme to point to jar files stored inside the main jar file. This scheme is not understood by your URL stream handler factory, so the openStream() method returns null which causes the problem that you're seeing.
This answers the second part of your question about separate jars - not only will this work, it's the only way that it will work. You need to change your main application to use separate jars instead of bundling them all up inside the main jar. If you're building a web application, copy them into the WEB-INF/lib directory and you're fine. If you're building a desktop application, add a relative path reference in the META-INF/MANIFEST.MF to the other jars, and they will automatically be included as part of the classpath when you run the main jar.
The code may or may not result into the jar file where I18n resides. Also getProtectionDomain can be null. It depends how the classloader is implemented.
ProtectionDomain domain = I18n.class.getProtectionDomain();
CodeSource src = domain.getCodeSource();
URL url = src.getLocation();
about the rsrc:./ protocol, the classloader is free to use whatever URL they please (or name it for that matter)
try this out, you might get lucky :)
URL url = getClass().getResource(getClass().getSimpleName()+".class");
java.net.JarURLConnection conn = (java.net.JarURLConnection) url.openConnection();
Enumeration<JarEntry> e = conn.getJarFile().entries();
...
and good luck!
Eclipse's jarinjarloader loads everything using the system classloader and it never knows what jar file it was loaded from. That's why you can't get the jar URL for a rsrc: url.
I suggest storing the list of locales in a file in each application jar, e.g. META-INF/locales. Then you can use ClassLoader.getResources("META-INF/locales") to get the list of all the files with that name in the classpath and combine them to obtain the full list of locales.
I use System.getProperty("java.class.path") for getting the location of the jar. I do not know if that makes a difference. I have not explored the ProtectDomain path so I cannot help you there, sorry. As for multiple jars, just iterate through those jar file also.