Spring Boot热部署

热部署的使用

  • 引入依赖
<!-- spring boot热部署的依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 启动项目

  • 修改代码

在项目运行的过程中我们可以修改程序的代码

  • 编译代码

接着我们需要把修改的代码重新编译,在idea中可以通过下面的操作实现编译,点击下面锤子的按钮

接着项目就会重新启动,自然就会运用我们修改后的代码

热部署机制(原理)

在分析热部署的机制之前我们需要了解一下java的类加载机制,这个可以参考《类加载和JVM性能调优监控工具》这篇文章。

总之我们必须知道两个机制全盘负责委托机制和双亲委派机制,还有java中三种类加载器,BootstrapClassLoader、ExtensionClassLoader和ApplicationClassLoader,还有它们负责加载的类的类型。

现在我们想一想热部署是如何实现的,首先我们修改了java文件,然后将这个java文件编译成了class文件,相当于我们修改了class文件。那么JVM中class文件自然就发生了改变,但是这显然是不行的,因为这些修改的类已经加载到我们的JVM中了,它们的class类对象已经是存在的了。如果我们再需要创建这个类,会通过全盘负责委托机制和双亲委派机制找到可以加载的类加载器,显然这个类加载器是有之前加载过的缓存的,它也不会尝试重新加载,那么我们修改后的class文件其实是没有用到的。

自然无法实现热部署,那么我们如果需要实现热部署,就需要打破全盘负责委托机制和双亲委派机制。那么,问题来了,如何实现呢?

这里先提出一个简单的思路,我们自己实现一个类加载器,然后我们的类通过这个类加载器去加载,并且能够加载到我们修改后的class文件。

如果我们需要实现一个自己的类加载器需要继承一个抽象类ClassLoader,注意它的loadClass方法,它的基本流程如下:

  1. 通过缓存拿到class对象
  2. 如果存在父类加载器调用父类加载器的loadClass方法
  3. 最后依然没有拿到class对象才会自己去加载
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 通过缓存拿到class对象
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 调用父类加载器的loadClass方法
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
} if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name); // this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

这里就体现了双亲委派机制的逻辑,我们所实现的类加载器就必须打破双亲委派机制,因为如果不打破的话我们自己写的代码写的类必然会交给ApplicationClassLoader类加载器去加载,这样的话就实现不了热部署的效果。

所以我们可以重写这个方法的逻辑,还有就是我们可以保证每一次都可以从我们自己的类加载器的缓存中拿到class对象,这样也算是打破了双亲委派机制,这里的实现很多,不过目的都是一致的。

其次就是我们必须解决new对象所使用的类加载器的问题,new对象所使用的类加载器可以通过全盘负责委托机制判断,简单来说就是new关键字在哪里使用的那么new出来的对象就是通过new出来的位置使用的类加载器加载的。

所以我们的自己写的类对象一定要在通过我们自己实现的类加载器加载出来的类中new出来,只有这样才能保证new关键字使用的是我们自己实现的类加载器。

自己实现热部署

基本思路如下:

  1. 通过自己实现的类加载器加载我们自己写的类
  2. 开启一个文件监听
  3. 一旦我们修改文件就通过自己实现的类加载器重新类,刷新缓存
  • 自定义类加载器

下面是自定义的类加载器,我们自己的代码的类就是通过这个类加载器加载的对象,它也间接打破了双亲委派机制,在这个类的构造方法中会将所有我们自己的类加载一遍。

注意这里的类加载首先是扫描项目找到所有的class文件,获得class文件的字节流,然后通过defineClass方法将class文件转换成class对象,并且把生成的class对象放在我们的类加载器的缓存中。

public class MyClassLoader extends ClassLoader {

    //目的 让缓存里面永远能返回一个Class对象 这样就不需要走父类加载器了
//在构造方法里面加载类 loadClass //项目的根路径
public String rootPath; //所有需要由我这个类加载器加载的类存在这个集合
public List<String> clazzs;
//两个classloader 一个是负责加载 需要被热部署的代码的
//一个是加载系统的一些类的 //classPaths: 需要被热部署的加载器去加载的目录
public MyClassLoader(String rootPath,String... classPaths) throws Exception{
this.rootPath = rootPath;
this.clazzs = new ArrayList<>(); for (String classPath : classPaths) {
scanClassPath(new File(classPath));
}
} //扫描项目里面传进来的一些class
public void scanClassPath(File file) throws Exception{
if (file.isDirectory()){
for (File file1 : file.listFiles()) {
scanClassPath(file1);
}
}else{
String fileName = file.getName();
String filePath = file.getPath();
String endName = fileName.substring(fileName.lastIndexOf(".")+1);
if (endName.equals("class")){
//现在我们加载到的是一个Class文件
//如何吧一个Class文件 加载成一个Class对象????
InputStream inputStream = new FileInputStream(file);
byte[] bytes = new byte[(int) file.length()];
inputStream.read(bytes); String className = fileNameToClassName(filePath);
//文件名转类名
//类名
defineClass(className, bytes, 0, bytes.length);
clazzs.add(className);
//loadClass 是从当前ClassLoader里面去获取一个Class对象
}
} } public String fileNameToClassName(String filePath){
//d: //project//com//luban//xxxx
String className = filePath.replace(rootPath,"").replaceAll("\\\\",".");
// com.luban.className.class
className = className.substring(1,className.lastIndexOf("."));
return className;
//com.luban.classNamexxxx
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> loadClass = findLoadedClass(name);
//第一情况 这个类 不需要由我们加载
//第二种情况 这个类需要由我们加载 但是 确实加载不到
if (loadClass ==null){
if (!clazzs.contains(name)){
loadClass = getSystemClassLoader().loadClass(name);
}else{
throw new ClassNotFoundException("没有加载到类");
}
}
return loadClass;
} //先做热替换 先加载单个Class //new Test().xxx //当文件被修改的时候再进行热部署 public static void main(String[] args) throws Exception{ //双亲委派机制 Application.run(MyClassLoader.class); // new Test(); //给一个程序入口 // while (true){
// MyClassLoader myClassLoader = new MyClassLoader(rootPath,rootPath+"/com");
// Class<?> aClass = myClassLoader.loadClass("com.Test");
// aClass.getMethod("test").invoke(aClass.newInstance());
// new Test().test();
//
//
//
// Thread.sleep(2000);
// } }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 文件监听器

这里通过引入的依赖使用的文件监听器,依赖如下

<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5

文件监听器对象,如果我们class文件发生变化,这个监听器会主动常见类加载器对象重新加载我们的类对象。

public class FileListener  extends FileAlterationListenerAdaptor{

    @Override
public void onFileChange(File file) {
if (file.getName().indexOf(".class")!= -1){ try {
MyClassLoader myClassLoader = new MyClassLoader(Application.rootPath,Application.rootPath+"/com");
Application.stop();
Application.start0(myClassLoader); } catch (Exception e) {
e.printStackTrace();
}
}
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 程序入口

这是我们的程序入口对象,就是为了解决全盘负责委托机制所带来的影响,因为我们不能直接在main方法中new对象,否则使用的就是ApplicationClassLoader类加载器去加载了,所以我们提供一个类去执行我们的启动代码,并且保证我们的这个类对象使用的是我们自定义的类加载加载的就够了。

public class Application {

    public static String rootPath;

    public  static void run(Class<?> clazz) throws Exception{
String rootPath = MyClassLoader.class.getResource("/").getPath().replaceAll("%20"," ");
// / \
rootPath = new File(rootPath).getPath();
Application.rootPath = rootPath;
MyClassLoader myClassLoader = new MyClassLoader(rootPath,rootPath+"/com");
//用我们自己的类加载器加载程序入口
startFileListener(rootPath);
start0(myClassLoader); } public static void startFileListener(String rootPath) throws Exception {
FileAlterationObserver fileAlterationObserver = new FileAlterationObserver(rootPath);
fileAlterationObserver.addListener(new FileListener());
FileAlterationMonitor fileAlterationMonitor = new FileAlterationMonitor(500);
fileAlterationMonitor.addObserver(fileAlterationObserver);
fileAlterationMonitor.start(); //要实现文件监听: 写一个线程 去定时监听某个路径下所有的文件
//如果文件发生改动 就回调监听器 // fileAlterationMonitor.getInterval()
} //新的classload
public static void start(){
System.out.println("启动我们的应用程序");
//Tomcat tomcat = new Tomcat(); //Controller ...xxxx
new Test().test();
} public static void stop(){ // ApplicationContext.stop;
System.out.println("程序退出");
//告诉jvm需要gc了
System.gc();
//告诉jvm可以清除对象引用
System.runFinalization();
} // @Override
// protected void finalize() throws Throwable {
// super.finalize();
// } public static void start0(MyClassLoader classLoader) throws Exception { Class<?> aClass = classLoader.loadClass("com.Application"); aClass.getMethod("start").invoke(aClass.newInstance()); }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

Spring Boot热部署原理

Spring Boot的热部署原理和我们自己实现的热部署基本相似,有一点区别的就是它借助了spring的事件监听。

基本流程如下:

  1. springboot中也存在一个文件监听器,这个监听器同样会监听我们是否改变了class文件,如果发现我们改变了class文件的内容会发布一个ClassPathChangedEvent事件;
  2. springboot会调用restart方法处理这个事件;
  3. 首先是通过stop方法关掉我们的spring容器,再通知我们的gc垃圾回收;
  4. 接着通过start方法实现项目的重启,通过类加载器重新加载class文件,然后通过反射重新执行我们的main方法。
  • 文件监听器

我们先来看看是如何注入文件监听器的,还是打开热部署依赖下的spring.factories文件,有这么一个类LocalDevToolsAutoConfiguration,文件监听器正是通过此类注入spring容器的。

下图就是注入的文件监听器,一个FileSystemWatcher对象


类中的这个方法正是添加了一个文件监听器

其中这个监听器的onChange方法会发布一个ClassPathChangedEvent事件

简单来说就是文件监听器监听到class文件改变事件后会发布一个ClassPathChangedEvent事件

同样还是那个自动配置类中存在这么一个方法会处理文件监听器发布的ClassPathChangedEvent事件,调用restart方法,在这个方法中就会实现热部署的操作。

  • 类加载器

还是承接上面的restart方法,其中注意这里call方法中的实现

首先是stop方法

  1. 拿到我们spring的容器对象
  2. 调用spring容器对象的close对象关闭spring
  3. 调用gc方法提醒标记,实现垃圾回收


接着是start方法

同样还是逻辑在doStart方法中,主要就是两件事

  1. 通过类加载器加载我们需要的class文件,具体实现可参考我们自己实现的类加载器
  2. 然后就是重新启动我们的应用程序,这个在relaunch方法中实现的


启动我们的app,这里是通过反射调用我们的main方法的

2019.12.10笔记——Spring Boot热部署的使用和实现自己的热部署(类加载器相关)的更多相关文章

  1. Spring Boot 在IDEA中debug时的hot deployment(热部署)

    因为Spring Boot的项目一般会打包成jar发布, 在开发阶段debug时, 不能像传统的web项目那样, 选择exploded resources进行debug, 也没有热更新按钮, 如果每次 ...

  2. Spring Boot 项目在 IntelliJ IDEA 中配置 DevTools 实现热部署(macOS 系统)

    要配置的内容: 1.Preference -> Build, Execution, Deployment -> Complier -> Build project automatic ...

  3. (10)Spring Boot修改端口号【从零开始学Spring Boot】

    Spring boot 默认端口是8080,如果想要进行更改的话,只需要修改applicatoin.properties文件,在配置文件中加入: server.port=9090 常用配置: #### ...

  4. Spring Boot学习笔记——Spring Boot与Redis的集成

    一.添加Redis缓存 1.添加Redis起步依赖 在pom.xml中添加Spring Boot支持Redis的依赖配置,具体如下: <dependency> <groupId> ...

  5. 10个Spring Boot快速开发的项目,接私活利器(快速、高效)

    本文为大家精选了 码云 上优秀的 Spring Boot 语言开源项目,涵盖了企业级系统框架.文件文档系统.秒杀系统.微服务化系统.后台管理系统等,希望能够给大家带来一点帮助:) 1.项目名称:分布式 ...

  6. 一起玩转微服务(10)——spring boot介绍

    对于Spring,相信大家都非常熟悉,从出现开始,一直是企业级开发的主流.但是随着软件的发展和应用开发的不断演化,它的一些缺点也逐渐胡暴露了出来,下面,我们就一起看一下Spring的发展历程并且认识一 ...

  7. Spring Boot学习笔记---Spring Boot 基础及使用idea搭建项目

    最近一段时间一直在学习Spring Boot,刚进的一家公司也正好有用到这个技术.虽然一直在学习,但是还没有好好的总结,今天周末先简单总结一下基础知识,等有时间再慢慢学习总结吧. Spring Boo ...

  8. Spring Cloud学习笔记--Spring Boot初次搭建

    1. Spring Boot简介 初次接触Spring的时候,我感觉这是一个很难接触的框架,因为其庞杂的配置文件,我最不喜欢的就是xml文件,这种文件的可读性很不好.所以很久以来我的Spring学习都 ...

  9. 微服务学习笔记——Spring Boot特性

    1. 创建独立的Spring应用程序 2. 嵌入的Tomcat,无需部署WAR文件 3. 简化Maven配置 4. 自动配置Spring 5. 提供生产就绪型功能,如指标,健康检查和外部配置 6. 开 ...

  10. Spring boot 官网学习笔记 - Spring Boot 属性配置和使用(转)-application.properties

    Spring Boot uses a very particular PropertySource order that is designed to allow sensible overridin ...

随机推荐

  1. 本地图片上传服务器返回在线地址接口 - file - input -修改头像-带预览功能- 然后使用cropperjs 进行裁剪

    说明:上传的图片是 file 类型 ,核心就是获取图片文件(file类型的) : 实现一:使用 vant2 的图片加载组件 ,选择文件后会触发afterRead方法 ,参数 file 就是文件列表fi ...

  2. KubeSphere 社区双周报 | KubeKey v3.0.0 发布 | 2022-11-10

    KubeSphere 从诞生的第一天起便秉持着开源.开放的理念,并且以社区的方式成长,如今 KubeSphere 已经成为全球最受欢迎的开源容器平台之一.这些都离不开社区小伙伴的共同努力,你们为 Ku ...

  3. 云原生爱好者周刊:PromLabs 开源 PromQL 可视化工具 PromLens

    开源项目推荐 PromLens PromLabs 开源了旗下的 PromQL 可视化工具 PromLens,它可以通过图形化的方式展示 PromQL 的语法特征,对相关查询参数进行解释,并提供告警和常 ...

  4. 快速部署sqlserver AlwaysOn集群

    点击查看代码 丐版sqlserver集群 之前试过docker的,k8s的,然后发现,还是最朴素的是最简单的,希望有大佬能够汉化,他妈的,那些英文看得人要发癫啊. 前置准备,参照丐版pxc集群: ht ...

  5. 生成文本聚类java实现1

    本章主要的学习是中文分词 和两种统计词频(传统词频和TF-IDF算法 ) 的方法. 学习目的:通过N多的新闻标题 or 新闻摘要 or 新闻标签,生成基本的文本聚类,以便统计当天新闻的热点内容. 扩展 ...

  6. 《大话设计模式》java实现:第二章-策略模式

    <大话设计模式>java实现:第二章-策略模式 第二章是使用策略模式实现商场收银系统,使收银系统的促销策略可以灵活更改. 1. 原始代码实现: package gof; /* * < ...

  7. 从 Git 提交历史生成 Release Note

    发布软件时写 Release Note 算是常规操作,但每次从头手打也有点累,可以考虑从 Git 的提交历史中自动生成. Git 提交信息一般是三段式结构,段落之间使用空行隔开: <subjec ...

  8. 终于找到了英特尔CPU缩缸的原因!如何自救?

    地址: https://www.youtube.com/watch?v=D0wOiillq_A

  9. 鸿蒙NEXT开发案例:光强仪

    [引言] 本文将介绍如何使用鸿蒙NEXT框架开发一个简单的光强仪应用,该应用能够实时监测环境光强度,并给出相应的场景描述和活动建议. [环境准备] 电脑系统:windows 10 开发工具:DevEc ...

  10. 卡特兰数 Catalan 数列

    卡特兰数 Catalan 数列 引入 有一个无限大的栈,进栈的顺序为 \(1,2,\cdots,n\),求有多少种不同的出栈序列. 设 \(h[n]\) 为 \(n\) 个数的出栈序列方案数. 可以这 ...