Dubbo源码解析一Dubbo SPI

news2024/11/15 11:48:50

Dubbo SPI

    • 概述
    • 节点角色说明
    • 1. JDK SPI
      • 1.1 JDK SPI使用
      • 1.2 JDK SPI加载过程
      • 1.3 JDK SPI优缺点
        • 1.3.1 优点
        • 1.3.2 缺点
    • 2. Dubbo中的SPI
      • 2.1 概述
      • 2.2 入门案例
      • 2.3 源码分析
        • 2.3.1 依赖注入
        • 2.3.2 动态增强
          • 2.3.2.1 装饰者模式
          • 2.3.2.2 Dubbo中的AOP
        • 2.3.3 动态编译
          • 2.3.3.1 SPI中的自适应
            • 2.3.3.1.1 javassist入门
            • 2.3.3.1.2 源码分析
      • 2.4 总结Dubbo的SPI扩展机制

概述

Dubbo是阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring框架无缝集成。
Dubbo是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
dubbo运行架构

节点角色说明

节点角色说明
Provider暴露服务的服务提供方
Consumer调用远程服务的服务消费方
Registry统计服务的调用次数和调用时间的监控中心
Container服务运行容器

1. JDK SPI

SPI(Service Provider Interface) 是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件
Java SPI是一种以接口为基础,使用配置文件来加载(或称之为服务发现)的动态加载机制,主要使用JDK中 java.util.ServiceLoader 来实现
在这里插入图片描述

1.1 JDK SPI使用

1、创建一个接口
2、在jar包的 META-INF/services 目录下创建一个以"接口全限定名"为命名的文件,内容为实现类的全限定名(SPI的实现类必须带一个无参构造方法)
3、以接口的全路径做为名称,创建文件(注意不要带文件格式后缀)
4、在文件中写入实现类的全路径
5、在代码中调用ServiceLoader.load(接口.class)获取迭代器
6、遍历迭代器即可获取对应实现类的实例,通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM
如 MySQL驱动类
在这里插入图片描述
触发顺序为:
1、DriverManager在静态块中执行方法loadInitialDrivers()
2、loadInitialDrivers()中会有地方调用ServiceLoader.load(Driver.class)
3、触发hasNext()时,由于com.mysql.jdbc.Driver被ClassLoder扫到于是执行静态块,实例化一个自己并注册到DriverManager


public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }
 
    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }

1.2 JDK SPI加载过程

ServiceLoader位于java.util.ServiceLoader,所以这个类是会被启动类加载器所加载

1、首先找到ServiceLoader中的静态方法:load()
根据类加载的原理:如果一个类由类加载器A加载,那么这个类的依赖类也会被类加载器A加载(前提是这个依赖类尚未被加载过)
执行ServiceLoader.load(Driver.class),如果不使用线程上下文类加载器来打破双亲委托模型,那么该方法的关联类也会被启动类加载器加载

  1. 初始化了一个:ServiceLoader对象,new ServiceLoader<>(service, loader)
  2. 执行reload()
  3. new LazyIterator(service, loader)
public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

2、new了一个ServiceLoader的实例,并执行reload();调用另一个重载的load方法去加载Driver(即所谓的Service)

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    // 实例化中reload
    reload();
}

3、在reload中创建了一个懒加载迭代器

public void reload() {
    providers.clear();
    // 懒加载迭代器
    lookupIterator = new LazyIterator(service, loader);
}

4、当这个迭代器被调用hasNext时,跟着代码走下去,会发现有个PREFIX, 即 META-INF/services

// 迭代方法
public boolean hasNext() {
    if (acc == null) {
        return hasNextService();
    } else {
        PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
            public Boolean run() { return hasNextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
}
 
private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            // 由PREFIX + name获取了文件全路径
            String fullName = PREFIX + service.getName();
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                configs = loader.getResources(fullName);
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    }
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return false;
        }
        pending = parse(service, configs.nextElement());
    }
    nextName = pending.next();
    return true;
}
private static final String PREFIX = "META-INF/services/";

5、在Iterator遍历时,通过PREFIX + service.getName(),使用ClassLoader.getSystemResources(fullName)获取了配置里的内容,然后通过读文件的方式加载到自己的缓存,然后再调用迭代器的next()时,就会将其实例化

public S next() {
    if (acc == null) {
        return nextService();
    } else {
        PrivilegedAction<S> action = new PrivilegedAction<S>() {
            public S run() { return nextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
}
 
private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service,
             "Provider " + cn + " not found");
    }
    if (!service.isAssignableFrom(c)) {
        fail(service,
             "Provider " + cn  + " not a subtype");
    }
    try {
        // 实例化
        S p = service.cast(c.newInstance());
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();          // This cannot happen
}

1.3 JDK SPI优缺点

1.3.1 优点

使用 Java SPI 机制的优势是实现解耦,使得接口的定义与具体业务实现分离,而不是耦合在一起。应用进程可以根据实际业务情况启用或替换具体组件。

1.3.2 缺点

  • 不能按需加载。虽然 ServiceLoader 做了延迟载入,但是基本只能通过遍历全部获取,也就是接口的实现类得全部载入并实例化一遍。如果你并不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费
  • 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类
  • 多个并发多线程使用 ServiceLoader 类的实例是不安全的
  • 加载不到实现类时抛出并不是真正原因的异常,错误很难定位

2. Dubbo中的SPI

2.1 概述

Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类

2.2 入门案例

1、在使用Dubbo SPI 时,需要在接口上标注 @SPI 注解。

@SPI
public interface Robot {
    void sayHello();
}

2、Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下,与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置(key自己指定,value为实现类的全路径),配置内容如下。

注意:每一对key=value,我们把它称之为dubbo的扩展点

optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee

3、通过 ExtensionLoader,我们可以加载指定的实现类,下面来演示 Dubbo SPI 。

public class DubboSPITest {

    @Test
    public void sayHello() throws Exception {
        ExtensionLoader<Robot> extensionLoader = 
            ExtensionLoader.getExtensionLoader(Robot.class);
        Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
        optimusPrime.sayHello();
        Robot bumblebee = extensionLoader.getExtension("bumblebee");
        bumblebee.sayHello();
    }
}

测试结果如下:
在这里插入图片描述
Dubbo SPI优点:

  1. 能够实现按需加载,JDK SPI仅仅通过接口类名获取所有实现,在通过迭代器获取指定实现,而ExtensionLoader则通过接口类名和key值获取一个实现
  2. 支持AOP(将实现类包装在Wrapper中,Wrapper中实现公共增强逻辑)
  3. 支持IOC(能够通过set方法注入其他扩展点)

IOC 和 AOP 等特性,这些特性将会在接下来的源码分析中进行介绍。

2.3 源码分析

  1. 获取扩展点加载器, 此方法传入我们要加载的接口
    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
        /**
         * 校验
         * 1、不为空
         * 2、是接口
         * 3、接口上需要有@SPI注解
         */
        if (type == null) {
            throw new IllegalArgumentException("Extension type == null");
        }
        if (!type.isInterface()) {
            throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
        }
        if (!withExtensionAnnotation(type)) {
            throw new IllegalArgumentException("Extension type (" + type +
                    ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
        }
        /**
         * 先从EXTENSION_LOADERS(已加载的ExtensionLoader) 中获取
         * 每个接口type都对应一个 ExtensionLoader,该接口下会对应多个扩展点
         */
        ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        if (loader == null) {
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type)); // 每个接口type都对应一个 ExtensionLoader
            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        }
        return loader;
    }

可以看出传入的必须要是一个接口, 而且要加上@SPI注解

  1. 我们从 ExtensionLoader 的 getExtension 方法作为入口,对拓展类对象的获取过程进行详细的分析
public T getExtension(String name) {
    if (StringUtils.isEmpty(name)) {
        throw new IllegalArgumentException("Extension name == null");
    }
    if ("true".equals(name)) {
        return getDefaultExtension();
    }
    /**
     * name为 META-INF/dubbo下配置文件中配置的key
     *
     * 1、获取key对应Extension实例的holder (双重锁校验)--->getOrCreateHolder内部通过cachedInstances缓存所有实例key及对应的Holder
     * 2、创建key对应的Extension实例并存入holder
     * 3、返回对应的Extension实例
     */
    final Holder<Object> holder = getOrCreateHolder(name);
    Object instance = holder.get();
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
                //创建实例的核心方法
                instance = createExtension(name);
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}

上面代码的逻辑比较简单,首先检查缓存,缓存未命中则创建拓展对象。下面我们来看一下创建拓展对象的过程是怎样的。双重检查锁dcl(double-checked locking)保证线程安全

private T createExtension(String name) {
    // 从配置文件中加载所有的拓展类,可得到“配置项名称”到“配置类”的映射关系表
    Class<?> clazz = getExtensionClasses().get(name);
    if (clazz == null) {
        throw findException(name);
    }
    try {
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
            // 通过反射创建实例
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
            instance = (T) EXTENSION_INSTANCES.get(clazz);
        }
        // 向实例中注入依赖
        injectExtension(instance);
        Set<Class<?>> wrapperClasses = cachedWrapperClasses;
        if (CollectionUtils.isNotEmpty(wrapperClasses)) {
              // 循环创建 Wrapper 实例
            for (Class<?> wrapperClass : wrapperClasses) {
                // 将当前 instance 作为参数传给 Wrapper 的构造方法,并通过反射创建 Wrapper 实例。
                // 然后向 Wrapper 实例中注入依赖,最后将 Wrapper 实例再次赋值给 instance 变量
                instance = injectExtension(
                    (T) wrapperClass.getConstructor(type).newInstance(instance));
            }
        }
        return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("...");
    }
}

createExtension 方法的逻辑稍复杂一下,包含了如下的步骤:
1、通过 getExtensionClasses 获取所有的拓展类
2、通过反射创建拓展对象
3、向拓展对象中注入依赖
4、将拓展对象包裹在相应的 Wrapper 对象中
以上步骤中,第一个步骤是加载拓展类的关键,第三和第四个步骤是 Dubbo IOC 与 AOP 的具体实现。由于此类设计源码较多,这里简单的总结下ExtensionLoader整个执行逻辑:

getExtension(String name)  #根据key获取拓展对象
    -->createExtension(String name) #创建拓展实例
        -->getExtensionClasses #根据路径获取所有的拓展类
            -->loadExtensionClasses #加载拓展类
                -->cacheDefaultExtensionName #解析@SPI注解
            -->loadDirectory #方法加载指定文件夹配置文件
                -->loadResource #加载资源
                    -->loadClass #加载类,并通过 loadClass 方法对类进行缓存

值得一看的是loadResource 加载扩展点时将不同的扩展点保存在了不同的容器中

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
    if (!type.isAssignableFrom(clazz)) {
        throw new IllegalStateException("Error occurred when loading extension class (interface: " +
                type + ", class line: " + clazz.getName() + "), class "
                + clazz.getName() + " is not subtype of interface.");
    }
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        // 扩展点Class上有 Adaptive注解
        cacheAdaptiveClass(clazz);
    } else if (isWrapperClass(clazz)) { // 扩展点类有接口类型的构造函数,表明是Wrapper
        // 添加到Set<Class<?>> cachedWrapperClasses 缓存起来
        cacheWrapperClass(clazz);
    } else { // 证明是普通 extensionClasses
        clazz.getConstructor();
        if (StringUtils.isEmpty(name)) {
            name = findAnnotationName(clazz);
            if (name.length() == 0) {
                throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
            }
        }

        String[] names = NAME_SEPARATOR.split(name);
        if (ArrayUtils.isNotEmpty(names)) {
            cacheActivateClass(clazz, names[0]);
            for (String n : names) {
                cacheName(clazz, n);
                saveInExtensionClass(extensionClasses, clazz, n);
            }
        }
    }
}

2.3.1 依赖注入

Dubbo IOC 是通过 setter 方法注入依赖。Dubbo 首先会通过反射获取到实例的所有方法,然后再遍历方法列表,检测方法名是否具有 setter 方法特征。若有,则通过 ObjectFactory 获取依赖对象,最后通过反射调用 setter 方法将依赖设置到目标对象中。整个过程对应的代码如下:

private T injectExtension(T instance) {
    try {
        if (objectFactory != null) {
            // 遍历目标类的所有方法
            for (Method method : instance.getClass().getMethods()) {
                // 检测方法是否以 set 开头,且方法仅有一个参数,且方法访问级别为 public
                if (method.getName().startsWith("set")
                    && method.getParameterTypes().length == 1
                    && Modifier.isPublic(method.getModifiers())) {
                    // 获取 setter 方法参数类型
                    Class<?> pt = method.getParameterTypes()[0];
                    try {
                        // 获取属性名,比如 setName 方法对应属性名 name
                        String property = getSetterProperty(method);
                        /* getSetterProperty :
                        method.getName().length() > 3 ? 
                            method.getName().substring(3, 4).toLowerCase() + 
                                method.getName().substring(4) : "";
                        */
                        // 从 ObjectFactory 中获取依赖对象
                        Object object = objectFactory.getExtension(pt, property);
                        if (object != null) {
                            // 通过反射调用 setter 方法设置依赖
                            method.invoke(instance, object);
                        }
                    } catch (Exception e) {
                        logger.error("fail to inject via method...");
                    }
                }
            }
        }
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    }
    return instance;
}

在上面代码中,objectFactory 变量的类型为 AdaptiveExtensionFactory,AdaptiveExtensionFactory 内部维护了一个 ExtensionFactory 列表,用于存储其他类型的 ExtensionFactory。Dubbo 目前提供了两种 ExtensionFactory,分别是 SpiExtensionFactory 和 SpringExtensionFactory。前者用于创建自适应的拓展,后者是用于从 Spring 的 IOC 容器中获取所需的拓展。这两个类的类的代码不是很复杂,这里就不一一分析了。

Dubbo IOC 目前仅支持 setter 方式注入,总的来说,逻辑比较简单易懂。

2.3.2 动态增强

在用Spring的时候,我们经常会用到AOP功能。在目标类的方法前后插入其他逻辑。比如通常使用Spring AOP来实现日志,监控和鉴权等功能。 Dubbo的扩展机制,是否也支持类似的功能呢?答案是yes。

在Dubbo中,有一种特殊的类,被称为Wrapper类。通过装饰者模式,使用包装类包装原始的扩展点实例。在原始扩展点实现前后插入其他逻辑,实现AOP功能。

2.3.2.1 装饰者模式

装饰者模式:在不改变原类文件以及不使用继承的情况下,动态地将责任附加到对象上,从而实现动态拓展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。
在这里插入图片描述
一般来说装饰者模式有下面几个参与者:

  • Component:装饰者和被装饰者共同的父类,是一个接口或者抽象类,用来定义基本行为
  • ConcreteComponent:定义具体对象,即被装饰者
  • Decorator:抽象装饰者,继承自Component,从外类来扩展ConcreteComponent。对于ConcreteComponent来说,不需要知道Decorator的存在,Decorator是一个接口或抽象类
  • ConcreteDecorator:具体装饰者,用于扩展ConcreteComponent
    注:装饰者和被装饰者对象有相同的超类型,因为装饰者和被装饰者必须是一样的类型,这里利用继承是为了达到类型匹配,而不是利用继承获得行为
2.3.2.2 Dubbo中的AOP

Dubbo AOP 是通过装饰者模式完成的,接下来通过一个简单的案例来学习dubbo中AOP的实现方式。

首先定义一个接口

package com.demo.dubbo;

import org.apache.dubbo.common.extension.SPI;

@SPI
public interface Phone {
    void call();
}

定义接口的实现类,也就是被装饰者

package com.demo.dubbo;

public class IphoneX implements Phone {

    @Override
    public void call() {
        System.out.println("iphone正在拨打电话");
    }
}

为了简单,这里省略了装饰者接口。仅仅定义一个装饰者,实现phone接口,内部配置增强逻辑方法

package com.demo.dubbo;

public class MusicPhone implements Phone {

    private Phone phone;

    public MusicPhone(Phone phone) {
        this.phone = phone;
    }

    @Override
    public void call() {
        System.out.println("播放彩铃");
        this.phone.call();
    }
}

添加拓展点配置文件META-INF/dubbo/com.demo.dubbo.Phone,内容如下

iphone=com.demo.dubbo.IphoneX
wrapper=com.demo.dubbo.MusicPhone

具体执行效果如下:
在这里插入图片描述
先调用装饰者增强,再调用目标方法完成业务逻辑。
通过测试案例,可以看到在Dubbo SPI中具有增强AOP的功能,我们只需要关注dubbo源码中这样一行代码就够了 可以看出实现类可以被多层包装

// 检查是否具有装饰者类,如果有调用装饰者类的构造方法,并返回实例对象
if (CollectionUtils.isNotEmpty(wrapperClasses)) {
    for (Class<?> wrapperClass : wrapperClasses) {
        instance = injectExtension(
            (T) wrapperClass.getConstructor(type).newInstance(instance));
    }
}

2.3.3 动态编译

2.3.3.1 SPI中的自适应

我们知道在 Dubbo 中,很多拓展都是通过 SPI 机制 进行加载的,比如 Protocol、Cluster、LoadBalance、ProxyFactory 等。有时,有些拓展并不想在框架启动阶段被加载,而是希望在拓展方法被调用时,根据运行时参数进行加载,即根据参数动态加载实现类。如下所示:

根据参数动态选择的意思是:
通过ExtensionLoader.getExtensionLoader(XXXClass).getExtension(key)的形式来获取接口的某个实现类。
但这种形式本质上还是通过硬编码的形式在代码中固定的获取了接口的一个实现,诸如Protocol(实现有Dubbo、Redis、Thrift等),或者Transporter(实现有Netty、Mina等)这些接口,我们是可以在Dubbo服务声明时指定具体实现的
在这里插入图片描述
这种在运行时,根据方法参数才动态决定使用具体的拓展,在dubbo中就叫做扩展点自适应实例。其实是一个扩展点的代理,将扩展的选择从Dubbo启动时,延迟到RPC调用时。Dubbo中每一个扩展点都有一个自适应类,如果没有显式提供,Dubbo会自动为我们创建一个,默认使用Javaassist。

自适应拓展机制的实现逻辑是这样的
首先 Dubbo 会为拓展接口生成具有代理功能的代码;
通过 javassist 或 jdk 编译这段代码,得到 Class 类;
通过反射创建代理类;
在代理类中,通过URL对象的参数来确定到底调用哪个实现类;

2.3.3.1.1 javassist入门

Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态AOP框架。javassist是jboss的一个子项目,其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。为了方便更好的理解dubbo中的自适应,这里通过案例的形式来熟悉下Javassist的基本使用

package com.demo.compiler;

import java.io.File;
import java.io.FileOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtField;
import javassist.CtMethod;
import javassist.CtNewMethod;

/**
 *  Javassist是一个开源的分析、编辑和创建Java字节码的类库
 *  能动态改变类的结构,或者动态生成类
 */
public class CompilerByJavassist {

    public static void main(String[] args) throws Exception {

        // ClassPool:class对象容器
        ClassPool pool = ClassPool.getDefault();

        // 通过ClassPool生成一个User类
        CtClass ctClass = pool.makeClass("com.itheima.domain.User");

        // 添加属性     -- private String username
        CtField enameField = new CtField(pool.getCtClass("java.lang.String"),
                "username", ctClass);

        enameField.setModifiers(Modifier.PRIVATE);

        ctClass.addField(enameField);

        // 添加属性    -- private int age
        CtField enoField = new CtField(pool.getCtClass("int"), "age", ctClass);

        enoField.setModifiers(Modifier.PRIVATE);

        ctClass.addField(enoField);

        //添加方法
        ctClass.addMethod(CtNewMethod.getter("getUsername", enameField));
        ctClass.addMethod(CtNewMethod.setter("setUsername", enameField));
        ctClass.addMethod(CtNewMethod.getter("getAge", enoField));
        ctClass.addMethod(CtNewMethod.setter("setAge", enoField));


        // 无参构造器
        CtConstructor constructor = new CtConstructor(null, ctClass);
        constructor.setBody("{}");
        ctClass.addConstructor(constructor);

        // 添加构造函数
        //ctClass.addConstructor(new CtConstructor(new CtClass[] {}, ctClass));

        CtConstructor ctConstructor = new CtConstructor(new CtClass[] {pool.get(String.class.getName()),CtClass.intType}, ctClass);
        ctConstructor.setBody("{\n this.username=$1; \n this.age=$2;\n}");
        ctClass.addConstructor(ctConstructor);

        // 添加自定义方法
        CtMethod ctMethod = new CtMethod(CtClass.voidType, "printUser",new CtClass[] {}, ctClass);
        // 为自定义方法设置修饰符
        ctMethod.setModifiers(Modifier.PUBLIC);
        // 为自定义方法设置函数体
        StringBuffer buffer2 = new StringBuffer();
        buffer2.append("{\nSystem.out.println(\"用户信息如下\");\n")
                .append("System.out.println(\"用户名=\"+username);\n")
                .append("System.out.println(\"年龄=\"+age);\n").append("}");
        ctMethod.setBody(buffer2.toString());
        ctClass.addMethod(ctMethod);

        //生成一个class
        Class<?> clazz = ctClass.toClass();

        Constructor cons2 = clazz.getDeclaredConstructor(String.class,Integer.TYPE);

        Object obj = cons2.newInstance("itheima",20);

        //反射 执行方法
        obj.getClass().getMethod("printUser", new Class[] {})
                .invoke(obj, new Object[] {});

        // 把生成的class文件写入文件
        byte[] byteArr = ctClass.toBytecode();
        FileOutputStream fos = new FileOutputStream(new File("C://User.class"));
        fos.write(byteArr);
        fos.close();
    }
}

通过以上代码,我们可以知道使用javassist可以方便的在运行时,按需动态的创建java对象,并执行内部方法。而这也是dubbo中动态编译的核心

2.3.3.1.2 源码分析

Adaptive注解

在开始之前,我们有必要先看一下与自适应拓展息息相关的一个注解,即 Adaptive 注解。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {
    String[] value() default {};
}

从上面的代码中可知,Adaptive 可注解在类或方法上。

  • 标注在类上:Dubbo 不会为该类生成代理类。Adaptive 注解在类上的情况很少,在 Dubbo 中,仅有两个类被 Adaptive 注解了,分别是 AdaptiveCompiler 和 AdaptiveExtensionFactory。此种情况,表示拓展的加载逻辑由人工编码完成。更多时候,Adaptive 是注解在接口方法上的,表示拓展的加载逻辑需由框架自动生成
  • 标注在方法上:Dubbo 则会为该方法生成代理逻辑,表示当前方法需要根据 参数URL 调用对应的扩展点实现。例如 Protocol的SPI类有 injvm dubbo registry filter listener等等 很多扩展未知类,
    它设计了Protocol$Adaptive的类,通过ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(spi类);来提取对象

获取自适应拓展类
dubbo中每一个扩展点都有一个自适应类,如果没有显式提供,Dubbo会自动为我们创建一个,默认使用Javaassist。 先来看下创建自适应扩展类的代码:

// 精简代码如下
public T getAdaptiveExtension() {
    Object instance = cachedAdaptiveInstance.get();
    if (instance == null) {
            synchronized (cachedAdaptiveInstance) {
                instance = cachedAdaptiveInstance.get();
                if (instance == null) {
                      instance = createAdaptiveExtension();
                      cachedAdaptiveInstance.set(instance); 
                }
            }        
    }

    return (T) instance;
}

继续看createAdaptiveExtension方法

private T createAdaptiveExtension() {        
    return injectExtension((T) getAdaptiveExtensionClass().newInstance());
}

继续看getAdaptiveExtensionClass方法

private Class<?> getAdaptiveExtensionClass() {
        getExtensionClasses();
        if (cachedAdaptiveClass != null) {
            return cachedAdaptiveClass;
        }
        return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }

继续看createAdaptiveExtensionClass方法,绕了一大圈,终于来到了具体的实现了。看这个createAdaptiveExtensionClass方法,它首先会生成自适应类的Java源码,然后再将源码编译成Java的字节码,加载到JVM中。

private Class<?> createAdaptiveExtensionClass() {
        String code = createAdaptiveExtensionClassCode();
        ClassLoader classLoader = findClassLoader();
        org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
        return compiler.compile(code, classLoader);
    }

Compiler的代码,默认实现是javassist

@SPI("javassist")
public interface Compiler {
    Class<?> compile(String code, ClassLoader classLoader);
}

方法实现比较长,这里就不贴代码了。这种生成字节码的方式也挺有意思的,先生成Java源代码,然后编译,加载到jvm中。通过这种方式,可以更好的控制生成的Java类。而且这样也不用care各个字节码生成框架的api等。因为xxx.java文件是Java通用的,也是我们最熟悉的。只是代码的可读性不强,需要一点一点构建xx.java的内容。
示例:以 Protocol 接口为例,Protocol接口定义如下:

@SPI("dubbo")
public interface Protocol {

    int getDefaultPort();

    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;

    @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;

    void destroy();
    default List<ProtocolServer> getServers() {
        return Collections.emptyList();
    }

}

下面给大家展示一下生成的 Protocol$Adaptive 的源码,如下

package org.apache.dubbo.rpc;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {
    
    public org.apache.dubbo.rpc.Invoker refer(java.lang.Class arg0, org.apache.dubbo.common.URL arg1) throws org.apache.dubbo.rpc.RpcException {
        if (arg1 == null) throw new IllegalArgumentException("url == null");
        org.apache.dubbo.common.URL url = arg1;
        String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
        if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])");
        org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.refer(arg0, arg1);
    }
    
    public java.util.List getServers()  {
        throw new UnsupportedOperationException("The method public default java.util.List org.apache.dubbo.rpc.Protocol.getServers() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
    }
    
    public org.apache.dubbo.rpc.Exporter export(org.apache.dubbo.rpc.Invoker arg0) throws org.apache.dubbo.rpc.RpcException {
        if (arg0 == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
        if (arg0.getUrl() == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");
        org.apache.dubbo.common.URL url = arg0.getUrl();
        String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
        if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])");
        org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.export(arg0);
    }
    
    public void destroy()  {
        throw new UnsupportedOperationException("The method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
    }
    public int getDefaultPort()  {
        throw new UnsupportedOperationException("The method public abstract int org.apache.dubbo.rpc.Protocol.getDefaultPort() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
    }
}

调用 Protocol 的接口的 refer方法为例,下面给大家看一下自适应拓展的整个过程:

  1. 先通过 Protocol REF_PROTOCOL = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension(); 生成了 Protocol$Adaptive 代理类
  2. 传参 Url 为:dubbo://192.168.1.247:20887/org.apache.dubbo.config.spring.api.DemoService,调用 Protocol 的 refer 方法,此时直接调用是 Protocol$Adaptive 代理类的 refer 方法
  3. 在 Protocol$Adaptive 的 refer 方法中先调用 url 中的 getProtocol() 方法获取拓展类名称,赋值给 extName 变量
  4. 然后调用 org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName); 语句获取到具体的实现类的实例
  5. 最后执行 extension.refer(arg0, arg1) 语句,调用 4 中获取到的具体实现类的 refer 方法,最终返回结果

2.4 总结Dubbo的SPI扩展机制

  1. 能完成类型Spring的IOC(依赖注入)的功能, 只能通过set方法注入
  2. wrapper机制, 通过装饰者模式可以对其他的扩展点进行增强(wrapper中提供接口类型的构造), 同时wrapper中也可以注入其他扩展点, 可以实现Spring的AOP功能
  3. 配置默认扩展点: @SPI(“默认扩展的key”)
  4. Dubbo SPI的自适应(Adaptive), 由自适应扩展点去做类似适配的工作, 该注解(@Adaptive)即可以标注在类上(少数, dubbo大部分使用javassist代理技术生成), 也可以标注在方法上

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/511397.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数据结构】- 几个步骤教你认识并实现一个链表之带头(哨兵位)双向循环链表(中)

文章目录 前言&#x1f31f;一、带头双向循环链表&#x1f30f;1.1头删&#xff1a;&#x1f4ab;1.1.1代码&#xff1a;&#x1f4ab;1.1.2流程图&#xff1a; &#x1f30f;1.2尾删&#xff1a;&#x1f4ab;1.2.1代码&#xff1a;&#x1f4ab;1.2.2流程图&#xff1a; &…

5. Mysql索引优化实战二

MySQL性能调优 1. 分页查询优化1.1 根据自增且连续的主键排序的分页查询1.2 根据非主键字段排序的分页查询 2. Join关联查询优化 本文是按照自己的理解进行笔记总结&#xff0c;如有不正确的地方&#xff0c;还望大佬多多指点纠正&#xff0c;勿喷。 本节课内容&#xff1a; 1…

HTTP第七讲——HTTP报文

报文结构 拿 TCP 报文来举例&#xff0c;它在实际要传输的数据之前附加了一个 20 字节的头部数据&#xff0c;存储 TCP 协议必须的额外信息&#xff0c;例如发送方的端口号、接收方的端口号、包序号、标志位等等。 有了这个附加的 TCP 头&#xff0c;数据包才能够正确传输&…

HTML 基础知识

HTML基础知识 1. VSCode的安装与配置 下载地址 https://code.visualstudio.com/ 安装插件 Live Server Auto Rename Tag 自动格式化 点击 settings&#xff0c;然后输入format&#xff0c;然后勾选上 Format On Save。 2. HTML 基础标签 2.1 文件结构 快捷键&#xff1…

安卓实现左侧列表选择项点击,右侧fragment切换

安卓实现左侧列表选择项点击&#xff0c;右侧fragment切换 问题背景 安卓日常开发中&#xff0c;有时候需要开发页面中&#xff0c;显示左侧会列表选择项&#xff0c;点击不同的选项后右侧切换fragment显示&#xff0c;本文将介绍实现的一个思路。 问题分析 要实现的效果如…

前端三剑客 HTML+CSS+JS

文章目录 一、HTML1.1 基础标签1.2 列表1.3 表格1.4 表单 二、CSS2.1 引入方式2.2 CSS 选择器2.2.1 基本选择器2.2.2 组合选择器 2.3 常用属性2.3.1 背景2.3.2 文本2.3.3 字体2.3.4 display元素类型2.3.5 浮动2.3.6 盒子模型 三、JavaScript3.1 引入方式3.2 数据类型3.2.1 数组…

tomcat集群下的session共享和负载均衡

环境 操作系统&#xff1a;windows tomcat1&#xff1a;Apache Tomcat/7.0.52&#xff08;8085&#xff09; tomcat2&#xff1a;Apache Tomcat/7.0.52&#xff08;8086&#xff09; jre&#xff1a;1.7.0_80 nginx&#xff1a;nginx-1.20.1&#xff08;8070&#xff09; redis…

cmd@快捷键方式@静默执行命令@修复桌面空白快捷方式图标

文章目录 ref前言快捷方式执行命令行或打开文件eg:直接打开某个文件 创建快捷方式eg:快捷方式运行命令 修复快捷方式图标空白问题逐个修复批量修复一次性操作:逐步操作 执行效果第三方工具修复 ref How can I execute a Windows command line in background? - Super Userstb…

2022年5个不寻常的Web3预测

正如埃隆马斯克所说&#xff0c;“最有趣的结果是最有可能的”。所以&#xff0c;这是我对web3的5个不同寻常的预测&#xff0c;下面我将详细介绍我是如何得出这些想法的&#xff1a; 口袋妖怪训练师将是一份全职工作有人会使用JPEG支持的贷款购买房屋(IRL)DAO将收购一家上市公…

并发编程11:Synchronized与锁升级

文章目录 11.1 面试题11.2 Synchronized的性能变化11.3 Synchronized锁种类及升级步骤11.3.1 多线程访问情况11.3.2 升级流程11.3.3 无锁11.3.4 偏锁11.3.5 轻锁11.3.6 重锁11.3.7 小总结 11.4 JIT编译器对锁的优化11.4.1 JIT11.4.2 锁消除11.4.3 锁粗化 11.5 小总结 11.1 面试…

电影《银河护卫队3》观后感

上周看了电影《银河护卫队3》&#xff0c;本部电影&#xff0c;主要是围绕着主角团队中的一个队员展开叙事的&#xff0c;在团队中&#xff0c;这名队员叫“火箭”&#xff0c;是一只经过基因改造过的浣熊。 当初进行改造的团队&#xff0c;是一家拥有基因改造技术的团队&…

基于SpringBoot, Vue实现的校园二手书交易系统

背景 在Internet高速发展的今天&#xff0c;计算机的应用几乎完全覆盖我们生活的各个领域&#xff0c;互联网在经济&#xff0c;生活等方面有着举足轻重的地位&#xff0c;成为人们资源共享&#xff0c;信息快速传递的重要渠道。在中国&#xff0c;网上管理的兴起也同时飞速发…

Solidity中哈希函数的编码与解码

起因 写这篇文章的起因&#xff0c;是我在前端调试合约的时候&#xff0c;发现合约报错了&#xff0c;点开命令行报错&#xff0c;发现返回的是合约的 callData&#xff0c;我直接表演一个眼前一黑&#xff0c;我怎么直接的知道是调用哪个方法的时候报错呢&#xff1f; 于是有…

【网络基础知识概念】路由器,交换机,无线AP,DHCP,DNS,WAN接口和LAN接口是什么?(附实物图详解)

【写在前面】其实在做一些试题的时候&#xff0c;经常会有些概念性的东西完全不清楚&#xff0c;今天我就带大家整理一下&#xff0c;交换机是啥&#xff1f;路由器是啥&#xff1f;无线AP是啥&#xff1f;ADSL又是什么&#xff0c;啥叫DHCP&#xff0c;DNS又是啥&#xff1f;W…

改进YOLOv5 | C3模块改动篇 | 轻量化设计 |骨干引入动态卷积|CondConv

CondConv: Conditionally Parameterized Convolutions for Efficient Inference 卷积是当前CNN网络的基本构成单元之一,它的一个基本假设是:卷积参数对所有样例共享。作者提出一种条件参数卷积,它可以为每个样例学习一个特定的卷积核参数,通过替换标准卷积,CondConv可以提…

shell脚本之“sort“、“uniq“、“tr“、“cut“命令详解

文章目录 sort命令uniq命令tr命令cut命令 sort命令 以行为单位对文件内容进行排序&#xff0c;也可以根据不同的数据类型来排序. 比较原则&#xff1a;从首字符向后&#xff0c;依次按ASCII码值进行比较&#xff0c;最后将他们按升序输出. 语法格式 sort [选项] 参数 cat …

c++ this指针

this指针介绍&#xff1a; c中成员变量和成员函数分开存储&#xff0c;每一个非静态成员函数只会有一个实例&#xff0c;多个同类型对象共用这一个成员函数。那么代码怎么区分哪个对象调用自己呢&#xff1f;this指针由此应运而生。 c通过提供对象指针&#xff0c;this指针。…

2020年下半年软件设计师下午试题

【试题四】希尔排序 【说明】 希尔排序算法又称最小增量排序算法&#xff0c;其基本思想是&#xff1a; 步骤1 :构造一个步长序列delta、deltak、 deltak &#xff0c;其中delta1n/2 &#xff0c;后面的每个delta是前一个的1/2 &#xff0c; deltak1&#xff1b; 步骤2 :根…

【shell脚本里的命令】

目录 一、sort命令1.1、命令演示 二、unip命令1、命令演示1、列题:2、使用脚本来查看用户有没有被恶意登录&#xff0c;查看登录用户的对应ip地址 三、tr命令1.1、命令演示1.2、使用tr命令对数组进行排序 五、从Windows里拉文件到Linux系统中要做的潜在条件六、cut命令 一、sor…

vue 阻止事件冒泡和捕获

文章目录 1. js 事件的三阶段2. js 阻止事件冒泡&#xff0c;捕获3、JavaScript基础知识&#xff1a;preventDefault和stopPropagationpreventDefault()事件方法stopPropagation()事件方法 click.stop : 阻止事件冒泡 click.prevent : 阻止事件默认行为 click.self : 事件只作用…