java类加载器和jar路径解析
声明:本文转载,原文链接:
java类加载器和jar路径解析 - 简书 https://www.jianshu.com/p/546a7e3dc427
一、类加载器基本原理
虚拟机提供了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)
所以,我们去自定义一个类加载器时,一般都会继承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!/
我们会发现这些路径带有 !/ 这样的符号,这个其实代表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.Handler、sun.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包读取方式读取。
2、当我们运行一个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://www.jianshu.com/p/546a7e3dc427
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。