文档章节

Java服务器热部署的实现原理

Java_Coder
 Java_Coder
发布于 2015/12/31 11:26
字数 2510
阅读 152
收藏 26
点赞 0
评论 0

今天发现早年在大象笔记中写的一篇笔记,之前放在ijavaboy上的,现在它已经访问不了了。前几天又有同事在讨论这个问题。这里拿来分享一下。


在web应用开发或者游戏服务器开发的过程中,我们时时刻刻都在使用热部署。热部署的目的很简单,就是为了节省应用开发和发布的时间。比如,我们在使用Tomcat或者Jboss等应用服务器开发应用时,我们经常会开启热部署功能。热部署,简单点来说,就是我们将打包好的应用直接替换掉原有的应用,不用关闭或者重启服务器,一切就是这么简单。那么,热部署到底是如何实现的呢?在本文中,我将写一个实例,这个实例就是一个容器应用,允许用户发布自己的应用,同时支持热部署。


在Java中,要实现热部署,首先,你得明白,Java中类的加载方式。每一个应用程序的类都会被ClassLoader加载,所以,要实现一个支持热部署的应用,我们可以对每一个用户自定义的应用程序使用一个单独的ClassLoader进行加载。然后,当某个用户自定义的应用程序发生变化的时候,我们首先销毁原来的应用,然后使用一个新的ClassLoader来加载改变之后的应用。而所有其他的应用程序不会受到一点干扰。先看一下,该应用的设计图:




有了总体实现思路之后,我们可以想到如下几个需要完成的目标:


1、定义一个用户自定义应用程序的接口,这是因为,我们需要在容器应用中去加载用户自定义的应用程序。

2、我们还需要一个配置文件,让用户去配置他们的应用程序。

3、应用启动的时候,加载所有已有的用户自定义应用程序。

4、为了支持热部署,我们需要一个监听器,来监听应用发布目录中每个文件的变动。这样,当某个应用重新部署之后,我们就可以得到通知,进而进行热部署处理。


实现部分:


首先,我们定义一个接口,每一个用户自定义的程序中都必须包含唯一一个实现了该接口的类。代码如下:

[java] view plaincopy

  1. public interface IApplication {  

  2.   

  3.         public void init();  

  4.          

  5.         public void execute();  

  6.          

  7.         public void destory();  

  8.          

  9. }  



在这个例子中,每一个用户自定义的应用程序,都必须首先打包成一个jar文件,然后发布到一个指定的目录,按照指定的格式,然后首次发布的时候,还需要将应用的配置添加到配置文件中。所以,首先,我们需要定义一个可以加载指定目录jar文件的类:

[java] view plaincopy在CODE上查看代码片派生到我的代码片

  1.  public ClassLoader createClassLoader(ClassLoader parentClassLoader, String... folders) {  

  2.   

  3.        List<URL> jarsToLoad = new ArrayList<URL>();  

  4.         for (String folder : folders) {  

  5.               List<String> jarPaths = scanJarFiles(folder);  

  6.   

  7.                for (String jar : jarPaths) {  

  8.   

  9.                      try {  

  10.                            File file = new File(jar);  

  11.                            jarsToLoad.add(file.toURI().toURL());  

  12.   

  13.                     } catch (MalformedURLException e) {  

  14.                            e.printStackTrace();  

  15.                     }  

  16.               }  

  17.        }  

  18.   

  19.        URL[] urls = new URL[jarsToLoad.size()];  

  20.        jarsToLoad.toArray(urls);  

  21.   

  22.         return new URLClassLoader(urls, parentClassLoader);  

  23. }  




这个方法很简单,就是从多个目录中扫描jar文件,然后返回一个新的URLClassLoader实例。至于scanJarFiles方法,你可以随后下载本文的源码。然后,我们需要定义一个配置文件,用户需要将他们自定义的应用程序信息配置在这里,这样,该容器应用随后就根据这个配置文件来加载所有的应用程序:

[html] view plaincopy在CODE上查看代码片派生到我的代码片

  1. <apps>  

  2.         <app>  

  3.                <name> TestApplication1</name >  

  4.                <file> com.ijavaboy.app.TestApplication1</file >  

  5.         </app>  

  6.         <app>  

  7.                <name> TestApplication2</name >  

  8.                <file> com.ijavaboy.app.TestApplication2</file >  

  9.         </app>  

  10. </apps>  



这个配置是XML格式的,每一个app标签就表示一个应用程序,每一个应用程序,需要配置名称和那个实现了IApplication接口的类的完整路径和名称。

有了这个配置文件,我们需要对其进行解析,在这个例子中,我使用的是xstream,很简单,你可以下载源码,然后看看就知道了。这里略过。这里需要提一下:每个应用的名称(name),是至关重要的,因为该例子中,我们的发布目录是整个项目发布目录下的applications目录,这是所有用户自定义应用程序发布的目录。而用户发布一个应用程序,需要首先在该目录下新建一个和这里配置的name一样名称的文件夹,然后将打包好的应用发布到该文件夹中。(你必须这样做,否则在这个例子中,你会发布失败)。

好了,现在加载jar的方法和配置都有了,下面将是整个例子的核心部分,对,就是应用程序管理类,这个类就是要完成对每一个用户自定义应用程序的管理和维护。首先要做的,就是如何加载一个应用程序:


[java] view plaincopy在CODE上查看代码片派生到我的代码片

  1. public void createApplication(String basePath, AppConfig config){  

  2.       String folderName = basePath + GlobalSetting. JAR_FOLDER + config.getName();  

  3.       ClassLoader loader = this.jarLoader .createClassLoader(ApplicationManager. class.getClassLoader(), folderName);  

  4.         

  5.        try {  

  6.              Class<?> appClass = loader. loadClass(config.getFile());  

  7.                

  8.              IApplication app = (IApplication)appClass.newInstance();  

  9.                

  10.              app.init();  

  11.                

  12.               this.apps .put(config.getName(), app);  

  13.                

  14.       } catch (ClassNotFoundException e) {  

  15.              e.printStackTrace();  

  16.       } catch (InstantiationException e) {  

  17.              e.printStackTrace();  

  18.       } catch (IllegalAccessException e) {  

  19.              e.printStackTrace();  

  20.       }  



可以看到,这个方法接收两个参数,一个是基本路径,一个是应用程序配置。基本路径其实就是项目发布目录的地址,而AppConfig其实就是配置文件中app标签的一个实体映射,这个方法从指定的配置目录中加载指定的类,然后调用该应用的init方法,完成用户自定义应用程序的初始化。最后将,该加载的应用放入内存中。

现在,所有的准备工作,都已经完成了。接下来,在整个应用程序启动的时候,我们需要加载所有的用户自定义应用程序,所以,我们在ApplicationManager中添加一个方法:


[java] view plaincopy在CODE上查看代码片派生到我的代码片

  1.  public void loadAllApplications(String basePath){  

  2.          

  3.         for(AppConfig config : this.configManager.getConfigs()){  

  4.                this.createApplication(basePath, config);  

  5.        }  

  6. }  



这个方法,就是将用户配置的所有应用程序加载到该容器应用中来。好了,现在我们是不是需要写两个独立的应用程序试试效果了,要写这个应用程序,首先我们新建一个java应用程序,然后引用这个例子项目,或者将该例子项目打包成一个jar文件,然后引用到这个独立的应用中来,因为这个独立的应用程序中,必须要包含一个实现了IApplication接口的类。我们来看看这个例子包含的一个独立应用的样子:

[java] view plaincopy在CODE上查看代码片派生到我的代码片

  1. public class TestApplication1 implements IApplication{  

  2.   

  3.         @Override  

  4.         public void init() {  

  5.               System. out.println("TestApplication1-->init" );  

  6.        }  

  7.   

  8.         @Override  

  9.         public void execute() {  

  10.               System. out.println("TestApplication1-->do something" );  

  11.        }  

  12.   

  13.         @Override  

  14.         public void destory() {  

  15.               System. out.println("TestApplication1-->destoryed" );  

  16.        }  

  17.   

  18. }  




是不是很简单?对,就是这么简单。你可以照这个样子,再写一个独立应用。接下来,你还需要在applications.xml中进行配置,很简单,就是在apps标签中增加如下代码:

[html] view plaincopy在CODE上查看代码片派生到我的代码片

  1. <app>  

  2.        <name> TestApplication1</name >  

  3.        <file> com.ijavaboy.app.TestApplication1</file >  

  4. </app>  



接下来,进入到本文的核心部分了,接下来我们的任务,就全部集中在热部署上了,其实,也许现在你还觉得热部署很神秘,但是,我相信一分钟之后,你就不会这么想了。要实现热部署,我们之前说过,需要一个监听器,来监听发布目录applications,这样当某个应用程序的jar文件改变时,我们可以进行热部署处理。其实,要实现目录文件改变的监听,有很多种方法,这个例子中我使用的是apache的一个开源虚拟文件系统——common-vfs。如果你对其感兴趣,你可以访问http://commons.apache.org/proper/commons-vfs/。这里,我们继承其FileListener接口,实现fileChanged 即可:


[java] view plaincopy在CODE上查看代码片派生到我的代码片

  1. public void fileChanged (FileChangeEvent event) throws Exception {  

  2.   

  3.       String ext = event.getFile().getName().getExtension();  

  4.        if(!"jar" .equalsIgnoreCase(ext)){  

  5.               return;  

  6.       }  

  7.         

  8.       String name = event.getFile().getName().getParent().getBaseName();  

  9.         

  10.       ApplicationManager. getInstance().reloadApplication(name);  

  11.         



当某个文件改变的时候,该方法会被回调。所以,我们在这个方法中调用了ApplicationManager的reloadApplication方法,重现加载该应用程序。


[java] view plaincopy在CODE上查看代码片派生到我的代码片

  1. public void reloadApplication (String name){  

  2.       IApplication oldApp = this.apps .remove(name);  

  3.         

  4.        if(oldApp == null){  

  5.               return;  

  6.       }  

  7.         

  8.       oldApp.destory();     //call the destroy method in the user's application  

  9.         

  10.       AppConfig config = this.configManager .getConfig(name);  

  11.        if(config == null){  

  12.               return;  

  13.       }  

  14.         

  15.       createApplication(getBasePath(), config);  



重现加载应用程序时,我们首先从内存中删除该应用程序,然后调用原来应用程序的destory方法,最后按照配置重新创建该应用程序实例。

到这里,你还觉得热部署很玄妙很高深吗?一切就是如此简单。好了,言归正传,为了让我们自定义的监听接口可以有效工作起来,我们还需要指定它要监听的目录:


[java] view plaincopy在CODE上查看代码片派生到我的代码片

  1.  public void initMonitorForChange(String basePath){  

  2.         try {  

  3.                this.fileManager = VFS.getManager();  

  4.                 

  5.               File file = new File(basePath + GlobalSetting.JAR_FOLDER);  

  6.               FileObject monitoredDir = this.fileManager .resolveFile(file.getAbsolutePath());  

  7.               FileListener fileMonitorListener = new JarFileChangeListener();  

  8.                this.fileMonitor = new DefaultFileMonitor(fileMonitorListener);  

  9.                this.fileMonitor .setRecursive(true);  

  10.                this.fileMonitor .addFile(monitoredDir);  

  11.                this.fileMonitor .start();  

  12.               System. out.println("Now to listen " + monitoredDir.getName().getPath());  

  13.                 

  14.        } catch (FileSystemException e) {  

  15.               e.printStackTrace();  

  16.        }  

  17. }  



这里,就是初始化监听器的地方,我们使用VFS的DefaultFileMonitor完成监听。而监听的目录,就是应用发布目录applications。接下来,为了让整个应用程序可以持续的运行而不会结束,我们修改下启动方法:

[java] view plaincopy在CODE上查看代码片派生到我的代码片

  1.  public static void main(String[] args){  

  2.          

  3.        Thread t = new Thread(new Runnable() {  

  4.                 

  5.                @Override  

  6.                public void run() {  

  7.                     ApplicationManager manager = ApplicationManager.getInstance();  

  8.                     manager.init();  

  9.               }  

  10.        });  

  11.          

  12.        t.start();  

  13.          

  14.         while(true ){  

  15.                try {  

  16.                     Thread. sleep(300);  

  17.               } catch (InterruptedException e) {  

  18.                     e.printStackTrace();  

  19.               }  

  20.        }  

  21. }  



好了,到这里,一切都要结束了。现在,你已经很明白热部署是怎么一回事了,对吗?不明白?OK,还有最后一招,去看看源码吧!


源码我已经放到了GitHub上面了,地址:https://github.com/chenjie19891104/ijavaboy/tree/master/AppLoader,欢迎下载使用,你拥有一切的权利对其进行修改。


最后,如果本文有什么地方说的不准确,欢迎指正,谢谢!


© 著作权归作者所有

共有 人打赏支持
Java_Coder
粉丝 58
博文 155
码字总数 102864
作品 0
杭州
Jenkins 教程(一)实现自动化打包及邮件通知

个人不喜欢装腔作势一堆专业术语放上去,让大多数人看不懂来提升逼格(所谓的专家),所以我简单的介绍jenkins是干啥的。本文使用jenkins,就是让它把git仓库里的东西取出来,然后在jenkins容器...

FantJ ⋅ 05/26 ⋅ 0

加强Docker容器与Java 10集成

很多运行在Java虚拟机(JVM)中的应用,包括数据服务如Apache Spark和Kafka以及传统企业应用,都运行在容器中。最近,运行在容器里的JVM出现了由于内存和CPU资源限制和使用率导致性能损失问题...

java高级架构牛人 ⋅ 06/04 ⋅ 0

RMI:Java中的分布式计算框架

RMI全称是Remote Method Invocation-远程方法调用,Java RMI在JDK1.1中实现的,其威力就体现在它强大的开发分布式网络应用的能力上,是纯Java的网络分布式应用系统的核心解决方案之一。其实...

qq_39521554 ⋅ 05/15 ⋅ 0

sharding-jdbc源码分析—准备工作

原文作者:阿飞Javaer 原文链接:https://www.jianshu.com/p/7831817c1da8 接下来对sharding-jdbc源码的分析基于tag为源码,根据sharding-jdbc Features深入学习sharding-jdbc的几个主要特性...

飞哥-Javaer ⋅ 05/03 ⋅ 0

Java开发学习之三版本简介 java编程

  Java编程语言,在更迭迅速的互联网领域多年屹立不倒,足以得见Java这门语言旺盛的生命力,因此,会有很多想要进入互联网领域的朋友,想要学Java来转行开发。但是,所谓“隔行如隔山”,j...

老男孩Linux培训 ⋅ 06/05 ⋅ 0

面试必看!2018年4月份阿里最新的java程序员面试题目

目录 技术一面(23问) 技术二面(3大块) 性能优化(21点) 项目实战(34块) JAVA方向技术考察点(15点) JAVA开发技术面试中可能问到的问题(17问) 阿里技术面试1 1.Java IO流的层次结构...

美的让人心动 ⋅ 04/16 ⋅ 0

学习Java和Spring Boot Cloud ,不妨看看这个

专注于编程、互联网动态。最终将总结的技术、心得、经验(数据结构与算法、源码分析等)分享给大家,这里不只限于技术!还有职场心得、生活感悟、以及面经。 1 java版web项目 java版web项目,...

b644rofp20z37485o35m ⋅ 05/04 ⋅ 0

Netweaver和CloudFoundry是如何运行Web应用的?

Netweaver 在Jerry的微信公众号文章SAP Fiori应用的三种部署方式里提到SAP Fiori应用以BSP应用的方式部署在ABAP Front-End Server上。那么这些BSP应用在运行时为什么能够接受和发送HTTP请求呢...

JerryWang_SAP ⋅ 06/16 ⋅ 0

J2EE中一些常用的名词【简】

web容器:给处于其中的应用程序组件(JSP,SERVLET)提供一个环境,使JSP,SERVLET直接更容器中的环境变量接口交互,不必关注其它系统问题。主要有WEB服务器来实现。例如:TOMCAT,WEBLOGIC,W...

anlve ⋅ 昨天 ⋅ 0

主流Java数据库连接池比较及前瞻

本文转载自微信公众号「工匠小猪猪的技术世界」 主流数据库连接池 常用的主流开源数据库连接池有C3P0、DBCP、Tomcat Jdbc Pool、BoneCP、Druid等 C3p0: 开源的JDBC连接池,实现了数据源和JND...

渣渣(Charles) ⋅ 04/30 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

JavaScript零基础入门——(八)JavaScript的数组

JavaScript零基础入门——(八)JavaScript的数组 欢迎大家回到我们的JavaScript零基础入门,上一节课我们讲了有关JavaScript正则表达式的相关知识点,便于大家更好的对字符串进行处理。这一...

JandenMa ⋅ 今天 ⋅ 0

sbt网络问题解决方案

转自:http://dblab.xmu.edu.cn/blog/maven-network-problem/ cd ~/.sbt/launchers/0.13.9unzip -q ./sbt-launch.jar 修改 vi sbt/sbt.boot.properties 增加一个oschina库地址: [reposit......

狐狸老侠 ⋅ 今天 ⋅ 0

大数据,必须掌握的10项顶级安全技术

我们看到越来越多的数据泄漏事故、勒索软件和其他类型的网络攻击,这使得安全成为一个热门话题。 去年,企业IT面临的威胁仍然处于非常高的水平,每天都会看到媒体报道大量数据泄漏事故和攻击...

p柯西 ⋅ 今天 ⋅ 0

Linux下安装配置Hadoop2.7.6

前提 安装jdk 下载 wget http://mirrors.hust.edu.cn/apache/hadoop/common/hadoop-2.7.6/hadoop-2.7.6.tar.gz 解压 配置 vim /etc/profile # 配置java环境变量 export JAVA_HOME=/opt/jdk1......

晨猫 ⋅ 今天 ⋅ 0

crontab工具介绍

crontab crontab 是一个用于设置周期性被执行的任务工具。 周期性执行的任务列表称为Cron Table crontab(选项)(参数) -e:编辑该用户的计时器设置; -l:列出该用户的计时器设置; -r:删除该...

Linux学习笔记 ⋅ 今天 ⋅ 0

深入Java多线程——Java内存模型深入(2)

5. final域的内存语义 5.1 final域的重排序规则 1.对于final域,编译器和处理器要遵守两个重排序规则: (1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用...

江左煤郎 ⋅ 今天 ⋅ 0

面试-正向代理和反向代理

面试-正向代理和反向代理 Nginx 是一个高性能的反向代理服务器,但同时也支持正向代理方式的配置。

秋日芒草 ⋅ 今天 ⋅ 0

Spring 依赖注入(DI)

1、Setter方法注入: 通过设置方法注入依赖。这种方法既简单又常用。 类中定义set()方法: public class HelloWorldOutput{ HelloWorld helloWorld; public void setHelloWorld...

霍淇滨 ⋅ 昨天 ⋅ 0

马氏距离与欧氏距离

马氏距离 马氏距离也可以定义为两个服从同一分布并且其协方差矩阵为Σ的随机变量之间的差异程度。 如果协方差矩阵为单位矩阵,那么马氏距离就简化为欧氏距离,如果协方差矩阵为对角阵,则其也...

漫步当下 ⋅ 昨天 ⋅ 0

聊聊spring cloud的RequestRateLimiterGatewayFilter

序 本文主要研究一下spring cloud的RequestRateLimiterGatewayFilter GatewayAutoConfiguration @Configuration@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMi......

go4it ⋅ 昨天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部