Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[native-image] URLClassLoader getResourceAsStream returns null #1956

Closed
borkdude opened this issue Dec 8, 2019 · 32 comments
Closed

[native-image] URLClassLoader getResourceAsStream returns null #1956

borkdude opened this issue Dec 8, 2019 · 32 comments
Assignees

Comments

@borkdude
Copy link

borkdude commented Dec 8, 2019

Problem statement:

I want to find and retrieve a file in a given collection of .jar files or directories. URLClassLoader seems like the perfect fit and works on the JVM. But not on GraalVM.

Repro:

Given the following Java code in ClassLoader.java:

import java.net.*;
import java.io.File;
public class ClassLoader {
    public static void main(String [] args) throws MalformedURLException {
        File path = new File(args[0]);
        URL url = path.toURI().toURL();
        URL[] urls = { url };
        URLClassLoader uc = new URLClassLoader(urls);
        System.out.println(uc.getResourceAsStream("foo/bar.clj"));
    }
}

and a file /tmp/foo/bar.clj, when I compile and run this with the JVM:

javac ClassLoader.java
java ClassLoader /tmp

I see:

java.io.BufferedInputStream@6d06d69c

but when compiled and run with native image:

native-image ClassLoader
./classloader /tmp

I get null.

Expected result:

I expected the same result as on the JVM.

@SergejIsbrecht
Copy link

SergejIsbrecht commented Dec 8, 2019

Did you include the resource while generating the native-image?

See: https://github.com/oracle/graal/blob/master/substratevm/RESOURCES.md

As a note: dynamic classloading / resource-loading from jars is not supported, because everything reachable must be available during generation-time. If the resource is unavailable/ unknown during generation-time, it will not be found.

See: https://github.com/oracle/graal/blob/master/substratevm/LIMITATIONS.md

@borkdude
Copy link
Author

borkdude commented Dec 8, 2019

@SergejIsbrecht No, the point of this code is to search through a collection of directories and .jar files for a file at runtime. The string consisting of directories and jar files is not known at compile time.

@SergejIsbrecht
Copy link

Well, as I see it you are out of luck on this one. I also updated my previous answer.

Please have a look at the limitation of native-image for further information:
https://github.com/oracle/graal/blob/master/substratevm/LIMITATIONS.md

@borkdude
Copy link
Author

borkdude commented Dec 9, 2019

Note that the below program does exactly what I expected with native-image. What is the reason not to support this kind of behavior? It seems the functionality is already there, but only disabled in a higher level somehow.

import java.net.*;
import sun.misc.*;
import java.io.File;
import java.io.InputStream;
import java.io.IOException;

public class ClassLoader {

    public static void main(String [] args) throws IOException {
        String classpath = args[0];
        URL[] urls = URLClassPath.pathToURLs(classpath);
        URLClassPath cp = new URLClassPath(urls);
        Resource r = cp.getResource("foo/bar.clj");
        if (r != null) {
            System.out.println(r.getInputStream());
        }
    }

}
$ ./classloader /tmp
java.io.FileInputStream@104603c98

EDIT: this code as is doesn't work with native image JDK 11 since the package sun.misc has been renamed to jdk.internal.loader.

A workaround for me would be to re-implement URLClassLoader functionality myself.

@sogaiu
Copy link

sogaiu commented Dec 9, 2019

IIUC, the official position has to do with dynamic "class" loading.

getResourceAsStream can be used for things other than classes IIUC. That seems like a reasonable thing to be able to do.

@tjwatson
Copy link

tjwatson commented Mar 4, 2020

I had a similar need, but I never considered using a URLClassLoader to load resources out of JARs on disk. Instead I just used the JarFile API directly to read the JARs. That seems to work just fine in a native-image.

@borkdude
Copy link
Author

borkdude commented Mar 4, 2020

@tjwatson That is also how I solved that problem. I basically replicated classpath functionality myself.

@jovanstevanovic
Copy link
Member

I've tested the given examples. This issue does not longer exist.

@borkdude
Copy link
Author

borkdude commented Feb 3, 2023

@jovanstevanovic I'm still seeing this problem with native image 22.3.1 and 23.0.0-dev:

$ java ClassLoader /tmp
java.io.BufferedInputStream@591f989e
$ ./classloader /tmp
null

I think this may be due to the substitution:

@borkdude
Copy link
Author

borkdude commented Feb 3, 2023

I tried to work around this by undo-ing the substitution, but I'm getting:

[1/7] Initializing...                                                                                    (0,0s @ 0,25GB)
Error: Substition: java.net.URLClassLoader.getResourceAsStream(String) conflicts with previously registered: java.net.URLClassLoader.getResourceAsStream(String)
com.oracle.svm.core.util.UserError$UserException: Substition: java.net.URLClassLoader.getResourceAsStream(String) conflicts with previously registered: java.net.URLClassLoader.getResourceAsStream(String)

Instead I wrote my own extension of URLClassLoader which re-implements a bunch of stuff.

// This file is mostly a workaround for https://github.com/oracle/graal/issues/1956

package babashka.impl;

import java.util.WeakHashMap;
import java.io.*;
import java.util.Objects;
import java.net.*;
import java.util.jar.*;

public class URLClassLoader extends java.net.URLClassLoader implements Closeable {

    private WeakHashMap<Closeable,Void>
        closeables = new WeakHashMap<>();

    public URLClassLoader(java.net.URL[] urls) {
        super(urls);
    }

    public URLClassLoader(java.net.URL[] urls, java.net.URLClassLoader parent) {
        super(urls, parent);
    }

    public void _addURL(java.net.URL url) {
        super.addURL(url);
    }

    // calling super.getResource() returned nil in native-image
    public java.net.URL getResource(String name) {
        return findResource(name);
    }

    // calling super.getResourceAsStream() returned nil in native-image
    public InputStream getResourceAsStream(String name) {
        Objects.requireNonNull(name);
        URL url = getResource(name);
        try {
            if (url == null) {
                return null;
            }
            URLConnection urlc = url.openConnection();
            InputStream is = urlc.getInputStream();
            if (urlc instanceof JarURLConnection) {
                JarFile jar = ((JarURLConnection)urlc).getJarFile();
                synchronized (closeables) {
                    if (!closeables.containsKey(jar)) {
                        closeables.put(jar, null);
                    }
                }
            } else {
                synchronized (closeables) {
                    closeables.put(is, null);
                }
            }
            return is;
        } catch (IOException e) {
            return null;
        }
    }

    public java.util.Enumeration<java.net.URL> getResources(String name) throws java.io.IOException {
        return findResources(name);
    }

    public void close() throws IOException {
        super.close();

        java.util.List<IOException> errors = new java.util.ArrayList<IOException>();

        synchronized (closeables) {
            java.util.Set<Closeable> keys = closeables.keySet();
            for (Closeable c : keys) {
                try {
                    c.close();
                } catch (IOException ex) {
                    errors.add(ex);
                }
            }
            closeables.clear();
        }

        if (errors.isEmpty()) {
            return;
        }

        IOException firstEx = errors.remove(0);

        for (IOException error: errors) {
            firstEx.addSuppressed(error);
        }
        throw firstEx;
    }

}

@jovanstevanovic
Copy link
Member

jovanstevanovic commented Feb 3, 2023

Hey @borkdude, maybe I made a mistake. Could you please then create a GitHub repo with a minimalistic reproducer and a set of commands that I can use locally to reproduce? (native image agent, build and run)

@borkdude
Copy link
Author

borkdude commented Feb 3, 2023

@jovanstevanovic Will do!

@borkdude
Copy link
Author

borkdude commented Feb 3, 2023

@jovanstevanovic The repro is here:

https://github.com/borkdude/native-image-1956-repro

You can run the repro with ./repro and also view it running in Github actions:

https://github.com/borkdude/native-image-1956-repro/actions/runs/4084260102/jobs/7040753788

Note that the output is:

JIT:
getResourceAsStream: java.io.BufferedInputStream@24d46ca6
getResource: file:/home/runner/work/native-image-1956-repro/native-image-1956-repro/resources/my_file.txt
findResource: file:/home/runner/work/native-image-1956-repro/native-image-1956-repro/resources/my_file.txt
getResourceS: [file:/home/runner/work/native-image-1956-repro/native-image-1956-repro/resources/my_file.txt]
findResourceS: [file:/home/runner/work/native-image-1956-repro/native-image-1956-repro/resources/my_file.txt]

and with native-image:

getResourceAsStream: null
getResource: null
findResource: file:/home/runner/work/native-image-1956-repro/native-image-1956-repro/resources/my_file.txt
getResourceS: []
findResourceS: [file:/home/runner/work/native-image-1956-repro/native-image-1956-repro/resources/my_file.txt]

Note that findResource* works well, but getResource* doesn't.

@borkdude
Copy link
Author

borkdude commented Feb 4, 2023

@jovanstevanovic Btw, the workaround I have above works perfectly for now and this issue may be an edge case that is pretty niche so if the answer is that I should be using the workaround that I currently have, I would be fine with that (as long as it keeps working in future releases!)

@jovanstevanovic
Copy link
Member

I was on vacation for some time, so I'm back now.

You're right, it is a problem from our side. Here is an internal ticket GR-44424 so that you can track it on master once it is done.

@borkdude
Copy link
Author

Thank you! I'm subscribed to this Github issue but not sure how I can track master without reading the all commit messages on master 🤣

@jovanstevanovic
Copy link
Member

No worries, I'll send you the exact commit. 😄

@jovanstevanovic
Copy link
Member

jovanstevanovic commented May 24, 2023

The fix is on the master.

@borkdude
Copy link
Author

borkdude commented May 24, 2023 via email

@borkdude
Copy link
Author

borkdude commented Oct 12, 2023

@jovanstevanovic I finally got around testing this but it still doesn't work completely. An example:

import java.net.URL;
import java.io.File;
import java.io.InputStream;
import java.io.IOException;

public class ClassLoader {

    public static void main(String [] args) throws IOException {
        String classPath = args[0];
        String toFind = args[1];
        String[] parts = classPath.split(java.io.File.pathSeparator);
        var urls = new java.util.ArrayList<URL>();
        for (String u : parts) {
            urls.add(new File(u).toURL());
        }
        var cp = new java.net.URLClassLoader(urls.toArray(URL[]::new));
        URL r = cp.getResource(toFind);
        System.out.println(r);
    }

    // call with java ClassLoader "$(clojure -Spath)" "clojure/core.clj"

}
$ javac ClassLoader.java
$ java ClassLoader "$(clojure -Spath)" "clojure/core.clj"
jar:file:/Users/borkdude/.m2/repository/org/clojure/clojure/1.12.0-alpha2/clojure-1.12.0-alpha2.jar!/clojure/core.clj

Note that when called through java the URLClassLoader successfully finds a file in a jar file on the classpath.
But when compiled with native-image, the file isn't found:

$ ./classloader "$(clojure -Spath)" "clojure/core.clj"
null

@jovanstevanovic
Copy link
Member

@borkdudeI've focused on a reproducer that you sent me. It's possible that finding it in a JAR is not exercising the same part of the code. Two things:

  • The previous use case is working, right?
  • Can you update the repo with the new use case with a minimal reproducer, so that I can start working on it?

@borkdude
Copy link
Author

Can you update the repo with the new use case with a minimal reproducer, so that I can start working on it?

I didn't quite understand this sentence. Do you want me to create a new Github issue, if so, why did you re-open this one? Or did you want me to create a new repro? Didn't I post this just above?

@jovanstevanovic
Copy link
Member

jovanstevanovic commented Oct 16, 2023

Do you want me to create a new Github issue, if so, why did you re-open this one?

I've reopened this issue already.

Or did you want me to create a new repro? Didn't I post this just above?

  • Your initial reproducer is now working fine. The one for a couple of months ago. I've tested it now.
  • This use case although looks similar is not the same.
  • I've modified the reproducer and added a code from the snippet that you've provided, and it is working again.
  • What I'm suggesting now, use the already existing repo, and create a new minimal reproducer that reflects your current problem, so that I can start working on it.

@jovanstevanovic
Copy link
Member

Here is the example that I've tried: new_reproducer.txt

@borkdude
Copy link
Author

@jovanstevanovic Gotcha!

@jovanstevanovic
Copy link
Member

jovanstevanovic commented Oct 17, 2023

@borkdude I've reproduced it locally. For some reason, NI can handle folders in URLClassLoader but not jars. I'll come back to you once I solve it.

@borkdude
Copy link
Author

Yep, that's what I found too.

@jovanstevanovic
Copy link
Member

@borkdude can you rerun the app with -H:EnableURLProtocols=jar? The test example is working.

@borkdude
Copy link
Author

Confirmed, that was it!

@borkdude
Copy link
Author

Thanks a lot

@olpaw
Copy link
Member

olpaw commented Oct 23, 2023

@borkdude can you rerun the app with -H:EnableURLProtocols=jar? The test example is working.

Please use the API option variant: --enable-url-protocols=jar then you do not need -H:+UnlockExperimentalVMOptions.

@borkdude
Copy link
Author

Thanks @olpaw

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants