基于SPI的增强式插件框架设计
Tech
导读
很久之前,为了诊断线上的问题,就想要是能有工具可以在线上出问题的时候,放个诊断包进去马上生效,就能看到线上问题的所在,那该是多么舒服的事情。后来慢慢的切换到java领域后,这种理想也变成了现实,小如IDEA中更改页面就能马上生效,大如利用Althas工具进行线上数据诊断,可谓是信手拈来,极大的方便了开发和诊断。后来深入研究之后,就慢慢的不满足框架本身带来的便利了,造轮子的想法慢慢在脑中挥之不去,这也是本文产生的原因了。接下来,你无需准备任何前置知识,因为本文已经为你准备好了ClassLoader甜点,Javassist配菜,JavaAgent高汤,手写插件加载器框架主食,外加SPI知识做调料,且让我们整理餐具,开始这一道颇有点特色的吃播旅程吧。
Tech
导读
很久之前,为了诊断线上的问题,就想要是能有工具可以在线上出问题的时候,放个诊断包进去马上生效,就能看到线上问题的所在,那该是多么舒服的事情。后来慢慢的切换到java领域后,这种理想也变成了现实,小如IDEA中更改页面就能马上生效,大如利用Althas工具进行线上数据诊断,可谓是信手拈来,极大的方便了开发和诊断。后来深入研究之后,就慢慢的不满足框架本身带来的便利了,造轮子的想法慢慢在脑中挥之不去,这也是本文产生的原因了。接下来,你无需准备任何前置知识,因为本文已经为你准备好了ClassLoader甜点,Javassist配菜,JavaAgent高汤,手写插件加载器框架主食,外加SPI知识做调料,且让我们整理餐具,开始这一道颇有点特色的吃播旅程吧。
01 双亲委派模型
在今年的敏捷团队建设中,我通过Suite执行器实现了一键自动化单元测试。Juint除了Suite执行器还有哪些执行器呢?由此我的Runner探索之旅开始了!
开始前,先聊聊双亲委派这个话题,因为无论是做热部署,还是做字节码增强,甚至于日常的编码,这都是绕不开的一个话题。先看如下图示:
通过上面的整体流程描述,是不是感觉双亲委派机制也不是那么难理解。本质就是先查缓存,缓存中没有就委托给父加载器查询缓存,直至查到Bootstrap加载器,如果Bootstrap加载器在缓存中也找不到,就抛错,然后这个错误再被一层层的捕捉,捕捉到错误后就查自己的类搜索路径,然后层层处理。
02 自定义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);
看看,整体是不是很清晰?
03 自定义类加载器实现类的热交换
理解,首先 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);
04 SPI实现类的热交换
04 SPI实现类的热交换
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.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");
}
}
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
05 自定义类加载器实现Jar热部署
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。
上面讲解的内容,一般是类的热交换,但是如果需要对整个jar包进行热部署,该怎么做呢?虽然现在有很成熟的技术,比如OSGI等,但是这里本文将从原理层面来讲解如何对Jar包进行热部署操作。
由于内置的URLClassLoader本身可以对jar进行操作,所以只需要自定义一个基于URLClassLoader的类加载器即可:
public class BizClassLoader extends URLClassLoader {
public BizClassLoader(URL[] urls) {
super(urls);
}
}
<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 !!!!
bizClassLoader.close();
06 代码增强 技术拾忆
理解,首先 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);
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的搜索路径.
<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>
<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>
首先是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");
}
}
Manifest-Version: 1.0
Premain-Class: com.tinywhale.deploy.javaAgent.AgentPre
Agent-Class: com.tinywhale.deploy.javaAgent.AgentMain
-javaagent:D:\app\tinywhale\tinywhale-deploy\target\tinywhale-deploy-1.0-SNAPSHOT-biz.jar
execute premain method
execute main method
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);
}
}
}
execute premain method
execute main method
execute agentmain method
看到这里,相信对java agent已经有个初步的认识了吧。接下来就基于Java SPI + Java Agent + Javassist来实现一个插件系统,这个插件系统比较特殊的地方,就是可以增强spring框架,使其路径自动注册到component-scan路径中,颇有点霸道(鸡贼)的意思。Javassist框架的使用方式,本文这里不细细的展开,感兴趣的可以看翻译的中文版:javassist中文技术文档
07 插件框架 玉汝于成
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。
首先来说下这个框架的主体思路,使用Java SPI来做插件系统;使用Java Agent来使得插件可以在main主入口方法前或者是方法后执行;使用Javassist框架来进行字节码增强,即实现对spring框架的增强。
针对插件部分,可以定义公共的接口契约:
public interface IPluginExecuteStrategy {
/**
* 执行方法
* @param agentArgs
* @param inst
*/
void execute(String agentArgs, Instrumentation inst);
}
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);
}
}
}
}
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);
}
}
}
}
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;
}
}
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
@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() {
}
}
下面是具体的增强操作:
@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;
}
}
}
}
写到这里,相信大家对整体框架有个大概的认识了。但是这个框架有个缺陷,就是插件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);
}
}
这里需要说明的是,从本地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!/")
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;
}
/**
* 加载插件
*
* @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;
}
}
/**
* 执行插件
*/
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();
}
}
08 总结
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。
到这里,我们的用餐进入到尾声了。也不知道这餐,您享用的是否高兴?
其实本文的技术,从双亲委派模型到自定义类加载器,再到基于自定义类加载器实现的类交换,基于Java SPI实现的类交换,最后到基于Java SPI+ Java Agent + Javassist实现的插件框架及框架支持远程插件化,来一步一步的向读者展示所涉及的知识点。当然,由于笔者知识有限,疏漏之处,还望海涵,真诚期待我的抛砖,能够引出您的玉石之言。