實(shí)戰(zhàn)之jvm-sandbox動(dòng)態(tài)加載插件實(shí)現(xiàn)

在實(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)思路

  1. 插件jar開發(fā)完成之后,直接放到特定的位置茂卦。
  2. 應(yīng)用程序去特定的位置讀取jar
  3. 通過classload去加載jar中的類
  4. 通過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>

屬性注入

  1. 定義注入的接口規(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);

}
  1. 基于一個(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)加載過的包或者刷新等功能本文就不過多贅述摄欲。

如果你有好的方式也可以留言交流喔。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末疮薇,一起剝皮案震驚了整個(gè)濱河市胸墙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌按咒,老刑警劉巖迟隅,帶你破解...
    沈念sama閱讀 211,290評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異励七,居然都是意外死亡智袭,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門掠抬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吼野,“玉大人,你說我怎么就攤上這事剿另◇锎福” “怎么了?”我有些...
    開封第一講書人閱讀 156,872評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵雨女,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我阳准,道長(zhǎng)氛堕,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,415評(píng)論 1 283
  • 正文 為了忘掉前任野蝇,我火速辦了婚禮讼稚,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘绕沈。我一直安慰自己锐想,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評(píng)論 6 385
  • 文/花漫 我一把揭開白布乍狐。 她就那樣靜靜地躺著赠摇,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上藕帜,一...
    開封第一講書人閱讀 49,784評(píng)論 1 290
  • 那天烫罩,我揣著相機(jī)與錄音,去河邊找鬼洽故。 笑死贝攒,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的时甚。 我是一名探鬼主播隘弊,決...
    沈念sama閱讀 38,927評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼荒适!你這毒婦竟也來了长捧?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,691評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤吻贿,失蹤者是張志新(化名)和其女友劉穎串结,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體舅列,經(jīng)...
    沈念sama閱讀 44,137評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡肌割,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評(píng)論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了帐要。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片把敞。...
    茶點(diǎn)故事閱讀 38,622評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖榨惠,靈堂內(nèi)的尸體忽然破棺而出奋早,到底是詐尸還是另有隱情,我是刑警寧澤赠橙,帶...
    沈念sama閱讀 34,289評(píng)論 4 329
  • 正文 年R本政府宣布耽装,位于F島的核電站,受9級(jí)特大地震影響期揪,放射性物質(zhì)發(fā)生泄漏掉奄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評(píng)論 3 312
  • 文/蒙蒙 一凤薛、第九天 我趴在偏房一處隱蔽的房頂上張望姓建。 院中可真熱鬧,春花似錦缤苫、人聲如沸速兔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽涣狗。三九已至谍婉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間屑柔,已是汗流浹背屡萤。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留掸宛,地道東北人死陆。 一個(gè)月前我還...
    沈念sama閱讀 46,316評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像唧瘾,于是被迫代替她去往敵國(guó)和親措译。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容