ClassLoader在项目中的应用
项目背景
数据资产需要引入kafka数据源,将kafka的topic当成一张表进行资产权限管理,同时给下游的实时flinksql使用。这里当用户的topic中的数据是以DTO的方式进行反序列化时,需要用户传一个jar包将数据传给服务器,里面包含了反序列化的DTO类。
目标:
加载用户的Jar包,可以使用原生KafkaClient进行反序列化,且Jar不会发生冲突。
为了实现目标,我们明显需要使用Java的ClassLoader,同时这个ClassLoader还需要实现反双亲委派。
ClassLoader双亲委派的作用
双亲委派相信大家都知道是干啥的,简而言之就是类加载器在加载类的时候会把类先丢给父类去加载,只有父类加载不了的时候才会由子类加载。
应用举例:maven依赖中发生了依赖冲突,实际上只会有一个版本的包会被加载。
优势:
1、避免类的重复加载
2、保护程序的安全,放置核心API被随意篡改
为什么要反双亲委派
反双亲委派,就是先加载自己的类,自己加载不了再从父类加载
举例1:用户传的Jar包中需要使用的日志的实现类版本为V3.0,我们自己的系统版本为V1.0,按照双亲委派的原则,V1.0的日志实现类肯定已经在我们的系统中先加载了,这个时候用户传的Jar包使用就可能会出问题(明明需要使用V3.0的,却只能使用V1.0的日志)
举例2:我们系统要兼容用户使用2.X和1.X版本的KafkaClient(或者其他的Client),这两种Client的包名啥的全都一样,按照双亲委派的规范肯定只能加载一个。
自定义ClassLoader的使用
理解ClassLoader中的方法
要理解ClassLoader如何实现双亲委派的,我们实际上只需要了解类中四个方法即可
1.findClass(String name)
查找并加载具有指定名称的类
2.loadClass(String name)
加载Class,它和findClass都可以根据全类名加载类,有什么区别呢?
区别就是,loadClass是实际上保证双亲委派规则的类,正常情况下,所有继承了ClassLoader的类都不会重写这个方法(有一些会增加一些安全校验),当需要反双亲委派时才需要重写。loadClass中会调用findClass,如果不同的自定义类加载器需要不同的加载方式时,重写findClass即可。
3.findLoadedClass(String name)
调用这个方法,查看这个Class是否已经别加载
4.definclass(String name, byte[] b, int off, int len)
把字节码转化为Class
(看代码,先看ClassLoader的loadClass方法,再看我自定义的JarByteLoader,讲述一个类加载的全部过程)
通过URLClassLoader进行自定义
在java.net包中,JDK提供了一个更加易用的类加载器URLClassLoader,它扩展了ClassLoader,能够从本地或者网络上指定的位置加载类。我们可以使用该类作为自定义的类加载器使用。
构造方法:
public URLClassLoader(URL[] urls):指定要加载的类所在的URL地址,父类加载器默认为系统类加载器。
public URLClassLoader(URL[] urls, ClassLoader parent):指定要加载的类所在的URL地址,并指定父类加载器。
案例1:加载磁盘上的类
public static void main(String[] args) throws Exception{
File file = new File("d:/");
URI uri = file.toURI();
URL url = uri.toURL();
URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
System.out.println(classLoader.getParent());
Class aClass = classLoader.loadClass("com.leon.Demo");
Object obj = aClass.newInstance();
}
案例2:加载网络上的类
public static void main(String[] args) throws Exception{
URL url = new URL("http://localhost:8080/examples/");
URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
System.out.println(classLoader.getParent());
Class aClass = classLoader.loadClass("com.leon.Demo");
aClass.newInstance();
}
案例3:快速实现反双亲委派
public class MyClassLoader extends URLClassLoader {
public MyClassLoader(File file) throws MalformedURLException {
super(new URL[] {file.toURI().toURL()});
}
// 修改加载顺序,优先自己加载,加载不了再让双新加载
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> c = null;
try {
c = findClass(name);
} catch (Exception e) {
}
return c == null ? c = super.loadClass(name, resolve) : c;
}
}
如何继承AppClassLoader
AppClassLoader就是JDK的Launcher中用来加载CLASSPATH下的Jar包中的类,正常情况下我们都应该继承这个类(毕竟它作用域中的类最全了)。但是我们实际继承的时候时指定不了这个类的。
实际上,我们如果自定义一个类加载器,如果继承了ClassLoader,则默认设置的父类加载器是AppClassLoader,这个原因是在因为在初始化自定义类加载器的时候,会指定其parentClassLoader为AppClassLoader 。
Launcher这个类是jre中用来启动main()方法的入口,在这个类中,我们着重关注的是初始化构造方法。
public Launcher() {
Launcher.ExtClassLoader var1;
try {
// 初始化extClassLoader
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
// 初始化appClassLoader,这里的这个赋值是比较重要的
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
//线程上下文设置
Thread.currentThread().setContextClassLoader(this.loader);
...
}
这个构造方法中,我目前所了解到的,就是初始化了AppClassLoader和ExtClassLoader,并且,我们只需要关心this.loader这个全局变量,这个全局变量存放的是AppClassLoader的对象信息。
而自定义的类解析器对应的parentClassLoader,就是在空参构造函数中被赋值的。因为MyClassLoaderTest继承了ClassLoader,所以,会调用到ClassLoader的空参构造函数 。
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
if (ParallelLoaders.isRegistered(this.getClass())) {
parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
domains =
Collections.synchronizedSet(new HashSet<ProtectionDomain>());
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
domains = new HashSet<>();
assertionLock = this;
}
}
我们会发现,parentClassLoader就是getSystemClassLoader()返回的
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
if (scl == null) {
return null;
}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}
// 这个方法中的其他变量我们可以暂时先不关心,我们看到有获取到一个Launcher对象
private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
throw new IllegalStateException("recursive invocation");
//获取Launcher!!!!
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
// 在这里调用其getClassLoader()方法,将返回的值,赋值给scl,而这个scl就是入参中的parent
scl = l.getClassLoader();
try {
scl = AccessController.doPrivileged(
new SystemClassLoaderAction(scl));
} catch (PrivilegedActionException pae) {
oops = pae.getCause();
if (oops instanceof InvocationTargetException) {
oops = oops.getCause();
}
}
if (oops != null) {
if (oops instanceof Error) {
throw (Error) oops;
} else {
// wrap the exception
throw new Error(oops);
}
}
}
sclSet = true;
}
}
所以,通过上面的代码,可以发现,我们自定义的类解析器,是在初始化的时候,指定了parent为AppClassLoader
Class.forName()报错
在KafkaClient实际调用我们自定义类加载器加载的类的时候,会报错。
原因:
kafkaClient是通过Class.forName()+线程上下文加载器对指定全类名进行加载的!
Class.forName和ClassLoader.loadClass都可以用来进行类型加载,而在Java进行类型加载的时刻,一般会有多个ClassLoader可以使用,并可以使用多种方式可以获取类加载器。
CurrentClassLoader,称之为当前类加载器,简称CCL,一般可以通过一些native方法拿到,在代码中对应的就是(Class<?> caller = Reflection.getCallerClass())。
SpecificClassLoader,指定类加载器,简称SCL,一般就是A.getClassLoader()。
ThreadContextClassLoader,称之为线程上下文类加载器,简称TCCL,每个线程都会拥有一个ClassLoader引用,而且可以通过**Thread.currentThread().setContextClassLoader(ClassLoader classLoader)**进行切换。
详细说明一下Class.forName()加载器
看Demo --classForNameDemo。
反双亲委派还会Jar包冲突吗
反双亲委派,明明加载顺序是,先加载自己的, 自己作用域没有再加载父类的,还有可能Jar包冲突吗?
报错含义: LoggerFactory中的getILoggerFactory()的方法的返回声明对象的ClassLoader和调用方法StaticLoggerBinder.getSingleton().getLoggerFactory()的ClassLoader不一致。
原因:StaticLoggerBinder没有打到包里面,则使用了AppClassLoader来加载,而LoggerFactory是我们自定义ClassLoader加载的。打包的时候provide了部分,比如一些spi上下游依赖的包,就可能出现上述情况。
看DEMO—classDeclaredMethod()
Tomcat的类加载机制
ExtClassLoader → Bootstrap ClassLoader→WebAppClassLoader→ AppClassLoader
这样的加载顺序有什么好处?
Tomcat要解决什么问题?
作为一个Web容器,Tomcat要解决什么问题 , Tomcat 如果使用默认的双亲委派类加载机制能不能行?
1.我们知道Tomcat可以部署多个应用,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离 。举个例子 假设APP1 使用的是 Spring4 , APP2 使用的是Spring5 , 毫无疑问 Spring4 和 Spring 5 肯定有 类的全路径一样的类吧,如果使用双亲委派 ,父加载器加载谁?
2.web容器 自己依赖的类库 (tomcat lib目录下),不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
3.部署在同一个web容器中相同的类库相同的版本可以共享, 比如jdk的核心jar包,否则,如果服务器有n个应用程序,那么要有n份相同的类库加载进虚拟机。
反双亲委派步骤
-
先在本地cache查找该类是否已经加载过,看看 Tomcat 有没有加载过这个类。
-
如果Tomcat 没有加载过这个类,则从系统类加载器的cache中查找是否加载过。
-
如果没有加载过这个类,尝试用ExtClassLoader类加载器类加载,重点来了,这里并没有首先使用 AppClassLoader 来加载类。这个Tomcat 的 WebAPPClassLoader 违背了双亲委派机制,直接使用了 ExtClassLoader来加载类。这里注意 ExtClassLoader 双亲委派依然有效,ExtClassLoader 就会使用 Bootstrap ClassLoader 来对类进行加载,保证了 Jre 里面的核心类不会被重复加载。 比如在 Web 中加载一个 Object 类。**ExtClassLoader → Bootstrap ClassLoader→WebAppClassLoader→ AppClassLoader **,这个加载链,就保证了 Object 不会被重复加载。
-
如果 BoostrapClassLoader,没有加载成功,就会调用自己的 findClass 方法由自己来对类进行加载,findClass 加载类的地址是自己本 web 应用下的 class。
-
加载依然失败,才使用 AppClassLoader 继续加载。
-
都没有加载成功的话,抛出异常。
源码如何实现的:
总结一下以上步骤,WebAppClassLoader 加载类的时候,故意打破了JVM 双亲委派机制,绕开了 AppClassLoader,直接先使用 ExtClassLoader 来加载类,目的就是不重复加载JAVA基础类。
Jsp热加载如何实现的
web容器要支持jsp的修改, jsp 文件最终也是要编译成class文件才能在虚拟机中运行, web容器需要支持 jsp 修改后不用重启 ,就是热加载的功能。
要怎么实现jsp文件的热加载呢? jsp 文件其实也就是class文件,是调用 Compiler.compile() 重新把 jsp 转换成 servlet,并编译 servlet 成 class 文件。那么如果 jsp修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?可以直接卸载掉这jsp文件的类加载器 .当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。
源码详见: org.apache.jasper.servlet.JasperLoader