【JAVA】类加载机制

目录

一、快速梳理JAVA类加载机制

1、JDK8的类加载体系

2、沙箱保护机制

3、Linking链接过程

二、一个用类加载机制加薪的故事

三、通过类加载器引入外部Jar包

四、自定义类加载器实现Class代码混淆

五、自定义类加载器实现热加载

六、打破双亲委派,实现同类多版本共存

七、使用类加载器能不能不用反射?


一、快速梳理JAVA类加载机制

​ 三句话总结JDK8的类加载机制:

  1. 类缓存:每个类加载器对他加载过的类都有一个缓存。
  2. 双亲委派:向上委托查找,向下委托加载。
  3. 沙箱保护机制:不允许应用程序加载JDK内部的系统类。

1、JDK8的类加载体系

​ 先来一个简单的Demo,看下JDK8的类加载体系:

 

 

public class LoaderDemo {public static String a ="aaa";public static void main(String[] args) throws ClassNotFoundException {// 父子关系 AppClassLoader <- ExtClassLoader <- BootStrap ClassloaderClassLoader cl1 = LoaderDemo.class.getClassLoader();System.out.println("cl1 > " + cl1);System.out.println("parent of cl1 > " + cl1.getParent());// BootStrap Classloader由C++开发,是JVM虚拟机的一部分,本身不是JAVA类。System.out.println("grant parent of cl1 > " + cl1.getParent().getParent());// String,Int等基础类由BootStrap Classloader加载。ClassLoader cl2 = String.class.getClassLoader();System.out.println("cl2 > " + cl2);System.out.println(cl1.loadClass("java.util.List").getClass().getClassLoader());// java指令可以通过增加-verbose:class -verbose:gc 参数在启动时打印出类加载情况// 这些参数来自于 sun.misc.Launcher 源码// BootStrap Classloader,加载java基础类。System.out.println("BootStrap ClassLoader加载目录:" + System.getProperty("sun.boot.class.path"));// Extention Classloader 加载一些扩展类。 可通过-D java.ext.dirs另行指定目录System.out.println("Extention ClassLoader加载目录:" + System.getProperty("java.ext.dirs"));// AppClassLoader 加载CLASSPATH,应用下的Jar包。可通过-D java.class.path另行指定目录System.out.println("AppClassLoader加载目录:" + System.getProperty("java.class.path"));}
}

​ 可以看到JDK8中的两个类加载体系:

image.png

​ 左侧是JDK中实现的类加载器,通过parent属性形成父子关系。应用中自定义的类加载器的parent都是AppClassLoader

​ 右侧是JDK中的类加载器实现类。通过类继承的机制形成体系。未来我们就可以通过继承相关的类实现自定义类加载器。

简而言之,左侧是对象,右侧是类。

​ JDK8中的类加载器都继承于一个统一的抽象类ClassLoader,类加载的核心也在这个父类中。其中,加载类的核心方法如下:

 

 

//类加载器的核心方法
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// 每个类加载起对他加载过的类都有一个缓存,先去缓存中查看有没有加载过Class<?> c = findLoadedClass(name);if (c == null) {】//没有加载过,就走双亲委派,找父类加载器进行加载。long t0 = System.nanoTime();try {if (parent != null) {c = parent.loadClass(name, false);} else {c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {}if (c == null) {long t1 = System.nanoTime();// 父类加载起没有加载过,就自行解析class文件加载。c = findClass(name);sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}//这一段就是加载过程中的链接Linking部分,分为验证、准备,解析三个部分。// 运行时加载类,默认是无法进行链接步骤的。if (resolve) {resolveClass(c);}return c;}}

​ 这个方法就是最为核心的双亲委派机制。并且这个方法是protected声明的,这意味着,这个方法是可以被子类覆盖的。所以,双亲委派机制也是可以被打破的。

​ 当一个类加载器要加载一个类时,整体的过程就是通过双亲委派机制向上委托查找,如果没有查找到,就向下委托加载。整个过程整理如下图:

image.png

2、沙箱保护机制

​ 双亲委派机制有一个最大的作用就是要保护JDK内部的核心类不会被应用覆盖。而为了保护JDK内部的核心类,JAVA在双亲委派的基础上,还加了一层保险。就是ClassLoader中的下面这个方法。

private ProtectionDomain preDefineClass(String name,ProtectionDomain pd){if (!checkName(name))throw new NoClassDefFoundError("IllegalName: " + name);// 不允许加载核心类if ((name != null) && name.startsWith("java.")) {throw new SecurityException("Prohibited package name: " +name.substring(0, name.lastIndexOf('.')));}if (pd == null) {pd = defaultDomain;}if (name != null) checkCerts(name, pd.getCodeSource());return pd;}

 

​ 这个方法会用在JAVA在内部定义一个类之前。这种简单粗暴的处理方式,当然是有很多时代的因素。也因此在JDK中,你可以看到很多javax开头的包。这个奇怪的包名也是跟这个沙箱保护机制有关系的。

3、Linking链接过程

​ 在ClassLoader的loadClass方法中,还有一个不起眼的步骤,resolveClass。这是一个native方法。而其实现的过程称为linking-链接。链接过程的实现功能如下图:

image.png

​ 其中关于半初始化状态就是JDK在处理一个类的static静态属性时,会先给这个属性分配一个默认值,作用是占住内存。然后等连接过程完成后,在后面的初始化阶段,再将静态属性从默认值修改为指定的初始值。

这里注意,static静态的属性,是属于类的,他是在类初始化过程中维护的。而普通的属性是属于对象的,他是在创建对象的过程中维护的。这两个不要搞混了。

对应到class文件当中,一个是方法,一个是方法。

​ 例如参照一下下面这个案例:

 

 

class Apple{static Apple apple = new Apple(10);static double price = 20.00;double totalpay;public Apple (double discount) {System.out.println("===="+price);totalpay = price - discount;}
}
public class PriceTest01 {public static void main(String[] args) {System.out.println(Apple.apple.totalpay);}
}

​ 程序打印出的结果是-10 ,而不是10。 这感觉有点反直觉,为什么呢?就是因为这个半初始化状态。

​ 其中Apple.apple访问了类的静态变量,会触发类的初始化,即加载-》链接-》初始化

​ 当main方法执行构造函数时,price还没有初始化完成,处于链接阶段的准备阶段,其值为默认值0。这时构造函数的price就是0,所以最终打印出来的结果是-10 而不是 10 。

思考问题: 如何让结果打印出正常的10呢?

​ 后面解析的过程有两个核心的概念:符号引用和直接引用。这两个概念了解即可。

​ 如果A类中有一个静态属性,引用了另一个B类。那么在对类进行初始化的过程中,因为A和B这两个类都没有初始化,JVM并不知道A和B这两个类的具体地址。所以这时,在A类中,只能创建一个不知道具体地址的引用,指向B类。这个引用就称为符号引用。而当A类和B类都完成初始化后,JVM自然就需要将这个符号引用转而指向B类具体的内存地址,这个引用就称为直接引用

思考问题:为什么在ClassLoader的这个loadClass方法中,reslove参数只能传个false,而不让传true?

二、一个用类加载机制加薪的故事

​ 故事背景:模拟一个OA系统,每个月需要定时计算大家的工资。

 

 

public class OADemo1 {public static void main(String[] args) throws InterruptedException {Double salary = 15000.00;Double money = 0.00;//模拟不停机状态while (true) {try {money = calSalary(salary);System.out.println("实际到手Money:" + money);}catch(Exception e) {System.out.println("加载出现异常 :"+e.getMessage());}Thread.sleep(5000);}}private static Double calSalary(Double salary) {SalaryCaler caler = new SalaryCaler();return caler.cal(salary);}
}

​ 而具体计算工资的方法,根据面向对象的设计思想,会交由一个单独的SalaryCaler类来处理。

 

 

public class SalaryCaler {public Double cal(Double salary) {return salary;}
}

​ 这时,一个程序员老王,想要给大家都偷偷加一点工资,于是他想到的方法是直接修改OA系统中计算工资的方法,给大家都加点工资。

 

 

public class SalaryCaler {public Double cal(Double salary) {return salary*1.4;}
}

​ 老王偷偷给大家加了工资,但是,经理肯定是不会同意的。于是,程序员与资本家的一个斗智斗勇的故事,拉开了序幕。

三、通过类加载器引入外部Jar包

​ 计算工资的方法都在OA系统里,经理直接在代码仓库就能看到。于是老王就要开始思考,如何让经理看不到OA系统中计算工资的源码。

​ 基础的思路是将计算工资的方法,从OA系统中抽出来,放到另外一个jar包中。然后,就希望OA系统能够从这个jar包中读取SalaryCaler类,这样就可以绕开经理的视线了。

​ 于是,就可以基于JDK提供的URLClassLoader,从jar包当中加载计算类

 

 

public class OADemo2 {public static void main(String[] args) throws Exception {Double salary = 15000.00;Double money = 0.00;URL jarPath = new URL("file:/Users/roykingw/DevCode/ClassLoadDemo/out/artifacts/SalaryCaler_jar/SalaryCaler.jar");URLClassLoader urlClassLoader = new URLClassLoader(new URL[] {jarPath});//模拟不停机状态while (true) {try {money = calSalary(salary,urlClassLoader);System.out.println("实际到手Money:" + money);}catch(Exception e) {e.printStackTrace();System.out.println("加载出现异常 :"+e.getMessage());}Thread.sleep(5000);}}private static Double calSalary(Double salary,ClassLoader classloader) throws Exception {Class<?> clazz = classloader.loadClass("com.roy.oa.SalaryCaler");if(null != clazz) {Object object = clazz.newInstance();return (Double)clazz.getMethod("cal", Double.class).invoke(object, salary);}return -1.00;}
}

​ 拓展思考:  在真实项目中,这个思路有什么用呢?

1、哪些jar包适合放到外部加载?

​ 那些流程比较统一,但是具体实现规则容易经常产生变化的场景。例如:规则引擎、统一审批规则、订单状态规则.....

2、外部jar包可以放到哪些地方?

​ URLClassLoader可以定义URL从远程Web服务器加载Jar包。

​ drools规则引擎实现了从maven仓库远程加载核心规则文件。

四、自定义类加载器实现Class代码混淆

​ 虽然经理在OA系统里看不到SalaryCaler类的源码了,但是通过OA系统的源码最终还是可以找到这个jar包。那么就可以对jar包进行反编译,查看到jar包对应的源码了。所以,老王还需要考虑如何对class文件进行代码混淆,让经理无法反编译出源码。

​ 解决的思路有两个:

  1. 简单一点的,将class文件的后缀改一下,从.class转为.myclass。就像大家把游戏软件改成.txt结尾一样。
  2. 只是修改后缀,那么经理还可以把后缀改回来再反编译。所以稳妥一点的方法,是要改一改class文件当中的二进制内容。

​ JDK只能加载标准的class文件,所以,这一类反常规的思路,JDK就没办法提供帮助了,这时,就需要用自定义的类加载器来解决了。

关于如何实现自定义类加载器,可以查看ClassLoader类开头的注释。里面介绍了如何实现一个NetWorkClassLoader。

​ 于是,老王就可以先定义一个自定义类加载器,实现从.myclass文件中加载类。

 

 

public class SalaryClassLoader extends SecureClassLoader {private String classPath;public SalaryClassLoader(String classPath) {this.classPath = classPath;}@Overrideprotected Class<?> findClass(String fullClassName) throws ClassNotFoundException {//查找.myclass文件String filePath = this.classPath + fullClassName.replace(".", "/").concat(".myclass");int code;try {FileInputStream fis = new FileInputStream(filePath);// fis.read();ByteArrayOutputStream bos = new ByteArrayOutputStream();try {while ((code = fis.read()) != -1) {bos.write(code);}} catch (IOException e) {e.printStackTrace();}//将.myclass文件的二进制内容读到内存byte[] data = bos.toByteArray();bos.close();//调用defineClass方法,将二进制数组转化成一个JVM中的类。return defineClass(fullClassName, data, 0, data.length);} catch (Exception e) {e.printStackTrace();}return null;}
}

​ 然后,在OA系统中通过这个自定义类加载器加载计算工资的SalaryCaler类。

 

 

public class OADemo3 {public static void main(String[] args) throws Exception {Double salary = 15000.00;Double money = 0.00;SalaryClassLoader salaryClassLoader = new SalaryClassLoader("/Users/roykingw/DevCode/ClassLoadDemo/out/production/SalaryCaler/");//模拟不停机状态while (true) {try {money = calSalary(salary,salaryClassLoader);System.out.println("实际到手Money:" + money);}catch(Exception e) {System.out.println("加载出现异常 :"+e.getMessage());System.exit(-1);}Thread.sleep(5000);}}private static Double calSalary(Double salary,ClassLoader classloader) throws Exception {Class<?> clazz = classloader.loadClass("com.roy.oa.SalaryCaler");if(null != clazz) {Object object = clazz.newInstance();return (Double)clazz.getMethod("cal", Double.class).invoke(object, salary);}return -1.00;}
}

这个简单的示例并没有修改class文件的内容,所以,myclass文件,可以通过修改.class文件生成。

​ 这个.myclass文件并没有修改文件的内容。如果要修改内容呢?二进制文件不太好直接编辑,可以使用流的方式做一点修改。

 

 

public class FileTransferTest {public static void main(String[] args) throws Exception {FileInputStream fis = new FileInputStream("/Users/roykingw/DevCode/ClassLoadDemo/out/production/SalaryCaler/com/roy/oa/SalaryCaler.class");File targetFile = new File("/Users/roykingw/DevCode/ClassLoadDemo/out/production/SalaryCaler/com/roy/oa/SalaryCaler.myclass");if(targetFile.exists()) {targetFile.delete();}FileOutputStream fos = new FileOutputStream(targetFile);int code = 0;//在读文件之前,先写一个没有意义的1fos.write(1);while((code = fis.read())!= -1 ) {fos.write(code);}fis.close();fos.close();System.out.println("文件转换完成");}
}

​ 这样就能生成一个简单加密后的.myclass文件了。在class文件的标准内容前面加了一个没用的1。对应的类加载器只需要把这个1忽略掉就可以了。

拓展思考

1、如何进一步提升关键代码的安全性?

​ 我们这个算法太简单了,经理看看类加载器的源码就知道,只要把.myclass文件前面的1去掉,就能拿到原来的class文件内容,从而进行反编译。有没有什么算法,可以让经理推导不出原始的class文件内容呢?

​ 常用的加密算法就派上用场了。MD5、对称加密、非对称加密...

​ 或者是不是能够有更多奇怪的思路,比如将类加载器的class文件也加密呢?通过自定义类加载器A,从一个加密class文件当中加载出一个类加载器B,再用后面这个类加载器B,加载加密过的核心代码。

2、如何在真实项目中用上这种机制?

​ 真实项目当中不会拿class文件直接部署,都是拿jar包进行部署。所以,我们要做的是,在自定义类加载器中,将从硬盘上读取class文件的实现方式,改为从jar包当中读取class文件。这个通过文件流照样很容易实现。

 

 

public class SalaryJARLoader extends SecureClassLoader {private String jarFile;public SalaryJARLoader(String jarFile) {this.jarFile = jarFile;}@Overrideprotected Class<?> findClass(String fullClassName) throws ClassNotFoundException {String classFilepath = fullClassName.replace('.', '/').concat(".class");System.out.println("重新加载类:"+classFilepath);int code;try {// 访问jar包的urlURL jarURL = new URL("jar:file:" + jarFile + "!/" + classFilepath);
//			InputStream is = jarURL.openStream();URLConnection urlConnection = jarURL.openConnection();// 不使用缓存 不然有些操作系统下会出现jar包无法更新的情况urlConnection.setUseCaches(false);InputStream is = urlConnection.getInputStream();ByteArrayOutputStream bos = new ByteArrayOutputStream();while ((code = is.read()) != -1) {bos.write(code);}byte[] data = bos.toByteArray();is.close();bos.close();return defineClass(fullClassName, data, 0, data.length);} catch (Exception e) {e.printStackTrace();System.out.println("加载出现异常 :"+e.getMessage());throw new ClassNotFoundException(e.getMessage());
//			return null;}}
}

​ 那么,对jar包中的class文件如何进行类似的加密操作呢?其实同样的用文件流就可以实现。这个留给大家自行尝试。

五、自定义类加载器实现热加载

​ 老王通过重重考验,终于瞒过了经理。但是这时又遇到一个头疼的情况。总公司需要时不时的核算工资,老王自然想要在总公司核算工资之前将计算工资的方式改回去,避免露馅。然后等总公司核算完成了再改回来。

​ 既然SalaryCaler类都是从jar包当中修改的,那么是不是直接修改jar包就可以了呢?很可惜,老王经过测试后,结果并不是那么令人满意。每次修改jar包后,都需要重启OA系统才能生效。总公司每次来核查工资就要重启一次OA系统,这样岂不是此地无银三百两了?

​ 其实深入分析就很容易找到愿意。SalaryCaler类无法及时更新的根本原因就在于SalaryJARLoader对他加载过的类都保存了一个缓存。只要这个缓存存在,SalaryClassLoader就不会去jar包中加载,而是从缓存当中加载。而这个缓存是在JVM层面实现的,JAVA代码接触不到这个缓存,所以解决的思路自然就只能简单粗暴的连这个SalaryJARLoader也一起重新创建一个了。

 

 

public class OADemo5 {public static void main(String[] args) throws Exception {Double salary = 15000.00;Double money = 0.00;//模拟不停机状态while (true) {try {money = calSalary(salary);System.out.println("实际到手Money:" + money);}catch(Exception e) {System.out.println("加载出现异常 :"+e.getMessage());}Thread.sleep(5000);}}private static Double calSalary(Double salary) throws Exception {SalaryJARLoader salaryClassLoader = new SalaryJARLoader("/Users/roykingw/lib/SalaryCaler.jar");System.out.println(salaryClassLoader.getParent());Class<?> clazz = salaryClassLoader.loadClass("com.roy.oa.SalaryCaler");if(null != clazz) {Object object = clazz.newInstance();return (Double)clazz.getMethod("cal", Double.class).invoke(object, salary);}return -1.00;}
}

​ 通过这种方式,每次都是创建出一个新的SalaryJARLoader对象,那么他的缓存肯定是空的。那么他自然就只能每次都从jar包当中加载类了。于是,老王可以愉快的随时切换jar包,实现热更新了。

拓展思考

1、这个热加载机制看似很好用,为什么在开源项目中没有见过这种用法?

​ 很显然,这种热加载机制需要创建出非常多的ClassLoader对象。而这些不用的ClassLoader对象加载过的缓存对象也会随之成为垃圾。这会让JVM中本来就不大的元数据区带来很大的压力,极大的增加GC线程的压力。

​ 但是在项目开发时,其实是有一些办法可以实现这种类似的热更新机制。例如IDEA中的JRebel插件,还有之前介绍过的Arthas。

2、加载SalaryCaler的时候真的只加载一个类吗?

​ 把SalaryJARLoader加载过的类打印出来,你会发现,在加载SalaryCaler时,其实不光加载了这个类,同时还加载了Double和Object两个类。这两个类哪里来的?这就是JVM实现的懒加载机制。

​ JVM为了提高类加载的速度,并不是在启动时直接把进程当中所有的类一次加载完成,而是在用到的时候才去加载。也就是懒加载。

六、打破双亲委派,实现同类多版本共存

​ 就在老王跟资本家们斗得不亦乐乎的时候,另一个新手程序员小王突然给老王来了个背刺。不知道什么原因,小王突然在OA系统当中也提交了个SalaryCaler类。这时老王突然发现,这个看似没用的SalaryCaler类却突然导致刚刚还挺得意的热加载机制失效了。不管jar包如何更新,OA系统总是只加载小王提交的那个SalaryCaler类。

​ 为什么会出现这种情况呢?这就是因为JDK的双亲委派机制。

​ 自定的SalaryJARLoader的parent属性指向的是JDK内的AppClassLoader。而AppClassLoader会加载OA系统当中的所有代码,当然就包括小王提交的SalaryCaler类。这时,SalaryJARLoader去加载SalaryCaler类时,通过双亲委派,自然加载出来的就是APPClassloader中的SalayrCaler了。

image.png

​ 所以,要保持热加载机制不失效,那就只能对这个双亲委派机制下手了。

​ 下手的逻辑也很简单,我们只需要让这个SalaryCaler类优先从jar包中加载就可以了。

 

 

 public class SalaryJARLoader6 extends SecureClassLoader {private String jarFile;public SalaryJARLoader6(String jarFile) {this.jarFile = jarFile;}@Overridepublic Class<?> loadClass(String name,boolean resolve) throws ClassNotFoundException {//MAC 下会不断加载 Object 类,出现栈溢出的问题.Windows下测试是没有问题的。
//		if(name.startsWith("com.roy")) {
//			return this.findClass(name);
//		}else {
//			return super.loadClass(name);
//		}// 把双亲委派机制反过来,先到子类加载器中加载,加载不到再去父类加载器中加载。Class<?> c = null;synchronized (getClassLoadingLock(name)) {c = findLoadedClass(name);if(c == null){c = findClass(name);if(c == null){c = super.loadClass(name,resolve);}}}return c;}@Overrideprotected Class<?> findClass(String fullClassName) throws ClassNotFoundException {String classFilepath = fullClassName.replace('.', '/').concat(".class");System.out.println("重新加载类:"+classFilepath);int code;try {// 访问jar包的urlURL jarURL = new URL("jar:file:" + jarFile + "!/" + classFilepath);URLConnection urlConnection = jarURL.openConnection();urlConnection.setUseCaches(false);InputStream is = urlConnection.getInputStream();
//			InputStream is = jarURL.openStream();ByteArrayOutputStream bos = new ByteArrayOutputStream();while ((code = is.read()) != -1) {bos.write(code);}byte[] data = bos.toByteArray();is.close();bos.close();return defineClass(fullClassName, data, 0, data.length);} catch (Exception e) {
//			e.printStackTrace();//当前类加载器出现异常,就会通过双亲委派,交由父加载器去加载
//			System.out.println("加载出现异常 :"+e.getMessage());
//			throw new ClassNotFoundException(e.getMessage());return null;}}
}

拓展思考

1、我们可以通过打破双亲委派绕过JDK的沙箱保护机制吗?

​ 显然不能。因为JDK内部的三个类加载器示例的实现是改不了的。只要这三个类加载器的加载改不了,那么JDK中那些核心的类就还是安全的。

​ 其实,这个问题也可以延伸到JDK8往后的版本当中。从JDK9开始,JDK中引入了模块化机制,而内部的类加载器实现也随之做了翻天覆地的改变。每个类加载器不再是单独负责一个工作目录,而是改为分工负责一部分的模块。但是,对于自定义类加载器,JDK还是保留了原有的双亲委派机制。在之后带大家分析JDK17的类加载机制时会看到,虽然JDK17内部的加载机制发生了变化,但是我们这些案例,几乎都可以平滑的转移过去。

还是要注意:是几乎,而不是完全。因为模块化影响的是整个方方面面。但是核心的加载流程,是没有问题的。

2、在真实项目中,有什么样的业务场景需要打破双亲委派呢?

​ 双亲委派机制是非常基础的一个底层体系,很多重要框架都需要进行定制。

​ 例如Tomcat的类加载体系如下:

image.png

tomcat的几个主要类加载器:

  • commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
  • catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
  • sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
  • WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见,比如加载war包里相关的类,每个war包应用都有自己的WebappClassLoader,实现相互隔离,比如不同war包应用引入了不同的spring版本,这样实现就能加载各自的spring版本;
  • Jsp类加载器:针对每个JSP页面创建一个加载器。这个加载器比较轻量级,所以Tomcat还实现了热加载,也就是JSP只要修改了,就创建一个新的加载器,从而实现了JSP页面的热更新。

​ 现在,你可以理解Tomcat为什么要这样设计类加载体系了吗?

​ 另外,如果大家对SpringBoot比较熟悉,那么应该知道SpringBoot实现了一套自己的SPI服务注入机制,例如以下的代码就可以加载出应用当中ApplicationContextInitializer接口下的所有实现类,包括SpringBoot框架内部实现的,以及应用自己实现的。

public class SPITest {public static void main(String[] args) {List<String> names = SpringFactoriesLoader.loadFactoryNames(ApplicationContextInitializer.class, null);names.forEach(System.out::println);System.out.println("==============");List<ApplicationContextInitializer> applicationContextInitializers = SpringFactoriesLoader.loadFactories(ApplicationContextInitializer.class, null);applicationContextInitializers.forEach(System.out::println);}
}

 

​ 这个简单的API里有个很奇怪的地方,loadFacotries方法第二个参数就是要传一个ClassLoader对象。但是明明传个null进去,他也能处理,但是为什么一定要传一个ClassLoader对象呢?直接在API层面去掉这个参数不是更好吗?为什么搞这么麻烦?那么下面的案例或许能够给你一点点启示。

强调!!如果你对SpringBoot暂时还不熟悉,那么请忽略这部分内容。但是请保留这个疑问,留待后面学习SpringBoot框架时验证。

七、使用类加载器能不能不用反射?

​ 对于一般程序员,故事到这也就结束了。接下来的部分,就属于有追求的程序员,继续打磨技术追求真理的过程了。没事找事的无聊时间

如果你觉得接下来的部分有点跟不上,那就不要强行去烧脑了。

​ 老王分析了热加载器失效的原因,其实就是因为在OA应用的多个类加载器中,同时存在了SalaryCaler类的多个版本。

image.png


​ AppClassLoader中的SalaryCaler对象,可以直接new出来,但是SalaryJARLoader中的那个SalaryCaler对象,在之前的例子当中,都只能通过很别扭的反射来使用。同样都是SalaryCaler,就不能让他也像一个正常的类那样使用吗?

​ 于是,老王想到了一个简单粗暴的方式,明明都是SalaryCaler对象,那是不是可以直接做类型转换呢?像这样

 

 

public class OADemo7 {public static void main(String[] args) throws Exception {Double salary = 15000.00;Double money = 0.00;//模拟不停机状态while (true) {SalaryCaler caler = new SalaryCaler();System.out.println("应该到手Money:" + caler.cal(salary));SalaryJARLoader6 salaryJARLoader = new SalaryJARLoader6("/Users/roykingw/lib/SalaryCaler.jar");Class<?> clazz = salaryJARLoader.loadClass("com.roy.oa.SalaryCaler");Object obj = clazz.newInstance();
//					通过反射进行操作,是没有问题的。money=(Double)clazz.getMethod("cal", Double.class).invoke(obj, salary);System.out.println("实际到手Money:" + money);
//          反射太麻烦,能不能进行类型强转?SalaryCaler caler2 = (SalaryCaler)obj;money = caler2.cal(salary);System.out.println("============");Thread.sleep(5000);}}private static Double calSalary(Double salary) throws Exception {SalaryJARLoader6 salaryClassLoader = new SalaryJARLoader6("/Users/roykingw/lib/SalaryCaler.jar");Class<?> clazz = salaryClassLoader.loadClass("com.roy.oa.SalaryCaler");
//        System.out.println(clazz.getClassLoader());
//        System.out.println(clazz.getClassLoader().getParent());if(null != clazz) {Object object = clazz.newInstance();return (Double)clazz.getMethod("cal", Double.class).invoke(object, salary);}return -1.00;}
}

​ 理想很美好,现实很骨感。这样强行的类型转换,只会得到一个让人怀疑人生的异常:

 

 

Exception in thread "main" java.lang.ClassCastException: com.roy.oa.SalaryCaler cannot be cast to com.roy.oa.SalaryCaler

​ 是的。我不能转换成我。那我到底是谁?

​ 有什么办法能够摆脱这个别扭的反射机制呢?这时,JDK提供的SPI扩展机制就开始重新引入眼帘了。

​ JDK提供了一种SPI扩展机制,其核心是通过这个神奇的API ServiceLoader.load(SalaryCalService.class)  就可以查找到某一个接口的全部实现类。应用所需要的,是提供一个配置文件。 这个配置文件需要放在 ${classpath}/META-INF/services这个固定的目录下。然后文件名是传入接口的全类名。而文件的内容则是一行表示一个实现类的全类名。

${classpath}表示JAVA项目的依赖路径,可以放在依赖的jar包当中,也可以放到当前项目下,所以SPI机制是一种非常好的扩展机制。很多开源框架都大量运用SPI机制来保留功能扩展点。最典型的就是大家以后会学习的ShardingSphere。而SpringBoot也是围绕SPI机制提供功能扩展,只不过SpringBoot的SPI机制是自己实现的,而没有用JDK提供的。

如果这些框架你还都不懂。还是那句话,保留这些疑问,在后面学习这些框架时去验证。

​ 而这个大家司空见惯的SPI机制,其实在他具体实现时,也是传入了ClassLoader的。

 

 

public static <S> ServiceLoader<S> load(Class<S> service) {ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);}

​ 所以,我们就可以用这样的方式,定义一个统一的接口,而将这些不同的实现类都作为接口的不同实现去加载。这样,虽然多定义了一个接口,但是至少摆脱了那些别扭的反射代码不是吗?

 

 

public class OADemo8 {public static void main(String[] args) throws Exception {Double salary = 15000.00;//使用 SalaryJARLoader6,就需要在 OADemo 中添加 SPI 的配置文件while (true) {SalaryJARLoader6 salaryJARLoader = new SalaryJARLoader6("/Users/roykingw/lib/SalaryCaler.jar");SalaryCalService salaryService = getSalaryService(salaryJARLoader);System.out.println("应该到手Money:" + salaryService.cal(salary));SalaryJARLoader6 salaryJARLoader2 = new SalaryJARLoader6("/Users/roykingw/lib2/SalaryCaler.jar");SalaryCalService salaryService2 = getSalaryService(salaryJARLoader2);System.out.println("实际到手Money:" + salaryService2.cal(salary));SalaryCalService salaryService3 = getSalaryService(null);System.out.println("OA系统计算的Money:" + salaryService3.cal(salary));Thread.sleep(5000);}}private static SalaryCalService getSalaryService(ClassLoader classloader){ServiceLoader<SalaryCalService> services;if(null == classloader){services = ServiceLoader.load(SalaryCalService.class);}else{ClassLoader c1 = Thread.currentThread().getContextClassLoader();Thread.currentThread().setContextClassLoader(classloader);services = ServiceLoader.load(SalaryCalService.class);Thread.currentThread().setContextClassLoader(c1);}SalaryCalService service = null;if(null != services){//这里只需要拿SPI加载到的第一个实现类Iterator<SalaryCalService> iterator = services.iterator();if(iterator.hasNext()){service = iterator.next();}}return service;}
}

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/681714.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

VBA技术资料MF118:在多个工作表中插入页眉和页脚

我给VBA的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。我的教程一共九套&#xff0c;分为初级、中级、高级三大部分。是对VBA的系统讲解&#xff0c;从简单的入门&#xff0c;到…

多模态论文串讲·下【论文精读·49】最近使用 transformer encoder 和 decoder 的一些方法

大家好&#xff0c;我们今天就接着上次多模态串讲&#xff0c;来说一说最近使用 transformer encoder 和 decoder 的一些方法。 1 BLIP&#xff1a;Bootstrapping Language-Image Pre-training for Unified Vision-Language Understanding and Generation 我们要过的第一篇论文…

计算机网络——11EMail

EMail 电子邮件&#xff08;EMail&#xff09; 3个主要组成部分 用户代理邮件服务器简单邮件传输协议&#xff1a;SMTP 用户代理 又名“邮件阅读器”撰写、编辑和阅读邮件输入和输出邮件保存在服务器上 邮件服务器 邮箱中管理和维护发送给用户的邮件输出报文队列保持待发…

LeetCode、435. 无重叠区间【中等,贪心 区间问题】

文章目录 前言LeetCode、435. 无重叠区间【中等&#xff0c;贪心 区间问题】题目链接及分类思路贪心、区间问题 资料获取 前言 博主介绍&#xff1a;✌目前全网粉丝2W&#xff0c;csdn博客专家、Java领域优质创作者&#xff0c;博客之星、阿里云平台优质作者、专注于Java后端技…

08:K8S资源对象管理|服务与负载均衡|Ingress

K8S资源对象管理&#xff5c;服务与负载均衡&#xff5c;Ingress DaemonSet控制器污点策略容忍容忍污点 其他资源对象Job资源对象 有限生命周期CronJob资源对象 集群服务服务自动发现headless服务 实现服务定位与查找 服务类型 Ingress插件 发布服务的方式 DaemonSet控制器 Da…

Elasticsearch:适用于 iOS 和 Android 本机应用程序的 Elastic APM

作者&#xff1a;来自 Elastic Akhilesh Pokhariyal, Cesar Munoz, Bryce Buchanan 适用于本机应用程序的 Elastic APM 提供传出 HTTP 请求和视图加载的自动检测&#xff0c;捕获自定义事件、错误和崩溃&#xff0c;并包括用于数据分析和故障排除目的的预构建仪表板。 适用于 …

【北邮鲁鹏老师计算机视觉课程笔记】08 texture 纹理表示

【北邮鲁鹏老师计算机视觉课程笔记】08 texture 纹理表示 1 纹理 规则和不规则的 2 纹理的用处 从纹理中恢复形状 3 分割与合成 4 分析纹理进行分类 通过识别纹理分析物理性质 如何区分纹理 5 寻找有效的纹理分类方法 发现模式、描述区域内模式 A对应图2 B对应图…

Java 基于微信小程序的电子商城购物系统

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12W、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

EL表达式和JSTL标签

1.1. EL表达式概述 EL&#xff08;Expression Language&#xff09;是一门表达式语言&#xff0c;它对应<%…%>。我们知道在JSP中&#xff0c;表达式会被输出&#xff0c;所以EL表达式也会被输出。 EL表达式的格式&#xff1a;${…}&#xff0c;例如&#xff1a;${12}…

【深度学习】S2 数学基础 P1 线性代数(上)

目录 基本数学对象标量与变量向量矩阵张量降维求和非降维求和累计求和 点积与向量积点积矩阵-向量积矩阵-矩阵乘法 深度学习的三大数学基础 —— 线性代数、微积分、概率论&#xff1b; 自本篇博文以下几遍博文&#xff0c;将对这三大数学基础进行重点提炼。 本节博文将介绍线…

mysql Day05

sql性能分析 sql执行频率 show global status like Com_______ 慢查询日志 执行时间超过10秒的sql语句 profile详情 show profiles帮助我们了解时间都耗费到哪里了 #查看每一条sql的耗时情况 show profiles#查看指定query_id的sql语句各个阶段的耗时情况 show profile fo…

单片机学习笔记---DS18B20温度传感器

目录 DS18B20介绍 模拟温度传感器的基本结构 数字温度传感器的应用 引脚及应用电路 DS18B20的原理图 DS18B20内部结构框图 暂存器内部 单总线介绍 单总线电路规范 单总线时序结构 初始化 发送一位 发送一个字节 接收一位 接收一个字节 DS18B20操作流程 指令介…

基于 Python 深度学习的电影评论情感分析系统,附源码

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12W、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

前端JavaScript篇之await 在等待什么呢?async/await 如何捕获异常

目录 await 在等待什么呢&#xff1f;async/await 如何捕获异常 await 在等待什么呢&#xff1f; await 关键字实际上是等待一个表达式的结果&#xff0c;这个表达式的计算结果可以是 Promise 对象或者其他值。如果 await 后面的表达式不是 Promise 对象&#xff0c;那么 awai…

Spring Boot3自定义异常及全局异常捕获

⛰️个人主页: 蒾酒 &#x1f525;系列专栏&#xff1a;《spring boot实战》 &#x1f30a;山高路远&#xff0c;行路漫漫&#xff0c;终有归途。 目录 前置条件 目的 主要步骤 定义自定义异常类 创建全局异常处理器 手动抛出自定义异常 前置条件 已经初始化好一个…

vue 获取 form表格 的值 的方法

vue 获取 form表格 的值 代码 let discountLastMoney this.form.getFieldValue(discountLastMoney)-0

格式化字符串的简单学习

文章目录 Format String格式化字符串函数格式化字符串参数原理 这几天学的少&#xff0c;过完年就一直在走亲戚&#xff08;现在看到肉就犯恶心 Format String 格式化字符串函数可以接受可变数量的参数&#xff0c;并将第一个参数作为格式化字符串&#xff0c;根据其来解析之…

[Python进阶] 识别验证码

11.3 识别验证码 我们再开发某些项目的时候&#xff0c;如果遇到要登录某些网页&#xff0c;那么会经常遇到输入验证码的情况&#xff0c;而每次人工输入验证码的话&#xff0c;比较浪费时间。于是&#xff0c;可以通过调用某些接口进行识别。 11.3.1 调用百度文字识别接口 …

中小学信息学奥赛CSP-J认证 CCF非专业级别软件能力认证-入门组初赛模拟题第一套(阅读程序题)

CCF认证CSP-J入门组模拟测试题 二、阅读程序题 (程序输入不超过数组或字符串定义的范围&#xff1b;除特殊说明外&#xff0c;判断题 1.5分&#xff0c;选择题3分&#xff0c;共计40分) 第一题 1 #include<iostream> 2 using namespace std; 3 int a,b,c; 4 int main…

【C++】内存五大区详解

&#x1f490; &#x1f338; &#x1f337; &#x1f340; &#x1f339; &#x1f33b; &#x1f33a; &#x1f341; &#x1f343; &#x1f342; &#x1f33f; &#x1f344;&#x1f35d; &#x1f35b; &#x1f364; &#x1f4c3;个人主页 &#xff1a;阿然成长日记 …