在實(shí)際應(yīng)用中,當(dāng)我們某些功能點(diǎn)開發(fā)完成的時(shí)候,需要重啟部署才能夠讓功能得到應(yīng)用斩狱。但這個(gè)功能比較適合插件開發(fā)掖疮,將功能拆分成一個(gè)個(gè)獨(dú)立的jar來提供功能點(diǎn)的拆組初茶。
簡(jiǎn)單場(chǎng)景
假設(shè)我們現(xiàn)在有發(fā)短信和發(fā)送郵件的功能,這個(gè)時(shí)候我們需要再加一個(gè)發(fā)送微信或者釘釘消息的功能浊闪。
我們希望這兩部分對(duì)接第三方的功能插件式開發(fā)恼布,分別是兩個(gè)獨(dú)立的jar,各自負(fù)責(zé)各自的功能搁宾。
在開發(fā)完成之后折汞,無需重啟應(yīng)用,直接放在特定的位置盖腿,讓應(yīng)用直接去刷新加載這兩個(gè)jar就行了爽待。
實(shí)際上確實(shí)有方法,最近開發(fā)jvm-sandbox
的時(shí)候翩腐,發(fā)現(xiàn)它就有一個(gè)這樣的功能鸟款。
它是如何去做的呢?
實(shí)現(xiàn)思路
- 插件jar開發(fā)完成之后,直接放到特定的位置茂卦。
- 應(yīng)用程序去特定的位置讀取jar
- 通過classload去加載jar中的類
- 通過SPI的方式去找特定的接口何什,并加入到應(yīng)用容器中。
實(shí)現(xiàn)方案
實(shí)例對(duì)象版本
給定一個(gè)jar的路徑等龙,然后去掃描以jar結(jié)尾的包路徑处渣。
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.*;
/**
* 插件實(shí)例加載工廠
*
* @author liukaixiong
* @Email liukx@elab-plus.com
* @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;
// }
/**
* 獲取對(duì)應(yīng)的插件模塊
*
* @return
*/
public List<Components> getComponents() {
return loadObjectList(Components.class);
}
public void loadComponents() {
loadObjectList(Components.class);
}
/**
* 加載對(duì)應(yīng)的實(shí)例對(duì)象
*
* @param clazz
* @param <T>
* @return
*/
public <T> List<T> loadObjectList(Class<T> clazz) {
List<T> objList = new ArrayList<>();
// 基于SPI查找
final ServiceLoader<T> moduleServiceLoader = ServiceLoader.load(clazz, this.urlClassLoader);
final Iterator<T> 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();
// 如果有注入對(duì)象
// 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<URL> 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> components = factory.getComponents();
System.out.println(components);
}
}
這只是一個(gè)實(shí)例版本的,如果還想基于屬性注入的話而咆,可以將注釋那塊解開霍比。
以上的案例是基于Components
接口來 掃描的,需要jar中定義META-INF\services\com.sandbox.manager.api._Components_
中的實(shí)現(xiàn)類暴备。比如
com.sandbox.application.plugin.cat.CatTransactionModule
com.sandbox.application.plugin.cat.listener.LogAdviceListener
你如果嫌麻煩可以使用kohsuke
包悠瞬,只需在類上要定義:(注意還需要實(shí)現(xiàn)該接口),無需手動(dòng)去創(chuàng)建文件和實(shí)現(xiàn)。
@MetaInfServices(Components.class)
public class LogAdviceListener implements Components {
}
pom文件引入:
<dependency>
<groupId>org.kohsuke.metainf-services</groupId>
<artifactId>metainf-services</artifactId>
<version>1.7</version>
<scope>compile</scope>
</dependency>
屬性注入
- 定義注入的接口規(guī)范
/**
* 注入資源對(duì)象
*
* @author liukaixiong
* @Email liukx@elab-plus.com
* @date 2021/12/7 - 16:23
*/
public interface InjectResource {
/**
* 獲取注入對(duì)象
*
* @param resourceField
* @return
*/
public Object getFieldValue(Class<?> resourceField);
/**
* 實(shí)例對(duì)象被返回的處理
*
* @param obj
*/
public void afterProcess(Object obj);
}
- 基于一個(gè)默認(rèn)實(shí)現(xiàn)
GroupContainerHelper
你可以理解為一個(gè)Map浅妆,前提是屬性的對(duì)象在Map中存在望迎,存在則將對(duì)象賦值出去
/**
* 默認(rèn)注入工廠
*
* @author liukaixiong
* @Email liukx@elab-plus.com
* @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();
// 分析類模型,將類分組保存關(guān)系
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();
// 將接口類進(jìn)行分組
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);
}
}
功能差不多就這樣實(shí)現(xiàn)的,如果是Spring的話凌外,可以使用工廠解析SPI掃描到的類辩尊。
當(dāng)然啦,后續(xù)的實(shí)現(xiàn)你想怎么玩都行康辑。
至于怎么已經(jīng)加載過的包或者刷新等功能本文就不過多贅述摄欲。
如果你有好的方式也可以留言交流喔。