java类加载器和jar路径解析

一、类加载器基本原理

未命名表单2.jpg

虚拟机提供了3种类加载器:Bootstrap类加载器、Ext类加载器、App类加载器。他们之间通过双亲委派模式进行类的加载

Bootstrap类加载器:主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 {jdk}/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。

Ext类加载器:是指sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载{jdk}/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。

App类加载器:sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器。

BootStrap 是最顶级类加载器,Ext持有BootStrap引用,App持有Ext引用。当去加载一个类时,首先由上级加载器去加载,上级加载器不能加载,才由自己进行加载。(具体可自行搜索 双亲委派模型)

其中ExtClassLaoder、AppClassLoader 都是URLClassLaoder子类(Bootstrap是C++实现的,所以不是它的子类),当我们去定义自己的ClassLoader时,一般去继承URLClassLoader。

ClassLoader

ClassLoader 是所有类加载器的父类,其中主要有三个方法:loadClass(加载一个class)、findClass(找到class文件所在磁盘的位置(也可以是网络流))、defineClass(将class转载到jvm内存)
当去加载一个类时,会通过loadClass去加载,loadClass主要逻辑如下:

// 代码只保留了核心逻辑
protected Class<?> loadClass(String name, boolean resolve) {
    Class<?> c = findLoadedClass(name);  //判断有没有加载过
    if (c == null) {
        if (parent != null) {
            c = parent.loadClass(name, false);   //首先父加载器加载
        }
        if (c == null) {
            c = findClass(name);    //找到该class并装在在内存中
        }
    }
    return c;
}

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

URLClassLoader

URLClassLoader 继承了ClassLoader,其主要实现的功能,就是通过类的全限定名(包名+类名)来定位到class文件的位置。
我们看一下URLClassLoader构造方法

URLClassLoader(URL[] urls, ClassLoader parent) 
URLClassLoader(URL[] urls, ClassLoader parent,AccessControlContext acc) 
public URLClassLoader(URL[] urls)
URLClassLoader(URL[] urls, AccessControlContext acc)

构造函数,都包含URL[] 这个参数。其实这个参数就代表 类所在的路径(可以是:文件路径、网络流、jar路径。)这样当去加载一个类时,就通过这些路径去寻找。
所以,我们去自定义一个类加载器时,一般都会继承URLClassLoader,这样我们把类所在的路径URL传递给URLClassLoader,urlClassLoader就会帮我们在路径寻找并加载类,不用我们过问其中的逻辑了。
URLClassLoader 对findClass进行了重写,主要逻辑如下

protected Class<?> findClass(final String name) {
    final Class<?> result;
    String path = name.replace('.', '/').concat(".class");  //name代表类的全限定名
    Resource res = ucp.getResource(path, false);   //ucp就是对 URL[] 封装,在URL[] 路径列表里查找要装载的类
    if (res != null) {
        try {
            return defineClass(name, res);  //将类装在jvm内存
        } catch (IOException e) {
            throw new ClassNotFoundException(name, e);
        }
    } else {
        return null;
    }

    if (result == null) {
        throw new ClassNotFoundException(name);
    }
    return result;
}

可以看到URLClassLoader实现了:在路径查找class文件,并装载到内存中。
下面我们演示一下

示例

例1:

public class TestClass {
    public static void main(String[] args) {
        TestClass testClass = new TestClass();
        ClassLoader classLoader = testClass.getClass().getClassLoader();
        URL[] urls = ((URLClassLoader) classLoader).getURLs();
        for(URL url :urls) {
            System.out.println(url);
        }
    }
}

通过上面我们知道,我们运行代码默认为AppClassLoader,也就是一个URLClassLoader,我们把其中的路径打印出来,结果如下:

file:/Library/Java/JavaVirtualMachines/jdk1.8.0_65.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_65.jdk/Contents/Home/jre/lib/deploy.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_65.jdk/Contents/Home/jre/lib/ext/cldrdata.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_65.jdk/Contents/Home/jre/lib/ext/dnsns.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_65.jdk/Contents/Home/jre/lib/ext/jaccess.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_65.jdk/Contents/Home/jre/lib/ext/jfxrt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_65.jdk/Contents/Home/jre/lib/ext/localedata.jar
.......

可以看到,都是我们classPath下面的jar包。
例2:
我们把这段测试代码放到springBoot项目中,然后打成一个jar包,进行运行。上面代码会得到下面的输出:

jar:file:/Users/yt/test/spring-boot-test.jar!/BOOT-INF/classes!/
jar:file:/Users/yt/test/spring-boot-test.jar!/BOOT-INF/lib/api-core-0.0.4-SNAPSHOT.jar!/
jar:file:/Users/yt/test/spring-boot-test.jar!/BOOT-INF/lib/raptor-es-common-1.0.3-SNAPSHOT.jar!/
jar:file:/Users/yt/test/spring-boot-test.jar!/BOOT-INF/lib/httpclient-4.5.7.jar!/
jar:file:/Users/yt/test/spring-boot-test.jar!/BOOT-INF/lib/httpmime-4.5.7.jar!/
jar:file:/Users/yt/test/spring-boot-test.jar!/BOOT-INF/lib/httpcore-4.4.11.jar!/

这些jar包,都是我们项目中引入的第三方jar包。我们可以看到这些jar包路径被传入到classloader中,供classloader加载类时,进行路径搜索。
我们会发现这些路径带有 !/ 这样的符号,这个其实代表java特有的路径符号,表示一个jar文件,这样java去读取的时候,就会使用jar形势进行解压读取。(因为读取jar文件不能像其他文件那样读取,jar其实是一种压缩文件,必须对其解压)

我们现在抛出一个问题:为什么例1中URL形式是file:/Library/Java/JavaVirtualMachines/jdk1.8.0_65.jdk/Contents/Home/jre/lib/charsets.jar 而不是:file:/Library/Java/JavaVirtualMachines/jdk1.8.0_65.jdk/Contents/Home/jre/lib/charsets.jar!/,结尾带上!/。既然!/代表是一个jar文件,jvm会使用jar形势解压读取,那么jar文件就要带有!/, 就像我们在例2的时候,jar以!/结尾。为什么这里的jar没有带有!/

二、jar文件路径解析

URL类解析

URLClassLoader 会通过URL[] 来搜索类所在的位置,我们看一下这个URL的实现,首先看一下构造函数:

 public URL(String spec) throws MalformedURLException {
        this(null, spec);
    }
public URL(URL context, String spec) throws MalformedURLException {
        this(context, spec, null);
    }
public URL(URL context, String spec, URLStreamHandler handler) {
        protocol = getProto(spec);  //解析出:前面的字符,作为该协议
        this.handler = getURLStreamHandler(protocol)  //获取该协议对应的处理类。负责对该协议进行读写
        this.handler.parseURL(this, spec, start, limit); //校验
    }

我们看一下getURLStreamHandler:

static URLStreamHandler getURLStreamHandler(String protocol) {
        //GetPropertyAction("java.protocol.handler.pkgs", "") 就是获取jvm有没有这个property变量,
       //也就说我们可以自己定义URL协议,自己定义协议处理方式。并把类名 写到jvm property变量中
        packagePrefixList = java.security.AccessController.doPrivileged(
                new sun.security.action.GetPropertyAction("java.protocol.handler.pkgs", "")
        );
        if (packagePrefixList != "") {
            packagePrefixList += "|";
        }

        packagePrefixList += "sun.net.www.protocol";

        StringTokenizer packagePrefixIter =
                new StringTokenizer(packagePrefixList, "|");

        while (handler == null && packagePrefixIter.hasMoreTokens()) {

            String packagePrefix = packagePrefixIter.nextToken().trim();
            try {
                String clsName = packagePrefix + "." + protocol +
                        ".Handler";
                Class<?> cls = null;
                try {
                    cls = Class.forName(clsName);
                } catch (ClassNotFoundException e) {
                    ClassLoader cl = ClassLoader.getSystemClassLoader();
                    if (cl != null) {
                        cls = cl.loadClass(clsName);
                    }
                }
                if (cls != null) {
                    handler =
                            (URLStreamHandler) cls.newInstance();
                }
            } catch (Exception e) {
                // any number of exceptions can get thrown here
            }
        }
        return handler;

    }

通过上面的代码我们可以看出。当我们new URL("jar:file:/yt/test/test.jar"),就会构造一个URL,其中负责和jar文件进行交互的Handler是sun.net.www.protocol.jar.Hnadler(除此之外,还有sun.net.www.protocol.file.Handlersun.net.www.protocol.http.Handler等)当我们对改URL进行读写时,其内部就是用这个Handler进行处理。这样对一个jar文件读取,就是用jar.Handler去处理;对一个http进行读取,就是使用http.Handler处理

this.handler.parseURL(this, spec, start, limit); 这段代码主要是对URL进行校验,对于jar这种协议,会校验字符含有!/,如果缺少会报错。所以我们要这样写new URL("jar:file:/yt/test/test.jar!/") 才不会报错。parseURL主要逻辑如下:

Object var2 = null;
boolean var3 = true;
int var6;
if ((var6 = indexOfBangSlash(var1)) == -1) {
    throw new NullPointerException("no !/ in spec");
} else {
    try {
        String var4 = var1.substring(0, var6 - 1);
        new URL(var4);
        return var1;
    } catch (MalformedURLException var5) {
        throw new NullPointerException("invalid url: " + var1 + " (" + var5 + ")");
    }
}

URLClassLoader

URLClassLoader最重要的功能,就是从URL[]列表中查询到要装在的类所在的路径,就是findClass这个方法

protected Class<?> findClass(final String name) {
        String path = name.replace('.', '/').concat(".class");  //name代表类的全限定名
        Resource res = ucp.getResource(path, false);   //ucp就是对 URL[] 封装,在URL[] 路径列表里查找转载的类
       return defineClass(name, res);  //将类装在jvm内存
}

ucp 就是 URLClassPath对象,我们看一下ucp.getResouce方法. (原方法太复杂,这边对其进行了抽象总结)

 public Enumeration<Resource> getResources(final String var1, final boolean var2) {
       for url: urls{     //urls 就是URLClassLoader那个URL[] 列表,用于搜索类的路径列表
            URLClassPath.Loader  loader = getLoader(url);
            res = loader.getResource(var1, var2);
            if (res != null) retun null;
       }
    //原方法会对这边逻辑进行缓存等高效运算处理
}

private URLClassPath.Loader getLoader(final URL var1) throws IOException {
        String var1x = var1.getFile();
        if (var1x != null && var1x.endsWith("/")) {
            return (URLClassPath.Loader)("file".equals(var1.getProtocol()) ? new URLClassPath.FileLoader(var1) : new URLClassPath.Loader(var1));
        } else {
            return new URLClassPath.JarLoader(var1, URLClassPath.this.jarHandler, URLClassPath.this.lmap);
        }
}

我们看一下URLClassPath.Loader这个内部类,getResource逻辑主要是:判断class是否在该url路径下。
现在我们回到上面的问题:

  1. 当我们运行一个非jar包时,其class路径是这样形势(其实对应AppClassLoader):file:/Library/Java/JavaVirtualMachines/jdk1.8.0_65.jdk/Contents/Home/jre/lib/charsets.jar
    那我们 getLoader的时候,就会走 new URLClassPath.JarLoader()逻辑,可以看到这是一个jarLoader,也就是说他会按jar包读取方式读取。
  1. 当我们运行一个springboot打包的jar时,其class路径是这样的形式(其实对应的是springboot自定义的classloader):jar:file:/Users/yt/test/spring-boot-test.jar!/BOOT-INF/lib/api-core-0.0.4-SNAPSHOT.jar!/
    那我们getLaoder的时候,会走这个逻辑,new URLClassPath.Loader(var1));本身该URL就是jar协议,所以会通过jar协议进行读取。

三、getResource

我们创建一个项目,其目录如下:

src/main/java: TestClass.java
src/main/resouce: /res.txt

public class TestClass {
    public static void main(String[] args) {
        TestClass testClass = new TestClass();
        URL fileURL = testClass.getClass().getResource("/res.txt");
        System.out.println(fileURL.getFile());
    }
}

我们运行这个方法

运行后结果:
/Users/yt/test/res.text
我们对这个项目打成jar包(test.jar),运行后的结果:
/Users/yt/test.jar!/res.text

所以对于jar包里的文件路径,其格式为 jar:file:{path}!/{path}

参考:
https://blog.csdn.net/javazejian/article/details/73413292

https://www.iteye.com/blog/hxraid-483115

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • 1 类加载器 Java 的类加载,就是把字节码格式“.class”文件加载到 JVM的方法区,并在 JVM 的堆区...
    贪睡的企鹅阅读 842评论 0 0
  • 转发:本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 ClassLoader翻译过来就是类加载...
    尼尔君阅读 509评论 0 1
  • 本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 ClassLoader翻译过来就是类加载器,普...
    尼尔君阅读 624评论 1 0
  • 一:ClassLoader 从JVM结构图中可以看到,类加载器的作用是将Java类文件加载到Java虚拟机。 只有...
    阿菜的博客阅读 1,755评论 0 8
  • ClassLoader翻译过来就是类加载器,普通的java开发者其实用到的不多,但对于某些框架开发者来说却非常常见...
    时待吾阅读 1,028评论 0 1