background

This.getclass ().getClassLoader ().getResource(“”).getPath() This.getclass ().getClassLoader ().getResource(“”)) returns null.

For example,

Imagine how we normally launch a Java application.

  • The IDE starts with the main method
  • Throw the project into a war package on the server, such as Tomcat, Jetty, etc
  • Start directly with the fat-jar method.
  • Boot by spring-boot.

It is worth mentioning that spring-boot and fat-jar are both launched by java-jar your.jar The LaunchedURLClassLoader in boot has been redefined to allow arbitrary loading of nested jars, while fat-Jars currently implement classloader simply. Here we mainly use two typical examples of startup via IDEmain and startup via fat-jar

Start from the IDE Main method

package com.example.test;

import java.net.URL;

/ * * *@author lican
 */
public class FooTest {

    public static void main(String[] args) {
        ClassLoader classLoader = FooTest.class.getClassLoader();
        System.out.println(classLoader);
        URL resource = classLoader.getResource(""); System.out.println(resource); }}Copy the code

The results of

sun.misc.Launcher$AppClassLoader@18b4aac2
file:/Users/lican/git/test/target/test-classes/
Copy the code

Start from fat-jar

package com.test.fastjar.fatjartest;


import java.net.URL;

public class FatJarTestApplication {

    public static void main(String[] args) throws Exception {
        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
        System.out.println(contextClassLoader);
        URL resource = contextClassLoader.getResource(""); System.out.println(resource); }}Copy the code

Package with MVN Clean install-dskipTests and start from the command line

Java jar target/fat - the jar - test - 0.0.1 - the SNAPSHOT - jar - with - dependencies. The jarCopy the code

Execution Result:

jdk.internal.loader.ClassLoaders$AppClassLoader@4f8e5cde
null
Copy the code

Classloader.getresource (“”) does not get the root path of the project execution as desired in some cases. What is the reason? Is there a universal way to avoid these problems? B: of course.

Analysis of the

First let’s take a look at the JDK about this section of the source code may be more clear. We call getResource(“”) first to java.lang.classLoader #getResource

    public URL getResource(String name) {
        URL url;
        if(parent ! =null) {
            url = parent.getResource(name);
        } else {
            url = getBootstrapResource(name);
        }
        if (url == null) {
            url = findResource(name);
        }
        return url;
    }
Copy the code

If we use main, the current classloader is AppClassloader,parent is ExtClassloader, If neither parent nor bootstrapResource can find the corresponding resource (via debug), the return value must be from findResource(name).

But the getResource method does

  protected URL findResource(String name) {
        return null;
    }
Copy the code

Obviously overridden by a subclass, so let’s look at the subclass that implements AppClassloader, and focus on that because AppClassloader inherits from URLClassloader

Here is an implementation of java.net.URLClassLoader#findResource

 public URL findResource(final String name) {
        /* * The same restriction to finding classes applies to resources */
        URL url = AccessController.doPrivileged(
            new PrivilegedAction<URL>() {
                public URL run(a) {
                    return ucp.findResource(name, true);
                }
            }, acc);

        returnurl ! =null ? ucp.checkURL(url) : null;
    }
Copy the code

Ucp.findresource (name, true); Locate the resource in sun.misc.URLClassPath#findResource

 public URL findResource(String name, boolean check) {
        Loader loader;
        int[] cache = getLookupCache(name);
        for (int i = 0; (loader = getNextLoader(cache, i)) ! =null; i++) {
            URL url = loader.findResource(name, check);
            if(url ! =null) {
                returnurl; }}return null;
    }
Copy the code

URL = loader.findResource(name, check); But what is this loader? Where is it loaded from the name that we’re looking for?

Loader is sun URLClassPath inside of a static inner class. The misc. URLClassPath. Loader has a total of two subclasses

FileLoader is the loader that loads files and JarLoader is the loader that loads JAR packages. The final findResource will find the respective Loader’s findResource to look up. Before analyzing these two loaders, let’s first look at how these two loaders are generated. sun.misc.URLClassPath#getLoader(java.net.URL)

/* * Returns the Loader for the specified base URL. */
    private Loader getLoader(final URL url) throws IOException {
        try {
            return java.security.AccessController.doPrivileged(
                new java.security.PrivilegedExceptionAction<Loader>() {
                public Loader run(a) throws IOException {
                    String file = url.getFile();
                    if(file ! =null && file.endsWith("/")) {
                        if ("file".equals(url.getProtocol())) {
                            return new FileLoader(url);
                        } else {
                            return newLoader(url); }}else {
                        return new JarLoader(url, jarHandler, lmap, acc);
                    }
                }
            }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw(IOException)pae.getException(); }}Copy the code

It should be noted that the parameter URL is popped out of the classpath, repeating the pop until the query is complete. Then we run the main method in the IDE, he is one of the classpath of file: / Users/lican/git/test/target/test – classes/and run in a jar package, One of the classpath is a jar package to run, for example /Users/lican/git/fat-jar-test/target/fat-jar-test-0.0.1- snapshot-jar-with-dependencies FileLoader, one goes to JarLoader, and the final reason is to locate the difference between the two loaders’ getResource.

FileLoader#getResource()

Resource getResource(final String name, boolean check) {
            final URL url;
            try {
                URL normalizedBase = new URL(getBaseURL(), ".");
                url = new URL(getBaseURL(), ParseUtil.encodePath(name, false));

                if (url.getFile().startsWith(normalizedBase.getFile()) == false) {
                    // requested resource had .. /.. 's in path
                    return null;
                }

                if (check)
                    URLClassPath.check(url);

                final File file;
                if (name.indexOf("..") != -1) {
                    file = (new File(dir, name.replace('/', File.separatorChar)))
                          .getCanonicalFile();
                    if ( !((file.getPath()).startsWith(dir.getPath())) ) {
                        /* outside of base dir */
                        return null; }}else {
                    file = new File(dir, name.replace('/', File.separatorChar));
                }

                if (file.exists()) {
                    return new Resource() {
                        public String getName(a) { return name; };
                        public URL getURL(a) { return url; };
                        public URL getCodeSourceURL(a) { return getBaseURL(); };
                        public InputStream getInputStream(a) throws IOException
                            { return new FileInputStream(file); };
                        public int getContentLength(a) throws IOException
                            { return (int)file.length(); }; }; }}catch (Exception e) {
                return null;
            }
            return null;
        }
Copy the code

Here, dir’s incoming classpath: file: / Users/lican/git/test/target/test – classes/so in the line of the file = new file (dir, name. The replace (‘/’, File.separatorChar)); Even if it is an empty string (“”), file exists because it is a directory, so the following exists checks and returns the url of this folder. So you get the root directory.

JarLoader#getResource()

 /* * Returns the JAR Resource for the specified name. */
        Resource getResource(final String name, boolean check) {
            if(metaIndex ! =null) {
                if(! metaIndex.mayContain(name)) {return null; }}try {
                ensureOpen();
            } catch (IOException e) {
                throw new InternalError(e);
            }
            final JarEntry entry = jar.getJarEntry(name);
            if(entry ! =null)
                return checkResource(name, check, entry);

            if (index == null)
                return null;

            HashSet<String> visited = new HashSet<String>();
            return getResource(name, check, visited);
        }
Copy the code

Final JarEntry entry = jare.getJarentry (name); I’m not going to get it, I’m going to return null, and I’m going to go down to return getResource(name, check, visited); Let’s look at the implementation here.

 Resource getResource(final String name, boolean check,
                             Set<String> visited) {

            Resource res;
            String[] jarFiles;
            int count = 0;
            LinkedList<String> jarFilesList = null;

            /* If there no jar files in the index that can potential contain * this resource then return immediately. */
            if((jarFilesList = index.get(name)) == null)
                return null;

            do{...Copy the code

If ((jarFilesList = index.get(name)) == null) this step will always be null. So using the jar package to retrieve the path always returns NULL.

This.getclass ().getClassLoader ().getResource(“”)

The solution

Make final JarEntry entry = jar.getJarentry (name); The return is not empty so we can get the path, so we’re using a workaround here. Implement the following, can get the path in any case, such as the current tool class is InstanceInfoUtils, then

private static String getRuntimePath(a) {
        String classPath = InstanceInfoUtils.class.getName().replaceAll("\ \."."/") + ".class";
        URL resource = InstanceInfoUtils.class.getClassLoader().getResource(classPath);
        if (resource == null) {
            return null;
        }
        String urlString = resource.toString();
        int insidePathIndex = urlString.indexOf('! ');
        boolean isInJar = insidePathIndex > -1;
        if (isInJar) {
            urlString = urlString.substring(urlString.indexOf("file:"), insidePathIndex);
            return urlString;
        }
        return urlString.substring(urlString.indexOf("file:"), urlString.length() - classPath.length());
    }
Copy the code

Verify the fat-JAR example above, and return the result

File: / Users/lican/git/fat - the jar - test/target/fat - the jar - test - 0.0.1 - the SNAPSHOT - jar - with - dependencies. The jarCopy the code

Meets expectations.

other

Why is Spring Boot available? Spring Boot customizes a number of things to deal with these complex situations, which will be explained in more detail later, in a nutshell

  • Spring Boot registers a Handler to handle urls for the “JAR:” protocol
  • Spring Boot extends JarFile and JarURLConnection to handle jar in JAR cases internally
  • When processing multiple JAR in JAR urls, Spring Boot loops and caches the JarFile that has been loaded
  • For multiple jars in jars, it is actually decompressed to a temporary directory for processing. See the code in JarFileArchive
  • When you get the InputStream of the URL, you get JarEntryData in JarFile