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. Vue的nextTick的原理

    知识储备:事件循环有宏任务和微任务,宏任务所处的队列就是宏任务队列,队列可以有多个,第一个队列只有一个任务就是执行主线程的js代码,剩余队列任务有setTimeout setInterval微任务所处 ...

  2. 2.flask 源码解析:应用启动流程

    目录 一.flask 源码解析:应用启动流程 1.1 WSGI 1.2 启动流程 Flask 源码分析完整教程目录:https://www.cnblogs.com/nickchen121/p/1476 ...

  3. 【技术分析】恶意 SPL 代币识别指南

    背景 在 EVM 生态上,存在各式各样的 ERC20 代币,因其实现方式有着极高的自由度,也催生了花样繁多的恶意代币.这些恶意代币通常会在代码中实现一些恶意的逻辑(禁止用户卖出,特权铸造或销毁等),其 ...

  4. 开源的口袋妖怪自走棋「GitHub 热点速览」

    作为一名 90 后,我对口袋妖怪(宝可梦)游戏有着特殊的感情,满满的都是回忆.如果你也喜欢宝可梦主题的游戏,这款开源的宝可梦自走棋游戏 pokemonAutoChess 一定要试试,它采用战棋(自走棋 ...

  5. AI网关在应用集成中起到什么作用?

    现在,国内外几乎每个SaaS服务商都找到办法把大型语言模型(LLM)集成到自己的产品里.印证了那句话"每款SaaS都值得用AI重做一遍"我们暂且不讨论是否值得用AI重做,但是增加A ...

  6. KubeSphere Helm 应用仓库源码分析

    作者:蔡锡生,LStack 平台研发工程师,近期专注于基于 OAM 的应用托管平台落地. 背景介绍 KubeSphere 应用商店简介 作为一个开源的.以应用为中心的容器平台,KubeSphere 在 ...

  7. 初学者浅析C++类与对象

    C++类与对象 class class基本语法 class ClassName { public: // 公有成员 Type memberVariable; // 数据成员 ReturnType me ...

  8. 不用PLC和板卡,一台电脑就可以控制伺服

    1.前言 大家好!我是付工. EtherCAT是运动控制领域使用最广泛的总线通信协议之一. 如果我们只有一台电脑,能不能直接控制EtherCAT总线伺服呢? 这个是完全可以的. 我们可以在电脑上安装实 ...

  9. vue中的prop组件封装

    学习vue有一段时间了,也写了一些东西.今天看文档突然看到了一个好玩的东西,那就是prop.prop的作用是父组件中引用了子组件并给子组件加了一个属性,这个属性可以是静态的,可以是动态的,可以是数字, ...

  10. 2个月搞定计算机二级C语言——真题(5)解析

    1. 前言 本篇我们讲解2个月搞定计算机二级C语言--真题 5 2. 程序填空题 2.1 题目要求 2.2 提供的代码 #include <stdio.h> double fun(int ...