<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          EXP 一款 Java 插件化熱插拔框架

          共 9853字,需瀏覽 20分鐘

           ·

          2023-11-09 04:30

          程序員的成長(zhǎng)之路
          互聯(lián)網(wǎng)/程序員/技術(shù)/資料共享 
          關(guān)注


          閱讀本文大概需要 5.5 分鐘。

          來(lái)自:juejin.cn/post/7267091417029148733

          1 前言

          多年以來(lái),ToB 的應(yīng)用程序都面臨定制化需求應(yīng)該怎么搞的問(wèn)題。

          舉例,大部分本地化軟件廠家,都有一個(gè)標(biāo)準(zhǔn)程序,這個(gè)程序支持大部分企業(yè)的功能需求,但面對(duì)世界 500 強(qiáng)等大客戶(hù)時(shí),他們的特殊需求,廠家通常是無(wú)法拒絕的(通常因?yàn)橛唵未?,給的多,可背書(shū))。比如使用非標(biāo)準(zhǔn)數(shù)據(jù)庫(kù),業(yè)務(wù)流程里加入一些安全檢查等,回調(diào)里加入一些定制字段等;

          由此而來(lái)的需求,一般有幾種解決方案;

          1. 將這個(gè)需求做進(jìn)標(biāo)準(zhǔn)產(chǎn)品里。讓這個(gè)功能有個(gè)配置開(kāi)關(guān),也可以被其他的客戶(hù)使用,通常這類(lèi)需求可能比較“通用”
          2. 由于客戶(hù)工期時(shí)間緊,雖然功能“通用”,但奈何時(shí)間不足,無(wú)法排進(jìn)標(biāo)準(zhǔn)產(chǎn)品里,只能使用 git 拉一個(gè)標(biāo)準(zhǔn)的客戶(hù)分支來(lái)進(jìn)行開(kāi)發(fā),后期可能將其 merge 到標(biāo)準(zhǔn)產(chǎn)品里;
          3. 這個(gè)功能太不常見(jiàn)了,無(wú)法放到到標(biāo)準(zhǔn)產(chǎn)品里,那就直接拉新的 git 分支開(kāi)發(fā);

          分支開(kāi)發(fā),他的好處是效率非??欤枥锱纠惨活D改,定制需求就完成了。但是,這種方式會(huì)帶來(lái)一個(gè)致命的問(wèn)題:后期程序升級(jí)成本非常巨大。

          本地化程序和 saas 服務(wù)不同,本地化的程序通常是需要手動(dòng)升級(jí)的,使用分支開(kāi)發(fā),升級(jí)的方式無(wú)非就是 merge 分支,解決沖突。

          如果改動(dòng)很小,merge 倒也問(wèn)題不大。但是如果改動(dòng)很大,merge 的方式,會(huì)帶來(lái)很大的問(wèn)題。因?yàn)槿绻e例定制開(kāi)發(fā)的時(shí)間久了,當(dāng)時(shí)拉分支改的代碼和后面的標(biāo)準(zhǔn)產(chǎn)品迭代代碼早就不兼容了,此時(shí),merge 升級(jí)就非常困難。

          由此,我們思考,到底什么樣的方式才能解決這種場(chǎng)景;

          目前來(lái)看,使用插件機(jī)制擴(kuò)展客戶(hù)需求,是其中一種方式。其本質(zhì)用簡(jiǎn)單一句話(huà)概括:主程序預(yù)留擴(kuò)展接口,定制客戶(hù)實(shí)現(xiàn)接口邏輯。

          你可以將其理解為一種更復(fù)雜的策略模式或者 SPI;只是,插件通常是 classloader 類(lèi)隔離的。

          大概如下圖:


          圖片


          本文我們假設(shè)插件系統(tǒng)是當(dāng)前解決定制需求痛點(diǎn)的方案之一,那我們今天就來(lái)設(shè)計(jì)一下這個(gè)插件系統(tǒng)。

          2 設(shè)計(jì)

          首先分析需求,插件系統(tǒng)需要哪些功能:

          1. 通常主程序定義接口,插件實(shí)現(xiàn)邏輯;即這個(gè)功能在主程序里是空的。在客戶(hù)側(cè)是安裝的。那么,我們可能需要一個(gè)可插拔的功能,即需要的時(shí)候,我們安裝,不需要的時(shí)候,不安裝或者卸載。
          2. 需要熱插拔嗎?我想不是必須的,但如果每次都需要重啟才能調(diào)整插件,用戶(hù)體驗(yàn)會(huì)很不好。那我們就加上熱插拔吧。
          3. 插件里可以寫(xiě) servlet or spring rest api 嗎?我想是需要的。插件里可以對(duì)外新增接口,為定制客戶(hù)提供新的服務(wù)能力。對(duì)了,插件還得支持事務(wù),Mybatis ,AOP,RPC 等。
          4. 一個(gè)擴(kuò)展點(diǎn)可以有多個(gè)實(shí)現(xiàn),那這個(gè)擴(kuò)展點(diǎn)可以同時(shí)存在多個(gè)插件嗎?我想是需要的,比方說(shuō)對(duì)接短信服務(wù)商,a 客戶(hù)走 s1 廠商,b 客戶(hù)走 s2 廠商。另外,一個(gè)客戶(hù)可能對(duì)同一個(gè)擴(kuò)展點(diǎn)有多個(gè)實(shí)現(xiàn),此時(shí)可能需要更復(fù)雜的路由策略,那么,這個(gè)時(shí)候,我們可以提供一種機(jī)制,支持這種策略。
          5. 插件里的配置怎么辦?插件配置通常是可以熱更新的,且通常是在一個(gè)單獨(dú)的插件系統(tǒng)里配置的。此時(shí),我們需要提供一個(gè)區(qū)分于 spring  application.yml 的配置策略,即幾個(gè)基于插件維度的配置 API。
          6. 出于安全考慮,插件包類(lèi)型不僅僅支持 jar 包,還需要支持 zip 包。
          7. 插件的技術(shù)問(wèn)題,要支持類(lèi)隔離,否則,如果插件開(kāi)發(fā)者引入了一個(gè)有問(wèn)題的 lib 或版本不兼容的 lib,將會(huì)導(dǎo)致災(zāi)難。另外,無(wú)法保證各個(gè)插件之間的包名完全不同。

          需要 7788 差不多了,我們來(lái)設(shè)計(jì)一下編程界面。

          1. 入口 API
             
             
          public interface ExpAppContext {

              /**
               * 加載插件
               */
              Plugin load(File file) throws Throwable;

              /**
               * 卸載插件
               */
              void unload(String id) throws Exception;

              /**
               * 獲取多個(gè)擴(kuò)展點(diǎn)的插件實(shí)例
               */
              <P> List<P> get(String extCode);

              /**
               * 簡(jiǎn)化操作, code 就是全路徑類(lèi)名
               */
              <P> List<P> get(Class<P> pClass);

              /**
               * 獲取單個(gè)插件實(shí)例.
               */
              <P> P get(String extCode, String pluginId);
          }

          ExpAppContext 接口,作為核心模型,提供以下能力

          1. 安裝一個(gè) file 插件,并在 jvm 里生效,返回插件信息,每個(gè)插件都有一個(gè) id
          2. 可以根據(jù) id 從 jvm spring 里卸載插件。
          3. 可以根據(jù)擴(kuò)展點(diǎn) code 獲取多個(gè)實(shí)現(xiàn),這個(gè)返回的實(shí)現(xiàn)是一個(gè)集合
          4. 可以根據(jù)擴(kuò)展點(diǎn) code + 插件 id 指定獲取多單個(gè)實(shí)現(xiàn),這個(gè)返回的實(shí)現(xiàn)是一個(gè)對(duì)象。

          這幾個(gè) API 可以實(shí)現(xiàn)插件的基本功能。

          我們?cè)偬砑雨P(guān)于租戶(hù)的 API

             
             
          public interface TenantService {
              /**
               * 獲取 TenantCallback 擴(kuò)展邏輯;
               */
              default TenantCallback getTenantCallback() {
                  return TenantCallback.TenantCallbackMock.instance;
              }

              /**
               * 設(shè)置 callback;
               */
              default void setTenantCallback(TenantCallback callback) {
              }
          }

          public interface TenantCallback {
              /**
               * 返回這個(gè)插件的序號(hào), 默認(rèn) 0;
               * {@link  cn.think.in.java.open.exp.client.ExpAppContext#get(java.lang.Class)} 函數(shù)返回的List 的第一位就是 sort 最高的.
               */
              Integer getSort(String pluginId);

              /**
               * 這個(gè)插件是否屬于當(dāng)前租戶(hù), 默認(rèn)是;
               * 這個(gè)返回值, 會(huì)影響 {@link  cn.think.in.java.open.exp.client.ExpAppContext#get(java.lang.Class)} 的結(jié)果
               * 即進(jìn)行過(guò)濾, 返回為 true 的 plugin 實(shí)現(xiàn), 才會(huì)被返回.
               */
              Boolean isOwnCurrentTenant(String pluginId);
          }

          在調(diào)用 ExpAppContext#get 時(shí),需要過(guò)濾租戶(hù)實(shí)現(xiàn),還需要對(duì)單個(gè)租戶(hù)的多個(gè)實(shí)現(xiàn)進(jìn)行排序。用戶(hù)可以實(shí)現(xiàn)自己的 getSort(pluginId) 和 isOwnCurrentTenant(pluginId) 邏輯。

          API 有了,我們的編程界面就出來(lái)了,他應(yīng)該是這樣的:

             
             
           public static void main(String[] args) throws Throwable {
                  Class<UserService> extensionClass = UserService.class;
                  ExpAppContext expAppContext = Bootstrap.bootstrap("exp-plugins/""workdir-simple-java-app");
                  expAppContext.setTenantCallback(new TenantCallback() {
                      @Override
                      public Integer getSort(String pluginId) {
                          return new Random().nextInt(10);
                      }

                      @Override
                      public Boolean isOwnCurrentTenant(String pluginId) {
                          return true;
                      }
                  });
                  Optional<UserService> first = expAppContext.get(extensionClass).stream().findFirst();
                  first.ifPresent(userService -> {
                      System.out.println(userService.getClass());
                      System.out.println(userService.getClass().getClassLoader());
                      userService.createUserExt();
                  });
              }
          1. 我們的擴(kuò)展點(diǎn)介紹名是UserService,方法名是 createUserExt
          2. 我們使用 Bootstrap 配置工作目錄和插件目錄,并啟動(dòng),啟動(dòng)過(guò)程中包含調(diào)用 load 方法,然后返回一個(gè)核心領(lǐng)域?qū)ο蟆?
          3. 可以使用 Context 配置租戶(hù)策略;
          4. 最后我們使用 expAppContext.get().findFirst() 方法,返回一個(gè)這個(gè)擴(kuò)展點(diǎn)優(yōu)先級(jí)最高的實(shí)現(xiàn)。

          讀取插件配置 API:

             
             
          public interface PluginConfig {
              String getProperty(String pluginId, String key, String defaultValue);
          }

          注意這個(gè) API 和正常的 config  api 不同,他新增了 pluginId 維度,使插件配置之間是互相隔離的。具體的 PluginConfig 還可以根據(jù)租戶(hù)再進(jìn)行配置隔離。

          表面的 API 已經(jīng)差不多了,內(nèi)部的實(shí)現(xiàn),需要開(kāi)始了,比如

          1. 類(lèi)加載機(jī)制,包含 zip jar 的類(lèi)隔離加載。
          2. 容器注入,需要將插件里代碼注入到 spring 里。
          3. 插件的熱插拔,怎么 unload,怎么 load,怎么從 spring 里 remove,怎么卸載等等。

          3 開(kāi)發(fā)

          具體細(xì)節(jié)本文不再展開(kāi),因?yàn)榇a都在 github stateis0/exp 項(xiàng)目里,這個(gè)項(xiàng)目包含實(shí)現(xiàn)代碼,example 代碼,api 使用,適配 springboot starter,最佳實(shí)踐等。

          項(xiàng)目代碼結(jié)構(gòu)依賴(lài):


          圖片


          4 總結(jié)

          EXP 全稱(chēng):Extension Plugin 擴(kuò)展點(diǎn)插件系統(tǒng);

          希望本項(xiàng)目可以幫助你解決本地化軟件的定制需求問(wèn)題。同時(shí),也歡迎為本項(xiàng)目提 issue,pr 等。

          項(xiàng)目地址 EXP 擴(kuò)展點(diǎn)插件系統(tǒng) for Github [1]

          參考資料

          [1]EXP 擴(kuò)展點(diǎn)插件系統(tǒng) for Github: https://github.com/stateIs0/exp

          <END>

          推薦閱讀:

          我把SpringBoot的banner換成了美女,老板說(shuō)工作不飽和,建議安排加班...

          優(yōu)雅的接口防刷處理方案

             
             
          互聯(lián)網(wǎng)初中高級(jí)大廠面試題(9個(gè)G)

          內(nèi)容包含Java基礎(chǔ)、JavaWeb、MySQL性能優(yōu)化、JVM、鎖、百萬(wàn)并發(fā)、消息隊(duì)列、高性能緩存、反射、Spring全家桶原理、微服務(wù)、Zookeeper......等技術(shù)棧!

          ?戳閱讀原文領(lǐng)?。?/span>                                  朕已閱 

          瀏覽 262
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产无码精品电影 | 在线观看几把的网站 | 在线sese| 蜜桃人妻系列 | 色福利在线 |