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

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


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


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



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

1、定义一个用户自定义应用程序的接口,这是因为,我们需要在容器应用中去加载用户自定义的应用程序。
2、我们还需要一个配置文件,让用户去配置他们的应用程序。
3、应用启动的时候,加载所有已有的用户自定义应用程序。
4、为了支持热部署,我们需要一个监听器,来监听应用发布目录中每个文件的变动。这样,当某个应用重新部署之后,我们就可以得到通知,进而进行热部署处理。

实现部分:

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

        public void init();
       
        public void execute();
       
        public void destory();
       
}


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

              List<URL> jarsToLoad = new ArrayList<URL>();
               for (String folder : folders) {
                     List<String> jarPaths = scanJarFiles(folder);

                      for (String jar : jarPaths) {

                            try {
                                  File file = new File(jar);
                                  jarsToLoad.add(file.toURI().toURL());

                           } catch (MalformedURLException e) {
                                  e.printStackTrace();
                           }
                     }
              }

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

               return new URLClassLoader(urls, parentClassLoader);
       }



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


这个配置是XML格式的,每一个app标签就表示一个应用程序,每一个应用程序,需要配置名称和那个实现了IApplication接口的类的完整路径和名称。
有了这个配置文件,我们需要对其进行解析,在这个例子中,我使用的是xstream,很简单,你可以下载源码,然后看看就知道了。这里略过。这里需要提一下:每个应用的名称(name),是至关重要的,因为该例子中,我们的发布目录是整个项目发布目录下的applications目录,这是所有用户自定义应用程序发布的目录。而用户发布一个应用程序,需要首先在该目录下新建一个和这里配置的name一样名称的文件夹,然后将打包好的应用发布到该文件夹中。(你必须这样做,否则在这个例子中,你会发布失败)。
好了,现在加载jar的方法和配置都有了,下面将是整个例子的核心部分,对,就是应用程序管理类,这个类就是要完成对每一个用户自定义应用程序的管理和维护。首先要做的,就是如何加载一个应用程序:

        public void createApplication(String basePath, AppConfig config){
              String folderName = basePath + GlobalSetting. JAR_FOLDER + config.getName();
              ClassLoader loader = this.jarLoader .createClassLoader(ApplicationManager. class.getClassLoader(), folderName);
              
               try {
                     Class<?> appClass = loader. loadClass(config.getFile());
                     
                     IApplication app = (IApplication)appClass.newInstance();
                     
                     app.init();
                     
                      this.apps .put(config.getName(), app);
                     
              } catch (ClassNotFoundException e) {
                     e.printStackTrace();
              } catch (InstantiationException e) {
                     e.printStackTrace();
              } catch (IllegalAccessException e) {
                     e.printStackTrace();
              }
       }


可以看到,这个方法接收两个参数,一个是基本路径,一个是应用程序配置。基本路径其实就是项目发布目录的地址,而AppConfig其实就是配置文件中app标签的一个实体映射,这个方法从指定的配置目录中加载指定的类,然后调用该应用的init方法,完成用户自定义应用程序的初始化。最后将,该加载的应用放入内存中。
现在,所有的准备工作,都已经完成了。接下来,在整个应用程序启动的时候,我们需要加载所有的用户自定义应用程序,所以,我们在ApplicationManager中添加一个方法:

        public void loadAllApplications(String basePath){
              
               for(AppConfig config : this.configManager.getConfigs()){
                      this.createApplication(basePath, config);
              }
       }


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

        @Override
        public void init() {
              System. out.println("TestApplication1-->init" );
       }

        @Override
        public void execute() {
              System. out.println("TestApplication1-->do something" );
       }

        @Override
        public void destory() {
              System. out.println("TestApplication1-->destoryed" );
       }

}



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


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

        public void fileChanged (FileChangeEvent event) throws Exception {

              String ext = event.getFile().getName().getExtension();
               if(!"jar" .equalsIgnoreCase(ext)){
                      return;
              }
              
              String name = event.getFile().getName().getParent().getBaseName();
              
              ApplicationManager. getInstance().reloadApplication(name);
              
       }


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

        public void reloadApplication (String name){
              IApplication oldApp = this.apps .remove(name);
              
               if(oldApp == null){
                      return;
              }
              
              oldApp.destory();     //call the destroy method in the user's application
              
              AppConfig config = this.configManager .getConfig(name);
               if(config == null){
                      return;
              }
              
              createApplication(getBasePath(), config);
       }


重现加载应用程序时,我们首先从内存中删除该应用程序,然后调用原来应用程序的destory方法,最后按照配置重新创建该应用程序实例。
到这里,你还觉得热部署很玄妙很高深吗?一切就是如此简单。好了,言归正传,为了让我们自定义的监听接口可以有效工作起来,我们还需要指定它要监听的目录:

        public void initMonitorForChange(String basePath){
               try {
                      this.fileManager = VFS.getManager();
                     
                     File file = new File(basePath + GlobalSetting.JAR_FOLDER);
                     FileObject monitoredDir = this.fileManager .resolveFile(file.getAbsolutePath());
                     FileListener fileMonitorListener = new JarFileChangeListener();
                      this.fileMonitor = new DefaultFileMonitor(fileMonitorListener);
                      this.fileMonitor .setRecursive(true);
                      this.fileMonitor .addFile(monitoredDir);
                      this.fileMonitor .start();
                     System. out.println("Now to listen " + monitoredDir.getName().getPath());
                     
              } catch (FileSystemException e) {
                     e.printStackTrace();
              }
       }


这里,就是初始化监听器的地方,我们使用VFS的DefaultFileMonitor完成监听。而监听的目录,就是应用发布目录applications。接下来,为了让整个应用程序可以持续的运行而不会结束,我们修改下启动方法:
        public static void main(String[] args){
              
              Thread t = new Thread(new Runnable() {
                     
                      @Override
                      public void run() {
                           ApplicationManager manager = ApplicationManager.getInstance();
                           manager.init();
                     }
              });
              
              t.start();
              
               while(true ){
                      try {
                           Thread. sleep(300);
                     } catch (InterruptedException e) {
                           e.printStackTrace();
                     }
              }
       }


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

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

最后,如果本文有什么地方说的不准确,欢迎指正,谢谢!
  • 30
    点赞
  • 136
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
面试题包括以下十九部分:Java 基础、容器、多线程、反射、对象拷贝、Java Web 模块、异常、网络、设计模式、Spring/Spring MVC、Spring Boot/Spring Cloud、Hibernate、Mybatis、RabbitMQ、Kafka、Zookeeper、MySql、Redis、JVM 。 目录: 一、Java 基础 1.JDK 和 JRE 有什么区别? 2.== 和 equals 的区别是什么? 3.两个对象的 hashCode()相同,则 equals()也一定为 true,对吗? 4.final 在 java 中有什么作用? 5.java 中的 Math.round(-1.5) 等于多少? 6.String 属于基础的数据类型吗? 7.java 中操作字符串都有哪些类?它们之间有什么区别? 8.String str="i"与 String str=new String(“i”)一样吗? 9.如何将字符串反转? 10.String 类的常用方法都有那些? 11.抽象类必须要有抽象方法吗? 12.普通类和抽象类有哪些区别? 13.抽象类能使用 final 修饰吗? 14.接口和抽象类有什么区别? 15.java 中 IO 流分为几种? 16.BIO、NIO、AIO 有什么区别? 17.Files的常用方法都有哪些? 二、容器 18.java 容器都有哪些? 19.Collection 和 Collections 有什么区别? 20.List、Set、Map 之间的区别是什么? 21.HashMap 和 Hashtable 有什么区别? 22.如何决定使用 HashMap 还是 TreeMap? 23.说一下 HashMap 的实现原理? 24.说一下 HashSet 的实现原理? 25.ArrayList 和 LinkedList 的区别是什么? 26.如何实现数组和 List 之间的转换? 27.ArrayList 和 Vector 的区别是什么? 28.Array 和 ArrayList 有何区别? 29.在 Queue 中 poll()和 remove()有什么区别? 30.哪些集合类是线程安全的? 31.迭代器 Iterator 是什么? 32.Iterator 怎么使用?有什么特点? 33.Iterator 和 ListIterator 有什么区别? 34.怎么确保一个集合不能被修改? 三、多线程 35.并行和并发有什么区别? 36.线程和进程的区别? 37.守护线程是什么? 38.创建线程有哪几种方式? 39.说一下 runnable 和 callable 有什么区别? 40.线程有哪些状态? 41.sleep() 和 wait() 有什么区别? 42.notify()和 notifyAll()有什么区别? 43.线程的 run()和 start()有什么区别? 44.创建线程池有哪几种方式? 45.线程池都有哪些状态? 46.线程池中 submit()和 execute()方法有什么区别? 47.在 java 程序中怎么保证多线程的运行安全? 48.多线程锁的升级原理是什么? 49.什么是死锁? 50.怎么防止死锁? 51.ThreadLocal 是什么?有哪些使用场景? 52.说一下 synchronized 底层实现原理? 53.synchronized 和 volatile 的区别是什么? 54.synchronized 和 Lock 有什么区别? 55.synchronized 和 ReentrantLock 区别是什么? 56.说一下 atomic 的原理? 四、反射 57.什么是反射? 58.什么是 java 序列化?什么情况下需要序列化? 59.动态代理是什么?有哪些应用? 60.怎么实现动态代理? 五、对象拷贝 61.为什么要使用克隆? 62.如何实现对象克隆? 63.深拷贝和浅拷贝区别是什么? 六、Java Web 64.jsp 和 servlet 有什么区别? 65.jsp 有哪些内置对象?作用分别是什么? 66.说一下 jsp 的 4 种作用域? 67.session 和 cookie 有什么区别? 68.说一下 session 的工作原理? 69.如果客户端禁止 cookie 能实现 session 还能用吗? 70.spring mvc 和 struts 的区别是什么? 71.如何避免 sql 注入? 72.什么是 XSS 攻击,如何避免? 73.什么是 CSRF 攻击,如何避免? 七、异常 74.throw 和 throws 的区别? 75.final、finally、finalize 有什么区别? 76.try-catch-finally 中哪个部分
JAVA相关基础知识 1、面向对象的特征有哪些方面 1.抽象: 抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只是选择其中的一部分,暂时不用部分细节。抽象包括两个方面,一是过程抽象,二是数据抽象。 2.继承: 继承是一种联结类的层次模型,并且允许和鼓励类的重用,它提供了一种明确表述共性的方法。对象的一个新类可以从现有的类中派生,这个过程称为类继承。新类继承了原始类的特性,新类称为原始类的派生类(子类),而原始类称为新类的基类(父类)。派生类可以从它的基类那里继承方法和实例变量,并且类可以修改或增加新的方法使之更适合特殊的需要。 3.封装: 封装是把过程和数据包围起来,对数据的访问只能通过已定义的界面。面向对象计算始于这个基本概念,即现实世界可以被描绘成一系列完全自治、封装的对象,这些对象通过一个受保护的接口访问其他对象。 4. 多态性: 多态性是指允许不同类的对象对同一消息作出响应。多态性包括参数化多态性和包含多态性。多态性语言具有灵活、抽象、行为共享、代码共享的优势,很好的解决了应用程序函数同名问题。 2、String是最基本的数据类型吗? 基本数据类型包括byte、int、char、long、float、double、boolean和short。 java.lang.String类是final类型的,因此不可以继承这个类、不能修改这个类。为了提高效率节省空间,我们应该用StringBuffer类 3、int 和 Integer 有什么区别 Java 提供两种不同的类型:引用类型和原始类型(或内置类型)。Int是java的原始数据类型,Integer是java为int提供的封装类。Java为每个原始类型提供了封装类。 原始类型封装类 booleanBoolean charCharacter byteByte shortShort intInteger longLong floatFloat doubleDouble 引用类型和原始类型的行为完全不同,并且它们具有不同的语义。引用类型和原始类型具有不同的特征和用法,它们包括:大小和速度问题,这种类型以哪种类型的数据结构存储,当引用类型和原始类型用作某个类的实例数据时所指定的缺省值。对象引用实例变量的缺省值为 null,而原始类型实例变量的缺省值与它们的类型有关。 4、String 和StringBuffer的区别 JAVA平台提供了两个类:String和StringBuffer,它们可以储存和操作字符串,即包含多个字符的字符数据。这个String类提供了数值不可改变的字符串。而这个StringBuffer类提供的字符串进行修改。当你知道字符数据要改变的时候你就可以使用StringBuffer。典型地,你可以使用StringBuffers来动态构造字符数据。 5、运行时异常与一般异常有何异同? 异常表示程序运行过程中可能出现的非正常状态,运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误。java编译器要求方法必须声明抛出可能发生的非运行时异常,但是并不要求必须声明抛出未被捕获的运行时异常。 6、说出Servlet的生命周期,并说出Servlet和CGI的区别。 Servlet被服务器实例化后,容器运行其init方法,请求到达时运行其service方法,service方法自动派遣运行与请求对应的doXXX方法(doGet,doPost)等,当服务器决定将实例销毁的时候调用其destroy方法。 与cgi的区别在于servlet处于服务器进程中,它通过多线程方式运行其service方法,一个实例可以服务于多个请求,并且其实例一般不会销毁,而CGI对每个请求都产生新的进程,服务完成后就销毁,所以效率上低于servlet。 7、说出ArrayList,Vector, LinkedList的存储性能和特性 ArrayList和Vector都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢,Vector由于使用了synchronized方法(线程安全),通常性能上较ArrayList差,而LinkedList使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。 8、EJB是基于哪些技术实现的?并说出SessionBean和EntityBean的区别,StatefulBean和StatelessBean的区别。 EJB包括Session Bean、Entity Bean、Message Driven Bea

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值