tomcat java.ext.dirs_如果你这样学Java类加载器,100%会

类加载器概述

类加载从JDK1.0就有,最初是为满足Java Applet的需要开发出来的,虽说Java Applet现在早已死翘翘,但是类加载器在别处绽放光彩,如热部署。

类加载器,顾名思义就是加载Java类到虚拟机中,负责读取Java字节码,并转换成java.lang.Class类的一个实例,通过newInstance()方法就可以创建出该类的一个对象,这里的读取可以从本地文件,或者从网络上读取,这个类由java.lang.ClassLoader定义。可以把类加载器比如成咖啡,程序员是字节码,程序员通过咖啡,生产出程序,程序就是java.lang.Class。

通过Class.getClassLoader()方法可以获取加载此类的类加载器。

Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由开发人员编写的。系统提供的类加载器主要有下面三个:

引导类加载器(BootstrapClassLoader):用来加载 Java 的核心库,也就是%JAVA_HOME%/jre/lib目录,用原生代码(C++)写。

扩展类加载器(ExtClassLoader):用来加载 Java 的扩展库,负责加载%JAVA_HOME%/jre/lib/ext下目录中的类库。实现类是sun.misc.Launcher$ExtClassLoader

应用类加载器(ApplicationClassLoader):也称之为系统类加载器,负责加载当前应用classpath路径下的类库,一般情况下,开发人员所编写的类都是由它来完成加载,可以通过 ClassLoader.getSystemClassLoader()来获取它。实现类是sun.misc.Launcher$AppClassLoader

也就是说对于不同的类,Class.getClassLoader()一般在没有其他干扰下,会返回以上三种类加载器,但是要注意的是,返回null不是没有类加载器,而是代表BootstrapClassLoader,并且除了BootstrapClassLoader,其他两种都是继承自ClassLoader。

类加载验证

BootstrapClassLoader

首先验证引导类加载器,可以通过以下代码获取BootstrapClassLoader所加载的目录或者jar。

URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs(); for (URL url : urls) { System.out.println(url.toExternalForm()); }

或者通过属性方式.

String[] split = System.getProperty("sun.boot.class.path").split(":");for (String s : split) { System.out.println(s);}

他输出如下,其中最后一行有个jre/classes目录,这个目录默认不存在,应该是留给用户的。

0d2e8f23112e1262c4d01afc2b0a53bf.png

也就是这些都是由引导类加载器加载,其中就包括了核心类rt.jar。通过System.out.println(String.class.getClassLoader());获取String类的ClassLoader,此时会发现是null,则代表是引导类加载器加载,同样其他jar包中的类一个道理,比如jsse.jar中的SunRsaSign类System.out.println(SunRsaSign.class.getClassLoader());同样会输出null。

如果想让我们的类让他加载,可以指定参数-Xbootclasspath/a: 。

比如HxlClass的包名是com.company,把他带包放入/home/test目录下,并指定参数-Xbootclasspath/a:/home/test,通过Class.forName("com.company.HxlClass")方式加载他,同样输出null。这时候可以通过反射对HxlClass类为所欲为,其实这个可以放在%JAVA_HOME%/jre/classes下,也就不用指定参数,效果也一样。

7e9470ac4f333192bfb53aa28585e246.png

9da8fe7dbe070c80e5ea63436ba86625.pngObject o = Class.forName("com.company.HxlClass").newInstance(); System.out.println(o.toString());

当然在加载jar包的时候,要指明jar的名字,如-Xbootclasspath/a:/home/test/LibJava.jar。

ExtClassLoader

接下来是扩展类加载器,负责加载%JAVA_HOME%/jre/lib/ext下的所有类,通过以下方式可以获取加载的路径。

System.out.println(System.getProperty("java.ext.dirs"));

他输出如下,而另一个路径也是不存在的,需要自己创建。

a16de283b3ae197328e9e50314eafc4a.png

到%JRE_HOME%/jre/lib/ext目录下,有很多扩展包。

2447498bffa9b46b483ecf1f2efece17.png

拿zipfs.jar来说,里面有一个ZipFileSystem类,输出他的加载器的时候是sun.misc.Launcher$ExtClassLoader,则表示他是由扩展类加载器加载。

System.out.println(ZipFileSystem.class.getClassLoader());

同样的操作,如果想让我们的类让他加载,要指定参数-Djava.ext.dirs=,如-Djava.ext.dirs=/home/test,在次使用Class.forName("com.company.HxlClass")加载此类,并获取他的加载器,则会输出sun.misc.Launcher$ExtClassLoader,也可以放入另一个目录下,需要自己创建,也就是上面说的。

Object o = Class.forName("com.company.HxlClass").newInstance(); System.out.println(o.getClass().getClassLoader());

但是当指定-Djava.ext.dirs=/home/test的时候,会发现以前扩展类的路径下的类无法加载。原因是-Djava.ext.dirs有覆盖性,解决办法是指明原来的扩展路径,多个路径用:分割。

7904949c6bd45939ef3f8f02678a1b3d.png

加入原来的扩展目录后再次运行。

25dcb508b7a4fa0f58e85a67d4de2519.png

但是在Idea中运行时,ZipFileSystem是由sun.misc.Launcher$AppClassLoader加载,并没有报错。这是因为idea把扩展类的目录增加到了classpath中,由SystemClassLoader加载了,这是由于双亲委派导致。

SystemClassLoader

主要负责加载classpath所指定位置下的类或者jar,通过以下方式可以获取路径。

System.out.println("---"+System.getProperty("java.class.path"));

一般情况下,我们自己编写的类是由他加载,可以通过-classpath指定路径。当你程序运行抛出ClassNotFoundException时候,可以通过他指明缺少类的路径来解决。

双亲委派

思想是自己不想干,让父亲帮忙干。

类加载器在尝试自己查找某个类的字节代码并加载时,会先委托给他的父类加载器,由父类加载器先去尝试加载,以此类推。如果父亲能加载成功,那就直接返回,如果父亲加载不了,则在向下传递,由子类完成,比如SystemClassLoader尝试加载类的时候,先委托给ExtClassLoader,ExtClassLoader又委托给BootstrapClassLoader,在没有更上一层了,如果BootstrapClassLoader无法加载,那就向下让ExtClassLoader加载,成功则直接返回,ExtClassLoader加载不成功则SystemClassLoader加载,如果SystemClassLoader加载不了,则抛出异常。

用一个例子可以验证,首先在/home/test目录下放一个LibJava.jar,其中有个类是com.company.HxlClass,然后尝试加载他,并输出他的类加载器。

0e65de34ff9a238946100234719ad3af.png

测试代码如下:

public static void main(String[] args) throws ClassNotFoundException{ System.out.println(Class.forName("com.company.HxlClass").getClassLoader()); }

如果不向三个类加载器中某一个指明这个jar路径时,肯定是抛出ClassNotFoundException异常,意味着三个类加载都不知道他的路径,都无法加载。当向其中一个类加载器指明这个路径后,加载com.company.HxlClass的一定是他,如下图,因为只有他知道。

4fada49634636017c6ffe23576aa793f.png

如果三个类加载器加载的路径下都有这个jar路径,则一定是BootstrapClassLoader加载,如下图。因为遵守规则,父亲能干的父亲干。

47be240d0bf761233ddfbc65e6daf6b6.png

判断是否同一个类

Java 虚拟机不仅要看类的全名是否相同,还要看加载这个类的类加载器是否一样,只有都相同的情况下,才认为两个类是相同的,否则即便是同样的字节代码,被不同的类加载器加载之后,会认为是不同的。这个很容易就验证。编写一个Dog类,代码如下。首先创建两个自定义的类加载,并加载同一个类,在利用对象的强制转换,如果转换失败则会抛异常。

public class Dog { public void setDog(Object o){ System.out.println("setDog>>"+o); Dog dog =(Dog)o; }}

测试代码如下。

public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException { class TestClassLoader extends ClassLoader { @Override protected Class> findClass(String name) throws ClassNotFoundException { try { FileChannel channel = new FileInputStream("/home/test/Dog.class").getChannel(); ByteBuffer byteBuffer = ByteBuffer.allocate(2048); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); int size = 0; while ((size = channel.read(byteBuffer)) != -1) {                    byteBuffer.limit();                    byteArrayOutputStream.write(byteBuffer.array(), 0, size); } return defineClass(name, byteArrayOutputStream.toByteArray(), 0, byteArrayOutputStream.size()); } catch (FileNotFoundException e) {                e.printStackTrace(); } catch (IOException e) {                e.printStackTrace(); } throw new ClassNotFoundException(); } } TestClassLoader classLoader1 = new TestClassLoader(); TestClassLoader classLoader2 = new TestClassLoader(); Class> cls1 = classLoader1.loadClass("Dog"); Class> cls2 = classLoader2.loadClass("Dog"); System.out.println(cls1.getClassLoader() +"   "+ cls2.getClassLoader()); Object o1 = cls1.newInstance(); Object o2 = cls2.newInstance(); Method setDog1 = cls1.getDeclaredMethod("setDog", Object.class); System.out.println(setDog1 +"  "+setDog1);    setDog1.invoke(o1,o2);}

运行后,发现他会报ClassCastException异常。

bef2e2bc7456ca03cb7c967aba84bff7.png

自定义类加载器

在上面已经演示了一个自定义类加载器TestClassLoader,要想自定义,首先继承ClassLoader,然后重写findClass方法,返回值是Class对象,通过内部defineClass将class文件的byte[]转换成Class对象,defineClass是java层的方法,最终会调用到defineClass1这个native方法。

下面是完成从网络中读取字节码并加载的NetworkClassLoader。

public class NetworkClassLoader extends ClassLoader { @Override protected Class> findClass(String name) throws ClassNotFoundException { try {            URL url = new URL("http://blog.houxinlin.com/Dog.class"); InputStream inputStream = url.openStream(); byte[] data = new byte[2048]; ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); int size = 0; while ((size = inputStream.read(data)) != -1) {                byteArrayOutputStream.write(data, 0, size); } return defineClass(name, byteArrayOutputStream.toByteArray(), 0, byteArrayOutputStream.size()); } catch (IOException ex) {            ex.printStackTrace(); } throw new ClassNotFoundException(); }}

测试代码。

NetworkClassLoader networkClassLoader =new NetworkClassLoader(); System.out.println(networkClassLoader.loadClass("Dog"));

如果想从jar中加载某个类,可以使用URLClassLoader。

String jarFile ="/home/test/LibJava.jar"; URL url1  =new File(jarFile).toURL(); URLClassLoader myClassLoader = new URLClassLoader(new URL[]{url1}); JarFile file =new JarFile(jarFile); Enumeration entries = file.entries(); while (entries.hasMoreElements()){ JarEntry jarEntry = entries.nextElement(); if (!jarEntry.isDirectory()){ if (jarEntry.getName().endsWith(".class")){ String name =jarEntry.getName().replaceAll("/",".");             name=name.substring(0,name.length()-6); System.out.println(myClassLoader.loadClass(name).newInstance().toString()); } } }

源码分析

这要追随到Launcher类,java的入口。这个类由BootstrapClassLoader加载。其中this.loader作为getClassLoader方法的返回值,也就是说可以通过调用Launcher.getLauncher().getClassLoader()也可以拿到AppClassLoader。

public Launcher() {//扩展类加载器Launcher.ExtClassLoadervar1;try {//实例化扩展类加载器,单例模式var1= Launcher.ExtClassLoader.getExtClassLoader();} catch (IOExceptionvar10) {throw new InternalError("Could not create extension class loader",var10);}try {//实例化AppClassLoader,单例模式,并将AppClassLoader的父加载器设置成ExtClassLoader。this.loader= Launcher.AppClassLoader.getAppClassLoader(var1);} catch (IOExceptionvar9) {throw new InternalError("Could not create application class loader",var9);}//对当前线程设置类加载器Thread.currentThread().setContextClassLoader(this.loader);}

其中在getExtClassLoader调用层中调用到了getExtDirs方法,获取扩展类的目录集合,最后把这个File[]传递到ExtClassLoader构造方法中。

private static File[] getExtDirs() { String var0 = System.getProperty("java.ext.dirs"); File[] var1; if (var0 != null) { StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator); int var3 = var2.countTokens();         var1 = new File[var3]; for(int var4 = 0; var4 

同样的getAppClassLoader也是获取到java.class.path的值,实例化AppClassLoader需要两个参数,一个是java.class.path,一个是父ClassLoader。

616c96ee4442a1a6a5241e24efe835e7.png

接下来是ClassLoader中的loadClass方法,双亲委派也就在这里。调用所有爸爸们的loadClass如果都无法加载,则调用自己的findClass尝试加载。

protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { //如果已经被加载。直接返回 Class> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { //如果存在父ClassLoader,则让父ClassLoader先尝试加载 if (parent != null) {                     c = parent.loadClass(name, false); } else { //不存在,则交给BootstrapClass, //BootstrapClass会调用到findBootstrapClass这个native层方法                     c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } //如果还等于null,意味着各位爸爸们都无法加载,自己来,调用findClass,也就是为什么自定义类加载器要重写findClass if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime();                 c = findClass(name); // this is the defining class loader; record the stats                 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);                 sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);                 sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) {             resolveClass(c); } return c; } }

Tomcat类加载器

在Tomcat中,每个 Web 应用都有一个对应的类加载器,不同的是首先自己去尝试加载某个类,如果找不到则交给父加载器,与上面双亲委派的顺序相反,这是 Java Servlet 规范中的推荐做法。比如,有两个Web应用,都采用了某个类库,一个采用1.0版本,一个采用2.0版本,此时如果采用一个类加载器,那么导致jar覆盖,可能无法启动成功。

Tomcat这样的作法就保证了隔离性,灵活性,和性能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
java -djava.ext.dirs和--class-path是两个Java的命令行选项,都用于指定Java虚拟机运行时查找类文件的路径。 Java的类库是由许多Java类文件组成的,这些类文件需要被Java虚拟机加载并执行。当我们编写Java程序时,我们需要通过某种方式告知Java虚拟机去哪里查找这些类文件。 -djava.ext.dirs选项用于指定Java虚拟机搜索Java扩展(Java Extension)类库的路径。Java扩展类库是指Java虚拟机内置的一些功能库,比如Java Database Connectivity(JDBC)类库,Java Naming and Directory Interface(JNDI)类库等。Java扩展类库通常存放在JRE的ext文件夹中,但是有时候我们可能需要自己定义一些Java扩展类库,那么就需要使用-djava.ext.dirs选项来告知Java虚拟机去哪里查找这些类文件。 --class-path选项用于指定Java虚拟机搜索Java应用程序类库的路径。Java应用程序类库是指我们编写的Java程序所依赖的一些类库,比如在编写Spring框架的应用程序时需要依赖Spring框架的类库。在编写Java应用程序时,我们需要告知Java虚拟机去哪里查找这些类库,那么就需要使用--class-path选项来指定Java应用程序类库的路径。 综上所述,Java -djava.ext.dirs和--class-path选项都用于指定Java虚拟机运行时查找类文件的路径,但是-djava.ext.dirs选项用于指定Java扩展类库的路径,而--class-path选项用于指定Java应用程序类库的路径。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值