java热部署功能

        之前android项目里面用到了微信的Tinker热修复框架,果断不能知其然,而不知其所以然啊,于是就一番源码看下去,发现其中很多都是关于 ClassLoader有关,想起刚13年毕业那会写了一个关于ClassLoader小demo,于是依照热修复的思路,看能不能扩存成一个java的热部署功能。花了小半天的时间,总算是搞定,虽不尽完美,但是还是想写到博客上,算作一个小案例。

       1、什么是ClassLoader

一个java程序,由无数个class文件组成,当程序运行时,则会调用主函数入口,开始运行相关的功能,这些功能代码都会被封装在不同的class文件中,当然不会一股脑的全部加载所有的class文件,根据类链接,依次通过ClassLoader 加载到内存中,而只有加载到内存中,才能被其他class所引用,完成相应的功能。

    ClassLoader的生命周期依次为 :加载-》验证-》准备-》解析-》初始化-》使用-》卸载。

java热部署功能_第1张图片       2 类加载

  java提供两种类加载器,一为  Java虚拟机自带的加载器,二为  用户自定义的类加载器。

       Java虚拟机自带的加载器

            • 根类加载器( Bootstrap,使用 c++ 编写,无法在 Java 代码中得到该类)
            • 扩展类加载器( Extension,使用 Java 实现)
            • 系统类加载器( System,应用加载器,使用Java代码实现)
 

      java.lang.ClassLoader 的子类

            • 用户可以定制类的加载方式,由其中defineClass 函数,加载指定的calss流。

      3、加载原理

 ClassLoader使用的是双亲委托机制来实现类的加载,每一个ClassLoader都有一个父类ClassLoader引用,不是继承关系,只是引用关系,当一个ClassLoader需要加载某个类的时候,会在loadClass函数中判断,当前加载器是否加载过这个类,如果没有,则让父加载器去搜索是否加载过,如果有则返回,如果还没有,则在往上抛,一直到Bootstrap ClassLoader 开始试图加载class,先从JDK的核心类库中加载所需要的class文件,如果系统中加载不到,则到Java的扩展类库中加载所需要的class文件,如果还没有,则去App ClassLoader 进行加载,如果它也没有加载得到的话,在由委托发起者,自己去加载本地或者网络上的class文件。如果都没有加载成功,则会抛出ClassNotFoundException异常。

java热部署功能_第2张图片

(图片来自csdnjobinZhang)

   4、为啥要这样?

为什么要使用双亲委托机制?因为可以避免类的重复加载,以及类的安全性,如果用户随便定义一个 Integer 类让ClassLoader加载顶替了java api 中Integer类,就会引发很大的安全问题。而双亲委托机制就很好的解决了这一问题,当一个类被顶层ClassLoader加载完成之后,就不会被顶替加载,就保证了安全性。

   5、 热部署

    既然了解了ClassLoader的加载原理,那我们就可以自己加载指定的class文件,来实现代码的热部署功能。

    首先代码分三部分

    1、自定义ClassLoader部分,用于加载热部署的class文件。

    2、注解IOC注入功能,用于注入class对象。

    3、监听热部署class文件目录,如果目录发生变化,则通过自定义ClassLoader重新加载。

Service层用于模拟热部署的业务层,精力有限,目前就不使用微信的Tinker热修复框架那样通过二进制文件流比对整个工程制作差分包,然后在合成一个新的工程,这里只通过一部分的代码来演示热部署功能点。
 

java热部署功能_第3张图片

 话不多说,上代码

  5.1、自定义ClassLoader

package cn.app.wuzhi.classload;

import java.io.*;

public class MyClassLoader extends ClassLoader {

    private String baseDir;

    private String clazzName;

    public MyClassLoader() {
    }


    /**
     *  指定文件 读取byte 数组
     */
    public byte[] readClassFile(String filename) {
        BufferedInputStream bufferedInput = null;
        ByteArrayOutputStream bytesArray = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        try {
            bufferedInput = new BufferedInputStream(new FileInputStream(filename));
            int bytesRead = 0;
            while ((bytesRead = bufferedInput.read(buffer)) != -1) {
                bytesArray.write(buffer, 0, bytesRead);
            }
        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
        } catch (IOException ex) {
            ex.printStackTrace();
        } finally {
            //关闭 BufferedInputStream
            try {
                if (bufferedInput != null)
                    bufferedInput.close();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
        return bytesArray.toByteArray();
    }

  /**
     *  加载class
     */

    @Override
    public Class findClass(String name) throws ClassNotFoundException {


        String classFile = getClassPath();

        //本地 读取class  文件字节流
        byte[] classbyte = readClassFile(classFile);

        //defineClass 函数  允许用户以byte 数组的方式加载 calss 对象

        Class clazz = defineClass(name, classbyte, 0, classbyte.length);

        return clazz;
    }




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

    public String getBaseDir() {
        return baseDir;
    }

    public void setBaseDir(String baseDir) {
        this.baseDir = baseDir;
    }

    public void setClazzName(String clazzName) {
        this.clazzName = clazzName;
    }

    public MyClassLoader(String baseDir, String clazzName) {
        this.baseDir = baseDir;
        this.clazzName = clazzName;
    }

    public String getClassPath() {
        return baseDir +"/"+clazzName;
    }

}

 5.2 热部署补丁文件目录监听

  这一块代码就不贴了,用的是NIO的WatchService,网上代码讲解得也非常详细。

 5.3 类加载与IOC注入

首先在Service上标记一个注解,定义一个name,作为此Service的唯一标识。IOC注入时,以此为标识查找。

java热部署功能_第4张图片

IocFactory 类用于对象注入,代码不多。

package cn.app.wuzhi.iocinjection;

import cn.app.wuzhi.IocTest;
import cn.app.wuzhi.classload.MyClassLoader;
import cn.app.wuzhi.filechange.FileListener;
import cn.app.wuzhi.filechange.FileWatcher;
import cn.app.wuzhi.iocinjection.annotation.AotoWrite;
import cn.app.wuzhi.iocinjection.annotation.Service;

import java.io.File;
import java.lang.reflect.Field;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;

public class IocFactory {


    //工厂主目录
    private String baseDir = "D:/it.work/classLoaderTest/out/production/classLoaderTest";

    // service 包名
    private String servicePackage = "cn.app.wuzhi.service.impl";

    //热部署测试目录
    private String serviceDir = baseDir + "/" + servicePackage.replace(".", "/");

    //补丁目录
    private String patchDir = "D:/it.work/classLoaderTest/patch";

    private static IocFactory singleton = null;

    //记录通过class newInstance构造出来的所有对象
    private HashMap servicesMap = new HashMap<>();


    //使用ioc注入功能的对象
    private CopyOnWriteArrayList iocTargetArray = new CopyOnWriteArrayList<>();


    public static IocFactory getInstance() {
        if (singleton == null) {
            singleton = new IocFactory();
        }
        return singleton;
    }


    private IocFactory() {
        loadAllService();
        patchListener();
    }

    /**
     *  通过 自定义MyClassLoader 对象,加载 service impl目录下所有的class文件,
当然可以通过class活的工程目录,但是简便,这边就写死了
     *
     */
    public void loadAllService() {

        MyClassLoader myClassLoader = new MyClassLoader(serviceDir);

        File serviceDirFile = new File(serviceDir);

        File[] serviceFile = serviceDirFile.listFiles();
        for (File f : serviceFile) {
            if (f.isFile()) {
                String fileName = f.getName();
                loadClass(myClassLoader, fileName);
            }
        }

    }

    /**
     * 根据ClassLoader 加载class 对象,并存储至内存缓存
     * @param myClassLoader
     * @param fileName
     */
    private void loadClass(MyClassLoader myClassLoader, String fileName) {
        try {
            //设置要加载的class 文件名称
            myClassLoader.setClazzName(fileName);

            //全限定名称
            String javaName = toJavaPackageName(servicePackage, fileName);

            //加载class
            Class loadClazz1 = myClassLoader.findClass(javaName);

            //读取class 文件上 Service注解信息 ,以其中name为key 缓存至Map 中
            //用于IOC注入时,以此name为key

            Service service = (Service) loadClazz1.getAnnotation(Service.class);

            Object obj = loadClazz1.newInstance();

            servicesMap.put(service.name(), obj);


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

    /**
     * 当监听到补丁目录发生变化时,重写注入 service 对象
     */
    public   void rewriteTarget() {
        synchronized (iocTargetArray){
            for (Object target : iocTargetArray) {
                iocInjection(target);
            }
        }

    }

    /**
     *  通过反射读取对象中的所有字段,并得到其中AotoWrite 注解中的name 字段 ,
以此为key 去ioc缓存中查找对应的对象,并赋值。
     * @param o  执行对象
     *
     *           本来想通过注解的静态编译解决反射的性能问题,不过时间问题,就用反射来代替下
     */
    public void iocInjection(Object o) {

        //缓存 执行对象
        iocTargetArray.add(o);

        Class clazz = o.getClass();

        //活的所有的 Field
        Field[] Fields = clazz.getDeclaredFields();

        for (Field field : Fields) {

            //判断改字段是否有AotoWrite ,并得到其中的name 属性值 ,并获得对应的 Service对象 ,并赋值
            AotoWrite aotoWrite = field.getAnnotation(AotoWrite.class);
            if (aotoWrite != null) {
                Object fieldTagObj = servicesMap.get(aotoWrite.name());
                try {
                    if (fieldTagObj != null) {
                        boolean access = field.isAccessible();
                        if (!access) field.setAccessible(true);
                        field.set(o, fieldTagObj);
                        if (!access) field.setAccessible(false);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

    }

    /**
     * 热部署目录文件监听,当有 .class 后缀的文件信息时,重新通过MyClassLoader 去加载信息
     */
    public void patchListener() {
        Path path = Paths.get(patchDir);

        FileWatcher fileWatcher = new FileWatcher(path, new FileListener() {
            @Override
            public void onCreate(Path file) {

                //新增补丁,根据class名称,重新加载 class至内存中

                MyClassLoader myClassLoader = new MyClassLoader();
                myClassLoader.setBaseDir(patchDir);
                String fileName = file.getFileName().toString();
                System.out.println("onCreate :" + fileName);

                if (fileName.endsWith(".class")) {
                    loadClass(myClassLoader, fileName);

                    //重新写入IOC对象
                    rewriteTarget();
                }

            }

            @Override
            public void onModify(Path file) {

            }

            @Override
            public void onDelete(Path file) {

                //class文件删除 ,不使用补丁calss文件,重新加载工程目录中的class文件

                String fileName = file.getFileName().toString();
                System.out.println("onDelete :" + fileName);
                 MyClassLoader myClassLoader = new MyClassLoader();
                myClassLoader.setBaseDir(serviceDir);

                if (fileName.endsWith(".class")) {
                    loadClass(myClassLoader, fileName);

                    //重新写入IOC对象
                    rewriteTarget();
                }

            }
        });

        fileWatcher.watch();
    }


    public static void main(String[] age) {

        IocFactory iocFactory = new IocFactory();

        iocFactory.loadAllService();

        IocTest iocTest = new IocTest();

        iocFactory.iocInjection(iocTest);


    }


    public void test() {
        Set keys = servicesMap.keySet();
        for (String clazzkey : keys) {
            Object tagObj = servicesMap.get(clazzkey);
            System.out.println(tagObj);
        }
    }


    private static String toJavaPackageName(String packageName, String fileName) {
        return packageName + "." + fileName.replace(".class", "");
    }

    public HashMap getServicesMap() {
        return servicesMap;
    }

    public void setServicesMap(HashMap servicesMap) {
        this.servicesMap = servicesMap;
    }
}

IOC测试使用,这边以Shop、Consumer 类演示热部署功能。被自定义ClassLoader加载出来的类,由于和工程中的类,全限定名称一致,但是,由于不是同一个ClassLoader加载出来,不能作为同一个类型的对象,于是我们用一个接口作为引用。

 

java热部署功能_第5张图片

  在test函数中,我们每隔一秒钟调用shop对象 的 helloShop函数。

工程中的Shop类                                                                      修改之后作为补丁的Shop类   

java热部署功能_第6张图片         java热部署功能_第7张图片

 

重新编译工程,把编译好的ShopService.class 复制出来,先运行工程,然后在拷贝到补丁目录,然后在删除,控制台打印如下。

 

 

java热部署功能_第8张图片

java热部署功能_第9张图片

至此一个热部署的功能点依然完成,当然其中还有很多问题。不过学习就是要这样一点 一点的进步,一天一天的努力,持之以恒才能有所收获。

你可能感兴趣的:(android技术)