【Dubbo系列】无处不在的SPI之独当一面

5 篇文章 0 订阅

本文是Dubbo系列】无处不在的SPI系列文章的第三小节。
【Dubbo系列】无处不在的SPI:崭露头角 中主要介绍了JDK的SPI机制,以及ServiceLoader的使用。

【Dubbo系列】无处不在的SPI之羽翼渐丰 中补充了SPI和API的区别,并对JDK的SPI进行改写,通过ExtensionLoader实现了仅加载所需要的扩展实现类,达到节约资源的目的。

上文文末提到一个问题,如何为不同的服务(Dubbo中称为扩展点,下文中统一把 服务称为扩展点,服务提供者称为扩展点实现类)提供不同的ExtensionLoader类?

我们可以通过工厂方法来生成ExtensionLoader类,并把扩展加载类缓存起来,这样在调用ExtensionLoader.getExtensionLoader方法时首先获取缓存中的值,不存在则创建。以得到单一实例对象。

静态工厂来生成 ExtensionLoader

// 缓存ExtensionLoader类
private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<> ();

// 通过静态工厂获取ExtensionLoader
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
    // 参数校验
    if (type == null) {
        throw new IllegalArgumentException (" getExtensionLoader params type == null");
    }
    // 服务接口校验
    if(!type.isInterface ()){
        throw new IllegalArgumentException (" extension type " + type + " is not interface! ");
    }
    if (!withExtensionAnnotation(type)){
        throw new IllegalArgumentException (" type WITHOUT @" + SPI.class.getSimpleName () + " Annotation ");
    }

    ExtensionLoader<T> extensionLoader = (ExtensionLoader<T>) EXTENSION_LOADERS.get (type);
    if (extensionLoader == null) {
        // 通过增加参数type,确定ExtensionLoader对象的服务类型
        EXTENSION_LOADERS.putIfAbsent (type,new ExtensionLoader<T> (type));
        extensionLoader = (ExtensionLoader<T>) EXTENSION_LOADERS.get (type);
    }
    return extensionLoader;
}

接下来,需要把构造函数改为私有函数,并添加私有变量type,用于指定ExtensionLoader对象的服务类型。

这样可以移除掉getExtension、getExtensionClasses、getExtensionClasses、loadResource等方法中的type参数,使方法所需要的参数 type 内聚到类中,增强ExtensionLoader类的内聚性。

// 构造函数改为私有
private ExtensionLoader(Class<T> type) {
    this.type = type;
}

// 获取Extension class 的接口
private Class<T> type;

写个测试类进行测试:

@Test
public void invokeExtension(){
    Protocol protocol = ExtensionLoader.getExtensionLoader (Protocol.class).getExtension ("dubbo");
    String echo = protocol.invoke ();
    assertEquals("dubbo_protocol", echo);
}

扩展点自动注入

如果一个A服务接口中使用了B服务接口,那么生成扩展类A实现对象时,如何自动注入B服务的扩展实现类呢?

举个例子:

新建一个 Ext1 扩展点:

package com.ryan.myspi.ext1;

import com.ryan.SPI;

@SPI
public interface Ext1 {

    String echo();
}

建立扩展点实现类:

package com.ryan.myspi.ext1.impl;

import com.ryan.myspi.Protocol;
import com.ryan.myspi.ext1.Ext1;

public class Ext1Impl1 implements Ext1 {

    private Protocol hession;

    public void setHession(Protocol protocol) {
        this.hession = protocol;
    }

    @Override
    public String echo() {
        hession.invoke ();
        return "Ext1Impl1-echo";
    }
}

Ext1Impl1扩展点实现类中使用Protocol的扩展点,这里使用约束 :属性名表示属性Protocol的扩展实现类name

然后在生成Ext1的扩展实现类是,通过反射注入name为hessionProtocol扩展实现类,实现流程如下:

img

优化getExtension方法,使用Holder对象来持有经过注入的扩展实现类:

// 经过注入的扩展实现类缓存
private final ConcurrentMap<String, Holder<Object>> injectedCachedInstances = new     ConcurrentHashMap<String, Holder<Object>>();

public T getExtension(String name) {

    if (name == null || name == "") {
        throw new IllegalArgumentException (" getExtension param name == null ");
    }
    // 通过ConcurrentMap保证线程安全
    // Holder类作为方法参数传入方式时,其中的value引用可以被改变
    Holder<Object> objectHolder = injectedCachedInstances.get (name);
    if (objectHolder == null) {
        // 线程同时到时,ConcurrentMap保证线程安全性,使得拿到的objectHolder为同一个对象
        injectedCachedInstances.putIfAbsent (name,new Holder<Object> ());
        objectHolder = injectedCachedInstances.get (name);
    }

    Object instance = objectHolder.get ();
    if (instance == null) {
        synchronized (objectHolder) {
            Object o = objectHolder.get ();
            if (o == null) {
                objectHolder.set (createExtension (name));
                instance = objectHolder.get ();
            }
        }
    }
    return (T) instance;

}

如果缓存不存在时,调用createExtension:

// 新增经过注入的扩展类
public T createExtension(String name) {
    if (name == null || name == "") {
        throw new IllegalArgumentException (" getExtension param name == null ");
    }
    try {
        Class<?> clazz = getExtensionClasses ().get (name);

        // 查缓存
        T instance = (T) EXTENSION_INSTANCES.get (clazz);
        if (instance == null) {
            EXTENSION_INSTANCES.putIfAbsent (clazz, clazz.newInstance ());
            instance = (T) EXTENSION_INSTANCES.get (clazz);
        }
        // 注入依赖的扩展点
        injectExtension (instance);
        return instance;
    } catch (Throwable e) {
        throw new IllegalArgumentException (" clazz " + type + " newInstance error");
    }
}

其中injectExtension方法用于注入依赖的扩展点实现类:

private T injectExtension(T instance) {
    if (instance == null) {
        throw new IllegalArgumentException (" injectExtension param instance == null");
    }

    // 通过反射注入扩展类
    for (Method method : instance.getClass ().getMethods ()) {
        if (method.getName ().startsWith ("set")
                && method.getParameterTypes ().length == 1
                && Modifier.isPublic (method.getModifiers ())) {
            // 注入
            Class<?> pt = method.getParameterTypes ()[0];
            // 获取参数
            try {
                String methodName = method.getName ();
                // 属性名称,也就是依赖的扩展实现类name
                String property = methodName.length ()>3 ? methodName.substring (3, 4).toLowerCase () + methodName.substring (4) : "";
                // 获取type为property的扩展类,Ext1Impl1中为hession
                Object ob = ExtensionLoader.getExtensionLoader (pt).getExtension (property);
                // 反射调用set方法注入该扩展类
                if(ob!=null) {
                    method.invoke (instance, ob);
                }
            } catch (Exception e) {
                System.out.println ("fail to inject via method :" + method.getName ());
            }
        }

    }
    return instance;

}

经过上面方法实现了依赖扩展类注入,写一个测试来验证一下:

@Test
public void injectExtension(){
    Ext1 ext1 = ExtensionLoader.getExtensionLoader (Ext1.class).getExtension ("ext1Impl1");
    assertEquals ("hessian_protocol",ext1.echo ());
}

小结

本文通过静态工厂实现ExtensionLoader类的创建,并加入缓存;同时通过反射实现扩展点依赖的注入。但是不太灵活。Duboo中使用了 扩展点自适应 的方式,实现了扩展点自动引入,在方法调用时,才选择具体执行的是哪一个扩展点实现。下文接着讨论。

如果需要原文代码的朋友请关注公众号 : *码农的日常记录,并在后台回复:spi1*

推荐阅读:

Mybatis拦截器源码深度解析

线程池的前世今生
了解更多请关注微信公众号

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值