你是否遇到过这样的困境:这个问题线上才能出现,可是线上不支持debug,并且怀疑是引入的jar引起的,不能加log怎么办?亦或者你觉得引入的jar的某个方法有bug,应该这么写才对,如何才能去证实呢。
不知道你是如何解决上诉问题的,笔者的方案就是在项目创建一个一模一样的需要debug或者修改的Class(package+class name都一样),然后把jar中的代码copy该class,我们可以在copy的这个class做任何代码修改。这样当项目中需要该Class时,调用的是我们copy的class,并不会是jar中的class。
为什么上述修改可以实现我们想要的功能呢?这就要从Java中的类加载器的工作原理。
类加载器
类加载器分类
类加载器主要分成两类,一类是系统提供的,另外一类是由Java应用开发人员编写的。可以具体分为4种,引导类加载器(Boostrap ClassLoader)、扩展类加载器(Extension ClassLoader)、系统类加载器(App ClassLoader)、自定义类加载器(Custom ClassLoader)。
-
引导类加载器:用来加载Java的核心类库。这个加载器主要是加载<JAVA_HOME>/lib目录下的class,也可以通过JVM参数-Xbootclasspath去指定,但是必须是JVM能够识别的文件。
-
扩展类加载器:用来加载Java的扩展类库,这个类加载器使用C语言实现。它主要是加载<JAVA_HOME>\lib\ext目录下的class,也可以通过java.ext.dirs这个系统变量改变所指定的类库。
-
系统类加载器:系统类加载器的加载路径是Java应用的类路径(CLASSPATH),也可以通过java.class.path这个系统变量指定加载路径。也就是说在没有自定义加载器的情况下,Java应用的类都是由系统类加载器加载的。而且该加载器可以用Classloader类的getSystemClassloader( )方法直接获取到。
-
自定义类加载器:在很多场景下,类并步子上面的路径上,我们就可以通过自定义类加载器去加载class,比如从网络中加载,比如class被加密,那么也需要自定义类加载器。
双亲委派模型
类加载器通过一个叫双亲委派模式进行加载。
双亲委派是如何实现的?
实现类加载器必须要继承ClassLoader,该类中有个loadClass方法,调用者就是通过调用该方法加载Class,其具体的代码如下:
方法的执行需要一下几个步骤:
-
调用findLoadedClass方法检查这个类是否已经加载过了。
-
调用其加载器的loadClass,若其父加载器没有,则使用BoostrapClassLoader去执行类加载。
-
若父类没有加载到,则调用findClass去加载类。
一般来说我们只需要实现findClass即可。
ClassLoader这个抽象类有两个构造方法,一个含有parent参数,一个不含任何参数。从代码可以看出若是不传参数默认使用的是App ClassLoader。
从上面的执行步骤我们可以发现,在实现ClassLoader中很容易破坏双亲委托,第一、可以把parent传递为null,第二、重新实现loadClass方法。所以说双亲委托模式只是Java设计者推荐给开发者实现类加载器的方式,并不是一个强制性的约束模型。
四类类加载器加载器的层级关系如下:
系统默认的类加载器如何工作的?
系统默认类加载器是AppClassLoader,它属于Launcher中的内部类。他继承了URLClassLoader,URLClassLoader根据在构造函数中传递过来的文件和目录,顺序查找Class。AppClassLoader具体的代码如下:
从代码中很明显看出AppClassLoader的parent传递的确实是ExtClassLoader。代码中也显示AppClassLoader主要用于加载“java.class.path”(默认值就是CLASSPATH) 这个系统变量中的Class。可以使用“jinfo -sysprop pid” 来查看系统配置,显示截图如下:
从截图中很明显看出,CLASSPATH主要包含两部分,一个是WEB-INF/classes(这个主要是项目自己开发的类)下的Class,另外一部分就是WEB-INF/lib/(这个目录下都是项目依赖的jar)目录下的jar。上面已经说过URLClassLoader是根据传递的文件的顺序查找类,那么classes/目录下的类就会优先比lib/加载,也就是说我们在项目中定义的Class会优先被加载。截图似乎显示lib/下的jar是按照字母顺序来的,实际上按照linux下的inode,但是基本上可以认为是按照字母顺序来的。
总结
本文只是简单的介绍了利用系统默认的类加载器工作机制来解决需要在引入的jar中加log或者修改实现。但是其实可以利用自定义类加载器做很多事情,比如Tomcat使用自定义加载器去加载/common、/server和/shared等目录下的jar;Spring Boot把所有jar都压缩成一个jar,使用自定义加载器是解压该jar,得到WEB-INF/classes和WEB-INF/lib/下所有的class。