专栏首页Java StudyGroovy、热部署和热加载(自定义类加载器)及spring loaded 部分源码分析
原创

Groovy、热部署和热加载(自定义类加载器)及spring loaded 部分源码分析

热部署:在服务器运行时,重新全部更新部署项目,但服务器不会重启。

优点:不需要重启tomcat服务器,如果一个tomcat多个项目,不必因为tomcat停止而停止其他的项目。

缺点:会清空运行期间产生的数据内存。

热加载(热更新):在程序的运行期间,类有了内容上的改变,更新类到运行的项目中。

不释放内存,比如修改类了,原先的类内存不会释放,新的类还会增加内存,同样不重启tomcat。

热加载和热部署的联系

1.不重启服务器。

2.基于Java类加载器实现。

元编程 与 groovy  MOP

http://www.groovy-lang.org/metaprogramming.html#xform-BaseScript

元编程(meta programming)意味着编写能够操作程序的程序,包括操作程序自身;Groovy通过MOP(元对象协议 MetaObject Protocol)实现。

实现方式:

  groovy文件的方式 ,修改groovy 文件来 实现热加载。

  数据库Groovy脚本方式 。

  Spring 注入中Groovy脚本 的方式 。   

运行时元编程

  针对的对象:

  1.  POJO
  2.  POGO
  3. Groovy Interceptor

将groovy 集成到应用程序中

    嵌入(运行)到应用程序的几种方式

  Eval 

  Groovy shell 

  GroovyClassloader 

  GroovyScriptEngine (常用)

  ScriptEngineManager

Spring boot 的实现 热加载的方式 :spring loaded 和 devtools

spring loader 是属于使用 Java agent 在应用运行前 指定  spring loader jar  的路径,然后 -java agent 或者使用maven 打包 ,然后使用maven 的命令行实现。

前提:自己在看如果实现热加载时,看到可以自定义的实现classloader 然后用一个线程去通过对比文件记录的LastModifedTime ,不断检查文件是否发生了改变,如果时间不对应,就要去利用自己的类加载器 加载一次改文件,实现了热加载。从表面上来看没有什么问题,但实际你加载的对象和原来的对象是两个对象,spring loaded是如何将通过热加载的文件重新指向之前的对象应该是一个要思考的问题。

分析spring loader 源码   

首先它是一个 agent 查看 MANIFEST.MF 找到他的 PreMain-Class

Can-Redefine-Classes: true // 其中注意一个配置为能够重新定义类  为true 

点开 PreMain-Class

public class SpringLoadedAgent {         
   //熟悉的ClassFileTransformer 查看其他别的方法没有能切入的点,查看 ClassFileTransformer 的实体类 private static ClassFileTransformer transformer = new ClassPreProcessorAgentAdapter(); 
 private static Instrumentation instrumentation;
 public static void premain(String options, Instrumentation inst) {
      // Handle duplicate agents
 if (instrumentation != null) {
         return;
 }
      instrumentation = inst;
 instrumentation.addTransformer(transformer);
 }

   public static void agentmain(String options, Instrumentation inst) {
      if (instrumentation != null) {
         return;
 }
      instrumentation = inst;
 instrumentation.addTransformer(transformer);
 }

   /**
 * @return the Instrumentation instance
 */
 public static Instrumentation getInstrumentation() {
      if (instrumentation == null) {
         throw new UnsupportedOperationException("Java 5 was not started with preMain -javaagent for SpringLoaded");
 }
      return instrumentation;
 }

}

点击查看 ClassPreProcessorAgentAdapter

public class ClassPreProcessorAgentAdapter implements ClassFileTransformer {

 private static Logger log = Logger.getLogger(ClassPreProcessorAgentAdapter.class.getName());
 private static SpringLoadedPreProcessor preProcessor; // 存在一个 这个属性 可以从名字查看出来为 spingload 之前执行器
 private static ClassPreProcessorAgentAdapter instance;

 public ClassPreProcessorAgentAdapter() {
      instance = this;
  }
// 执行这个 静态代码块初始化 SpringLoadedPreProcessor 
  static {
      try {
         preProcessor = new SpringLoadedPreProcessor();
  preProcessor.initialize();
  }
      catch (Exception e) {
         throw new ExceptionInInitializerError("could not initialize JSR163 preprocessor due to: " + e.toString());
  }
   }

   //  override 应该是重要的 修改类的方法:在Java  agent 实现aop时 也是这个关键类  
 public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
  ProtectionDomain protectionDomain,
 byte[] bytes) throws IllegalClassFormatException {
      try {  

         if (GlobalConfiguration.isRuntimeLogging && log.isLoggable(Level.INFO)) {
            log.info("> (loader=" + loader + " className=" + className + ", classBeingRedefined="
   + classBeingRedefined
                     + ", protectedDomain=" + (protectionDomain != null) + ", bytes= "
   + (bytes == null ? "null" : bytes.length));
  }

 

  if (classBeingRedefined != null) {
   
  TypeRegistry typeRegistry = TypeRegistry.getTypeRegistryFor(loader);
 if (typeRegistry == null) {
               return null;
   }
              // 判断是否是要被重新加载的 ,
            boolean isRTN = typeRegistry.isReloadableTypeName(className);
 if (isRTN) {
                // 就可以得到可以被加载类型 
               ReloadableType rtype = typeRegistry.getReloadableType(className, false);

  if (GlobalConfiguration.isRuntimeLogging && log.isLoggable(Level.INFO)) {
                  log.info("Tricking HCR for " + className);
  }
               return rtype.bytesLoaded; // returning original bytes 返回被加载数据的bytes 数组

  }
            return null;
  }
 

      // 否则让preProcessor 去处理 return preProcessor.preProcess(loader, className, protectionDomain, bytes);
  }
        catch (Throwable t) {
         new RuntimeException("Reloading agent exited via exception, please raise a jira", t).printStackTrace();
 return bytes;
 }
 }

    public static void reload(ClassLoader loader, String className, Class<?> classBeingRedefined,
  ProtectionDomain protectionDomain, byte[] bytes) throws IllegalClassFormatException {
      instance.transform(loader, className, classBeingRedefined, protectionDomain, bytes);
  }

}

查看多次出现的 TypeRegistry 篇幅过多查看 对改类的说明

* The type registry tracks all reloadable types loaded by a specific class loader. It is configurable via a 
* springloaded.properties file (which it will discover as resources through the classloader) or directly via a
* configure(Properties) method call. 根据具体的类加载器会跟踪所有的重新加载的类型 

// 和关键的方法 , 根据classloader返回一个TypeRegistry
public static TypeRegistry getTypeRegistryFor(ClassLoader classloader) {}
// 根据类的名称 去判断是否是一个被加载类型 true if the type is reloadable, false otherwise 
public ReloadableTypeNameDecision isReloadableTypeName(String slashedName, ProtectionDomain protectionDomain,
 byte[] bytes) {}

看到这里也没有看到 具体spring loaded 是怎么进行 对类的热加载的 看到一个

ReloadableFileChangeListener    可重新加载的文件的改变的监听器。 
其中的  fileChanged 文件已经改变的方法,
public void fileChanged(File file) {

   if (GlobalConfiguration.isRuntimeLogging && log.isLoggable(Level.INFO)) {
      log.info(" processing change for " + file);
 }
   ReloadableType rtype = correspondingReloadableTypes.get(file);
 // 如果文件是 以jar 为结尾的
 if (file.getName().endsWith(".jar")) {
      if (GlobalConfiguration.isRuntimeLogging && log.isLoggable(Level.INFO)) {
         log.info(" processing change for JAR " + file);
 }
      try {


         ZipFile zf = new ZipFile(file); // 文件中的jar
  Set<JarEntry> entriesBeingWatched = watchedJarContents.get(file);

 for (JarEntry entryBeingWatched : entriesBeingWatched) {
        
  ZipEntry ze = zf.getEntry(entryBeingWatched.slashname);
 long lmt = ze.getTime();//getLastModifiedTime().toMillis(); // getLastModifiedTime()

 // 如果时间大于了 
  if (lmt > entryBeingWatched.lmt) {
               // entry in jar has been updated
  if (GlobalConfiguration.isRuntimeLogging && log.isLoggable(Level.INFO)) {
                  log.info(" detected update to jar entry. jar=" + file.getName() + " class="
  + entryBeingWatched.slashname + " OLD LMT=" + new Date(entryBeingWatched.lmt)
                        + " NEW LMT=" + new Date(lmt));
  }
                  // 去加载一个新的版本
  typeRegistry.loadNewVersion(entryBeingWatched.rtype, lmt, zf.getInputStream(ze));
  entryBeingWatched.lmt = lmt;
 }

         }
         zf.close();
 }
      catch (IOException e) {
         e.printStackTrace();
 }
   }

   //
 else {
      typeRegistry.loadNewVersion(rtype, file);
 }
}
可以看到 如文件的时间大于了 说明文件修改过了,将会去重新加载新的一个版本 ,
可以看到这是实现它的方法,这个方法被谁调用呢
是属于 FileSystemWatcher 类中的determineChangesSince 方法,
其中FileSystemWatcher 它的内部类 Watcher 实现 runnable 接口,它的run 方法
有一个 while(!timeToStop) 循环 timeToStop默认为 false ,
开始执行这个方法时会一直执行 循环体中的内容我们可以看到,遍历比较类是否需要被reload 。
for (File changedFile : changedFiles) {
  determineChangesSince(changedFile, lastScanTime);
}

自己学习到这里 大致也明白了作者实现的思路和我们自己自定义的classloader 实现思路上大体一致,支持更加细化的 模块对整个 spring 应用进行热加载。

devtools 其实是当监测到有代码改动后会,自动重启jvm 进行reload ,不是真正意义上的热部署,经常出现当build 完后,发现你在session 中存放的值失效了登陆失败。

1、devtools会监听classpath下的文件变动,并且会立即重启应用(发生在保存时机),注意:因为其采用的虚拟机机制,该项重启是很快的。 2、devtools可以实现页面热部署(即页面修改后会立即生效,这个可以直接在application.properties文件中配置spring.thymeleaf.cache=false来实现(这里注意不同的模板配置不一样)。


自定义加载器实现热加载

用户自定义加载器需要继承ClassLoader,实现原理就是通过一个线程去监听文件的修改时间,然后重写findClass方法,把文件以流的形式读进来,然后调defineClass方法。在JDK1.2之后,双亲委派模式已经被引入到类加载体系中,因此不建议重写loadClass方法,只需要重写findClass就可以了

如果自己实现一个类加载器去实现热加载需要注意哪些点呢?

根据网上的文章和实现方式,自己心里有疑问,从网上的几个例子来看,要实现的热部署的过程好像和类加载器没有什么关系,但自己又有疑问那为什么要自己实现类加载器?

  网上demo的实现的步骤 

自定义一个类加载器 --》选定一个要进行热加载的文件目录,并定义一个map记录文件目录下文件的lastModified - -》线程去定期去监测文件路径下的文件如果lastModified与之前记录的 是否不一样 --》则用自己自定义的类进行类加载。

demo中实现的样子

解决上面划删除线的疑惑,为什么大家要费劲心思的去自己实现一个类加载器,当监测到文件发生变化后,重新自己的类加载器加载一次不可以吗?:

其中一个人的说法:

(应该是要表达前提是程序运行期间) 由于要想实现同一个类的不同版本的共存,这些不同的版本必须由不同的类加载器进行加载,因此就不能把这些类的加载工作委托给类加载器来完成,因为它们只有一份。

还有这句话 :

我们编写的应用类默认情况下都是通过 AppClassLoader 进行加载的。当我们使用 new 关键字或者 Class.forName 来加载类时,所要加载的类都是由调用 new 或者 Class.forName 的类的类加载器(也是 AppClassLoader)进行加载的。要想实现 Java 类的热替换,首先必须要实现系统中同名类的不同版本实例的共存,通过上面的介绍我们知道,要想实现同一个类的不同版本的共存,我们必须要通过不同的类加载器来加载该类的不同版本。另外,为了能够绕过 Java 类的既定加载过程,我们需要实现自己的类加载器,并在其中对类的加载过程进行完全的控制和管理。

看到这里,spring-loaded 框架也是用到了一个自己定义的类加载器,那么groovy 中实现的热加载具体是怎么进行实现的呢?

我们知道 groovy 有自己的类加载器 ,是不是和我们用到的,自定义类加载器原理是一样的。

参考文章:

Java自定义classloader引发的思考

Java类的热替换

自己动手写一个实现热加载的类加载器

自己要定义类加载器的原因 为什么要编写自定义的 ClassLoader?

 默认的classloader 只能从本地文件系统中加载文件,一般情况下,当你只是从本地编写代码时就足够了,也没有人,没有老师教你把编写一个classloader类,来运行自己写的程序,JAVA语言最新颖的特点之一就是可以从本地硬盘或着网络的地方获取类。

还有如果觉得你想 在你编写的程序的基础上去 增添一些代码(正常情况下,估计没有人要使用使用自定义有类加载器的方式去 添加一些代码的这个想法)因此主要是为了实现 aop 这个功能,那你编写好自定义的classloader ,只需要让规定项目路径下的某些类,去使用你自定义的类加载器,就能在类加载时期去实现某些类的 aop 效果,这也就是 Java -agent实现的框架的工作原理,好像明白了当时学agent 的时候要 学类加载机制了,当时只是知道了 是在 defineClass()方法中对 读取的类的 byte[] 数组的进行 修改 插入代码,其实开始是要从自定义类加载器的方向出发的。

网上的一些回答

除了从本地或是网上加载类文件,还可以用类加载器来:

  • 在执行不受信任的代码之前自动验证数字签名
  • 使用用户提供的密码透明的解密代码
  • 根据用户的特定需求创建自定义的动态类
  • 额外拓展Grails 框架热更新实现方式 
  • Monitoring Resources for Changes  

监控更新的资源

Often it is valuable to monitor resources for changes and perform some action when they occur. This is how Grails implements advanced reloading of application state at runtime. For example, consider this simplified snippet from the Grails ServicesPlugin:

class ServicesGrailsPlugin extends Plugin { ... def watchedResources = "file:./grails-app/services/**/*Service.groovy" //监控资源的路径 ... void onChange( Map<String, Object> event) { // 当被监视的资源发生了改变,将会被自动的重新加载经由 onChange,event 对象 if (event.source) { def serviceClass = grailsApplication.addServiceClass(event.source) def serviceName = "${serviceClass.propertyName}" beans { "$serviceName"(serviceClass.getClazz()) { bean -> bean.autowire = true } } } } }

First it defines watchedResources as either a String or a List of strings that contain either the references or patterns of the resources to watch. If the watched resources specify a Groovy file, when it is changed it will automatically be reloaded and passed into the onChange closure in the event object.

首先,它定义watchedResources为包含要监视的资源的引用或模式的字符串或字符串列表。如果监视的资源指定了Groovy文件,则更改该文件后,它将自动重新加载该文件并将其传递到对象的onChange闭包中event

event对象定义了许多有用的属性:  

  • event.source-事件的来源,无论是重新加载Class还是SpringResource
  • event.ctx-Spring ApplicationContext实例
  • event.plugin-管理资源的插件对象(通常为this
  • event.applicationGrailsApplication实例
  • event.managerGrailsPluginManager实例

这些对象可用于帮助您基于更改内容应用适当的更改。在上面的“服务”示例中,ApplicationContext当其中一个服务类发生更改时,将使用来重新注册新的服务bean 。

grails官网文档 participatingInAutoReloadEvents部分

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 类加载机制浅记

    Class 类加载机制,面试考察方面挺频繁的,今天在项目中也遇到了要了解类加载机制的地方,要了解Javaagent,Javaagent 大家知道它是在类加载时期...

    猎户星座1
  • Spring中的异步请求、异步调用及demo测试

    背景:做项目过程中,一些耗时长的任务可能需要在后台线程池中运行;典型的如发送邮件等,由于需要调用外部的接口来进行实际的发送操作,如果客户端在提交发送请求后一直等...

    猎户星座1
  • Java并发机制的底层实现原理--Java并发编程的艺术

    当volatile 修饰的共享变量时,在进行写操作时,查看Java程序经 编译 解释为机器语言,汇编语言时,发现多了一个lock 的前缀。

    猎户星座1
  • JVM类加载机制和双亲委派模型

    虚拟机类加载机制:虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。

    用户3467126
  • Tomcat 类加载器打破双亲委派模型

    1. 什么是类加载机制? 2. 什么是双亲委任模型? 3. 如何破坏双亲委任模型? 4. Tomcat 的类加载器是怎么设计的?

    爱撸猫的杰
  • 虚拟机类加载机制(2)——类加载器

    《深入理解Java虚拟机》一书中将类的加载过程放到了类加载器前面一节,但在这里我想先讲“类加载器”。在上一篇类加载时机中我们用大量篇幅来讲解了类加载过程中的5个...

    用户1148394
  • 类加载过程,双亲委派模型?

    java通过字节码和JVM机制,提供了强大的跨平台能力,理解Java的类加载机制能让我们更加了解java的运行过程

    居士
  • Java虚拟机 - 超级详细的类加载说明

    java文件在编译时会被JVM编译成.class字节码文件,这篇主要讲解的是JVM如何将.class文件加载的加载过程。

    虞大大
  • 图解Tomcat类加载机制

      说到本篇的tomcat类加载机制,不得不说翻译学习tomcat的初衷。   之前实习的时候学习javaMelody的源码,但是它是一个Maven的项目,...

    用户1154259
  • 从微信、钉钉等APP,看六种常见的loading 加载设计

    当页面的框架固定时,只需要加载框架内数据时,采用这种刷新样式,即先加载框架,再加载框架内的数据。为了反之框架内的内容为空,会用占位符或者预设图片来填充。 上面简...

    企鹅号小编

扫码关注云+社区

领取腾讯云代金券