常用代码扩展点设计方式

[TOC]

在平时业务开中经常会遇到不同业务走不同的业务逻辑,为了代码的扩展性,不得不采取一些手段来对进行解耦,本文将介绍常用的代码扩展点实现方式,包括 Java SPI、dubbo SPI、策略模式及改进扩展点实现、Cola扩展点和抽象业务扩展点实现方式。

Java SPI

1)简介

Java SPI,即 Java Service Provider Interface,是 Java 提供的一套供第三方实现或者扩展的 API,用于为某个接口寻找服务实现类,实现代码的解耦。这里以示例说明:假设消息服务器有 email、dingding、qq 三种类型,每个具体的消息服务器都具有发送消息的能力,类图如下:

javaspi_uml.png

2)代码示例

示例代码详细参考 extension-examples 工程 com.zqh.extension.javaspi 包,github 地址:https://github.com/zhuqiuhui/extension-examples

  • 创建接口及实现类
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);
    }
}
  • 定义工厂类用于根据不同的类型获取不同的 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 目录下创建 META-INF/services 目录,同时该目录下新建一个与上述接口的全限定名一致的文件名,在这个文件中写入接口的实现类的全限定名:

    // 文件名
    com.zqh.extension.javaspi.IMessageServer
      
    // 文件内容
    com.zqh.extension.javaspi.impl.DingDingServer
    com.zqh.extension.javaspi.impl.EmailServer
    com.zqh.extension.javaspi.impl.QQServer
    
  • 客户端调用示例代码

    public class JavaSpiTest {
    
        @Test
        public void testJavaSpi() {
            // init message server factory(只实例化一次)
            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)实现原理优缺点

java SPI 本质上采用“基于接口编程+策略模式+配置文件”来实现服务的动态获取,ServiceLoader 类的 load 方法会从 META-INF/services 目录下找到待实例化的服务,依次进行实例化。所以这里的缺点是如果不使用某些类就会造成资源浪费,不能实例懒加载机制(有兴趣的可以解读下 ServiceLoader 源代码)。

dubbo SPI

1)简介

dubbo SPI 又称为 dubbo 扩展自适应机制,即 dubbo 定义了 @SPI 注解表示该接口是一个扩展点,同时若实现类或方法上存在 @Adaptive 注解,则表示该类或方法是一个自适应的扩展点。相对于 Java SPI 优化了以下几点:

  • 文件内容通过 KV 配置,key 是服务别名,value 是服务类实现的全限定名

  • 实现按需实例化,而不是一次性将某接口的所有实现类全部加载到内存

更详细的 dubbo 扩展自适应机制源码,可以参考:dubbo源码一:ExtensionLoader及获取适配类过程解析:https://blog.csdn.net/zhuqiuhui/article/details/83820876

2)代码示例

示例代码详细参考 extension-examples 工程 com.zqh.extension.dubbospi 包,github 地址:https://github.com/zhuqiuhui/extension-examples

  • 定义扩展点和实现类,如下:
@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!");
    }
}
  • 在以下三个任意一个目录下定义文件:com.zqh.extension.dubbospi.HumanService,内容如下:
  // 目录(任选其一)
  META-INF/services/
  META-INF/dubbo/
  META-INF/dubbo/internal/
  
  // 文件内容
  maleHumanService=com.zqh.extension.dubbospi.impl.MaleHumanServiceImpl
  femaleHumanService=com.zqh.extension.dubbospi.impl.FemaleHumanServiceImpl
  • 客户端调用示例代码
public class DubboSpiTest {

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

// 输出
this is man human say!

策略模式及改进版扩展点实现

策略模式扩展点实现

这里和 Java SPI 很相似,只不过加载服务实现类的方式不同,Java SPI 加载服务实例使用 ServiceLoader.load 方法,本方法使用手动创建对象,示例中直接进行 new 对象,如果在 Spring 容器中还可以使用类型自动注入或构造器注入方式。示例代码详细参考 extension-examples 工程 com.zqh.extension.strategy 包,github 地址:https://github.com/zhuqiuhui/extension-examples

  • 定义扩展点和实现类,如下:
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);
    }
}
  • 客户端调用示例代码
public class StrategyTest {

    @Test
    public void testStrategy() {
        /**
         * 初始化 MessageServerFactory,在Spring 容器中可使用构造器注入方式进行服务类进行自动注入
         */
        IMessageServer[] iMessageServers = new IMessageServer[]{
                new DingDingServer(),
                new EmailServer(),
                new QQServer()
        };
        MessageServerFactory messageServerFactory = new MessageServerFactory(iMessageServers);

        // 调用
        IMessageServer emailMessageServer = messageServerFactory.getByType("email");
        emailMessageServer.sendMessage("hello world");
    }
}

策略模式改进扩展点实现

使用策略模式更高级的做法将服务实例工厂类进行封装,做到业务无感多业务类型支持,示例中将各不同业务实现类统一由启动类 ExtensionPluginBoot 来管理,详细见代码说明(示例代码详细参考 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() {
        // 加载扩展点,将服务实现类 put 进 extendPlugins
        loadExtendPluginClasses();
        instance = this;
    }

    private void loadExtendPluginClasses() {
        // 这里可使用扫描注解、配置文件等方式,下面直接 new 做为示例
        /**
         * 消息服务器
         */
        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);
    }


    /**
     * 根据扩展接口和名称,获取具体的实现
     * @param extensionPoint 扩展接口
     * @param name 名称
     * @param <T> 扩展类实例
     * @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);
    }
}

客户端调用代码如下:

public class StrategyImproveTest {

    @Test
    public void testStrategyImprove() {
        // 使用 qq 服务器进行发送
        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 扩展点设计

1)cola 框架简介

cola 框架是以 DDD 思想为依据定义了应用工程就有的框架和组件,为业务应用工程提供了参考,可以详细参考 cola 的官方文档。cola 2.0 的扩展点支持到了“业务身份”,“用例”,“场景”的三级扩展,详细介绍参考:https://blog.csdn.net/significantfrank/article/details/100074716

cola_ext.png

2)示例代码

示例代码详细参考 cola 框架源码地址:https://github.com/alibaba/COLA/tree/master/cola-components/cola-component-extension-starter/src/test/java/com/alibaba/cola/extension/register

  • 定义扩展点 SomeExtPt 及实现类 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");
    }
}
  • 客户端调用
@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);
    }   
}

附参考文档:

抽象业务扩展点实现方式

基础概念理解

扩展点的实现离不开业务,业务的扩展点需要更高的抽象才能支持得更灵活,先明确几个关键词:

  • 业务流程与业务活动:用户完成某次业务操作的全过程,视为业务活动的编排。如用户执行一次下单操作包括:生成订单、营销优惠计算和库存扣减三个业务活动,业务活动即业务流程编排的基础单元。
  • 领域(@Domain):一个完整上下文的抽象,可大可小,视具体业务而定。常见的大的电商领域有订单域、支付域、库存域等,小的如营销域中的活动域、价格域等。
  • 领域服务(@DomainService):各个领域能对外提供的服务,比如活动域可以提供查询优惠领域服务等
  • 域能力(@Ability):领域具备的可扩展的能力,比如活动域的活动添加、删除能力等
  • 域能力扩展点(@AbilityExtension):域能力的可扩展点,通常是方法级的扩展,如针对于不同场景减库存的逻辑是不一样的,这个不同的逻辑处理就放到域能力扩展点上来实现。
  • 域能力实例(@AbilityInstance):域能力的子类实现,理解为具象的域能力

上面的关键词有点抽象,结合下面一句话来理解:小明可以搬运100斤大米

这句话抽象出来:

  • 小明是一个人,“人”即可视为一个领域,而小明则是“人”领域的一个实例。
  • “搬运货物”视为“人”可以提供的服务(领域服务),从某一方面讲“人”具备搬运货物的能力(域能力,除此之外人还具备看、吃、说话等能力)
  • “可以搬运100斤大米”这句话抽象出来是:“人”能搬运多重的货物,即域能力扩展点。“人”能搬运100斤重的货物,即域能力实例。
lattice_uml.png

示意代码结构如下(示例代码 github:https://github.com/zhuqiuhui/extension-examples):

  • Step 1:领域及领域服务定义
@Domain
public interface Human {

    /**
     * 搬运货物(领域服务)
     */
    @DomainService
    public void carry();
}

public class FemaleHuman implements Human {

    @Override
    public void carry() {
        // 1. 获取搬运货物的能力
        ICarryAbility carryAbility = getCarryAbility();

        // 2. 搬运货物
        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:搬运货物(可根据不同业务场景bizCode获取不的扩展点)
        ICarryBusinessExt iCarryBusinessExt = getICarryBusinessExt(bizCode);
        iCarryBusinessExt.carry();

        // Step 3:放置货物
        //......
    }
}
  • Step 3:定义扩展点
public interface IExtensionPoints {
}

public interface ICarryBusinessExt extends IExtensionPoints {
    /**
     * 扩展点实现类
     */
    @AbilityExtension
    void carry();
}

@AbilityInstance
public class XiaoMingExt implements ICarryBusinessExt {

    @Override
    public void carry() {
        System.out.println("我能搬运100斤");
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容