1 需求

spring boot项目通过上传jar包的方式,自动加载jar包中的类。应用场景:用户上传驱动实现,服务实现热部署。

2 实现过程

要实现热部署功能,我们首先需要实现两个基础功能:1 加载指定路径的class文件  2 指定路径下的文件有变动时,触发事件,重新加载

 

2.1 多jar包应用实现

2.1.1 类加载设计

1 需要加载其他路劲下的jar包,只能由代码自身来引入一个类加载器,指定加载某一路径下的jar包,并且在没有特殊情况下不破坏双亲委派机制。

2 ide上运行的程序,是使用的jvm默认的类加载机制,从main函数启动,但是spring boot的可运行jar包,是在原有的基础上再包装了一层启动配置。并引入了自己的类加载器来处理这个比较特殊的jar包。

(springboot类加载机制:http://hengyunabc.github.io/spring-boot-classloader/

java项目有没有办法自动热加载配置文件 jvm热加载_jar包

                                                                         图1  ide下类加载图和可执行jar包下类加载图

 

 

3 思考一下下图所示的类加载图能否实现需求?

java项目有没有办法自动热加载配置文件 jvm热加载_类加载_02

                             图2 可执行jar包下破坏双亲委派的类加载图

 

说明:这类加载图是我们在实现热部署需求时,比较易出现的现象,需要先对spring boot可执行jar的类加载机制有个初步认识。

解释: 这个类加载模式只有在self classLoader 和主程序不存在共用业务类时,可以正常使用(self classLoader和主程序业务完全独立),但是目前的需求下不能做到独立,所以该模式会有问题

 

2.1.2 代码实践

获取加载指定路径的类加载器,特别注意:新建类加载器对象时,需要指定其父类加载器,如果不指定:classLoader = new URLClassLoader(urls),默认为appClassLoader,这样就会造成图2 所示的的情形

public class ClassLoaderContext {

    public static String DEFAULT_PATH = "lib";

    private static ClassLoader classLoader;
    ....
  
    public static ClassLoader getClassLoader() {
        if (null == classLoader) {
            String libPath = FileUtil.getDefaultPath(DEFAULT_PATH);
            List<File> files = pathToJarFile(libPath);
            URL[] urls = fileToUrl(files);
//            classLoader = new URLClassLoader(urls);
            classLoader = new URLClassLoader(urls, ClassLoaderContext.class.getClassLoader());
        }
        return classLoader;
    }
}

 

由指定路径的类加载来加载class,调用业务逻辑

//获取类加载器
        ClassLoader cloader = ClassLoaderContext.getClassLoader();
        String result = null;
        try {
            Class clazz = cloader.loadClass("com.example.timyag.service.impl.TestServiceImpl");
            result =  ((ITestService)clazz.newInstance()).sayHello(p1);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
        return result;

 

由于接口 ITestService类文件在两个jar包中对会存在(不同时存在的话,编译都不过了,不多说看代码)。所以在图2 这种类加载图下,双亲委派被破坏,ITestService类被加载了两次,分别被加载在LaunchedURLClassLoader和self ClassLoader里,导致代码  “((ITestService)clazz.newInstance())” 就会报错,因为实例对象(clazz.newInstance())不是LaunchedURLClassLoader加载的ITestService接口的实现类,强制转换失败。

 

2.2 路径监听

系统热部署需要感知路径下文件的变化,及时重新加载类文件。我们使用 JDK 1.7提供的WatchService,利用底层文件系统提供的功能

参考:

http://www.oudahe.com/p/39580/

https://docs.oracle.com/javase/tutorial/essential/io/notification.html#concerns

 

主要业务代码:

在监听到路径下文件有变化时,通知订阅者。

public class PathWatcher extends Observable {

    @Getter
    private String watchPath;

    public long lastMod;

    public PathWatcher(String watchPath) {
        this.watchPath = watchPath;
    }

    public void doWatch() {
        try {
            final Path path = FileSystems.getDefault().getPath(watchPath);
            final WatchService watchService = FileSystems.getDefault().newWatchService();
            path.register(watchService, ENTRY_CREATE, ENTRY_MODIFY);
            boolean UPDATED = false;
            while (true) {
                final WatchKey wk = watchService.take();
                for (WatchEvent<?> event : wk.pollEvents()) {
                    WatchEvent.Kind<?> kind = event.kind();
                    if (kind == OVERFLOW) {
                        continue;
                    }
                    Path changed = (Path) event.context();
                    Path absolute = path.resolve(changed);
                    File configFile = absolute.toFile();
                    long lastModified = configFile.lastModified();
                    // 利用文件时间戳,防止触发两次
                    if (lastModified != lastMod && configFile.length() > 0) {
                        // 保存上一次时间戳
                        lastMod = lastModified;
                        UPDATED = true;
                    }
                }

                if (UPDATED) {
                    System.out.println("path file changed");
                    setChanged();
                    //通知订阅者
                    notifyObservers();
                    UPDATED = false;

                }
                // reset the key
                boolean valid = wk.reset();
                if (!valid) {
                    System.out.println("watch key invalid!");
                    break;
                }
                //10秒刷新一次
                Thread.sleep(10 * 1000);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

2.3 类重新加载

在更新了jar包之后,我们需要重新加载class类,实现起来很简单,只需要新建一个类加载器。

主逻辑:在接受到发布者发布的文件变更通知时,将静态的classLoader置为null。下载使用该类加载器时,就能新建一个对象。

public void reloadClassLoaderAndClass(String path) {
        System.out.println("reload class");
        classLoader = null;
//        getNewClassLoader(path);
    }

    @Override
    public void update(Observable o, Object arg) {
        reloadClassLoaderAndClass(((PathWatcher) o).getWatchPath());
    }

 

 

3 小结

git项目地址:https://gitee.com/guijiaoqqq/spring-boot-hot-deploy 分支:hot_deploy_simple

后续优化点:

1 外部jar包类的使用,需要在主程序中指明类名(Class clazz = cloader.loadClass("com.example.timyag.service.impl.TestServiceImpl");),这个耦合性太大,参考jdbc的驱动引入方式,可以使用SPI机制,由外部jar包来指定实现类类名。

2 外部jar包类在使用时,不能总是实例化一个对象来使用, 希望能将实例对象注入到spring中,后续使用都有spring容器来管理,在热部署重新加载类时,又要及时更新注入的实例对象

3 如果热部署频率高,那么加载进的类会越来越多,我们需要及时的去卸载。但是我们没有办法直接控制它的卸载,只能到jvm触发GC的时候才会去卸载多余的类