原来类加载器这么简单,手把手教你用纯java代码实现热部署

发布时间:2022-03-01 11:17:50 作者:yexindonglai@163.com 阅读(619)

1、什么是热部署

      相信大家在写代码的时候都有这个困扰,就是每次我改完代码之后都需要重启项目才能看到结果,小项目还好,启动不会占用太多时间,如果是一个大项目,每次重启都要耗费很多时间的情况下,这无疑对开发效率都会大幅下降,那么有没有这样一种技术? 我修改后无需重启服务器,就可以马上看到效果?我可以很肯定地回答你:“有”,就是热部署技术,在你修改完代码之后,这项技术会自动帮你重新加载被修改后class文件,真正实现实时查看改动的结果;

2、准备

    要知道热部署,就得先了解class加载机制,在我们启动项目的时候,首先编辑器会将后缀为.java的文件编译成.class文件,之后jvm虚拟机会把class文件转成二进制的字节码加载到内存里面,中间经过了一系列的链接过程,最后完成初始化,加载过程不是本文的重点,这里简单说明略过;

image.png

3、类加载器的分类

    类加载器基类叫做 ClassLoader,这是一个抽象类,既然是抽象就代表着它是可扩展的,所以衍生出了不同种类的类加器

  • BootstrapClassLoader :主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。
  • ExtClassLoader :主要负责加载 %JAVA_HOME%/jre/lib/ext  目录下的一些扩展的jar。
  • AppClassLoader:主要负责加载应用程序的主函数类, 加载classPath目录
  • UserClassLoader: 用户自定义类加载器,名字随便起,只需要继承ClassLoader类即可;

3.1、BootstrapClassLoader

    BootStrapClassLoader 是一个纯的C++实现,没有对应的Java类。所以在Java中是取不到的。如果我们在 idea 编辑器中搜索BootStrapClassLoader,是找不到这个类的

如果一个类的classLoader是null。已经足可以证明他就是由BootStrapClassLoader 加载的,现在我们用一组代码测试下

  1. public static void main(String[] args) throws Exception {
  2. String str = new String("123");
  3. System.out.println("String类的类加载器为:"+str.getClass().getClassLoader());
  4. }

java.lang.String 这个类是系统的类,当我们获取这个类时,可以看到它的类加载器是null值,所以就可以断定,String 使用的是BootStrapClassLoader加载的;

3.2、ExtClassLoader

    ExtClassLoader主要加载%JAVA_HOME%/jre/lib/ext,此路径下的所有classes目录以及java.ext.dirs系统变量指定的路径中类库 ,那么都有哪些类呢?看下图就知道啦,主要加载下图jar包中的类!此类继承ClassLoader加载器;

需要注意的是,ExtClassLoader这个类是在 Launcher  类里面的一个内部类

3.3、AppClassLoader

   App就是应用程序的意思,所以这个类加载器就是用来加载我们用户自己写的java类;此类继承ClassLoader加载器,需要注意的是,这个类也是在 Launcher  类里面的一个内部类

接下来我们自己定义一个类 User.java ,然后在控制台打印一下这个类的类加载器; 

  1. package com;
  2. public class User {
  3. public static void main(String[] args) {
  4. System.out.println("User类的类加载器为:"+User.class.getClassLoader());
  5. }
  6. }

通过控制台打印的结果可以得知,自定义的 User类 使用的是AppClassLoader 类加载器;

3.5、关于类加载,还有2点需要注意:

1、同一个classLoader只会生成一个相同的class对象

2、不同的ClassLoader 加载相同的Class对象不能互相强转

为什么不能强转呢?我们来写一组代码测试下就知道啦

先定一个 自动义类加载器    MyClassLoader.java

  1. package com;
  2. import java.io.InputStream;
  3. public class MyClassLoader extends ClassLoader{
  4. /**
  5. * 加载class文件
  6. * 重写此方法的目的是为了能让此方法被外部调用,父类的 findClass 是 protected 修饰的,只能被子类调用
  7. * @param name 类的全类名 示例: com.xd.User
  8. * @return
  9. * @throws ClassNotFoundException
  10. */
  11. @Override
  12. public Class<?> findClass(String name) throws ClassNotFoundException {
  13. try {
  14. String fileName = name.replaceAll("\\.","/") + ".class";
  15. fileName = "/"+ fileName;
  16. // 获取文件输入流
  17. InputStream is = this.getClass().getResourceAsStream(fileName);
  18. // 读取字节
  19. byte[] b = new byte[is.available()];
  20. is.read(b);
  21. // 将byte字节流解析成jvm能够识别的Class对象
  22. return defineClass(name, b, 0, b.length);
  23. } catch (Exception e) {
  24. throw new ClassNotFoundException();
  25. }
  26. }
  27. }

定义一个 HotUserModel.java 类,用来实例化用的

  1. package com.hot.deploy.model;
  2. public class HotUserModel {
  3. }

 主程序入口测试

  1. public static void main(String[] args) throws Exception {
  2. // 默认加载器加载的 HotUserModel
  3. HotUserModel hotUserModel = null;
  4. // 使用自定义加载器加载 HotUserModel
  5. MyClassLoader myClassLoader = new MyClassLoader();
  6. Class<?> aClass =myClassLoader.findClass("com.hot.deploy.model.HotUserModel");
  7. Object o = aClass.newInstance();
  8. System.out.println("普通 HotUserModel 使用的类加载器为:" + HotUserModel.class.getClassLoader());
  9. System.out.println();
  10. System.out.println("使用自动义类加载器的 HotUserModel 使用的加载器为:" + o.getClass().getClassLoader());
  11. System.out.println();
  12. // 以下语句赋值会报错
  13. hotUserModel = (HotUserModel) o;
  14. }

运行后打印结果如下

通过打印的结果可以看到,相同的类无法强转, 因为使用了不同的类加载器;

4、双亲委派加载机制

    当类加载器在加载一个类的时候,它首先不会马上去加载这个类,而是先把这个请求委派给父类去完成,而父类接受到加载请求的时候也不会马上去加载这个类,而是在交给自己的父类去加载,每一个层次加载器都是如此,直到父类加载器无法加载的时候,子类才会自己去尝试完成加载,我们的要实现的热部署原理就是要打破双亲委派加载机制,直接由子类来加载;而不去请求父加载器;

加载流程如下

  1. 将自定义加载器挂载到应用程序加载器
  2. 应用程序加载器将类加载请求委托给扩展类加载器
  3. 扩展类加载器将类加载请求委托给启动类加载器
  4. 启动类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交由扩展类加载器加载;
  5. 扩展类加载器在加载路径下未找到目标Class文件,交由应用程序类加载器加载
  6. 应用程序类加载器在加载路径下未找到目标Class文件,交由自定义加载器加载
  7. 自定义加载器在加载路径下未找到目标Class文件,抛出ClassNotFound异常

4.1、双亲委派的作用

    1、通过以上的委托流程,双亲委派机制保障了类的唯一性,

    2、保证核心.class不被篡改,即使被篡改也不会被加载,即使被加载也不会是同一个class对象,因为不同的加载器加载同一个.class也不是同一个Class对象。这样则保证了Class的执行安全。

5、全盘负责委托机制

    当一个A加载器加载一个类的时候,除非显式地使用另一个加载器B,否则该类的所有依赖和引用都使用这个A加载器 也就是说,如果一个类 为User类,这个User类的加载器是AppClassLoader加载器加载的,那么这个User类下面的所有引用类的加载器都是AppClassLoader;

接下来测试一把,先创建一个自定义类加载器 MyClassLoader.java

  1. package com;
  2. import java.io.InputStream;
  3. import java.net.URL;
  4. public class MyClassLoader extends ClassLoader{
  5. /**
  6. * 加载class文件
  7. * 重写此方法的目的是为了能让此方法被外部调用,父类的 findClass 是 protected 修饰的,只能被子类调用
  8. * @param name 类的全类名 示例: com.xd.User
  9. * @return
  10. * @throws ClassNotFoundException
  11. */
  12. @Override
  13. public Class<?> findClass(String name) throws ClassNotFoundException {
  14. try {
  15. // 获取class文件名称 去掉包路径
  16. String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
  17. // 获取文件输入流
  18. InputStream is = this.getClass().getResourceAsStream(fileName);
  19. // 读取字节
  20. byte[] b = new byte[is.available()];
  21. is.read(b);
  22. // 将byte字节流解析成jvm能够识别的Class对象
  23. return defineClass(name, b, 0, b.length);
  24. } catch (Exception e) {
  25. throw new ClassNotFoundException();
  26. }
  27. }
  28. }

用来创建对象的类 User.java 

注意:在User的show方法里面创建了一个新的User对象,我们就是要测试这个新的User对象是否使用了全盘负责委托机制

  1. package com;
  2. public class User {
  3. public void show(){
  4. System.out.println("当前User使用的类加载器为:"+this.getClass().getClassLoader());
  5. User user = new User();
  6. System.out.println("新的User对象使用的类加载器为:"+user.getClass().getClassLoader());
  7. }
  8. }

创建main方法测试

  1. public static void main(String[] args) throws Exception {
  2. MyClassLoader myClassLoader = new MyClassLoader();
  3. Class<?> aClass = myClassLoader.findClass("com.User");
  4. // 实例化User对象
  5. Object o = aClass.newInstance();
  6. Method show = aClass.getDeclaredMethod("show");
  7. // 执行show方法
  8. show.invoke(o);
  9. }

运行后打印结果如下,很明显,全盘负责委托机制生效了,用的都是自定义加载类加载的class对象;

  1. 当前User使用的类加载器为:com.MyClassLoader@1d44bcfa
  2. 新的User对象使用的类加载器为:com.MyClassLoader@1d44bcfa
  3. Process finished with exit code 0

6、热部署代码

     实现热部署需要做2件事,一是打破双亲委派机制,二是使用全盘委托机制实现热部署(废话,默认是使用的);废话不多说,上代码,需要注意的是,热部署的代码和上面5步的代码没有任何关系,请不要将其混淆

HotUserModel.java

  1. package com.hot.deploy.model;
  2. public class HotUserModel {
  3. // 用户名
  4. private String userName="yexindong";
  5. // 年龄
  6. private Integer age = 18;
  7. public void show(){
  8. System.out.println("大家好,我叫:"+userName+",我的年龄是:"+age);
  9. System.out.println("当前使用的类加载器:"+this.getClass().getClassLoader());
  10. }
  11. }

自定义类加载器  HotClassLoader.java

  1. package com.hot.deploy;
  2. import java.io.File;
  3. import java.io.FileInputStream;
  4. import java.io.IOException;
  5. import java.io.InputStream;
  6. import java.util.ArrayList;
  7. import java.util.List;
  8. public class HotClassLoader extends ClassLoader{
  9. // 项目目录
  10. private String projectPath;
  11. // class的包路径列表 格式: [com.xxx.App.class]
  12. private List<String> classList;
  13. /**
  14. *
  15. * @param projectPath 项目绝对路径
  16. * @param classPaths class文件的完整路径列表
  17. * @throws IOException
  18. */
  19. public HotClassLoader(String projectPath,String... classPaths) throws IOException {
  20. // 空格解码
  21. this.projectPath = projectPath.replaceAll("%20"," ");
  22. classList = new ArrayList<String>();
  23. // 扫描包路径,并加载类,打破双亲委派机制
  24. for (String classPath : classPaths) {
  25. // 转为路径
  26. classPath = classPath.replaceAll("\\.", "/");
  27. // 空格解码
  28. classPath.replaceAll("%20"," ");
  29. File file = new File(classPath);
  30. readFile(file);
  31. }
  32. }
  33. public void readFile(File file) throws IOException {
  34. if (file.isDirectory()) {
  35. for (File child : file.listFiles()) {
  36. readFile(child);
  37. }
  38. } else {
  39. // 加载类
  40. String fileName = file.getName();
  41. String suffix = fileName.substring(fileName.lastIndexOf(".")+1);
  42. if(!"class".equals(suffix)){
  43. return;
  44. }
  45. //将class文件转为字节码
  46. InputStream inputStream = new FileInputStream(file);
  47. byte[] classByte = new byte[(int)file.length()];
  48. inputStream.read(classByte);
  49. // 获取全类名 com.xx.xx.class
  50. String className = this.getClassName(file);
  51. // 记录已被加载过的class文件
  52. classList.add(className);
  53. // 加载class到jvm虚拟机
  54. defineClass(className,classByte, 0,classByte.length);
  55. }
  56. }
  57. /**
  58. * 获取全类名 com.xx.xx.class
  59. * @param file
  60. * @return
  61. */
  62. public String getClassName(File file){
  63. String className = file.getPath().replace(projectPath, "");
  64. // 去掉.class
  65. className = className.substring(1,className.indexOf("."));
  66. // 将斜杠转为.
  67. className = className.replaceAll("/",".");
  68. return className;
  69. }
  70. // 这个方法可改可不改
  71. @Override
  72. public Class<?> loadClass(String name) throws ClassNotFoundException {
  73. Class<?> loadedClass = findLoadedClass(name);
  74. if(loadedClass == null ){
  75. if(classList.contains(name)){
  76. throw new ClassNotFoundException("找不到类");
  77. }
  78. // 调用系统类加载器
  79. loadedClass = getSystemClassLoader().loadClass(name);
  80. }
  81. return loadedClass;
  82. // return super.loadClass(name);
  83. }
  84. }

Application.java

  1. package com.hot.deploy;
  2. import com.hot.deploy.model.HotUserModel;
  3. import java.io.File;
  4. import java.lang.reflect.Method;
  5. public class Application {
  6. public void showApplication(){
  7. // 创建新对象,这个创建对象时使用的类加载器也是自定义的加载器
  8. new HotUserModel().show();
  9. }
  10. /**
  11. * 开始加载
  12. * @param classLoader
  13. * @throws Exception
  14. */
  15. public static void start0(HotClassLoader classLoader) throws Exception {
  16. // 加载当前类Application
  17. Class<?> aClass = classLoader.loadClass("com.hot.deploy.Application");
  18. // 调用showApplication方法
  19. Method show = aClass.getDeclaredMethod("showApplication");
  20. Object o = aClass.newInstance();
  21. Object invoke = show.invoke(o);
  22. // 延时一秒
  23. Thread.sleep(1000);
  24. }
  25. /**
  26. * 使用死循环来重新加载class,每次循环加载的class都是new出来的新对象
  27. * @throws Exception
  28. */
  29. public static void run() throws Exception {
  30. for(;;){
  31. // 获取项目的目录
  32. String projectPath = HotClassLoader.class.getResource("/").getPath().replaceAll("%20"," ");
  33. projectPath = new File(projectPath).getPath();
  34. // 使用自定义加载器加载所有的对象
  35. HotClassLoader hotClassLoader = new HotClassLoader(projectPath, projectPath + "/com");
  36. // 开始加载
  37. start0(hotClassLoader);
  38. }
  39. }
  40. }

  创建一个启动类来运行  MainApp.java

  1. package com.hot.deploy;
  2. public class MainApp {
  3. /**
  4. * 运行main 方法后,修改 HotUserModel 类的System.out.println()打印的内容,然后按一下右上角的绿色锤子(Build Project)就可以看到效果了
  5. *
  6. * 热部署实现原理: 是因为打破了双亲委派机制 和 全盘委托 机制;
  7. * @param args
  8. * @throws Exception
  9. */
  10. public static void main(String[] args) throws Exception {
  11. Application.run();
  12. }
  13. }

先运行main方法,在运行的同时,先修改HotUserModel类的userName属性

然后点击idea 右上角的绿色的锤子进行手动编译

编译完成后,马上就可以在控制台看到修改的内容了

    热部署这块确实有些晦涩难懂,但是只要认真学习,多去网上找找资料学习,总会茅塞顿开的,俗话说,难的不会,会的不难,说的就是这个道理,当你不知道的时候,你会觉得好复杂,当你学会之后会觉得也就那样!也希望童鞋们不要停下学习的脚步,向着诗和远方,我们一起出发!!

关键字Java