常用代碼擴(kuò)展點(diǎn)設(shè)計(jì)方式

[TOC]

在平時(shí)業(yè)務(wù)開中經(jīng)常會(huì)遇到不同業(yè)務(wù)走不同的業(yè)務(wù)邏輯,為了代碼的擴(kuò)展性,不得不采取一些手段來對進(jìn)行解耦,本文將介紹常用的代碼擴(kuò)展點(diǎn)實(shí)現(xiàn)方式,包括 Java SPI、dubbo SPI、策略模式及改進(jìn)擴(kuò)展點(diǎn)實(shí)現(xiàn)、Cola擴(kuò)展點(diǎn)和抽象業(yè)務(wù)擴(kuò)展點(diǎn)實(shí)現(xiàn)方式。

Java SPI

1)簡介

Java SPI,即 Java Service Provider Interface,是 Java 提供的一套供第三方實(shí)現(xiàn)或者擴(kuò)展的 API,用于為某個(gè)接口尋找服務(wù)實(shí)現(xiàn)類,實(shí)現(xiàn)代碼的解耦。這里以示例說明:假設(shè)消息服務(wù)器有 email、dingding、qq 三種類型,每個(gè)具體的消息服務(wù)器都具有發(fā)送消息的能力,類圖如下:

javaspi_uml.png

2)代碼示例

示例代碼詳細(xì)參考 extension-examples 工程 com.zqh.extension.javaspi 包,github 地址:https://github.com/zhuqiuhui/extension-examples

  • 創(chuàng)建接口及實(shí)現(xiàn)類
public interface IMessageServer {
    void sendMessage(String message);
}

public class DingDingServer implements IMessageServer {

    @Override
    public String type() {
        return "DingDing";
    }

    @Override
    public void sendMessage(String message) {
        System.out.println("this id DingDing's message! " + message);
    }
}

public class EmailServer implements IMessageServer {

    @Override
    public String type() {
        return "email";
    }

    @Override
    public void sendMessage(String message) {
        System.out.println("this is email's message! " + message);
    }
}

public class QQServer implements IMessageServer {

    @Override
    public String type() {
        return "QQ";
    }

    @Override
    public void sendMessage(String message) {
        System.out.println("this is QQ's message! " + message);
    }
}
  • 定義工廠類用于根據(jù)不同的類型獲取不同的 MessageServer:
public class MessageServerFactory {

    private ServiceLoader<IMessageServer> messageServerServiceLoader = ServiceLoader.load(IMessageServer.class);

    public IMessageServer getByType(String type) {
        for (IMessageServer messageServer : messageServerServiceLoader) {
            if(Objects.equals(messageServer.type(), type)) {
                return messageServer;
            }
        }
        return null;
    }
}
  • 在 resources 目錄下創(chuàng)建 META-INF/services 目錄,同時(shí)該目錄下新建一個(gè)與上述接口的全限定名一致的文件名,在這個(gè)文件中寫入接口的實(shí)現(xiàn)類的全限定名:

    // 文件名
    com.zqh.extension.javaspi.IMessageServer
      
    // 文件內(nèi)容
    com.zqh.extension.javaspi.impl.DingDingServer
    com.zqh.extension.javaspi.impl.EmailServer
    com.zqh.extension.javaspi.impl.QQServer
    
  • 客戶端調(diào)用示例代碼

    public class JavaSpiTest {
    
        @Test
        public void testJavaSpi() {
            // init message server factory(只實(shí)例化一次)
            MessageServerFactory messageServerFactory = new MessageServerFactory();
    
            // client invoke
            IMessageServer emailMessageServer = messageServerFactory.getByType("email");
            emailMessageServer.sendMessage("I am hungry");
        }
    }
    
    // 輸出
    this is email's message! I am hungry
    

3)實(shí)現(xiàn)原理優(yōu)缺點(diǎn)

java SPI 本質(zhì)上采用“基于接口編程+策略模式+配置文件”來實(shí)現(xiàn)服務(wù)的動(dòng)態(tài)獲取,ServiceLoader 類的 load 方法會(huì)從 META-INF/services 目錄下找到待實(shí)例化的服務(wù),依次進(jìn)行實(shí)例化。所以這里的缺點(diǎn)是如果不使用某些類就會(huì)造成資源浪費(fèi),不能實(shí)例懶加載機(jī)制(有興趣的可以解讀下 ServiceLoader 源代碼)。

dubbo SPI

1)簡介

dubbo SPI 又稱為 dubbo 擴(kuò)展自適應(yīng)機(jī)制,即 dubbo 定義了 @SPI 注解表示該接口是一個(gè)擴(kuò)展點(diǎn),同時(shí)若實(shí)現(xiàn)類或方法上存在 @Adaptive 注解,則表示該類或方法是一個(gè)自適應(yīng)的擴(kuò)展點(diǎn)。相對于 Java SPI 優(yōu)化了以下幾點(diǎn):

  • 文件內(nèi)容通過 KV 配置,key 是服務(wù)別名,value 是服務(wù)類實(shí)現(xiàn)的全限定名

  • 實(shí)現(xiàn)按需實(shí)例化,而不是一次性將某接口的所有實(shí)現(xiàn)類全部加載到內(nèi)存

更詳細(xì)的 dubbo 擴(kuò)展自適應(yīng)機(jī)制源碼,可以參考:dubbo源碼一:ExtensionLoader及獲取適配類過程解析:https://blog.csdn.net/zhuqiuhui/article/details/83820876

2)代碼示例

示例代碼詳細(xì)參考 extension-examples 工程 com.zqh.extension.dubbospi 包,github 地址:https://github.com/zhuqiuhui/extension-examples

  • 定義擴(kuò)展點(diǎn)和實(shí)現(xiàn)類,如下:
@SPI
public interface HumanService {
    void say();
}

public class FemaleHumanServiceImpl implements HumanService {
    @Override
    public void say() {
        System.out.println("this is female human say!");
    }
}

public class MaleHumanServiceImpl implements HumanService {
    @Override
    public void say() {
        System.out.println("this is man human say!");
    }
}
  • 在以下三個(gè)任意一個(gè)目錄下定義文件:com.zqh.extension.dubbospi.HumanService,內(nèi)容如下:
  // 目錄(任選其一)
  META-INF/services/
  META-INF/dubbo/
  META-INF/dubbo/internal/
  
  // 文件內(nèi)容
  maleHumanService=com.zqh.extension.dubbospi.impl.MaleHumanServiceImpl
  femaleHumanService=com.zqh.extension.dubbospi.impl.FemaleHumanServiceImpl
  • 客戶端調(diào)用示例代碼
public class DubboSpiTest {

    @Test
    public void testDubboSpi() {
        HumanService maleHumanService = ExtensionLoader.getExtensionLoader(HumanService.class)
                .getExtension("maleHumanService");
        maleHumanService.say();
    }
}

// 輸出
this is man human say!

策略模式及改進(jìn)版擴(kuò)展點(diǎn)實(shí)現(xiàn)

策略模式擴(kuò)展點(diǎn)實(shí)現(xiàn)

這里和 Java SPI 很相似,只不過加載服務(wù)實(shí)現(xiàn)類的方式不同,Java SPI 加載服務(wù)實(shí)例使用 ServiceLoader.load 方法,本方法使用手動(dòng)創(chuàng)建對象,示例中直接進(jìn)行 new 對象,如果在 Spring 容器中還可以使用類型自動(dòng)注入或構(gòu)造器注入方式。示例代碼詳細(xì)參考 extension-examples 工程 com.zqh.extension.strategy 包,github 地址:https://github.com/zhuqiuhui/extension-examples

  • 定義擴(kuò)展點(diǎn)和實(shí)現(xiàn)類,如下:
public interface IMessageServer {
    String type();
    void sendMessage(String message);
}

public abstract class AbstractMessageServer implements IMessageServer {
    // 這里可以抽取一些公共流程
}

public class DingDingServer extends AbstractMessageServer {

    @Override
    public String type() {
        return "DingDing";
    }

    @Override
    public void sendMessage(String message) {
        System.out.println("this id DingDing's message! " + message);
    }
}

public class EmailServer  extends AbstractMessageServer {

    @Override
    public String type() {
        return "email";
    }

    @Override
    public void sendMessage(String message) {
        System.out.println("this is email's message! " + message);
    }
}

public class QQServer extends AbstractMessageServer {

    @Override
    public String type() {
        return "QQ";
    }

    @Override
    public void sendMessage(String message) {
        System.out.println("this is QQ's message! " + message);
    }
}
  • 定義 IMessageServer 工廠類
public class MessageServerFactory {

    private final Map<String, IMessageServer> messageServerMap = new HashMap<>();

    private final IMessageServer[] iMessageServers;

    public MessageServerFactory(IMessageServer[] iMessageServers) {
        this.iMessageServers  = iMessageServers;
        // init map
        for(IMessageServer iMessageServer : iMessageServers) {
            messageServerMap.put(iMessageServer.type(), iMessageServer);
        }
    }

    public IMessageServer getByType(String type) {
        return messageServerMap.get(type);
    }
}
  • 客戶端調(diào)用示例代碼
public class StrategyTest {

    @Test
    public void testStrategy() {
        /**
         * 初始化 MessageServerFactory,在Spring 容器中可使用構(gòu)造器注入方式進(jìn)行服務(wù)類進(jìn)行自動(dòng)注入
         */
        IMessageServer[] iMessageServers = new IMessageServer[]{
                new DingDingServer(),
                new EmailServer(),
                new QQServer()
        };
        MessageServerFactory messageServerFactory = new MessageServerFactory(iMessageServers);

        // 調(diào)用
        IMessageServer emailMessageServer = messageServerFactory.getByType("email");
        emailMessageServer.sendMessage("hello world");
    }
}

策略模式改進(jìn)擴(kuò)展點(diǎn)實(shí)現(xiàn)

使用策略模式更高級的做法將服務(wù)實(shí)例工廠類進(jìn)行封裝,做到業(yè)務(wù)無感多業(yè)務(wù)類型支持,示例中將各不同業(yè)務(wù)實(shí)現(xiàn)類統(tǒng)一由啟動(dòng)類 ExtensionPluginBoot 來管理,詳細(xì)見代碼說明(示例代碼詳細(xì)參考 extension-examples 工程 com.zqh.extension.strategyimprove 包,github 地址:https://github.com/zhuqiuhui/extension-examples):

public class ExtensionPluginBoot {

    private static ExtensionPluginBoot instance = null;

    /**
     * class --> (name, instance)
     */
    private static Map<Class<? extends IExtension>, Map<String, IExtension>> extendPlugins = new LinkedHashMap<>();


    public static ExtensionPluginBoot getInstance() {
        if(instance == null) {
            synchronized (ExtensionPluginBoot.class) {
                if(instance == null) {
                    new ExtensionPluginBoot().init();
                }
            }
        }
        return instance;
    }

    public void init() {
        // 加載擴(kuò)展點(diǎn),將服務(wù)實(shí)現(xiàn)類 put 進(jìn) extendPlugins
        loadExtendPluginClasses();
        instance = this;
    }

    private void loadExtendPluginClasses() {
        // 這里可使用掃描注解、配置文件等方式,下面直接 new 做為示例
        /**
         * 消息服務(wù)器
         */
        Map<String, IExtension> messageServerMap = new HashMap<>();
        messageServerMap.put("DingDing", new DingDingServer());
        messageServerMap.put("email", new DingDingServer());
        messageServerMap.put("QQ", new DingDingServer());
        extendPlugins.put(IMessageServer.class, messageServerMap);
        /**
         * 人類
         */
        Map<String, IExtension> humanMap = new HashMap<>();
        humanMap.put("maleHuman", new MaleHumanServiceImpl());
        humanMap.put("femaleHuman", new FemaleHumanServiceImpl());
        extendPlugins.put(HumanService.class, humanMap);
    }


    /**
     * 根據(jù)擴(kuò)展接口和名稱,獲取具體的實(shí)現(xiàn)
     * @param extensionPoint 擴(kuò)展接口
     * @param name 名稱
     * @param <T> 擴(kuò)展類實(shí)例
     * @return
     */
    public <T extends IExtension> T getNameExtension(Class<T> extensionPoint, String name) {
        Map<String, IExtension> pluginMap = extendPlugins.get(extensionPoint);
        if(pluginMap == null) {
            return null;
        }
        return (T) pluginMap.get(name);
    }
}

客戶端調(diào)用代碼如下:

public class StrategyImproveTest {

    @Test
    public void testStrategyImprove() {
        // 使用 qq 服務(wù)器進(jìn)行發(fā)送
        IMessageServer qqMessageServer = ExtensionRouterFactory.getPlugin(IMessageServer.class, "QQ");
        qqMessageServer.sendMessage("hello world");

        // 男人說話
        HumanService maleHumanService = ExtensionRouterFactory.getPlugin(HumanService.class, "maleHuman");
        maleHumanService.say();
    }
}

// 輸出
this id DingDing's message! hello world
this is man human say!

Cola 擴(kuò)展點(diǎn)設(shè)計(jì)

1)cola 框架簡介

cola 框架是以 DDD 思想為依據(jù)定義了應(yīng)用工程就有的框架和組件,為業(yè)務(wù)應(yīng)用工程提供了參考,可以詳細(xì)參考 cola 的官方文檔。cola 2.0 的擴(kuò)展點(diǎn)支持到了“業(yè)務(wù)身份”,“用例”,“場景”的三級擴(kuò)展,詳細(xì)介紹參考:https://blog.csdn.net/significantfrank/article/details/100074716

cola_ext.png

2)示例代碼

示例代碼詳細(xì)參考 cola 框架源碼地址:https://github.com/alibaba/COLA/tree/master/cola-components/cola-component-extension-starter/src/test/java/com/alibaba/cola/extension/register

  • 定義擴(kuò)展點(diǎn) SomeExtPt 及實(shí)現(xiàn)類 SomeExtensionA、SomeExtensionB
public interface SomeExtPt extends ExtensionPointI {
    public void doSomeThing();
}

@Extension(bizId = "A")
@Component
public class SomeExtensionA implements SomeExtPt {
    @Override
    public void doSomeThing() {
        System.out.println("SomeExtensionA::doSomething");
    }
}


@Extension(bizId = "B")
@Component
public class SomeExtensionB implements SomeExtPt {

    @Override
    public void doSomeThing() {
        System.out.println("SomeExtensionB::doSomething");
    }
}
  • 客戶端調(diào)用
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Application.class)
public class ExtensionRegisterTest {
    
    @Resource
    private ExtensionRegister register;

    @Resource
    private ExtensionExecutor executor;

    @Test
    public void test() {
        SomeExtPt extA = new SomeExtensionA();
        register.doRegistration(extA);

        SomeExtPt extB = CglibProxyFactory.createProxy(new SomeExtensionB());
        register.doRegistration(extB);
        
        executor.executeVoid(SomeExtPt.class, BizScenario.valueOf("A"), SomeExtPt::doSomeThing);
        executor.executeVoid(SomeExtPt.class, BizScenario.valueOf("B"), SomeExtPt::doSomeThing);
    }   
}

附參考文檔:

抽象業(yè)務(wù)擴(kuò)展點(diǎn)實(shí)現(xiàn)方式

基礎(chǔ)概念理解

擴(kuò)展點(diǎn)的實(shí)現(xiàn)離不開業(yè)務(wù),業(yè)務(wù)的擴(kuò)展點(diǎn)需要更高的抽象才能支持得更靈活,先明確幾個(gè)關(guān)鍵詞:

  • 業(yè)務(wù)流程與業(yè)務(wù)活動(dòng):用戶完成某次業(yè)務(wù)操作的全過程,視為業(yè)務(wù)活動(dòng)的編排。如用戶執(zhí)行一次下單操作包括:生成訂單、營銷優(yōu)惠計(jì)算和庫存扣減三個(gè)業(yè)務(wù)活動(dòng),業(yè)務(wù)活動(dòng)即業(yè)務(wù)流程編排的基礎(chǔ)單元。
  • 領(lǐng)域(@Domain):一個(gè)完整上下文的抽象,可大可小,視具體業(yè)務(wù)而定。常見的大的電商領(lǐng)域有訂單域、支付域、庫存域等,小的如營銷域中的活動(dòng)域、價(jià)格域等。
  • 領(lǐng)域服務(wù)(@DomainService):各個(gè)領(lǐng)域能對外提供的服務(wù),比如活動(dòng)域可以提供查詢優(yōu)惠領(lǐng)域服務(wù)等
  • 域能力(@Ability):領(lǐng)域具備的可擴(kuò)展的能力,比如活動(dòng)域的活動(dòng)添加、刪除能力等
  • 域能力擴(kuò)展點(diǎn)(@AbilityExtension):域能力的可擴(kuò)展點(diǎn),通常是方法級的擴(kuò)展,如針對于不同場景減庫存的邏輯是不一樣的,這個(gè)不同的邏輯處理就放到域能力擴(kuò)展點(diǎn)上來實(shí)現(xiàn)。
  • 域能力實(shí)例(@AbilityInstance):域能力的子類實(shí)現(xiàn),理解為具象的域能力

上面的關(guān)鍵詞有點(diǎn)抽象,結(jié)合下面一句話來理解:小明可以搬運(yùn)100斤大米

這句話抽象出來:

  • 小明是一個(gè)人,“人”即可視為一個(gè)領(lǐng)域,而小明則是“人”領(lǐng)域的一個(gè)實(shí)例。
  • “搬運(yùn)貨物”視為“人”可以提供的服務(wù)(領(lǐng)域服務(wù)),從某一方面講“人”具備搬運(yùn)貨物的能力(域能力,除此之外人還具備看、吃、說話等能力)
  • “可以搬運(yùn)100斤大米”這句話抽象出來是:“人”能搬運(yùn)多重的貨物,即域能力擴(kuò)展點(diǎn)?!叭恕蹦馨徇\(yùn)100斤重的貨物,即域能力實(shí)例。
lattice_uml.png

示意代碼結(jié)構(gòu)如下(示例代碼 github:https://github.com/zhuqiuhui/extension-examples):

  • Step 1:領(lǐng)域及領(lǐng)域服務(wù)定義
@Domain
public interface Human {

    /**
     * 搬運(yùn)貨物(領(lǐng)域服務(wù))
     */
    @DomainService
    public void carry();
}

public class FemaleHuman implements Human {

    @Override
    public void carry() {
        // 1. 獲取搬運(yùn)貨物的能力
        ICarryAbility carryAbility = getCarryAbility();

        // 2. 搬運(yùn)貨物
        carryAbility.carry();
    }
}
  • Step 2:定義域能力
public interface IAbility {
}

public interface ICarryAbility extends IAbility {
    void carry();
}

@Ability
public class DefaultCarryAbility implements ICarryAbility {

    @Override
    public void carry() {
        // Step 1:找到貨物
        //......

        // Step 2:搬運(yùn)貨物(可根據(jù)不同業(yè)務(wù)場景bizCode獲取不的擴(kuò)展點(diǎn))
        ICarryBusinessExt iCarryBusinessExt = getICarryBusinessExt(bizCode);
        iCarryBusinessExt.carry();

        // Step 3:放置貨物
        //......
    }
}
  • Step 3:定義擴(kuò)展點(diǎn)
public interface IExtensionPoints {
}

public interface ICarryBusinessExt extends IExtensionPoints {
    /**
     * 擴(kuò)展點(diǎn)實(shí)現(xiàn)類
     */
    @AbilityExtension
    void carry();
}

@AbilityInstance
public class XiaoMingExt implements ICarryBusinessExt {

    @Override
    public void carry() {
        System.out.println("我能搬運(yùn)100斤");
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容