热部署实现

对于Java应用程序来说,热部署就是在服务器运行时重新部署项目

热部署在java应用中可以说是非常常见的一个技术了,springboot内部就使用了热部署。

注意,热部署与热加载是不同的技术,热部署一般用在生产环境,而热加载一般用在开发环境。热部署是对整个应用的整体替换,而热加载是对某个class进行替换。

 

要想实现热部署,我们必须对java的classloader机制有一定的了解,当然了解这些网上有很多好的文章,大家可以去了解一下,我这里只做概述,帮助大家回忆。

1.不同类加载器加载的同一个class文件,在jvm中生成的class对象是不同的,他们之间也不能进行类型转换。

2.双亲委派机制,某个类加载器在加载某个类之前,会先让自己的父类加载器去加载这个类,如果父类加载成功了,自己就不会再进行加载了,没有加载到才由自己尝试进行加载。我们要实现热部署必须要打破这种机制,否则会导致我们修改后的.class文件不能被重新的加载,因为一个类加载器对象只会加载某个类一次,已经加载的类就不会去重新加载class文件了,如果我们自定义的类加载器有双亲委派机制,他会先交给AppClassLoader这个类加载器去进行加载,这就导致了即使我们换了自定义classLoader对象,也不能使被修改过的class重新加载。

3.classloader的全盘委托机制,这个机制决定了我们应用的热部署的关键所在,可以让我们灵活的替换运行中的应用。

什么叫全盘委托机制,即当你使用某个自定义类加载器加载了某个class之后,那么这个class中所关联的其他的类,也都由我们这自定义的类加载器来完成,这个就非常的关键

 

自定义的类加载器

package com.cp.classloader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * @author 12130
 * @date 2019/11/11
 * @time 19:59
 */
public class MyClassLoader extends ClassLoader {
    /**
     * 当前类加载器的根路径
     */
    public String rootPath;

    /**
     * 当前类加载器能加载的所有的类
     */
    public List<String> clazzName;

    public MyClassLoader(String rootPath, String... classpath) throws IOException {
        this.rootPath = rootPath;
        clazzName = new ArrayList<>();
        // 扫描并加载类  打破双亲委派机制
        for (String path : classpath) {
            findClass(new File(rootPath + path));
        }
    }

    private void findClass(File file) throws IOException {
        if (file.isDirectory()) {
            final File[] files = file.listFiles();
            for (File f : files) {
                findClass(f);
            }
        } else {
            FileInputStream fin = new FileInputStream(file);
            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            int d;
            while ((d = fin.read()) != -1) {
                bout.write(d);
            }
            fin.close();
            byte[] bytes = bout.toByteArray();
            String className = pathToClassName(file);
            clazzName.add(className);
            // 在当前类加载器中进行的define,该class就会被记录在当前类加载器中
            defineClass(className, bytes, 0, bytes.length);
        }
    }

    private String pathToClassName(File file) {
        String className = file.getAbsolutePath().replace(rootPath, "");
        String replace = className.replace(File.separator, ".");
        return replace.substring(0, replace.lastIndexOf(".class"));
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        // 查看当前类是否被当前类加载器加载过
        Class<?> loadedClass = findLoadedClass(name);
        if (loadedClass == null) {
            if (!clazzName.contains(name)) {
                loadedClass = getSystemClassLoader().loadClass(name);
            } else {
                throw new ClassNotFoundException();
            }
        }
        return loadedClass;
    }
}

代表一个应用,用该类作为中介去监听class文件的变化和应用的重新加载

package com.cp.boot;

import com.cp.classloader.MyClassLoader;
import com.cp.listener.ClassFileChangeListener;
import org.apache.commons.io.filefilter.FileFilterUtils;
import org.apache.commons.io.filefilter.IOFileFilter;
import org.apache.commons.io.monitor.FileAlterationMonitor;
import org.apache.commons.io.monitor.FileAlterationObserver;

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @author 12130
 * @date 2019/11/11
 * @time 21:16
 */
public class Application {
    /**
     * 程序根路径
     */
    public String rootPath;
    /**
     * 热部署程序的包路径,形如 com.cp.app
     */
    public String classPath;
    private FileAlterationMonitor monitor;
    /**
     * 需要被热部署的应用
     */
    private Object app;
    private String hotSwapClassName;
    private String initMethodName;

    /**
     * @param clazz            程序的根路径,绝对路径
     * @param hotSwapClassPath 需要被热替换的应用的class路径,形如 com\\cp\\a
     * @param hotSwapClassName 热替换应用的启动类
     * @param initMethodName   热替换应用的启动方法
     * @throws Exception
     */
    public Application(Class clazz, String hotSwapClassPath, String hotSwapClassName, String initMethodName) throws Exception {
        classPath = hotSwapClassPath;
        this.hotSwapClassName = hotSwapClassName;
        this.initMethodName = initMethodName;
        rootPath = clazz.getResource("/").getPath().substring(1).replace("/", File.separator);
        startFileMonitor(rootPath + classPath);
    }

    public Object getApp() {
        return app;
    }

    /**
     * 启动热部署的应用
     * @param classLoader   加载该应用使用的类加载器
     * @throws Exception
     */
    private void start0(ClassLoader classLoader) throws Exception {
        final Class<?> aClass = classLoader.loadClass(hotSwapClassName);
        app = aClass.newInstance();
        Method method = aClass.getMethod(initMethodName);
        new Thread(() -> {
            try {
                method.invoke(app);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }, "application-run-thread").start();

    }

    public void run() throws Exception {
        // 每一次加载都新建一个类加载器来进行加载,这样才能加载到新的class
        MyClassLoader classLoader = new MyClassLoader(rootPath, classPath);
        start0(classLoader);
    }

    /**
     * 文件变化监听
     *
     * @param monitorPath 监听的路径
     * @throws Exception
     */
    private void startFileMonitor(String monitorPath) throws Exception {
        IOFileFilter filesFilter = FileFilterUtils.and(
                FileFilterUtils.fileFileFilter(),
                FileFilterUtils.suffixFileFilter(".class"));
        ClassFileChangeListener classFileChangeListener = new ClassFileChangeListener();
        classFileChangeListener.setApplication(this);
        FileAlterationObserver observer = new FileAlterationObserver(new File(monitorPath), filesFilter);
        observer.addListener(classFileChangeListener);
        monitor = new FileAlterationMonitor(2000, observer);
        // 开始监控
        monitor.start();
    }

    /**
     * 关闭程序
     *
     * @throws Exception
     */
    public void close() throws Exception {
        if (monitor != null) {
            monitor.stop();
        }
    }
}

自定义的文件变化监听器,以此来监听文件的变化,然后通知上面的Application进行重新的加载 

package com.cp.listener;

import com.cp.boot.Application;
import org.apache.commons.io.monitor.FileAlterationListenerAdaptor;

import java.io.File;
import java.lang.reflect.Method;

/**
 * @author 12130
 * @date 2019/11/11
 * @time 21:30
 */
public class ClassFileChangeListener extends FileAlterationListenerAdaptor {
    private static final String LISTENER_SUFFIX = ".class";
    /**
     * 用来记录之前服务
     */
    private Application application;


    @Override
    public void onFileChange(File file) {
        try {
            Method closeMethod;
            // 加载启动了新的服务,必须要关闭之前的服务,否则之前的服务也会一直运行
            if (application.getApp() != null && (closeMethod = application.getApp().getClass().getMethod("close")) != null) {
                closeMethod.invoke(application.getApp());
            }
            /**
             * 重新进行应用的加载
             */
            application.run();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }


    public void setApplication(Application application) {
        this.application = application;
    }
}

测试启动 

public class Main {

    /**
     * 类加载器有两个机制
     * 1.双亲委派机制
     * 2.全盘委托机制(一个类加载器加载了一个class之后,该class所引用的所有的类都是该类的类加载器进行加载)
     * 也就是说如果我们使用自定义类加载器加载了一个类,那么该类中引用的所有的类以后都交给该自定义类加载器
     */
    public static void main(String[] args) throws Exception {
        Application app = new Application(Main.class, "com\\cp\\app\\", "com.cp.app.Tomcat", "run");
        app.run();
    }
}

测试的应用 

public class Tomcat implements Closeable {

    private boolean state = true;

    public void run() {
        while (state) {
            System.out.println("程序运行中 version-123   ");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void close() throws IOException {
        state = false;
    }
}

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值