Spring Boot热部署

  组成微服务架构的重要组成部分就是Spring Boot,它能大大的提高我们的开发效率,同时另一个提升开发效率的很重要的技术就是热部署技术。

一.热部署的使用场景

1.场景:本地调试、线上发布

  会不会还会因为改动一行代码而重新编译提交到服务器,重新启动线上的服务器加载修改的代码呢?每次重启服务器会浪费大量的时间。

2.思考

  一些网站或者服务,全年不间断运行,即使重新发布程序后也不需要重启服务,他们是如何做到的呢?

3.优点

  a.无论本地还是线上,都适用

  b.无需重启服务器

    提高开发、调试效率

    提升发布、运维效率,降低运维成本

 

二.热部署与热加载

1.Java热部署与热加载的联系

  a.不重启服务器编译/部署项目

  b.基于Java的类加载器实现

2.Java热部署与热加载的区别

  a.部署方式

    热部署在服务器运行时重新部署项目

    热加载在运行时重新加载class

  b.实现原理

    热部署直接重新加载整个应用

    热加载在运行时重新加载class,热加载它的主要原理依赖Java的类加载机制,在实现方式可以概括为在容器启动时启一条后台线程,定时检测类文件的时间戳变化,如果类的时间戳变了就将类重新载入,侧重在运行时通过重新加载改变类的信息直接改变程序行为

  c.使用场景

    热部署更多的是在生产环境使用

    热加载则更多的是在开发环境使用

    出于安全性的考虑,热加载这种直接修改Java虚拟机中字节码的方式是难以监控的,不同于SQL语句的执行可以记录日志,字节码的修改几乎无法代码逻辑的变化,对既有代码的影响也是比较难以控制的,对于超注重安全的应用,加载风险越高。在生产中使用类的热加载还是比较危险的,这种方式是难以监控,出于适用的情景考虑,技术大部分和需求挂勾,使用热部署的需求是频繁的部署并且启动耗时长的应用、无法停止服务的应用,在这些适用的生产情景中,都是使用热部署而不是热加载,相反在开发环境,热加载使用的比较多,因为开发调试中,频繁启动应用随处可见,热加载机制可以极大提高开发效率,在开发中还有另一种称呼叫开发者模式。

 

三.热部署原理解析

1.Java类的加载过程

  加载class文件过程中,最主要的是类加载器,ClassLoader这个类加载器它是用来加载Java类到虚拟机中的,和C、C++不一样的是,Java程序并不是本地的可执行程序,当运行Java程序时,首先要运行Java虚拟机,然后再把Java的class文件加到Java虚拟机里面运行,负责加载class这部分就叫做ClassLoader,也就是类加载器。

  我们从图上也可以这样简单的理解Java类的加载过程,首先是把Java文件编译为字节码文件,再把这些字节码序列化成相应的串,然后再把这些串编译成源码对象,再把这些源码对象编译成.class文件,类加载器也就是ClassLoader负责将class文件加载到虚拟机中解析运行,总之,运行Java文件不同于运行一些本地的程序,它是先运行Java虚拟机,然后再把.class文件放到虚拟机中运行。

2.类加载的五个阶段

  加载->验证->准备->解析->初始化。

  加载:找到类的静态存储结构并加载到虚拟机里面,然后转换成方法区的运行时数据结构,生成class对象,加载阶段用户可以自定义加载器加载进来,也就是说加载阶段是允许用户自定义加载器参与进来的。

  验证:验证阶段是确保字节码是安全的,确保不会对虚拟机的安全造成危害,可以通过Java虚拟机的一些启动参数来禁用一些验证,不过,在这里不推荐在验证阶段去通过一些设置去禁用一些验证,毕竟参数禁用了之后,可能会对虚拟机的安全造成危害。

  准备:准备阶段是确定内存布局,初始化类变量,这里有一个注意点,是赋初始值,不会执行程序自已定义的赋值操作,比如我们定义了一个静态的私有变量,比如private static int count=12;在准备阶段并不会赋值为12,会赋值为0。

  解析:将符号引用变成直接引用

  初始化:调用程序自定义的代码,比如private static int count=12;这里的count会赋值为12。同一个类加载器中,只会将一个类型初始化一次,Java虚拟机没有强制约束什么时候开始初始化阶段,但是规定了有且只有五种情况它是必须立刻进行初始化的,也就是说Java虚拟机中它规定有且只有五种情况必须立刻进行我们最后的初始化过程,当然在初始化之前,加载、验证、准备、解析都已经验证过了,下面我们来看一下初始化的时机,讲一下五个初始化的时机都是什么?

  a.遇到了new、get static、pull static、invoke static这四条字节码指令的时候,如果类没有初始化,则需要触发初始化。在这里需要注意的是:final修饰的类会在编译期把结果放到常量池,即使调用也不会触发初始化,final它修饰的是常量,会把常量放到常量池里面,调用常量不会触发初始化

  b.使用java.lang.ref包里面的方法,也就是对类进行反射调用时,如果类没有进行初始化,就需要初始化

  c.当初始化一个类的时候,如果发现其父类还没有进行过初始化,那么需触发其父类的初始化

  d.虚拟机启动的时候,用户需要制定一个要执行的主类,虚拟机会先初始化这个主类,最简单的例子就是我们写一个Java程序,在某一个类里面写了一个main方法,我们通过运行这个main方法来启动我们应用或者说运行我们程序,这时候虚拟机会初始化我们这个main方法所在的类

  e.使用JDK1.7动态语言支持的时候,如果java.lang.InvokeMethodHandler实例最后解析的结果是Ref_GetStatic、Ref_PutStatic、Ref_InvokeStatic方法句柄的时候呢,如果这个句柄对应的类没有初始化,就需要先初始化这个句柄对应的类

3.Java类加载器的特点

  a.由AppClass Loader(系统类加载器)开始加载指定的类

  b.类加载器将加载任务交给其父,如果父找不到,再由自已去加载

  c.Bootstrap Loader(启动类加载器)是最顶级的类加载器

4.Java类的热部署

  a.类的热加载

    Java类的热部署也可以通过Java类的热加载来实现

public class Test extends java.lang.ClassLoader{

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        System.out.println("加载类==="+name);
        byte[] data = loadClassData(name);
        return this.defineClass(name, data, 0, data.length);
    }
}

    它继承了java.lang.ClassLoader类,覆写了其中的findClass方法,这种方式中,findClass方法的目标是为自定义的类加载器容纳所有的代码,此时不需要重复写其它的代码,例如当加载时调用系统的ClassLoader,这个是不需要的,ClassLoader需要从源文件获取字节码,一旦找到字节码,则会调用defineClass方法,ClassLoader实例调用此方法也是非常重要的,因此,如果两个ClassLoader实例定义了两个来自不同或者相同文件的字节码,则被定义的类也将区别对待,因为一个类加载器只能初始化一次,所以说它要区别对待。

  b.配置Tomcat来实现热部署

    方法一:直接把项目web文件夹放在webapps里

    方法二:在tomcat\conf\server.xml中的<host></host>内部添加<context/>标签,由<context/>标签它的一些属性,通过它的一些配置来实现热部署

    方法三:在%tomcat_home%\conf\Catalina\localhost中添加一个XML,这种方法注意的是,我们访问的时候,是以localhost中XML文件的名字做为路径的一部分来访问的。

5.Java热部署实例

  实例工程及代码,并打包准备部署包:

  HotPublishServlet.java

package com.bijian.tomcat.test;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Servlet implementation class HotPublishServlet
 */
@WebServlet("/HotPublishServlet")
public class HotPublishServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    /**
     * Default constructor. 
     */
    public HotPublishServlet() {
        // TODO Auto-generated constructor stub
    }

    /**
     * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
     */
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // TODO Auto-generated method stub
        //response.getWriter().append("Served at: ").append(request.getContextPath());
        response.setCharacterEncoding("UTF-8");
        response.setHeader("content-type","text/html;charset=UTF-8");
        response.getWriter().println("我正在学习");
    }

    /**
     * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
     */
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // TODO Auto-generated method stub
        doGet(request, response);
    }
}

  首先在Eclipse中配置Tomcat,然后运行

  浏览器输入:http://localhost:8080/TomcatHotPublish/HotPublishServlet,运行正常,这说明项目是没有问题的

  然后在Eclipse中导出war包

方法一:直接把项目web文件夹放在webapps里

  这时把Eclipse中的tomcat服务停掉,然后点击运行Tomcat的bin目录下的startup.bat,这时再次访问返回404。

  将打包出来的war包解压,放到Tomcat的webapps里

  再次刷新,访问正常。

  其实我们注意观察Tomcat的输出,可以发现,当我们把解压出的TomcatHotPublish整个目录拷贝到webapps里时,就会发现Tomcat容器进行加载,如下所示:

方法二:在tomcat\conf\server.xml中的<host></host>内部添加<context/>标签,由<context/>标签它的一些属性,通过它的一些配置来实现热部署

配置内容如下:

<Context dubug="0" docBase="D:/study/web" path="/hot" privateged="true" reloadable="true"/>

  docBase设置为我们的D:/study/web,path就是我们配置的虚拟路径,把它映射为/hot,然后点击运行Tomcat的bin目录下的startup.bat,浏览器访问http://localhost:8080/hot/HotPublishServlet,返回404,是因为我们没有在D:/study/web部署项目。

  把上面打包出来的war包解压出来的TomcatHotPublish目录下的META-INF、META-INF拷贝到D:/study/web下

  上面标红处也表明它也自动实现了热部署的功能,这个时候,我们访问http://localhost:8080/hot/HotPublishServlet,访问正常。

方法三:在%tomcat_home%\conf\Catalina\localhost中添加一个XML,这种方法注意的是,我们访问的时候,是以localhost中XML文件的名字做为路径的一部分来访问的

  当然,配置实践之前,把方法二中D:/study/web目录下的内容删除,把tomcat\conf\server.xml中新增的配置也删除。

  然后,我们在%tomcat_home%\conf\Catalina\localhost中添加hot.xml文件,内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<Context docBase="D:/study/web" reloadable="true"/>

  然后点击运行Tomcat的bin目录下的startup.bat,浏览器访问http://localhost:8080/hot/HotPublishServlet,返回404

  把上面打包出来的war包解压出来的TomcatHotPublish目录下的META-INF、META-INF拷贝到D:/study/web下

  从Tomcat输出的日志来看,它也完成了web项目的热部署,这个时候,我们访问http://localhost:8080/hot/HotPublishServlet,访问正常。

  特别说明:这种方式,会使用conf\Catalina\localhost下.xml文件的名字做为path属性的名字。

 

四.Java类热加载案例分析

1.写一个Java类热加载的实际案例

  要求:

  a.类层次结构清晰、修改某一个Java类文件不需要重启服务或者重新编译运行程序

  b.可适当的运用一些设计模式使代码结构更加清晰明了,比如工厂模式等

2.代码实例

  a.核心类MyClassLoader.java

package com.bijian.classloader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;

/**
 * 自定义Java类加载器来实现Java类的热加载
 */
public class MyClassLoader extends ClassLoader {
    
    //要加载的Java类的classpath路径
    private String classpath;
    
    public MyClassLoader(String classpath){
        super(ClassLoader.getSystemClassLoader());
        this.classpath = classpath;
    }
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] data = this.loadClassData(name);
        return this.defineClass(name, data, 0, data.length);
    }

    /**
     * 加载class文件中的内容
     * @param name
     * @return
     */
    private byte[] loadClassData(String name) {
        try {
            name = name.replace(".", "//");
            FileInputStream is = new FileInputStream(new File(classpath + name + ".class"));
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int b = 0;
            while ((b = is.read()) != -1) {
                baos.write(b);
            }
            is.close();
            return baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

  b.接口与实现类的定义

package com.bijian.classloader;

/**
 * 实现这个接口的子类需要动态更新
 */
public interface BaseManager {
    public void logic();
}
package com.bijian.classloader;

/**
 * BaseManager的子类,此类需要实现java类的热加载功能
 */
public class MyManager implements BaseManager {

    @Override
    public void logic() {
        System.out.println("我在学习呢,我在学习Spring Boot热部署这门课程");
        System.out.println("来这里学习的人很多");
    }
}

  c.封装加载类的信息

package com.bijian.classloader;

/**
 * 封装加载类的信息
 */
public class LoadInfo {
    //自定义的类加载
    private MyClassLoader myLoader;
    //记录要加载的类的时间戳-->加载的时间
    private long loadTime;
    private BaseManager manager;
    
    public LoadInfo(MyClassLoader myLoader, long loadTime) {
        super();
        this.myLoader = myLoader;
        this.loadTime = loadTime;
    }

    public MyClassLoader getMyLoader() {
        return myLoader;
    }

    public void setMyLoader(MyClassLoader myLoader) {
        this.myLoader = myLoader;
    }

    public long getLoadTime() {
        return loadTime;
    }

    public void setLoadTime(long loadTime) {
        this.loadTime = loadTime;
    }

    public BaseManager getManager() {
        return manager;
    }

    public void setManager(BaseManager manager) {
        this.manager = manager;
    }
}

  d.工厂类的实现

package com.bijian.classloader;

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

/**
 * 加载manager的工厂
 */
public class ManagerFactory {
    //记录热加载类的加载信息
    private static final Map<String, LoadInfo> loadTimeMap = new HashMap<String, LoadInfo>();
    //要加载的类的classpath
    public static final String CLASS_PATH = "D:/develop/study/classloader/bin/";
    //实现热加载的类的全名称(包名+类名)
    public static final String MY_MANAGER = "com.bijian.classloader.MyManager";
    
    public static  BaseManager getManager(String className){
        File loadFile = new File(CLASS_PATH + className.replaceAll("\\.", "/") + ".class");
        long lastModified = loadFile.lastModified();
        //loadTimeMap不包含className为key的LoadInfo信息。证明这个类没有被加载,那么需要加载这个类到JVM
        if(loadTimeMap.get(className) == null){
            load(className, lastModified);
        }//加载类的时间戳变化了,我们同样要重新加载这个类到JVM
        else if(loadTimeMap.get(className).getLoadTime() != lastModified){    
            load(className, lastModified);
        }
        return loadTimeMap.get(className).getManager();
    }

    private static void load(String className, long lastModified) {
        MyClassLoader myClassLoader = new MyClassLoader(CLASS_PATH);
        Class<?> loadClass = null;
        try {
            loadClass = myClassLoader.loadClass(className);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        BaseManager manager = newInstance(loadClass);
        LoadInfo loadInfo = new LoadInfo(myClassLoader, lastModified);
        loadInfo.setManager(manager);
        loadTimeMap.put(className, loadInfo);
    }
    
    //以反射的方式创建BaseManager子类对象
    private static BaseManager newInstance(Class<?> loadClass) {
        try {
            return (BaseManager)loadClass.getConstructor(new Class[]{}).newInstance(new Object[]{});
        } catch (InstantiationException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (SecurityException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return null;
    }
}

  e.后台线程检测类

package com.bijian.classloader;

/**
 * 后台启动一条线程不断刷新重新加载实现了热加载的类
 */
public class MsgHandler implements Runnable {

    @Override
    public void run() {
        while (true) {
            BaseManager manager = ManagerFactory.getManager(ManagerFactory.MY_MANAGER);
            manager.logic();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

  f.测试类的编写

package com.bijian.classloader;

/**
 * 测试Java类的热加载
 */
public class ClassLoaderTest {
    public static void main(String[] args) {
        new Thread(new MsgHandler()).start();
    }
}

3.在Eclipse中Debug方式运行ClassLoaderTest

  运行过程中修改MyManager.java中的内容,在控制台能很快看到改变后的输出

 

  

五.Spring Boot简单介绍

1.Spring Boot简单介绍

  a.是一个全新框架,目的是简化Spring应用的搭建与开发过程

  b.该框架使开发人员不需要定义样板化的配置

  c.从根本上讲,是一些库的集合,构建项目,无需自行管理这些库的版本

2.Spring Boot特点

  a.创建独立的Spring应用程序

  b.嵌入的Tomcat,无需部署WAR文件

  c.简化Maven配置

  d.自动配置Spring

  e.提供生产就绪型功能,如指标、健康检查和外部配置

3.Spring Boot使用场景

  a.开发restful风格的微服务架构

  b.微服务、自动化、横向扩展

  c.精简配置与整合其他工具

 

六.Spring Boot项目的搭建

1.项目介绍

  以Spring Boot快速搭建一个简单的项目,实现如下简单功能:

  a.引入项目的起步依赖

  b.在页面上打印出"Hello Spring Boot"

  为后面的Spring Boot热部署做准备

2.项目搭建

  然后点击“Generate Project”下载,然后用IDEA引入工程

  并配置Maven的如下三处为本机Maven的配置。

  项目依赖的jar包,spring Boot框架会自动帮我们引入进来,这也是Spring Boot极大方便了我们的开发和运维,节省了我们成本。

  HotDeployApplication.java

package com.bijian;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class HotDeployApplication {

    public static void main(String[] args) {
        SpringApplication.run(HotDeployApplication.class, args);
    }
}

  HotDeployController.java

package com.bijian.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.http.HttpServletRequest;

@Controller
public class HotDeployController {

    @RequestMapping(value="/say", method = RequestMethod.GET)
    public String say(HttpServletRequest request) {
        request.setAttribute("say", "Hello Spring Boot");
        return "springBoot";
    }
}

  springBoot.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <title>Hello Spring Boot</title>
</head>
<body>
    <span th:text="${say}"></span>
</body>
</html>

  运行访问结果如下:

 

七.Spring Boot项目构建过程解析

 

 

 

八.Spring Boot项目热部署的实现

 

 

 

 

九.Spring Boot项目的发布方式

 

 

 

十.总结

 

 

 

 

 

 

学习地址:http://www.imooc.com/learn/915

posted on 2019-02-11 18:56  bijian1013  阅读(248)  评论(0编辑  收藏  举报

导航