• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

Dubbo源码一SPI vs Spring

武飞扬头像
程序猿阿越
帮助38

前言

本文基于Dubbo2.7.6版本,分析Dubbo SPI的实现。

笔者认为Dubbo SPI的比较对象更应该是Spring,而不是java的SPI。

不同于其他Dubbo SPI源码分析,笔者将在每个部分与Spring进行深度对比,并用一些小case辅助理解。

本文包括:

1)ExtensionLoader的基本介绍

2)普通扩展点,与SpringBean生命周期对比

3)包装扩展点,与SpringAOP对比

4)自适应扩展点,用Spring jdk动态代理实现同样效果

5)依赖注入,与Spring依赖注入对比

6)激活扩展点,引入背景,与Spring的Qualifier的异同

ExtensionLoader

1、私有构造

ExtensionLoader的构造方法传入一个Class,代表当前扩展点对应的SPI接口。

每个ExtensionLoader实例管理自己Class的扩展点,包括加载、获取等等。

学新通技术网

type:当前扩展点对应spi接口class;

objectFactory:扩展点工厂AdaptiveExtensionFactory,主要用于setter注入,后面再看。

学新通技术网

2、单例

ExtensionLoader提供静态方法,构造ExtensionLoader实例。

学新通技术网

单例往往要针对一个范围(scope)来说,比如Spring中所说的单例,往往是在一个BeanFactory中,而一个应用可以运行多个BeanFactory。又比如Class对象是单例,往往隐含的scope是同一ClassLoader。

ExtensionLoader在一个扩展点接口Class下只有一个实例,而每个扩展点实现实例在全局只有一个。

学新通技术网

3、成员变量

ExtensionLoader的成员变量可以分为几类

普通扩展点相关:

学新通技术网

active扩展点相关:

学新通技术网

adaptive扩展点相关:

学新通技术网

wrapper扩展点相关:

学新通技术网

4、加载扩展点Class

ExtensionLoader#getExtensionClasses:

当需要加载某个扩展点实现实例前,总会优先加载该扩展点所有实现Class,并缓存到cachedClasses中。

学新通技术网

ExtensionLoader#loadExtensionClasses:加载所有扩展点实现类

学新通技术网

ExtensionLoader#cacheDefaultExtensionName:加载SPI注解中的value属性,作为默认扩展点名称,默认扩展点只能存在一个。

学新通技术网

ExtensionLoader会扫描classpath三个路径下的扩展点配置文件:

  • META-INF/dubbo/internal:dubbo框架自己用的
  • META-INF/dubbo/:用户扩展用的
  • META-INF/services/:官方也没建议这样使用

学新通技术网

ExtensionLoader#loadDirectory:

1)类加载器:优先线程类加载器,其次ExtensionLoader自己的类加载器;

2)扫描扩展点配置文件;

3)加载扩展类;

学新通技术网

ExtensionLoader#loadResource:加载文件中每一行,key是扩展名,value是扩展实现类名。

学新通技术网

ExtensionLoader#loadClass:最终将每个扩展实现类Class按照不同的方式,缓存到ExtensionLoader实例中。

学新通技术网

普通扩展点

案例

对于一个扩展点MyExt:

@SPI
public interface MyExt {
    String echo(URL url, String s);
}

MyExtImplA实现MyExt:

public class MyExtImplA implements MyExt {
    @Override
    public String echo(URL url, String s) {
        return "ext1";
    }
}

可以配置多个扩展点实现META-INF/dubbo/x.y.z.MyExt:

A=x.y.z.impl.MyExtImplA
B=x.y.z.impl.MyExtImplB

使用ExtensionLoader#getExtention获取对应扩展点:

@Test
void testExtension() {
    ExtensionLoader<MyExt> extensionLoader = ExtensionLoader.getExtensionLoader(MyExt.class);
    MyExt a = extensionLoader.getExtension("A");
    assertTrue(a instanceof MyExtImplA);
    MyExt b = extensionLoader.getExtension("B");
    assertTrue(b instanceof MyExtImplB);
}

这种用法,对于用户来说,和beanFactory极为类似,当然实现并不同。

MyExt a = beanFactory.getBean("A", MyExt.class);

原理

ExtensionLoader#getExtention固然有单例缓存(cachedInstances),这个直接跳过。

学新通技术网

ExtensionLoader#createExtension:创建扩展点实现

0)getExtensionClasses:确保所有扩展点class被加载

1)通过无参构造,实例化扩展点instance

2)injectExtention:对扩展点instance执行setter注入,暂时忽略

3)包装类相关,暂时忽略

4)执行instance的初始化方法

学新通技术网

initExtension:初始化

学新通技术网

ExtensionLoader和Spring的创建bean流程相比,确实很像,比如:

1)Spring可以通过各种方式选择bean的一个构造方法创建一个bean(AbstractAutowireCapableBeanFactory#createBeanInstance),而ExtensionLoader只能通过无参构造创建扩展点;

2)Spring可以通过多种方式进行依赖注入(AbstractAutowireCapableBeanFactory#populateBean),比如Aware接口/setter/注解等,而ExtensionLoader只能支持setter注入;

3)Spring可以通过多种方式进行初始化(AbstractAutowireCapableBeanFactory#initializeBean),比如PostConstruct注解/InitializingBean/initMethod等,而ExtensionLoader只支持InitializingBean(LifeCycle)这种方式;

包装扩展点

案例

上面在ExtensionLoader#createExtension的第三步,可能会走包装扩展点逻辑。

假设有个扩展点MyExt2:

@SPI
public interface MyExt2 {
    String echo(URL url, String s);
}

有普通扩展点实现MyExt2ImplA:

public class MyExt2ImplA implements MyExt2 {
    @Override
    public String echo(URL url, String s) {
        return "A";
    }
}

除此以外,还有两个实现MyExt2的扩展点的MyExtWrapperA和MyExtWrapperB,特点在于他有MyExt2的单参数构造方法

public class MyExtWrapperA implements MyExt2 {

    private final MyExt2 myExt2;

    public MyExtWrapperA(MyExt2 myExt2) {
        this.myExt2 = myExt2;
    }

    @Override
    public String echo(URL url, String s) {
        return "wrapA>>>"   myExt2.echo(url, s);
    }
}
public class MyExtWrapperB implements MyExt2 {

    private final MyExt2 myExt2;

    public MyExtWrapperB(MyExt2 myExt2) {
        this.myExt2 = myExt2;
    }

    @Override
    public String echo(URL url, String s) {
        return "wrapB>>>"   myExt2.echo(url, s);
    }
}

然后编写配置文件META-INF/x.y.z.myext2.MyExt2:

A=x.y.z.myext2.impl.MyExt2ImplA
wrapperA=x.y.z.myext2.impl.MyExtWrapperA
wrapperB=x.y.z.myext2.impl.MyExtWrapperB

测试验证,echo方法输出wrapB>>>wrapA>>>A。

@Test
void testWrapper() {
    ExtensionLoader<MyExt2> extensionLoader = ExtensionLoader.getExtensionLoader(MyExt2.class);
    MyExt2 ext2 = extensionLoader.getExtension("A");
    System.out.println(ext2.echo(null, null)); // wrapB>>>wrapA>>>A
    assertTrue(ext2 instanceof MyExtWrapperB);
}

但是包装扩展点不能通过getExtension显示获取,比如:

// 包装类无法通过name直接获取
@Test
void testWrapper_IllegalStateException() {
    ExtensionLoader<MyExt2> extensionLoader = ExtensionLoader.getExtensionLoader(MyExt2.class);
    try {
        extensionLoader.getExtension("wrapperB");
    } catch (IllegalStateException e) {
        // No such extension x.y.z.myext2.MyExt2 by name wrapperB
        e.printStackTrace();
    }
}

原理

包装类之所以不暴露给用户直接获取,是因为包装类提供类似aop的用途,对于用户来说是透明的。

类加载阶段

在类加载阶段,isWrapperClass判断一个扩展类是否是包装类,如果是的话放入cachedWrapperClasses缓存。

对于包装类,不会放入普通扩展点的缓存map,所以无法通过getExtension显示获取。

学新通技术网

判断是否是包装类,取决于扩展点实现clazz是否有对应扩展点type的单参构造方法。

学新通技术网

实例化阶段

包装类实例化,是通过ExtensionLoader.getExtension("A")获取普通扩展点触发的,而返回的会是一个包装类。

如果一个扩展点存在包装类,客户端通过getExtension永远无法获取到原始扩展点实现

学新通技术网

包装类是硬编码实现的:

1)本质上包装的顺序是无序的,取决于扩展点配置文件的扫描顺序。(SpringAOP可以设置顺序)

2)包装类即使只关注扩展点的一个方法,也必须要实现扩展点的所有方法,扩展点新增方法如果没有默认实现,需要修改所有包装类。(SpringAOP如果用户只关心其中一个方法,也可以实现,因为是动态代理)

3)性能较好。(无反射)

自适应扩展点

对于一个扩展点type,最多只有一个自适应扩展点实现。

可以通过用户硬编码实现,也可以通过dubbo自动生成,优先取用户硬编码实现的自适应扩展点。

硬编码(Adaptive注解Class)

案例

假如有个水果扩展类,howMuch来统计交易上下文中该水果能卖多少钱。

@SPI
public interface Fruit {
    int howMuch(String context);
}

有苹果香蕉等实现,负责计算自己能卖多少钱。

public class Apple implements Fruit {
    @Override
    public int howMuch(String context) {
        return context.contains("apple") ? 1 : 0;
    }
}
public class Banana implements Fruit {
    @Override
    public int howMuch(String context) {
        return context.contains("banana") ? 2 : 0;
    }
}

这里引入一个AdaptiveFruit,在类上加了Adaptive注解,目的是统计上下文中所有水果能卖多少钱。

getSupportedExtensionInstances这个方法能加载所有扩展点,并依靠Prioritized接口实现排序,这个原理忽略,和Spring的Ordered差不多。

@Adaptive
public class AdaptiveFruit implements Fruit {
    private final Set<Fruit> fruits;
    public AdaptiveFruit() {
        ExtensionLoader<Fruit> loader = ExtensionLoader.getExtensionLoader(Fruit.class);
        // 加载所有扩展点实现(支持Prioritized实现有序)
        fruits = loader.getSupportedExtensionInstances();
    }

    @Override
    public int howMuch(String context) {
        return fruits.stream().mapToInt(t -> t.howMuch(context)).sum();
    }
}

测试方法如下,用户购买苹果和香蕉,共花费3元。

核心api是ExtensionLoader#getAdaptiveExtension获取自适应扩展点实现。

@Test
void testAdaptiveFruit() {
    ExtensionLoader<Fruit> extensionLoader = ExtensionLoader.getExtensionLoader(Fruit.class);
    Fruit adaptive = extensionLoader.getAdaptiveExtension();
    assertTrue(adaptive instanceof AdaptiveFruit);
    int money = adaptive.howMuch("apple&banana");
    assertEquals(3, money);
}

原理

在类加载阶段,被Adaptive注解修饰的扩展点Class会被缓存到cachedAdaptiveClass。

注意,Adaptive注解类也不会作为普通扩展点暴露给用户,即不能通过ExtensionLoader.getExtension通过扩展名直接获取。

学新通技术网

ExtensionLoader#getAdaptiveExtension获取自适应扩展点。

实例化阶段,无参构造反射创建Adaptive扩展点,并执行setter注入。

学新通技术网

dubbo优先选取用户实现的Adaptive扩展点实现,否则会动态生成Adaptive扩展点。

学新通技术网

动态生成(Adaptive注解Method)

案例

假设现在有个秒杀水果扩展点SecKillFruit。

相较于刚才的Fruit扩展点,区别在于入参改为了URL,且方法加了Adaptive注解

@SPI
public interface SecKillFruit {
    @Adaptive
    int howMuch(URL context);
}

苹果秒杀0元,香蕉秒杀1元。

public class SecKillApple implements SecKillFruit {
    @Override
    public int howMuch(URL context) {
        return 0;
    }
}
public class SecKillBanana implements SecKillFruit {
    @Override
    public int howMuch(URL context) {
        return 1;
    }
}

扩展点配置文件META-INF/x.y.z.myext4.SecKillFruit:

apple=x.y.z.myext4.impl.SecKillApple
banana=x.y.z.myext4.impl.SecKillBanana

假设场景,每次只能秒杀一种水果,需要根据上下文不同,决定秒杀的是哪种水果,计算不同的价钱。

有下面的测试案例,关键点在于URL里增加了sec.kill.fruit=扩展点名,零编码实现根据URL走不同策略。

sec.kill.fruit是SecKillFruit驼峰解析为小写后用点分割得到。

@Test
void testAdaptiveFruit2() {
    ExtensionLoader<SecKillFruit> extensionLoader = ExtensionLoader.getExtensionLoader(SecKillFruit.class);
    // 动态生成的自适应扩展类
    SecKillFruit adaptive = extensionLoader.getAdaptiveExtension();
    // x.y.z.myext4.SecKillFruit$Adaptive
    System.out.println(adaptive.getClass().getCanonicalName());
    URL url = new URL("myProtocol", "1.2.3.4", 1010, "path");
    // 0元秒杀苹果
    url = url.addParameters("sec.kill.fruit", "apple");
    int money = adaptive.howMuch(url);
    assertEquals(0, money);
    // 1元秒杀香蕉
    url = url.addParameters("sec.kill.fruit", "banana");
    money = adaptive.howMuch(url);
    assertEquals(1, money);
}

也可以通过指定Adaptive注解的value,让获取扩展点名字的逻辑更加清晰。

比如取URL中的fruitType作为获取扩展名的方式。

@SPI
public interface SecKillFruit {
    @Adaptive("fruitType")
    int howMuch(URL context);
}

原理

由于Dubbo内部就是用URL做全局上下文来用,你可以理解为字符串无所不能。

所以为了减少重复代码,很多策略都通过动态生成自适应扩展来实现。

ExtensionLoader#createAdaptiveExtensionClass:如果没有用户Adaptive注解实现扩展点,走这里动态生成。

学新通技术网

关键点在于AdaptiveClassCodeGenerator#generate如何生成java代码。

扩展点接口必须有Adaptive注解方法,否则getAdaptiveExtension会异常。

学新通技术网

关键在于generateMethodContent如何实现adaptive方法逻辑。

对于没有Adaptive注解的方法,直接抛出异常。

对于Adaptive注解的方法,分为四步:

1)获取URL:优先从参数列表里直接找URL,降级从一个有URL的getter方法的Class里获取URL,否则异常;

2)决定扩展名:优先从Adaptive注解value属性获取,否则取扩展点类名去驼峰加点;

3)获取扩展点:调用ExtensionLoader.getExtension;

4)委派给目标扩展实现:调用目标扩展的目标方法,传入原始参数列表;

学新通技术网

比如针对SecKillFruit,最终生成的代码如下。

对于Dubbo来说,虽然扩展点不同,但是都用URL上下文,就可以少写重复代码。

public class SecKillFruit$Adaptive implements x.y.z.myext4.SecKillFruit {
    // Adaptive注解方法,通过ContextHolder.getUrl获取URL
    public int howMuch2(x.y.z.myext4.ContextHolder arg0) {
        if (arg0 == null)
            throw new IllegalArgumentException("...");
        if (arg0.getUrl() == null)
            throw new IllegalArgumentException("...");
        org.apache.dubbo.common.URL url = arg0.getUrl();
        String extName = url.getParameter("fruitType");
        if (extName == null)
            throw new IllegalStateException("...");
        x.y.z.myext4.SecKillFruit extension = (x.y.z.myext4.SecKillFruit)
                                               ExtensionLoader
                                               .getExtensionLoader(x.y.z.myext4.SecKillFruit.class)
                                               .getExtension(extName);
        return extension.howMuch2(arg0);
    }
    // Adaptive注解方法,直接从参数列表中获取URL
    public int howMuch(org.apache.dubbo.common.URL arg0) {
        if (arg0 == null) throw new IllegalArgumentException("url == null");
        org.apache.dubbo.common.URL url = arg0;
        String extName = url.getParameter("fruitType");
        if (extName == null)
            throw new IllegalStateException("...");
        x.y.z.myext4.SecKillFruit extension = (x.y.z.myext4.SecKillFruit)
                                               ExtensionLoader
                                               .getExtensionLoader(x.y.z.myext4.SecKillFruit.class)
                                               .getExtension(extName);
        return extension.howMuch(arg0);
    }

    // 没有Adaptive注解的方法
    public int howMuch() {
        throw new UnsupportedOperationException("...");
    }
}

Spring jdk动态代理实现

上面原理分析不太好理解,这个事情也可以用Spring jdk动态代理来实现。

其实这个需求和feign的FeignClient、mybatis的Mapper都比较像,写完接口就相当于写完实现。

针对同一个扩展点type设计一个AdaptiveFactoryBean

public class AdaptiveFactoryBean implements FactoryBean<Object> {
    private final Class<?> type; /* 扩展点 */
    private final String defaultExtName; /* 默认扩展名 */
    private final Map<Method, Adaptive> cachedMethod2Adaptive = new HashMap<>();
    private final Map<Method, Integer> cachedMethod2URLIndex = new HashMap<>();
    public AdaptiveFactoryBean(Class<?> type) {
        SPI spi = type.getAnnotation(SPI.class);
        // 缓存Adaptive相关方法信息
        for (Method method : type.getMethods()) {
            Adaptive adaptive = method.getAnnotation(Adaptive.class);
            if (adaptive != null) {
                int idx = getUrlTypeIndex(method);
                if (idx != -1) {
                    // url字段所处参数列表下标
                    cachedMethod2Adaptive.put(method, adaptive);
                    // Adaptive注解
                    cachedMethod2URLIndex.put(method, idx);
                }
            }
        }
        this.type = type;
        this.defaultExtName = spi.value();
    }
    @Override
    public Object getObject() throws Exception {
        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                // ...
            }
        };
        return Proxy.newProxyInstance(type.getClassLoader(), new Class[]{type}, handler);
    }
    @Override
    public Class<?> getObjectType() {
        return type;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}

核心逻辑在InvocationHandler#invoke代理逻辑中,和AdaptiveClassCodeGenerator#generateMethodContent一样。

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (!cachedMethod2Adaptive.containsKey(method)) {
        throw new UnsupportedOperationException();
    }
    // 1. 获取URL
    int urlIdx = cachedMethod2URLIndex.get(method);
    URL url = (URL) args[urlIdx];
    // 2. 从url里获取扩展点名
    Adaptive adaptive = cachedMethod2Adaptive.get(method);
    String extName = null;
    for (String key : adaptive.value()) {
        extName = url.getParameter(key);
        if (extName != null) {
            break;
        }
    }
    if (extName == null) {
        extName = defaultExtName;
    }
    if (extName == null) {
        throw new IllegalStateException();
    }
    // 3. 获取扩展点
    Object extension = 
        ExtensionLoader.getExtensionLoader(type).getExtension(extName);
    // 4. 委派给扩展点
    return method.invoke(extension, args);
}

为了注入所有包含Adaptive注解方法的扩展点AdaptiveFactoryBean,提供一个批量注册BeanDefinition的AdaptiveBeanPostProcessor,实现比较粗糙,主要为了说明问题。

public class AdaptiveBeanPostProcessor implements BeanDefinitionRegistryPostProcessor, EnvironmentAware {
    private final String packageToScan;
    private Environment environment;
    public AdaptiveBeanPostProcessor(String packageToScan) {
        this.packageToScan = packageToScan;
    }
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        ClassPathScanningCandidateComponentProvider scanner =
                new ClassPathScanningCandidateComponentProvider(false, this.environment) {
                    @Override
                    protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
                        // 有Adaptive注解方法
                        return beanDefinition.getMetadata()
                                .hasAnnotatedMethods("org.apache.dubbo.common.extension.Adaptive");
                    }
                };
        scanner.addIncludeFilter(new AnnotationTypeFilter(SPI.class));
        Set<BeanDefinition> beanDefinitions = scanner.findCandidateComponents(this.packageToScan);
        for (BeanDefinition bd : beanDefinitions) {
            BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(AdaptiveFactoryBean.class);
            try {
                definition.addConstructorArgValue(Class.forName(bd.getBeanClassName()));
                registry.registerBeanDefinition(bd.getBeanClassName()   "$Adaptive_Spring", definition.getBeanDefinition());
            } catch (ClassNotFoundException ignore) {
            }
        }
    }
}

测试验证:

@Configuration
public class AdaptiveFactoryBeanTest {

    @Bean
    public AdaptiveBeanPostProcessor adaptiveBeanPostProcessor() {
        return new AdaptiveBeanPostProcessor("x.y.z.myext4");
    }

    @Test
    void test() {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(AdaptiveFactoryBeanTest.class);
        applicationContext.refresh();
        SecKillFruit secKillFruit = applicationContext.getBean("x.y.z.myext4.SecKillFruit$Adaptive_Spring", SecKillFruit.class);
        URL url = new URL("myProtocol", "1.2.3.4", 1010, "path");
        // 0元秒杀苹果
        url = url.addParameters("fruitType", "apple");
        int money = secKillFruit.howMuch(url);
        assertEquals(0, money);
        // 1元秒杀香蕉
        url = url.addParameters("fruitType", "banana");
        money = secKillFruit.howMuch(url);
        assertEquals(1, money);
        // 无URL方法异常
        assertThrows(UnsupportedOperationException.class, secKillFruit::howMuch);
    }
}

是不是用spring 动态代理来说明,更加容易理解了。

依赖注入

无论是对于普通扩展点/包装扩展点/自适应扩展点,所有的扩展点实例都会经过依赖注入。

案例

InjectExt是个扩展点,有实现InjectExtImplA,InjectExtImplA有一个Inner的setter方法。

public class InjectExtImplA implements InjectExt {
    private Inner inner;
    public void setInner(Inner inner) {
        this.inner = inner;
    }
    @Override
    public Inner getInner() {
        return inner;
    }
}

Inner是个扩展点,且能生成自适应扩展实现。

@SPI
public interface Inner {
    @Adaptive
    String echo(URL url);
}

Inner有InnerA实现。

public class InnerA implements Inner {
    @Override
    public String echo(URL url) {
        return "A";
    }
}

测试方法,InjectExtImplA被自动注入了Inner的自适应实现

@Test
void testInject() {
    ExtensionLoader<InjectExt> extensionLoader = ExtensionLoader.getExtensionLoader(InjectExt.class);
    InjectExt a = extensionLoader.getExtension("a");
    assertThat(a, instanceOf(InjectExtImplA.class));
    // org.apache.dubbo.common.extension.myext5.Inner$Adaptive
    System.out.println(a.getInner().getClass().getCanonicalName());
}

原理

ExtensionLoader#injectExtension依赖注入,循环每个setter方法,找到入参Class和属性名。

通过ExtensionFactory搜索依赖,整个注入过程的异常都被捕获。

学新通技术网

ExtensionFactory也是SPI接口。

这里走硬编码实现的AdaptiveExtensionFactory,循环每个ExtensionFactory扩展点,通过type和name找扩展点实现。

学新通技术网

ExtensionFactory扩展点有两个实现。

原生的SpiExtensionFactory没有利用setter的属性name,直接获取type对应的自适应扩展点。

这也是为什么案例中,被注入的扩展点用了Adaptive。

学新通技术网

Spring相关的SpringExtensionFactory支持从多个ioc容器中,通过getBean(setter属性名,扩展点)获取bean。

学新通技术网

激活扩展点

背景

ExtensionLoader#getExtension可以获取单个扩展点实现。

ExtensionLoader#getSupportedExtensionInstances可以获取所有扩展点实现。

现在需要根据条件,获取一类扩展点实现,这就是所谓的激活扩展点

以Spring为例,如何利用Qualifier做到这点。

假设现在有个用户接口,根据用户类型和用户等级有不同实现。

public interface User {
}

利用Qualifier注解,Category代表用户类型,Level代表用户等级。

@Qualifier
public @interface Category {
    String value();
}
@Qualifier
public @interface Level {
    int value();
}

针对User有四种实现,包括vip1级用户、vip2级用户、普通用户、普通2级用户。

@Component
@Category("vip")
@Level(1)
public static class VipUser implements User {
}
@Component
@Category("vip")
@Level(2)
public static class GoldenVipUser implements User {
}
@Component
@Category("normal")
public static class UserImpl implements User {
}
@Component
@Category("normal")
@Level(2)
public static class UserImpl2 implements User {
}

通过Qualifier,可以按照需求注入不同类型等级用户集合,做业务逻辑。

@Configuration
@ComponentScan
public class QualifierTest {
	@Autowired
    @Category("vip")
    private List<User> allVip; // 所有vip用户
    @Autowired
    @Category("normal")
    private List<User> allNormal; // 所有普通用户
    @Autowired
    @Category("vip")
    @Level(1)
    private List<User> allVipLevel1; // 所有vip1级用户
    @Autowired
    @Level(2)
    private List<User> allLevel2; // 所有2级用户
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(QualifierTest.class);
        context.refresh();
        QualifierTest bean = context.getBean(QualifierTest.class);
        // allVip -> GoldenVipUser
        // allVip -> VipUser
        bean.allVip.forEach(t -> System.out.println("allVip -> "   t.getClass().getSimpleName()));
        // allNormal -> UserImpl
        // allNormal -> UserImpl2
        bean.allNormal.forEach(t -> System.out.println("allNormal -> "   t.getClass().getSimpleName()));
        // allVipLevel1 -> VipUser
        bean.allVipLevel1.forEach(t -> System.out.println("allVipLevel1 -> "   t.getClass().getSimpleName()));
        // allLevel2 -> GoldenVipUser
        // allLevel2 -> UserImpl2
        bean.allLevel2.forEach(t -> System.out.println("allLevel2 -> "   t.getClass().getSimpleName()));
    }
}

案例

和上面Spring的案例一样,替换成ExtensionLoader实现,看起来语义差不多。

用户等级作为分组,在URL参数上获取用户等级。

@Activate(group = {"vip"}, value = {"level:1"})
public class VipUser implements User {
}
@Activate(group = {"vip"}, value = {"level:2"}, order = 1000)
public class GoldenVipUser implements User {
}
@Activate(group = {"normal"}, order = 10)
public class UserImpl implements User {
}
@Activate(group = {"normal"}, value = {"level:2"}, order = 500)
public class UserImpl2 implements User {
}

测试方法如下,发现与Spring的Qualifier有相同也有不同。

比如通过group=vip和url不包含level去查询:

1)UserImpl和UserImpl2查不到,因为group不满足;

2)VipUser和GoldenVipUser查不到,因为url必须有level,且分别为1和2;

又比如通过group=null和level=2去查询:

1)UserImpl没有设置Activate注解value,代表对url没有约束,且查询条件group=null,代表匹配所有group,所以可以查到;

2)VipUser对url有约束,必须level=1,所以查不到;

3)GoldenVipUser和UserImpl2,都满足level=2,且查询条件group=null,代表匹配所有group,所以都能查到;

@Test
void testActivate() {
    ExtensionLoader<User> extensionLoader = ExtensionLoader.getExtensionLoader(User.class);
    URL url = URL.valueOf("myProtocol://localhost/queryUsers");
    // 所有vip?
    List<User> allVip = extensionLoader.getActivateExtension(url, new String[]{}, "vip");
    assertEquals(0, allVip.size()); // no
    // 所有normal?
    List<User> allNormal = extensionLoader.getActivateExtension(url, new String[]{}, "normal");
    assertEquals(1, allNormal.size()); // no
    Assertions.assertSame(allNormal.get(0).getClass(), UserImpl.class); // just no level user
    // 所有vip1级
    url = url.addParameter("level", "1");
    List<User> allVipLevel1 = extensionLoader.getActivateExtension(url, new String[]{}, "vip");
    assertEquals(1, allVipLevel1.size());
    Assertions.assertSame(allVipLevel1.get(0).getClass(), VipUser.class);
    // 所有2级?
    url = url.addParameter("level", "2");
    List<User> allLevel2 = extensionLoader.getActivateExtension(url, new String[]{}, null/*空group*/);
    assertEquals(3, allLevel2.size()); // no
    Assertions.assertSame(allLevel2.get(0).getClass(), UserImpl.class); // 10
    Assertions.assertSame(allLevel2.get(1).getClass(), UserImpl2.class); // 500
    Assertions.assertSame(allLevel2.get(2).getClass(), GoldenVipUser.class); // 1000
}

原理

类加载阶段,激活扩展点在普通扩展点分支逻辑中。

所以激活扩展点只是筛选普通扩展点的方式,属于普通扩展点的子集。

学新通技术网

ExtensionLoader#getActivateExtension获取激活扩展点的入参包含三部分:

1)查询URL;2)查询扩展点名称集合;3)查询分组

其中1和3用于Activate匹配,2用于直接从getExtension获取扩展点加在Activate匹配的扩展点之后。

学新通技术网

重点看isMatchGroup和isActive两个方法。

isMatchGroup:如果查询条件不包含group,则匹配,如果查询条件包含group,注解中必须有group与其匹配。

学新通技术网

isActive:匹配url

1)Activate没有value约束,匹配

2)url匹配成功条件:如果注解value配置为k:v模式,要求url参数kv完全匹配;如果注解value配置为k模式,只需要url包含kv参数即可。其中k还支持后缀匹配。

比如@Activate(value = {"level"})只需要url中有level=xxx即可,

而@Activate(value = {"level:2"})需要url中level=2。

学新通技术网

总结

本文分析了Dubbo2.7.6的SPI实现。

ExtensionLoader相较于java的spi能按需获取扩展点,还有很多高级特性,与Spring的ioc和aop非常相似。

看似ExtensionLoader的功能都能通过Spring实现,但是Dubbo不想依赖Spring,所以造了套轮子。

题外话:非常夸张的是,Dubbo一个RPC框架,竟然有27w行代码,而同样是RPC框架的sofa-rpc5.9.0只有14w行。

除了很多com.alibaba的老包兼容代码,轮子是真的多,早期版本连json库都是自己实现的,现在是换成fastjson了。

普通扩展点

ExtensionLoader#getExtension(name),普通扩展点通过扩展名获取。

@SPI
public interface MyExt {
    String echo(URL url, String s);
}

创建普通扩展点分为四个步骤

1)无参构造

2)依赖注入

3)包装

4)初始化

包装扩展点

如果扩展点实现包含该扩展点的单参构造方法,被认为是包装扩展点。

public class WrapperExt implements Ext {
    public WrapperExt(Ext ext) {
    }
}

包装扩展点无法通过扩展名显示获取,而是在用户获取普通扩展点时,自动包装普通扩展点,返回给用户,整个过程是透明的。

自适应扩展点

ExtensionLoader#getAdaptiveExtension获取自适应扩展点。

每个扩展点最多只有一个自适应扩展点。

自适应扩展点分为两类:硬编码、动态生成。

硬编码自适应扩展点,在扩展点实现class上标注Adaptive注解,优先级高于动态生成自适应扩展点。

@Adaptive
public class AdaptiveFruit implements Fruit {
   
}

动态生成自适应扩展点。

出现的背景是,dubbo中有许多依赖URL上下文选择不同扩展点策略的场景,如果通过硬编码实现,会有很多重复代码。

动态生成自适应扩展点,针对@Adaptive注解方法且方法参数有URL的扩展点,采用javassist字节码技术,动态生成策略实现。

@SPI
public interface SecKillFruit {
    @Adaptive("fruitType")
    int howMuch(URL context);
}

激活扩展点

激活扩展点属于普通扩展点的子集。

激活扩展点利用Activate注解,根据条件匹配一类扩展点实现

@Activate(group = {"vip"}, value = {"level:2"}, order = 1000)
public class GoldenVipUser implements User {
}

ExtensionLoader#getActivateExtension:通过group和URL查询一类扩展点实现。

@Test
void testActivate() {
    ExtensionLoader<User> extensionLoader = ExtensionLoader.getExtensionLoader(User.class);
    URL url = URL.valueOf("myProtocol://localhost/test");
    // 所有vip1级
    url = url.addParameter("level", "1");
    List<User> allVipLevel1 = extensionLoader.getActivateExtension(url, new String[]{}, "vip");
}

依赖注入

无论是普通/包装/自适应扩展点,在暴露给用户使用前,都会进行setter依赖注入。

依赖注入对象可来源于两部分:

1)SpiExtensionFactory根据type获取自适应扩展点

2)SpringExtensionFactory根据setter属性 type从ioc容器获取扩展点

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanffacg
系列文章
更多 icon
同类精品
更多 icon
继续加载