在实际应用中,当我们某些功能点开发完成的时候,需要重启部署才能够让功能得到应用。但这个功能比较适合插件开发,将功能拆分成一个个独立的jar来提供功能点的拆组。
简单场景
假设我们现在有发短信和发送邮件的功能,这个时候我们需要再加一个发送微信或者钉钉消息的功能。
我们希望这两部分对接第三方的功能插件式开发,分别是两个独立的jar,各自负责各自的功能。
在开发完成之后,无需重启应用,直接放在特定的位置,让应用直接去刷新加载这两个jar就行了。
实际上确实有方法,最近开发jvm-sandbox
的时候,发现它就有一个这样的功能。
它是如何去做的呢?
实现思路
- 插件jar开发完成之后,直接放到特定的位置。
- 应用程序去特定的位置读取jar
- 通过classload去加载jar中的类
- 通过SPI的方式去找特定的接口,并加入到应用容器中。
实现方案
实例对象版本
给定一个jar的路径,然后去扫描以jar结尾的包路径。
import com.google.common.collect.Lists;
import com.lkx.jvm.sandbox.core.classloader.ManagerClassLoader;
import com.lkx.jvm.sandbox.core.compoents.DefaultInjectResource;
import com.lkx.jvm.sandbox.core.compoents.GroupContainerHelper;
import com.lkx.jvm.sandbox.core.compoents.InjectResource;
import com.lkx.jvm.sandbox.core.util.FileUtils;
import com.sandbox.manager.api.Components;
import com.sandbox.manager.api.PluginModule;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;
/**
* 插件实例加载工厂
*
* @author liukaixiong
* @Email [email protected]
* @date 2021/12/7 - 18:36
*/
public class JarFactory {
private Logger log = LoggerFactory.getLogger(getClass());
private final static String JAR_FILE_SUFFIX = ".jar";
// private InjectResource injectResource;
private URLClassLoader urlClassLoader;
public JarFactory(String jarFilePath) {
File file = new File(jarFilePath);
if (!file.exists()) {
throw new IllegalArgumentException("jar file does not exist, path=" + jarFilePath);
}
final URL[] urLs = getURLs(jarFilePath);
if (urLs.length == 0) {
throw new IllegalArgumentException("does not have any available jar in path:" + jarFilePath);
}
// this.injectResource = new DefaultInjectResource();
this.urlClassLoader = new URLClassLoader(urLs, this.getClass().getClassLoader());
}
/**
* 允许自定义
*
* @param injectResource
*/
// public void setInjectResource(InjectResource injectResource) {
// this.injectResource = injectResource;
// }
/**
* 获取对应的插件模块
*
* @return
*/
public List getComponents() {
return loadObjectList(Components.class);
}
public void loadComponents() {
loadObjectList(Components.class);
}
/**
* 加载对应的实例对象
*
* @param clazz
* @param
* @return
*/
public List loadObjectList(Class clazz) {
List objList = new ArrayList<>();
// 基于SPI查找
final ServiceLoader moduleServiceLoader = ServiceLoader.load(clazz, this.urlClassLoader);
final Iterator moduleIt = moduleServiceLoader.iterator();
while (moduleIt.hasNext()) {
final T module;
try {
module = moduleIt.next();
} catch (Throwable cause) {
log.error("error load jar", cause);
continue;
}
final Class> classOfModule = module.getClass();
// 如果有注入对象
// if (injectResource != null) {
// for (final Field resourceField : FieldUtils.getFieldsWithAnnotation(classOfModule, Resource.class)) {
// final Class> fieldType = resourceField.getType();
// Object fieldObject = injectResource.getFieldValue(fieldType);
// if (fieldObject != null) {
// try {
// FieldUtils.writeField(
// resourceField,
// module,
// fieldObject,
// true
// );
// } catch (Exception e) {
// log.warn(" set Value error : " + e.getMessage());
// }
// }
// }
// injectResource.afterProcess(module);
// }
objList.add(module);
}
return objList;
}
/**
* 获取模块jar的urls
*
* @param jarFilePath 插件路径
* @return 插件URL列表
*/
private URL[] getURLs(String jarFilePath) {
File file = new File(jarFilePath);
List jarPaths = Lists.newArrayList();
if (file.isDirectory()) {
File[] files = file.listFiles();
if (files == null) {
return jarPaths.toArray(new URL[0]);
}
for (File jarFile : files) {
if (isJar(jarFile)) {
try {
File tempFile = File.createTempFile("manager_plugin", ".jar");
tempFile.deleteOnExit();
FileUtils.copyFile(jarFile, tempFile);
jarPaths.add(new URL("file:" + tempFile.getPath()));
} catch (IOException e) {
log.error("error occurred when get jar file", e);
}
} else {
jarPaths.addAll(Arrays.asList(getURLs(jarFile.getAbsolutePath())));
}
}
} else if (isJar(file)) {
try {
File tempFile = File.createTempFile("manager_plugin", ".jar");
FileUtils.copyFile(file, tempFile);
jarPaths.add(new URL("file:" + tempFile.getPath()));
} catch (IOException e) {
log.error("error occurred when get jar file", e);
}
return jarPaths.toArray(new URL[0]);
} else {
log.error("plugins jar path has no available jar, use empty url, path={}", jarFilePath);
}
return jarPaths.toArray(new URL[0]);
}
/**
* @param file
* @return
*/
private boolean isJar(File file) {
return file.isFile() && file.getName().endsWith(JAR_FILE_SUFFIX);
}
public static void main(String[] args) {
String jarFile = "E:\\study\\sandbox\\sandbox-module\\";
JarFactory factory = new JarFactory(jarFile);
List components = factory.getComponents();
System.out.println(components);
}
}
这只是一个实例版本的,如果还想基于属性注入的话,可以将注释那块解开。
以上的案例是基于Components
接口来 扫描的,需要jar中定义META-INF\services\com.sandbox.manager.api._Components_
中的实现类。比如
com.sandbox.application.plugin.cat.CatTransactionModule
com.sandbox.application.plugin.cat.listener.LogAdviceListener
你如果嫌麻烦可以使用kohsuke
包,只需在类上要定义:(注意还需要实现该接口),无需手动去创建文件和实现。
@MetaInfServices(Components.class)
public class LogAdviceListener implements Components {
}
pom文件引入:
org.kohsuke.metainf-services
metainf-services
1.7
compile
属性注入
- 定义注入的接口规范
/**
* 注入资源对象
*
* @author liukaixiong
* @Email [email protected]
* @date 2021/12/7 - 16:23
*/
public interface InjectResource {
/**
* 获取注入对象
*
* @param resourceField
* @return
*/
public Object getFieldValue(Class> resourceField);
/**
* 实例对象被返回的处理
*
* @param obj
*/
public void afterProcess(Object obj);
}
- 基于一个默认实现
GroupContainerHelper
你可以理解为一个Map,前提是属性的对象在Map中存在,存在则将对象赋值出去
/**
* 默认注入工厂
*
* @author liukaixiong
* @Email [email protected]
* @date 2021/12/8 - 13:40
*/
public class DefaultInjectResource implements InjectResource {
@Override
public Object getFieldValue(Class> resourceField) {
return GroupContainerHelper.getInstance().getObject(resourceField);
}
@Override
public void afterProcess(Object obj) {
Class> clazz = obj.getClass();
// 分析类模型,将类分组保存关系
builderObjectCache(clazz, obj);
GroupContainerHelper.getInstance().registerObject(obj);
}
public void builderObjectCache(Class> clazz, Object obj) {
if (clazz == Object.class) {
return;
}
GroupContainerHelper.getInstance().registerList(clazz, obj);
Class>[] interfaces = clazz.getInterfaces();
// 将接口类进行分组
if (interfaces.length > 0) {
for (int i = 0; i < interfaces.length; i++) {
Class> anInterface = interfaces[i];
GroupContainerHelper.getInstance().registerList(anInterface, obj);
}
}
builderObjectCache(clazz.getSuperclass(), obj);
}
}
功能差不多就这样实现的,如果是Spring的话,可以使用工厂解析SPI扫描到的类。
当然啦,后续的实现你想怎么玩都行。
至于怎么已经加载过的包或者刷新等功能本文就不过多赘述。
如果你有好的方式也可以留言交流喔。