基于SPI的增强式插件框架设计

本文介绍了基于SPI和Java Agent的动态插件框架设计,包括双亲委派模型、自定义ClassLoader、类的热交换、SPI实现、Jar部署和代码增强技术。通过案例展示了如何利用这些技术实现实时修改线上代码,如使用Java Agent进行预加载(premain)和附加(attach)处理,以及Javassist进行字节码增强。
摘要由CSDN通过智能技术生成

很久之前,为了诊断线上的问题,就想要是能有工具可以在线上出问题的时候,放个诊断包进去马上生效,就能看到线上问题的所在,那该是多么舒服的事情。后来慢慢的切换到
java 领域后,这种理想也变成了现实,小如 IDEA 中更改页面就能马上生效,大如利用 Althas
工具进行线上数据诊断,可谓是信手拈来,极大的方便了开发和诊断。后来深入研究之后,就慢慢的不满足框架本身带来的便利了,造轮子的想法慢慢在脑中挥之不去,这也是本文产生的原因了。接下来,你无需准备任何前置知识,因为我已经为你准备好了
ClassLoader 甜点,Javassist 配菜,JavaAgent 高汤,手写插件加载器框架主食,外加 SPI
知识做调料,且让我们整理餐具,开始这一道颇有点特色的吃播旅程吧。

双亲委派模型

开始前,让我们先聊聊双亲委派这个话题,因为无论是做热部署,还是做字节码增强,甚至于日常的编码,这都是绕不开的一个话题。先看如下图示:

从如上图示,我们可以看到双亲委派模型整体的工作方式,整体讲解如下:

类加载器的 findClass (loadClass) 被调用

  1. 进入 App ClassLoader 中,先检查缓存中是否存在,如果存在,则直接返回
  2. 步骤 2 中的缓存中不存在,则被代理到父加载器,即 Extension ClassLoader
  3. 检查 Extension ClassLoader 缓存中是否存在
  4. 步骤 4 中的缓存中不存在,则被代理到父加载器,即 Bootstrap ClassLoader
  5. 检查 Bootstrap ClassLoader 缓存中是否存在
  6. 步骤 6 中的缓存中不存在,则从 Bootstrap ClassLoader 的类搜索路径下的文件中寻找,一般为 rt.jar 等,如果找不到,则抛出 ClassNotFound Exception
  7. Extension ClassLoader 会捕捉 ClassNotFound 错误,然后从 Extension ClassLoader 的类搜索路径下的文件中寻找,一般为环境变量 $JRE_HOME/lib/ext 路径下,如果也找不到,则抛出 ClassNotFound Exception
  8. App ClassLoader 会捕捉 ClassNotFound 错误,然后从 App ClassLoader 的类搜索路径下的文件中寻找,一般为环境变量 $CLASSPATH 路径下,如果找到,则将其读入字节数组,如果也找不到,则抛出 ClassNotFound Exception。如果找到,则 App ClassLoader 调用 defineClass () 方法。

通过上面的整体流程描述,是不是感觉双亲委派机制也不是那么难理解。本质就是先查缓存,缓存中没有就委托给父加载器查询缓存,直至查到 Bootstrap
加载器,如果 Bootstrap 加载器在缓存中也找不到,就抛错,然后这个错误再被一层层的捕捉,捕捉到错误后就查自己的类搜索路径,然后层层处理。

自定义 ClassLoader

了解了双亲委派机制后,那么如果要实现类的热更换或者是 jar 的热部署,就不得不涉及到自定义 ClassLoader 了,实际上其本质依旧是利用
ClassLoader 的这种双亲委派机制来进行操作的。遵循上面的流程,我们很容易的来实现利用自定义的 ClassLoader 来实现类的热交换功能:

public class CustomClassLoader extends ClassLoader {

    //需要该类加载器直接加载的类文件的基目录
    private String baseDir;
    public CustomClassLoader(String baseDir, String[] classes) throws IOException {
        super();
        this.baseDir = baseDir;
        loadClassByMe(classes);
    }
    private void loadClassByMe(String[] classes) throws IOException {
        for (int i = 0; i < classes.length; i++) {
            findClass(classes[i]);
        }
    }
    /**
     * 重写findclass方法
     *
     * 在ClassLoader中,loadClass方法先从缓存中找,缓存中没有,会代理给父类查找,如果父类中也找不到,就会调用此用户实现的findClass方法
     *
     * @param name
     * @return
     */
    @Override
    protected Class findClass(String name) {
        Class clazz = null;
        StringBuffer stringBuffer = new StringBuffer(baseDir);
        String className = name.replace('.', File.separatorChar) + ".class";
        stringBuffer.append(File.separator + className);
        File classF = new File(stringBuffer.toString());
        try {
            clazz = instantiateClass(name, new FileInputStream(classF), classF.length());
        } catch (IOException e) {
            e.printStackTrace();
        }
        return clazz;
    }
    private Class instantiateClass(String name, InputStream fin, long len) throws IOException {
        byte[] raw = new byte[(int) len];
        fin.read(raw);
        fin.close();
        return defineClass(name, raw, 0, raw.length);
    }
}

这里需要注意的是,在自定义的类加载器中,我们可以覆写 findClass,然后利用 defineClass 加载类并返回。

上面这段代码,我们就实现了一个最简单的自定义类加载器,但是能映射出双亲委派模型呢?

首先点开 ClassLoader 类,在里面翻到这个方法:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);
                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

如果对比着双亲委派模型来看,则 loadClass 方法对应之前提到的步骤 1-8,点进去 findLoadedClass 方法,可以看到底层实现是
native 的 native final Class<?> findLoadedClass0 方法,这个方法会从 JVM
缓存中进行数据查找。后面的分析方法类似。

而自定义类加载器中的 findClass 方法,则对应步骤 9:

clazz = instantiateClass(name, new FileInputStream(classF), classF.length());
//省略部分逻辑
return defineClass(name, raw, 0, raw.length);

看看,整体是不是很清晰?

自定义类加载器实现类的热交换

写完自定义类加载器,来看看具体的用法吧,我们创建一个类,拥有如下内容:

package com.tw.client;
public class Foo {
    public Foo() {
    }
    public void sayHello() {
        System.out.println("hello world22222! (version 11)");
    }
}

顾名思义,此类只要调用 sayHello 方法,便会打印出 hello world22222! (version 11) 出来。

热交换处理过程如下:

public static void main(String[] args) throws Exception {
        while (true) {
            run();
            Thread.sleep(1000);
        }
    }
    /**
     * ClassLoader用来加载class类文件的,实现类的热替换
     * 注意,需要在swap目录下,一层层建立目录com/tw/client/,然后将Foo.class放进去
     * @throws Exception
     */
    public static void run() throws Exception {
        CustomClassLoader customClassLoader = new CustomClassLoader("swap", new String[]{"com.tw.client.Foo"});
        Class clazz = customClassLoader.loadClass("com.tw.client.Foo");
        Object foo = clazz.newInstance();
        Method method = foo.getClass().getMethod("sayHello", new Class[]{});
        method.invoke(foo, new Object[]{});
    }

当我们运行起来后,我们会将提前准备好的另一个 Foo.class 来替换当前这个,来看看结果吧(直接将新的 Foo.class 类拷贝过去覆盖即可):

hello world22222! (version 11)
hello world22222! (version 11)
hello world22222! (version 11)
hello world22222! (version 11)
hello world22222! (version 11)
hello w
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值