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. 墨天轮访谈 | 阿里云捷熙:AnalyticDB,人人可用的数据分析服务

    分享嘉宾:李婧玮(捷熙) 阿里云数据库资深产品经理 整理:墨天轮社区 导读 大家好,我是来自阿里云的捷熙.AnalyticDB是融合数据库.大数据技术于一体的云原生企业级数据仓库平台,今天我为大家带来 ...

  2. 使用threejs创建一个长方体

    // 创建设备 正方体 // x1 X轴坐标 y1 Y轴坐标 item 设备的信息 可以把 item 嵌入到正方体里面 h : 高度 private initQuare1(x1:any,y1:any, ...

  3. 43.v-if和v-for的优先级

    v-for 的优先级高 延申问题:v-for 和 v-if 为什么不能在一起使用 ? 会造成性能的浪费,因为v-for 的优先级高,所以每次渲染都会执行v-if 判断条件,浪费时间 :比如 渲染 10 ...

  4. kali Linux 启动 apache 和 mysqll

    kali linux 自带 apache 服务 和 mysql 服务 # 启动 apache 服务 service apache2 start # 启动 mysql 服务 service mysql ...

  5. USACO 2023 December Contest, Gold

    Problem 1. Flight Routes 设原图的邻接矩阵为 \(e\),考虑它给我们的矩阵是什么东西. 设 \(d_{i, j}\) 表示 \(i\) 到 \(j\) 的路径数的奇偶性,那么 ...

  6. 云原生爱好者周刊:使用 GitOps 来动态管理 Grafana 的数据源

    文章推荐 使用 GitOps 来动态管理 Grafana 的数据源 通过 Grafana 的 Provisioning 特性,可以在 provisioning/datasources 目录下添加多个 ...

  7. 常见APR攻击及其防护

    0x01 什么是ARP 地址解析协议--ARP:是根据IP地址获取物理地址的一个TCP/IP协议.主机发送信息时将包含目标IP地址的ARP请求广播到局域网络上的所有主机,并接收返回消息,以此确定目标的 ...

  8. burpsuit无法启动

    在安装burpsuit时,最难的并不是找带有注册机的burpsuit安装包. 而是因为一些底层的原因,无法打开 burpsuit. 提示 Your JRE appears to be version ...

  9. Power BI 通过输入数据新建表后重新进入编辑状态

    在使用Power BI时,有时候我们会直接通过输入数据构建一些简单的表,但是构建好后我们可能还需要对表格进行增删改的操作,这时候我们需要怎么才会恢复到表格的编辑状态呢?其实很简单,我们回到PQ里面,双 ...

  10. 历史性突破:独立开发 .net core 在线客服系统累计处理聊天消息 48 万余条!

    业余时间用 .net core 写了一个在线客服系统.我把这款业余时间写的小系统丢在网上,陆续有人找我要私有化版本,我都给了,毕竟软件业的初衷就是免费和分享. 后来我索性就发了一个100%私有化版直接 ...