自定义类加载器实现热部署


使用自定义的类加载器,在运行时,对字节码进行重新加载,生成新的对象。

实现效果

运行时,启动线程每秒执行对象方法,启动另外一个线程监听是否修改文件。

修改源文件后,自行重新编译运行

实现思路

Ⅰ. 将java源文件编译为class文件
Ⅱ. 自定义类加载器,读取class文件字节,并加载到内存中
Ⅲ. 读取加载器生成的Class对象
Ⅳ. 通过反射使用Class对象

实现过程

  • 将java源文件编译为class文件,在当前目录下直接生成的class文件,使用编译器API来编译源码代替javac
 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
 compiler.run(null, null, null, FILEPATH);
  • 自定义类加载器,重写findClass方法,通过IO读取class文件的字节数组,并使用defineClass方法进行加载
public class MyClassLoad extends ClassLoader {
  private byte[] classByteArr;
  private String classPath;


  public MyClassLoad(ClassLoader parent) {
      super(parent);//指定父类加载器
  }

  @Override
  protected Class<?> findClass(String name) {
      try {
          classByteArr = readFileToByteArray(classPath);
      } catch (IOException e) {
          e.printStackTrace();
      }
      return defineClass(name, classByteArr, 0, classByteArr.length);
  }


  public static byte[] readFileToByteArray(String classFilePath) throws IOException {
      File directory = new File(classFilePath);
      InputStream inputStream = new FileInputStream(directory);
      ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
      try {
          byte[] buffer = new byte[1024];
          int bytesRead = 0;
          while ((bytesRead = inputStream.read(buffer)) != -1) {
              outputStream.write(buffer, 0, bytesRead);
          }
          return outputStream.toByteArray();
      } finally {
          inputStream.close();
      }
  }

  public void setClassPath(String classPath) {
      this.classPath = classPath;
  }
}
  • 获取对象时,使用单例模式
private static Object get() throws IllegalAccessException, InstantiationException {
      //使用单例模式
      if (helloObject != null) {
          return helloObject;
      }
      synchronized (Object.class) {
          if (helloObject == null) {
              setHelloClass();
              return helloClass.newInstance();
          }
      }

      return helloObject;
  }


  private static void setHelloClass() {
      //生成class文件
      JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
      compiler.run(null, null, null, FILEPATH);
      System.out.println("生成class文件成功");

      //获取自定义类加载器
      MyClassLoad myClassLoad = new MyClassLoad(null);//指定父类加载器,null为Booststrap类加载器
      //设置读取class文件路径
      myClassLoad.setClassPath(CLASSPATH);
      try {
          //获取对象的class对象
          helloClass = myClassLoad.loadClass("classLoad.HelloImp");
      } catch (ClassNotFoundException e) {
          e.printStackTrace();
      }

  }
  • 设置两个线程,一个执行方法,一个监听文件是否修改
//每5秒执行对象的方法
      Thread t1 = new Thread(() -> {
          while (true) {
              try {
                  helloObject = get();
                  helloClass.getMethod("sayHello").invoke(helloObject);
              } catch (IllegalAccessException e) {
                  e.printStackTrace();
              } catch (InstantiationException e) {
                  e.printStackTrace();
              } catch (NoSuchMethodException e) {
                  e.printStackTrace();
              } catch (InvocationTargetException e) {
                  e.printStackTrace();
              }
              try {
                  Thread.sleep(5000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }

      });
      t1.start();

      //监听文件是否被修改
      Thread t2 = new Thread(() -> {
          long lastModified = new File(FILEPATH).lastModified();
          while (true) {
              try {
                  Thread.sleep(1000);
                  long now = new File(FILEPATH).lastModified();
                  if (now != lastModified) {
                      lastModified = now;
                      helloObject = null;//将对象设置为null,另外一个线程执行方法时,会自动获取最新的对象
                  }
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }

          }

      });
      t2.start();

小结

使用自定义类加载器时,需要重写findClass方法。且在构造方法中指定父类加载器,默认不设置时,会使用当前的类加载器,根据双亲委派机制,我们写的自定义类加载器会委托上一层加载器AppClassLoader进行类的加载。
这时,我们定义的类加载器findClass方法没有作用。如果指定父加载器为null,即Booststrap加载器,当找不到我们要加载的类时,才会委托我们自定义的类加载器加载。