很久之前,为了确诊线上的问题,就想要是能有东西能够在线上出问题的时分,放个确诊包进去立刻收效,就能看到线上问题的地点,那该是多么舒畅的工作。后来渐渐的切换到java范畴后,这种理想也变成了实际,小如IDEA中更改页面就能立刻收效,大如运用Althas东西进行线上数据确诊,可谓是信手拈来,极大的便利了开发和确诊。后来深入研究之后,就渐渐的不满足结构自身带来的便利了,造轮子的主意渐渐在脑中挥之不去,这也是本文产生的原因了。接下来,你无需预备任何前置常识,因为本文现已为你预备好了ClassLoader甜点,Javassist配菜,JavaAgent高汤,手写插件加载器结构主食,外加SPI常识做调料,且让咱们整理餐具,开始这一道颇有点特征的吃播旅程吧。

一、双亲派遣模型

开始前,先聊聊双亲派遣这个话题,因为无论是做热布置,仍是做字节码增强,甚至于日常的编码,这都是绕不开的一个话题。先看如下图示:

基于SPI的增强式插件框架设计 | 京东云技术团队

从如上图示,能够看到双亲派遣模型全体的工作办法,全体解说如下:

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

  2. 进入App ClassLoader中,先检查缓存中是否存在,假如存在,则直接回来

  3. 过程2中的缓存中不存在,则被代理到父加载器,即Extension ClassLoader

  4. 检查Extension ClassLoader缓存中是否存在

  5. 过程4中的缓存中不存在,则被代理到父加载器,即Bootstrap ClassLoader

  6. 检查Bootstrap ClassLoader缓存中是否存在

  7. 过程6中的缓存中不存在,则从Bootstrap ClassLoader的类查找途径下的文件中寻找,一般为rt.jar等,假如找不到,则抛出ClassNotFound Exception

  8. Extension ClassLoader会捕捉ClassNotFound过错,然后从Extension ClassLoader的类查找途径下的文件中寻找,一般为环境变量$JRE_HOME/lib/ext途径下,假如也找不到,则抛出ClassNotFound Exception

  9. App ClassLoader会捕捉ClassNotFound过错,然后从App ClassLoader的类查找途径下的文件中寻找,一般为环境变量$CLASSPATH途径下,假如找到,则将其读入字节数组,假如也找不到,则抛出ClassNotFound Exception。假如找到,则App ClassLoader调用defineClass()办法。

经过上面的全体流程描绘,是不是感觉双亲派遣机制也不是那么难了解。实质便是先查缓存,缓存中没有就委托给父加载器查询缓存,直至查到Bootstrap加载器,假如Bootstrap加载器在缓存中也找不到,就抛错,然后这个过错再被一层层的捕捉,捕捉到过错后就查自己的类查找途径,然后层层处理。

二、自界说ClassLoader

了解,首要 MCube 会根据模板缓存状况判别是否需求网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产品转化为视图树的结构,转化完结后将经过表达式引擎解析表达式并获得正确的值,经过事情解析引擎解析用户自界说事情并完结事情的绑定,完结解析赋值以及事情绑定后进行视图的烘托,最终将方针页面展现到屏幕

了解了双亲派遣机制后,那么假如要完结类的热更换或许是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);

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

三、自界说类加载器完结类的热交流

了解,首要 MCube 会根据模板缓存状况判别是否需求网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产品转化为视图树的结构,转化完结后将经过表达式引擎解析表达式并获得正确的值,经过事情解析引擎解析用户自界说事情并完结事情的绑定,完结解析赋值以及事情绑定后进行视图的烘托,最终将方针页面展现到屏幕。

写完自界说类加载器,来看看详细的用法吧,先创立一个类,拥有如下内容:

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 world2222! (version 2)
hello world2222! (version 2)
hello world2222! (version 2)
hello world2222! (version 2)

能够看到,当替换掉本来运转的类的时分,输出也就变了,变成了新类的输出成果。全体类的热交流成功。

不知道咱们留意到一个细节没有,在上述代码中,先创立出Object的类方针然后运用Method.invoke办法来调用类:

 Object foo = clazz.newInstance();
 Method method = foo.getClass().getMethod("sayHello", new Class[]{});
 method.invoke(foo, new Object[]{});

有人在这儿会疑问,为啥不直接转化为Foo类,然后调用类的Foo.sayHello办法呢?像下面这种办法:

Foo foo2 = (Foo) clazz.newInstance();
foo2.sayHello();

这种办法是不可的,可是咱们知道为啥不可吗?

咱们都知道,咱们写的类,一般都是被AppClassloader加载的,也便是说,你写在main发动类中的一切类,只需你写出来,那么就会被AppClassloader加载,所以,假如这儿强转为Foo类型,那铁定是会被AppClassloader加载的,可是因为clazz方针是由CustomerClassloader加载的,所以这儿就会呈现这样的过错:

java.lang.ClassCastException: com.tw.client.Foo cannot be cast to com.tw.client.Foo

那有什么办法能够解决这个问题吗?其实是有的,便是对Foo方针抽象出一个Interface,比方说IFoo,然后转化的时分,转化成接口,就不会有这种问题了:

IFoo foo2 = (IFoo) clazz.newInstance();
foo2.sayHello();

经过接口这种办法,就很容易对运转中的组件进行类的热交流了,事实便利。

需求留意的是,主线程的类加载器,一般都是AppClassLoader,可是当创立出子线程后,其类加载器都会承继自其创立者的类加载器,可是在某些事务中,我想在子线程中运用自己的类加载器,有什么办法吗?其实这儿也便是打断双亲派遣机制。

因为Thread方针中现已附带了ContextClassLoader特点,所以这儿能够很便利的进行设置和获取:

//设置操作
Thread t = Thread.currentThread();
t.setContextClassLoader(loader);
//获取操作
Thread t = Thread.currentThread();
ClassLoader loader = t.getContextClassLoader();
Class<?> cl = loader.loadClass(className);

四、SPI完结类的热交流

说完根据自界说ClassLoader来进行类的热交流后,再来说说Java中的SPI。提到SPI信任咱们都听过,因为在java中天生集成,其内部机制也是运用了自界说的类加载器,然后进行了良好的封装暴露给用户,详细的源码咱们能够自定翻阅ServiceLoader类。

这儿写个简单的比方:

public interface HelloService {
    void sayHello(String name);
}
public class HelloServiceProvider implements HelloService {
    @Override
    public void sayHello(String name) {
        System.out.println("Hello " + name);
    }
}
public class NameServiceProvider implements HelloService{
    @Override
    public void sayHello(String name) {
        System.out.println("Hi, your name is " + name);
    }
}

然后根据接口的包名+类名作为途径,创立出
com.tinywhale.deploy.spi.HelloService文件到resources中的META-INF.services文件夹,里边放入如下内容:

com.tinywhale.deploy.spi.HelloServiceProvider
com.tinywhale.deploy.spi.NameServiceProvider

然后在发动类中运转:

public static void main(String...args) throws Exception {
        while(true) {
            run();
            Thread.sleep(1000);
        }
    }
    private static void run(){
        ServiceLoader<HelloService> serviceLoader = ServiceLoader.load(HelloService.class);
        for (HelloService helloWorldService : serviceLoader) {
            helloWorldService.sayHello("myname");
        }
    }

能够看到,在发动类中,运用ServiceLoader类来遍历META-INF.services文件夹下面的provider,然后履行,则输出成果为两个类的输出成果。之后在履行过程中,需求去target文件夹中,将
com.tinywhale.deploy.spi.HelloService文件中的NameServiceProvider注释掉,然后保存,就能够看到只有一个类的输出成果了。

Hello myname
Hi, your name is myname
Hello myname
Hi, your name is myname
Hello myname
Hi, your name is myname
Hello myname
Hello myname
Hello myname
Hello myname

这种根据SPI类的热交流,比自己自界说加载器愈加简洁,推荐运用。

五、自界说类加载器完结Jar热布置

了解,首要 MCube 会根据模板缓存状况判别是否需求网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产品转化为视图树的结构,转化完结后将经过表达式引擎解析表达式并获得正确的值,经过事情解析引擎解析用户自界说事情并完结事情的绑定,完结解析赋值以及事情绑定后进行视图的烘托,最终将方针页面展现到屏幕。

上面解说的内容,一般是类的热交流,可是假如需求对整个jar包进行热布置,该怎么做呢?尽管现在有很老练的技能,比方OSGI等,可是这儿本文将从原理层面来解说怎么对Jar包进行热布置操作。

因为内置的URLClassLoader自身能够对jar进行操作,所以只需求自界说一个根据URLClassLoader的类加载器即可:

public class BizClassLoader extends URLClassLoader {
    public BizClassLoader(URL[] urls) {
        super(urls);
    }
}

留意,这儿打的jar包,最好打成fat jar,这样处理起来便利,不至于少打东西:

<plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-shade-plugin</artifactId>
         <version>2.4.3</version>
         <configuration>
             <!-- 主动将一切不运用的类排除-->
             <minimizeJar>true</minimizeJar>
         </configuration>
         <executions>
             <execution>
                 <phase>package</phase>
                 <goals>
                     <goal>shade</goal>
                 </goals>
                 <configuration>
                     <shadedArtifactAttached>true</shadedArtifactAttached>
                     <shadedClassifierName>biz</shadedClassifierName>
                 </configuration>
             </execution>
         </executions>
     </plugin>

之后,就能够运用了:

public static void main(String... args) throws Exception {
       while (true) {
           loadJarFile();
           Thread.sleep(1000);
       }
   }
   /**
    * URLClassLoader 用来加载Jar文件, 直接放在swap目录下即可
    *
    * 动态改动jar中类,能够完结热加载
    *
    * @throws Exception
    */
   public static void loadJarFile() throws Exception {
       File moduleFile = new File("swap\tinywhale-client-0.0.1-SNAPSHOT-biz.jar");
       URL moduleURL = moduleFile.toURI().toURL();
       URL[] urls = new URL[] { moduleURL };
       BizClassLoader bizClassLoader = new BizClassLoader(urls);
       Class clazz = bizClassLoader.loadClass("com.tw.client.Bar");
       Object foo = clazz.newInstance();
       Method method = foo.getClass().getMethod("sayBar", new Class[]{});
       method.invoke(foo, new Object[]{});
       bizClassLoader.close();
   }

发动起来,看下输出,之后用一个新的jar覆盖掉,来看看成果吧:

I am bar, Foo's sister, can you catch me ?????????????
I am bar, Foo's sister, can you catch me ?????????????
I am bar, Foo's sister, can you catch me !!!!
I am bar, Foo's sister, can you catch me !!!!
I am bar, Foo's sister, can you catch me !!!!
I am bar, Foo's sister, can you catch me !!!!

能够看到,jar包被主动替换了。当然,假如想卸载此包,能够调用如下语句进行卸载:

bizClassLoader.close();

需求留意的是,jar包中不应有长期运转的使命或许子线程等,因为调用类加载器的close办法后,会开释一些资源,可是长期运转的使命并不会停止。所以这种情况下,假如你卸载了旧包,然后立刻加载新包,且包中有长期的使命,请承认做好事务防重,否则会引发不可知的事务问题。

因为Spring中现已有对jar包进行操作的类,能够合作上自己的annotation完结特定的功用,比方扩展点完结,插件完结,服务检测等等等等,用处十分广泛,咱们能够自行发掘。

上面解说的基本是原理部分,因为现在市面上有许多老练的组件,比方OSGI等,现已完结了热布置热交流等的功用,所以很推荐咱们去用一用。

提到这儿,信任咱们对类的热交流,jar的热布置应该有开始的概念了,可是这仅仅算是开胃小菜。因为热布置一般都是和字节码增强结合着来用的,所以这儿先来大致了解一下Java Agent技能。

六、代码增强 技能拾忆

了解,首要 MCube 会根据模板缓存状况判别是否需求网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产品转化为视图树的结构,转化完结后将经过表达式引擎解析表达式并获得正确的值,经过事情解析引擎解析用户自界说事情并完结事情的绑定,完结解析赋值以及事情绑定后进行视图的烘托,最终将方针页面展现到屏幕。

话说在JDK中,一向有一个比较重要的jar包,名称为rt.jar,他是java运转时环境中,最核心和最底层的类库的来历。比方java.lang.String, java.lang.Thread, java.util.ArrayList等均来历于这个类库。今天要解说的人物是rt.jar中的java.lang.instrument包,此包提供的功用,能够让咱们在运转时环境中动态的修正体系中的类,而Java Agent作为其间一个重要的组件,极具特征。

现在有个场景,比方说,每次恳求过来,我都想把jvm数据信息或许调用量上报上来,因为应用现已上线,无法更改代码了,那么有什么办法来完结吗?当然有,这也是Java Agent最擅长的场合,当然也不只仅只有这种场合,诸如大名鼎鼎的热布置JRebel,阿里的arthas,线上确诊东西btrace,UT覆盖东西JaCoCo等,不胜枚举。

在运用Java Agent前,需求了解其两个重要的办法:

/**
 * main办法履行之前履行,manifest需求装备特点Premain-Class,参数装备办法载入
 */
public static void premain(String agentArgs, Instrumentation inst);
/**
 * 程序发动后履行,manifest需求装备特点Agent-Class,Attach附加办法载入
 */
public static void agentmain(String agentArgs, Instrumentation inst);

还有个必不可少的东西是MANIFEST.MF文件,此文件需求放置到resources/META-INF文件夹下,此文件一般包含如下内容:

Premain-class: main办法履行前履行的agent类.
Agent-class: 程序发动后履行的agent类.
Can-Redefine-Classes: agent是否具有redifine类能力的开关,true表明能够,false表明不能够.
Can-Retransform-Classes: agent是否具有retransform类能力的开关,true表明能够,false表明不能够.
Can-Set-Native-Method-Prefix: agent是否具有生成本地办法前缀能力的开关,trie表明能够,false表明不能够.
Boot-Class-Path: 此途径会被参加到BootstrapClassLoader的查找途径.

在对jar进行打包的时分,最好打成fat jar,能够削减许多不必要的麻烦,maven参加如下打包内容:

<plugin>
       <groupId>org.apache.maven.plugins</groupId>
       <artifactId>maven-shade-plugin</artifactId>
       <executions>
           <execution>
               <phase>package</phase>
               <goals>
                   <goal>shade</goal>
               </goals>
           </execution>
       </executions>
   </plugin>

而MF装备文件,能够运用如下的maven内容进行主动生成:

<plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>3.2.0</version>
        <configuration>
            <archive>
                <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
            </archive>
        </configuration>
    </plugin>

工欲善其事必先利其器,预备好了之后,先来手写个Java Agent尝鲜吧,模仿premain调用,main调用和agentmain调用。

首要是premain调用类 ,agentmain调用类,main调用类:

//main履行前调用
public class AgentPre {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("execute premain method");
    }
}
//main主办法进口
public class App {
    public static void main(String... args) throws Exception {
        System.out.println("execute main method ");
    }
}
//main履行后调用
public class AgentMain {
    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("execute agentmain method");
    }
}

能够看到,逻辑很简单,输出了办法履行体中打印的内容。之后编译jar包,则会生成fat jar。需求留意的是,MANIFEST.MF文件需求手动创立下,里边参加如下内容:

Manifest-Version: 1.0
Premain-Class: com.tinywhale.deploy.javaAgent.AgentPre
Agent-Class: com.tinywhale.deploy.javaAgent.AgentMain

因为代码是在IDEA中发动,所以想要履行premain,需求在App4a发动类上右击:Run App.main(),之后IDEA顶部会呈现App的履行装备,需求点击Edit Configurations选项,然后在VM options中填入如下指令:

-javaagent:D:\app\tinywhale\tinywhale-deploy\target\tinywhale-deploy-1.0-SNAPSHOT-biz.jar

之后发动App,就能够看到输出成果了。留意这儿最好用fat jar,削减犯错的机率。

execute premain method
execute main method

可是这儿看不到agentmain输出,是因为agentmain的运转,是需求进行attach的,这儿对agentmain进行attach:

public class App {
    public static void main(String... args) throws Exception {
        System.out.println("execute main method ");
        attach();
    }
    private static void attach() {
        File agentFile = Paths.get("D:\app\tinywhale\tinywhale-deploy\target\tinywhale-deploy-1.0-SNAPSHOT.jar").toFile();
        try {
            String name = ManagementFactory.getRuntimeMXBean().getName();
            String pid = name.split("@")[0];
            VirtualMachine jvm = VirtualMachine.attach(pid);
            jvm.loadAgent(agentFile.getAbsolutePath());
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}

发动app后,得到的成果为:

execute premain method
execute main method
execute agentmain method

能够看到,整个履行都被串起来了。

讲到这儿,信任咱们基本上了解java agent的履行顺序和装备了吧, premain履行需求装备-javaagent发动参数,而agentmain履行需求attach vm pid。

看到这儿,信任对java agent现已有个开始的知道了吧。接下来就根据Java SPI + Java Agent + Javassist来完结一个插件体系,这个插件体系比较特别的地方,便是能够增强spring结构,使其途径主动注册到component-scan途径中,颇有点霸道(鸡贼)的意思。Javassist结构的运用办法,本文这儿不细细的展开,感兴趣的能够看翻译的中文版:javassist中文技能文档

(www.cnblogs.com/scy251147/p…

七、插件结构 玉汝于成

了解,首要 MCube 会根据模板缓存状况判别是否需求网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产品转化为视图树的结构,转化完结后将经过表达式引擎解析表达式并获得正确的值,经过事情解析引擎解析用户自界说事情并完结事情的绑定,完结解析赋值以及事情绑定后进行视图的烘托,最终将方针页面展现到屏幕。

首要来说下这个结构的主体思路,运用Java SPI来做插件体系;运用Java Agent来使得插件能够在main主进口办法前或许是办法后履行;运用Javassist结构来进行字节码增强,即完结对spring结构的增强。

针对插件部分,能够界说公共的接口契约:

public interface IPluginExecuteStrategy {
    /**
     * 履行办法
     * @param agentArgs
     * @param inst
     */
    void execute(String agentArgs, Instrumentation inst);
}

然后针对premain和agentmain,运用战略模式进行拼装如下:

premain处理战略类


public class PluginPreMainExecutor implements IPluginExecuteStrategy{
    /**
     * 扫描加载的plugin,识别出@PreMainCondition并加载履行
     */
    @Override
    public void execute(String agentArgs, Instrumentation inst) {
        //获取前置履行调集
        List<String> pluginNames = AgentPluginAnnotationHelper.annoProcess(PreMainCondition.class);
        ServiceLoader<IPluginService> pluginServiceLoader = ServiceLoader.load(IPluginService.class);
        //只履行带有PreMainCondition的插件
        for (IPluginService pluginService : pluginServiceLoader) {
            if (pluginNames.contains(pluginService.getPluginName())) {
                pluginService.pluginLoad(agentArgs, inst);
            }
        }
    }
}

agentmain处理战略类

public class PluginAgentMainExecutor implements IPluginExecuteStrategy {
    /**
     * 扫描加载的plugin,识别出@AgentMainCondition并加载履行
     */
    @Override
    public void execute(String agentArgs, Instrumentation inst) {
        //获取后置履行调集
        List<String> pluginNames = AgentPluginAnnotationHelper.annoProcess(AgentMainCondition.class);
        ServiceLoader<IPluginService> pluginServiceLoader = ServiceLoader.load(IPluginService.class);
        for (IPluginService pluginService : pluginServiceLoader) {
            //只履行带有AgentMainCondition的插件
            if (pluginNames.contains(pluginService.getPluginName())) {
                pluginService.pluginLoad(agentArgs, inst);
            }
        }
    }
}

针对premain和agentmain,履行器工厂如下:

public class AgentPluginContextFactory {
    /**
     * 创立agent pre履行上下文
     * @return
     */
    public static PluginExecutorContext makeAgentPreExecuteContext() {
        IPluginExecuteStrategy strategy = new PluginPreMainExecutor();
        PluginExecutorContext context = new PluginExecutorContext(strategy);
        return context;
    }
    /**
     * 创立agent main履行上下文
     * @return
     */
    public static PluginExecutorContext makeAgentMainExecuteContext() {
        IPluginExecuteStrategy strategy = new PluginAgentMainExecutor();
        PluginExecutorContext context = new PluginExecutorContext(strategy);
        return context;
    }
}

编写Premain-Class和Agent-Class指定的类:

public class AgentPluginPreWrapper {
    public static void premain(String agentArgs, Instrumentation inst) {
        AgentPluginContextFactory.makeAgentPreExecuteContext().execute(agentArgs, inst);
    }
}
public class AgentPluginMainWrapper {
    public static void agentmain(String agentArgs, Instrumentation inst) {
        AgentPluginContextFactory.makeAgentMainExecuteContext().execute(agentArgs, inst);
    }
}

装备文件中指定相应的类:

Manifest-Version: 1.0
Premain-Class: org.tiny.upgrade.core.AgentPluginPreWrapper
Agent-Class: org.tiny.upgrade.core.AgentPluginMainWrapper
Permissions: all-permissions
Can-Retransform-Classes: true
Can-Redefine-Classes: true

结构搭好后,来编写插件部分,插件的话,需求承继自
org.tiny.upgrade.sdk.IPluginService并完结:

@AgentMainCondition
@Slf4j
public class CodePadPluginServiceProvider implements IPluginService {
    @Override
    public String getPluginName() {
        return "增强插件";
    }
    @Override
    public void pluginLoad(String agentArgs, Instrumentation inst) {
        //获取已加载的一切类
        Class<?>[] classes = inst.getAllLoadedClasses();
        if (classes == null || classes.length == 0) {
            return;
        }
        //需求将事务类进行retransform一下,这样能够防止在transform履行的时分,找不到此类的情况
        for (Class<?> clazz : classes) {
            if (clazz.getName().contains(entity.getClassName())) {
                try {
                    inst.retransformClasses(clazz);
                } catch (UnmodifiableClassException e) {
                    log.error("retransform class fail:" + clazz.getName(), e);
                }
            }
        }
        //进行增强操作
        inst.addTransformer(new ByteCodeBizInvoker(), true);
    }
    @Override
    public void pluginUnload() {
    }
}

这儿需求留意的是,在插件load的时分,本文做了class retransform操作,这样操作的原因是因为,在程序发动的时分,有时分比方一些类,会在JavaAgent之前发动,这样会形成有些类在进行增强的时分,无法处理,所以这儿需求遍历并操作下,防止意外情况。

下面是详细的增强操作:

@Slf4j
public class ByteCodeBizInvoker implements ClassFileTransformer {
    /**
     * 在此处加载tprd-ut并运用类加载器加载
     *
     * @param loader
     * @param className
     * @param classBeingRedefined
     * @param protectionDomain
     * @param classfileBuffer
     * @return
     * @throws IllegalClassFormatException
     */
    @Override
    public byte[] transform(ClassLoader loader
                            , String className
                            , Class<?> classBeingRedefined
                            , ProtectionDomain protectionDomain
                            , byte[] classfileBuffer) throws IllegalClassFormatException {
        //java自带的办法不进行处理
        if (loader == null) {
            return null;
        }
        //增强spring5的componetscan,将org.tiny途径塞入
        if (className.contains("ComponentScanBeanDefinitionParser")) {
            try {
                System.out.println("增强spring");
                ClassPool classPool = new ClassPool(true);
                classPool.appendClassPath(ByteCodeBizInvoker.class.getName());
                CtClass ctClass = classPool.get(className.replace("/", "."));
                ClassFile classFile = ctClass.getClassFile();
                MethodInfo methodInfo = classFile.getMethod("parse");
                CtMethod ctMethod = ctClass.getDeclaredMethod("parse");
                addComponentScanPackage(methodInfo, ctMethod);
                return ctClass.toBytecode();
            } catch (Exception e) {
                log.error("handle spring 5 ComponentScanBeanDefinitionParser error", e);
            }
        }
    }
    /**
     * 遍历method,直至找到ReportTracer符号类
     *
     * @param ctMethod
     */
    private void addComponentScanPackage(MethodInfo methodInfo, CtMethod ctMethod) throws CannotCompileException {
        final boolean[] success = {false};
        CodeAttribute ca = methodInfo.getCodeAttribute();
        CodeIterator codeIterator = ca.iterator();
        //行遍历办法体
        while (codeIterator.hasNext()) {
            ExprEditor exprEditor = new ExprEditor() {
                public void edit(MethodCall m) throws CannotCompileException {
                    String methodCallName = m.getMethodName();
                    if (methodCallName.equals("getAttribute")) {
                        //将org.tiny追加进去
                        m.replace("{ $_ = $proceed($$); $_ = $_ +  ",org.tiny.upgrade";  }");
                        success[0] = true;
                    }
                }
            };
            ctMethod.instrument(exprEditor);
            if (success[0]) {
                break;
            }
        }
    }
}

从上面能够看出,本文是修正了spring中的
ComponentScanBeanDefinitionParser类,并将里边的parser办法中将org.tiny.upgrade包扫描途径主动注册进去,这样当他人集成咱们的结构的时分,就无须扫描到结构也能履行了。

写到这儿,信任咱们对全体结构有个大约的知道了。可是这个结构有个缺点,便是插件jar写完后,一定要放到项目的maven dependency中,然后打包布置才行。实际上有时分,项目上线后,根本就没有机会从头打包布置,那么接下来,就经过自界说Classloader来让插件不只仅能够本地集成,并且能够从网络中集成。

首要,需求界说自界说类加载器:

public class TinyPluginClassLoader extends URLClassLoader {
    /**
     * 带参构造
     * @param urls
     */
    public TinyPluginClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }
    /**
     * 增加URL途径
     * @param url
     */
    public void addURL(URL url) {
        super.addURL(url);
    }
}

这个类加载器,是不是很眼熟,和前面讲的相似,可是带了个parent classloader的符号,这是为什么呢?这个符号的意思是,当时自界说的TinyPluginClassLoader的父classloader是谁,这样的话,这个自界说类加载器就能够承继父类加载器中的信息了,防止呈现问题,这个细节咱们留意。

这儿需求说明的是,从本地jar文件加载仍是从网络jar文件加载,实质上是相同的,因为TinyPluginClassLoader是依照URL来的。

针对于本地jar文件,构造如下URL即可:

URL url = new URL("jar:file:/D:/project/tiny-plugin-hello/target/tiny-plugin-hello-1.0-SNAPSHOT.jar!/")

针对于网络jar文件,构造如下URL即可:

URL url = new URL("jar:http://111.111.111.111/tiny-plugin-hello-1.0-SNAPSHOT.jar!/")

这样,只需求界说好自界说类加载器加载逻辑即可:

  /**
     * 从jar文件中提取出对应的插件类
     *
     * @param pluginClass
     * @param jarFile
     * @return
     */
    public static Set<Class> loadPluginFromJarFile(Class pluginClass, JarFile jarFile, TinyPluginClassLoader tinyPluginClassLoader) {
        Set<Class> pluginClasses = new HashSet<Class>();
        Enumeration<JarEntry> jars = jarFile.entries();
        while (jars.hasMoreElements()) {
            JarEntry jarEntry = jars.nextElement();
            String jarEntryName = jarEntry.getName();
            if (jarEntryName.charAt(0) == '/') {
                jarEntryName = jarEntryName.substring(1);
            }
            if (jarEntry.isDirectory() || !jarEntryName.endsWith(".class")) {
                continue;
            }
            String className = jarEntryName.substring(0, jarEntryName.length() - 6);
            try {
                Class clazz = tinyPluginClassLoader.loadClass(className.replace("/", "."));
                if (clazz != null && !clazz.isInterface() && pluginClass.isAssignableFrom(clazz)) {
                    pluginClasses.add(clazz);
                }
            } catch (ClassNotFoundException e) {
                log.error("PluginUtil.loadPluginFromJarFile fail",e);
            }
        }
        return pluginClasses;
    }

之后,就能够用如下代码对一个详细的jar途径进行加载就行了:

 /**
     * 加载插件
     *
     * @return
     */
    @Override
    public Set<Class> loadPlugins(URL jarURL) {
        try {
            JarFile jarFile = ((JarURLConnection) jarURL.openConnection()).getJarFile();
            getTinyPluginClassLoader().addURL(jarURL);
            return PluginUtil.loadPluginFromJarFile(IPluginService.class, jarFile, getTinyPluginClassLoader());
        } catch (IOException e) {
            log.error("LoadPluginViaJarStrategy.loadPlugins fail", e);
            return null;
        }
    }

最终,只需求运用SPI进行动态加载:

  /**
     * 履行插件
     */
    public void processPlugins(URL... urls) {
        if (urls == null || urls.length == 0) {
            log.error("jar url path empty");
            return;
        }
        for (URL url : urls) {
            pluginLoadFactory.loadJarPlugins(url);
        }
        ServiceLoader<IPluginService> serviceLoader = ServiceLoader.load(IPluginService.class, pluginLoadFactory.getPluginLoader());
        for (IPluginService pluginService : serviceLoader) {
            pluginService.Process();
        }
    }

这样,本文不只完结了插件化,并且插件还支撑从本地jar文件或许网络jar文件加载。因为运用了agentmain对代码进行增强,所以当体系检测到这个jar的时分,下一次履行会从头对代码进行增强并收效。

八、总结

了解,首要 MCube 会根据模板缓存状况判别是否需求网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产品转化为视图树的结构,转化完结后将经过表达式引擎解析表达式并获得正确的值,经过事情解析引擎解析用户自界说事情并完结事情的绑定,完结解析赋值以及事情绑定后进行视图的烘托,最终将方针页面展现到屏幕。

到这儿,咱们的用餐进入到结尾了。也不知道这餐,您享受的是否高兴?

其实本文的技能,从双亲派遣模型到自界说类加载器,再到根据自界说类加载器完结的类交流,根据Java SPI完结的类交流,最后到根据Java SPI+ Java Agent + Javassist完结的插件结构及结构支撑远程插件化,来一步一步的向读者展现所触及的常识点。当然,因为笔者常识有限,疏漏之处,还望海涵,真挚期待我的抛砖,能够引出您的玉石之言。

作者:京东零售 石向阳

来历:京东云开发者社区