设计一个支持热加载的Java应用启动器

热加载是指在不重启服务的情况下使更改的代码生效。注意和热部署的区别,热加载主要是在开发环境下使用。

首先要知道Java程序是怎么运行起来的,Java类加载分为其7个阶段。

phase

其中加载阶段是用户可以自定义,而验证阶段、准备阶段、解析阶段、初始化阶段都是用 JVM 来处理的。
整个类加载是在Java 中一个叫做类加载器上进行的,如果我们能程序更改后,让程序所在的进程能够实时的获取到编译后的Class类字节码信息,然后重新加载的话,那么就可以实现热加载功能。

Java 类加载器

类加载器,顾名思义就是加载Java类的工具,Java默认设置了三个类加载器。

  • BootstrapClassloader
  • ExtClassloader
  • AppClassloader

BootstrapClassloader 叫做启用类加载器,用于加载JRE核心类库,使用C++实现。加载路径%JAVA_HOME%/lib下的所有类库。

ExtClassloader 扩展类加载器,加载%JAVA_HOME%/lib/ext中的所有类库。

AppClassloader 应用类加载器也叫系统类加载器System Classloader,加载%CLASSPATH%路径下的所有类库。

Java 也提供了扩展,可以让我们自己实现类加载的功能。类加载器在Java中是java.lang.ClassLoader这个类,如果要自定义类加载器,只要实现这个类,重写加载方法就好了。

在Java中,由不同的类加载器加载的两个相同的类在Java虚拟机里是两个不同的类,那么Java是怎么确保一个类不会被多个类加载器重复加载,并且保证核心API不会被篡改的呢?

这就需要Java的双亲委派机制。

双亲委派机制

classloader

当一个类加载器接到加载类的请求后,首先会交给父类去加载,如果所有父类都无法加载,自己加载,并将被加载的类缓存起来。。

每加载一个类,所有的类加载器都会判断是否可以加载,最终会委托到启动类加载器来首先加载。所有的类的加载都尽可能由顶层的类加载器加载,这样就保证了加载的类的唯一性。

启动类加载器、扩展类加载器、应用程序类加载器,都有自己加载的类的范围,因此并不是所有的类父类都可以加载。

Java 类加载器中还有一个全盘委托机制,当指定一个ClassLoader加载一个类时,该类所依赖或者引用的类也会由这个类加载器来加载,除非显示的用别的类加载器加载。

比如:程序入口默认用的是AppClassloader,那么以后创建出来的类也是用AppClassloader来加载,除非自己显示的用别的类加载器去加载。

热加载

OK,有了以上铺垫,现在可以来实现热加载的功能了,怎么实现呢?

1、首先要实现自己的类加载器,破坏双亲委派机制。
2、通过自定义的类加载器加载所需的类。
3、不断的轮询判断类是否有变化,如果有变化重新加载。

自定义类加载器

public class MyClassLoader extends ClassLoader {


    private static final String SUFFIX = ".class";

    private String rootPath;

    public MyClassLoader(String rootPath) {
        this.rootPath = rootPath;
    }

    /**
     * 破坏双亲委派机制,自定义类加载方式
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        Class<?> loadedClass = findLoadedClass(name);
        if (null == loadedClass) {
            try {
                return findClass(name);
            } catch (ClassNotFoundException e) {
                return super.loadClass(name);
            }
        }

        return loadedClass;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String path = rootPath + name.replace(".", "/") + SUFFIX;
        File file = new File(path);
        byte[] classBytes = null;
        try {
            classBytes = getClassBytes(file);
        } catch (Exception e) {
        }
        if (null != classBytes) {
            if (null != super.findLoadedClass(name)) {
                return super.findLoadedClass(name);
            }
            Class<?> aClass = defineClass(name, classBytes, 0, classBytes.length);
            if (null != aClass) {
                return aClass;
            }
        }
        return super.findClass(name);
    }

    /**
     * 加载类
     * @param file
     * @return
     * @throws Exception
     */
    private byte[] getClassBytes(File file) throws Exception {
        FileInputStream fileInputStream = new FileInputStream(file);
        FileChannel fc = fileInputStream.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel writableByteChannel = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);
        while (true) {
            int read = fc.read(by);
            if (read == 0 || read == -1) {
                break;
            }
            by.flip();
            writableByteChannel.write(by);
            by.clear();
        }
        fileInputStream.close();
        return baos.toByteArray();
    }
}

自定义类加载器重写了loadClass()方法和findClass方法,破坏了Java的双亲委派机制,先通过自定义的类加载所需的类,如果加载不到,再交给父类加载。

接下来,写启动器

public class Run {

    public static String rootPath;

    public static void run(Class cl) {
        rootPath = cl.getClass().getResource("/").getPath();
        MyClassLoader myClassLoader = new MyClassLoader(rootPath);
        startFileListener(rootPath);
        start0(myClassLoader);
    }

    public static void startFileListener(String rootPath) {
        FileAlterationObserver fileAlterationObserver = new FileAlterationObserver(rootPath);
        fileAlterationObserver.addListener(new FileListener());
        FileAlterationMonitor fileAlterationMonitor = new FileAlterationMonitor(5);
        fileAlterationMonitor.addObserver(fileAlterationObserver);
        try {
            fileAlterationMonitor.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    public static void start0(MyClassLoader classLoader) {
        Class<?> clazz = null;
        try {
            clazz = classLoader.findClass("com.example.Run");
            clazz.getMethod("start").invoke(clazz.newInstance());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 模拟启动应用程序
     */
    public static void start() {
        Application application = new Application();
        application.printApplicationName();
    }
}

run() 方法是入口,首先自定义了加载器,然后设置了文件监听,这个文件监听用的是commons-io

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.4</version>
</dependency>

最后调用start0来启动项目,在start0里通过自定义的类加载器又重新加载了Run本身,然后通过反射调用start()方法,start()方法里启动真正的项目。这样的目的是因为类加载器中的全盘委托机制。Java 默认用的是AppClassloader,所以只能显示的通过自定义的类加载器来加载启动类,再启动真正的项目。

文件的监听,使用commons-io

public class FileListener extends FileAlterationListenerAdaptor {

    @Override
    public void onFileCreate(File file) {
        System.out.println(file.getName());
        if (file.getName().indexOf(".class") != -1) {

            try {
                MyClassLoader myClassLoader = new MyClassLoader(Run.rootPath);
                Run.start0(myClassLoader);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        super.onFileCreate(file);
    }


    @Override
    public void onFileChange(File file) {
        System.out.println(file.getName());
        if (file.getName().indexOf(".class") != -1) {

            try {
                MyClassLoader myClassLoader = new MyClassLoader(Run.rootPath);
                Run.start0(myClassLoader);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

通过监听文件的创建和修改,如果文件有变化,定义一个新的类加载器,重新运行项目。

模拟的真正项目

public class Application {

    public void printApplicationName() {
        System.out.println("应用程序777");
    }
}

好了,现在来测试一下项目

public class Main {

    public static void main(String[] args) {
        Run.run(Main.class);
    }
}

设置一下idea,让编译器可以自动编译
idea

现在修改ApplicationprintApplicationName输出的内容,等编译器编译完后,可以看到修改的内容了。

本文只是一个供学习使用的简单小小的例子,项目github地址:https://github.com/yaocl0/hot-loading

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值