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. 2021年1月国产数据库排行榜:OceanBase重回前三,TDSQL增长趋势最强劲!

    墨天轮国产数据库排行榜新年第一期已发布.2021年1月份排行榜前三甲依次为 TiDB .DaMeng.OceanBase .PingCAP TiDB 稳居冠军的宝座,短时间内难以撼动,开源的商业数据库 ...

  2. 墨天轮沙龙 | 清华乔嘉林:Apache IoTDB,源于清华,建设开源生态之路

    在6月8日举办的[墨天轮数据库沙龙第七期-开源生态专场]中,清华大学博士,助理研究员,Apache IoTDB PMC 乔嘉林老师分享了<Apache IoTDB,源于清华,建设开源生态之路&g ...

  3. 018 人生中第一次用 Python 写的一个小程序_猜年龄(再次强调,重视基础)

    博客配套视频链接: https://space.bilibili.com/383551518?spm_id_from=333.1007.0.0 b 站直接看 配套 github 链接:https:// ...

  4. [Dest0g3 520迎新赛]funny_upload

    打开靶机抓包发现过滤代码 发现.htaccess能上传后传入图片马 发现内容对<?进行过滤 我们换一种方式写后门代码 <script language="php"> ...

  5. OpenCv Mat 数据结构

    前言 OpenCv的Mat数据结构可以存储图片信息.但是以坐标系构建来说,Mat是以左上角为原点,而我们自己的日常习惯是以左下角为原点. 本文提供了这两者之间的一种转换. 假设 Mat : (x,y) ...

  6. Redhat7重置root管理员密码

    如果要重置Red Hat Enterprise Linux Server release 7.0 的root常见有2种办法(均测试有效) rd.break方法 1.重启Linux系统主机并出现引导界面 ...

  7. 为什么我越来越喜欢用DDD — DDD架构篇(1)

    Hello DDD DDD 是一种软件设计方法,DDD 是指导我们做软件工程设计的一种手段.它提供了用切割工程模型的各类技巧,如:领域.界限上下文.实体.值对象.聚合.工厂.仓储等.通过 DDD 的指 ...

  8. ESP8266+ MQTT+SG90(舵机) platformio

    ESP8266 + MQTT + SG90(舵机) platformio 连线 ESP8266 MG90S(舵机) GND 棕色 VCC 红色 模拟引脚 橙色 源代码 https://gitee.co ...

  9. 题解:CSP-S2020] 函数调用

    题解:CSP-S2020] 函数调用 一句话题意:给定一个有初始值的序列,支持如下三种操作: 1.单点加 2.全局乘 3.递归某些操作1.2.3 求最终的序列. 标签:topsort,动态规划,转化贡 ...

  10. 【转载】《扩散模型是实时游戏引擎(Diffusion Models Are Real-Time Game Engines)》的论文,向我们展示了世界上第一个完全由神经模型驱动的游戏引擎,GameNGen。这也是历史上首次,AI能在不借助其他游戏引擎的情况下,为玩家生成实时游戏了,并且在单个TPU上速度可以达到每秒20帧

    地址: https://www.youtube.com/watch?v=VniPJII6ak0 8月29号,谷歌DeepMind发布了一篇名为<扩散模型是实时游戏引擎(Diffusion Mod ...