2.Dubbo动态扩展

Dubbo SPI机制解析
我们知道dubbo通过SPI机制实现了灵活的动态扩展,但是这些扩展的优先级怎么样?怎么去判断最后采用的是哪个实现?
先上结论:@Adaptive注解实现类 > 配置名对应的实现(例如:<provider protocol=dubbo ..>)> @Spi注解上的默认值
 
具体加载和选择执行流程图
 
接下来我们从这一行代码分析,Dubbo如何动态的选择接口具体的实现
private static final Protocol REF_PROTOCOL = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

 

1.ExtensionLoader.getExtensionLoader(Protocol.class) 
这一部分的作用是dubbo从指定位置加载配置文件,读取到Protocol接口所有可配置的实现。最终的实现是new ExtensionLoader<T>(type)
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() + "!");
}


ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
if (loader == null) {
    EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
    loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;

 

看一下ExtensionLoader的构造方法
private ExtensionLoader(Class<?> type) {
    this.type = type;
    objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}

 

因为我们这里的type是Protocol,所以这里objectFactory = ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension()
这里我们再拆成两部分来看
1.1ExtensionLoader.getExtensionLoader(ExtensionFactory.class)
这里的type是ExtensionFactory.class,所以就是简单的赋值type =ExtensionFactory.class, objectFactory = null
 
1.2 ExtensionLoader<ExtensionFactory>.getAdaptiveExtension()
这里基本上全是对缓存的处理,最终只有一行实现createAdaptiveExtension()
public T getAdaptiveExtension() {
    Object instance = cachedAdaptiveInstance.get();
    if (instance == null) {
        if (createAdaptiveInstanceError != null) {
            throw new IllegalStateException("Failed to create adaptive instance: " +
                    createAdaptiveInstanceError.toString(),
                    createAdaptiveInstanceError);
        }


        synchronized (cachedAdaptiveInstance) {
            instance = cachedAdaptiveInstance.get();
            if (instance == null) {
                try {
                    instance = createAdaptiveExtension();
                    cachedAdaptiveInstance.set(instance);
                } catch (Throwable t) {
                    createAdaptiveInstanceError = t;
                    throw new IllegalStateException("Failed to create adaptive instance: " + t.toString(), t);
                }
            }
        }
    }


    return (T) instance;
}

 

点进方法内部
private T createAdaptiveExtension() {
    try {
        return injectExtension((T) getAdaptiveExtensionClass().newInstance());
    } catch (Exception e) {
        throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
    }
}

 

从这里开始我们分成三个步骤来看
1.2.1getAdaptiveExtensionClass()
private Class<?> getAdaptiveExtensionClass() {
    getExtensionClasses();
    if (cachedAdaptiveClass != null) {
        return cachedAdaptiveClass;
    }
    return cachedAdaptiveClass = createAdaptiveExtensionClass();
}

 

先进入getExtensionClasses方法内部,穿过缓存处理,进入最终加载
private Map<String, Class<?>> loadExtensionClasses() {
    cacheDefaultExtensionName();


    Map<String, Class<?>> extensionClasses = new HashMap<>();
    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName());
    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
    loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
    loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
    loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
    loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
    return extensionClasses;
}

 

第一行 cacheDefaultExtensionName 是将@Spi注解上的值作为默认实现的key缓存起来,最多只能指定一个默认实现。比如Protocol指定dubbo为默认实现,这里的type是ExtensionFactory,没有默认值 
private void cacheDefaultExtensionName() {
    final SPI defaultAnnotation = type.getAnnotation(SPI.class);
    if (defaultAnnotation == null) {
        return;
    }


    String value = defaultAnnotation.value();
    if ((value = value.trim()).length() > 0) {
        String[] names = NAME_SEPARATOR.split(value);
        if (names.length > 1) {
            throw new IllegalStateException("More than 1 default extension name on extension " + type.getName()
                    + ": " + Arrays.toString(names));
        }
        if (names.length == 1) {
            cachedDefaultName = names[0];
        }
    }
}

 

接下来的6行是从指定目录位置加载接口的具体实现存储进Map里面,一共支持三个目录,因为dubbo捐献给Apache之后修改了目录,所以需要兼容迁移前后两种位置的接口名称,点进去看具体实现
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type) {
    String fileName = dir + type;
    try {
        Enumeration<java.net.URL> urls;
        ClassLoader classLoader = findClassLoader();
        if (classLoader != null) {
            urls = classLoader.getResources(fileName);
        } else {
            urls = ClassLoader.getSystemResources(fileName);
        }
        if (urls != null) {
            while (urls.hasMoreElements()) {
                java.net.URL resourceURL = urls.nextElement();
                loadResource(extensionClasses, classLoader, resourceURL);
            }
        }
    } catch (Throwable t) {
        logger.error("Exception occurred when loading extension class (interface: " +
                type + ", description file: " + fileName + ").", t);
    }
}

 

从每一个jar包的对应位置去加载资源,配置文件中的等号左边是实现名称,右边是具体实现类的全限定名,以extensionFactory为例
adaptive=org.apache.dubbo.common.extension.factory.AdaptiveExtensionFactory
spi=org.apache.dubbo.common.extension.factory.SpiExtensionFactory
spring=org.apache.dubbo.config.spring.extension.SpringExtensionFactory

 

这里使用Class.forname来把具体实现类加载为Class对象
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
    try {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
            String line;
            while ((line = reader.readLine()) != null) {
                final int ci = line.indexOf('#');
                if (ci >= 0) {
                    line = line.substring(0, ci);
                }
                line = line.trim();
                if (line.length() > 0) {
                    try {
                        String name = null;
                        int i = line.indexOf('=');
                        if (i > 0) {
                            name = line.substring(0, i).trim();
                            line = line.substring(i + 1).trim();
                        }
                        if (line.length() > 0) {
                            loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
                        }
                    } catch (Throwable t) {
                        IllegalStateException e = new IllegalStateException("Failed to load extension class (interface: " + type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t);
                        exceptions.put(line, e);
                    }
                }
            }
        }
    } catch (Throwable t) {
        logger.error("Exception occurred when loading extension class (interface: " +
                type + ", class file: " + resourceURL + ") in " + resourceURL, t);
    }
}

 

加载进来的所有实现类被分成了三种:1.带@Adaptive注解被缓存起来( cachedAdaptiveClass
                                                         2.构造方法带接口实例的,认为是接口的包装类,缓存在 cachedWrapperClasses中
                                                         3.其他的实例认为是可配置的实现,缓存在前面的map里面( extensionClasses )和 cachedActivates里面(带@Activate注解的)
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)) {
        cacheAdaptiveClass(clazz);
    } else if (isWrapperClass(clazz)) {
        cacheWrapperClass(clazz);
    } else {
        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);
            }
        }
    }
}

 

回到1.2.1  getAdaptiveExtensionClass方法
因为我们这里的type是ExtensionFactory,配置文件中存在@Adaptive的实现 AdaptiveExtensionFactory,所以这里我们使用了最高优先级的实现(存在缓存里面的AdaptiveExtensionFactory)直接返回了
注:Protocol文件里面没有@Adaptive的实现,所以会走下面的逻辑createAdaptiveExtensionClass,具体的实现放在标题2里面讲
 
1.2.2 Class.newInstance()
AdaptiveExtensionFactory类的实例化, 去加载其他扩展实现存储在factories里面
public AdaptiveExtensionFactory() {
    ExtensionLoader<ExtensionFactory> loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class);
    List<ExtensionFactory> list = new ArrayList<ExtensionFactory>();
    for (String name : loader.getSupportedExtensions()) {
        list.add(loader.getExtension(name));
    }
    factories = Collections.unmodifiableList(list);
}

 

去除缓存处理 ,最终反射实例化对象
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)) {
            for (Class<?> wrapperClass : wrapperClasses) {
                instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
            }
        }
        return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
                type + ") couldn't be instantiated: " + t.getMessage(), t);
    }
}

 

 
1.2.3 InjectExtension()
因为这里是extensionFactory这里的objectFactory为Null,直接返回了,否则反射调用set方法注入到实体内部
private T injectExtension(T instance) {


    if (objectFactory == null) {
        return instance;
    }


    try {
        for (Method method : instance.getClass().getMethods()) {
            if (!isSetter(method)) {
                continue;
            }
            /**
             * Check {@link DisableInject} to see if we need auto injection for this property
             */
            if (method.getAnnotation(DisableInject.class) != null) {
                continue;
            }
            Class<?> pt = method.getParameterTypes()[0];
            if (ReflectUtils.isPrimitives(pt)) {
                continue;
            }


            try {
                String property = getSetterProperty(method);
                Object object = objectFactory.getExtension(pt, property);
                if (object != null) {
                    method.invoke(instance, object);
                }
            } catch (Exception e) {
                logger.error("Failed to inject via method " + method.getName()
                        + " of interface " + type.getName() + ": " + e.getMessage(), e);
            }


        }
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    }
    return instance;
}

 

 
2. ExtensionLoader.getAdaptiveExtension()
这里是ExtensionLoader<Protocol>的 getAdaptiveExtension方法,这里其他部分和1.2完全相同,只不过protocol没有@Adaptive的实现,所以这里会走 createAdaptiveExtensionClass方法的逻辑
private Class<?> createAdaptiveExtensionClass() {
    String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
    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);
}

 

第一行是基于接口名称和缓存的默认实现名称来动态拼接一个java源文件出来,生成的代码如下:
可以看出来这是基于Protocol生成的动态代理实现,代理@Adaptive方法,
以refer方法为例,使用的扩展点名称 优先取参数url中的protocol,也就是我们写在配置中的protocol属性,最终会拼接在这个url上,如果没有则拿@Spi注解上的默认值 ,然后在缓存中通过扩展名拿到对应扩展实现类的class,反射创建扩展点实例,最终使用对应的扩展来完成对应的实现
package org.apache.dubbo.rpc;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {
    @Override
    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!");
    }
    @Override
    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!");
    }
    @Override
    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);
    }
    @Override
    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);
    }
}

 

后面的代码则是使用Compile来将生成的代码编译成Class对象,这里就不详细介绍了
 
 
 
 
 
 
<think>我们注意到用户的问题是关于Dubbo中SPI文件命名的差异:为什么Router的SPI文件路径是`org.apache.dubbo.rpc.cluster.Router`而Filter的是`alibaba.dubbo`开头? ### 问题核心 这是由于Dubbo项目发展过程中包名变更导致的。Dubbo最初由阿里巴巴开源,后来捐赠给Apache基金会,因此包名从`com.alibaba.dubbo`变更为`org.apache.dubbo`。 #### 详细解释 1. **历史版本差异**: - **Dubbo 2.6.x及之前**:使用`com.alibaba.dubbo`作为基础包名 ```properties # Filter SPI 文件路径 (旧版) META-INF/dubbo/com.alibaba.dubbo.rpc.Filter ``` - **Dubbo 2.7.x及之后**:使用`org.apache.dubbo`作为基础包名 ```properties # Router SPI 文件路径 (新版) META-INF/dubbo/org.apache.dubbo.rpc.cluster.Router ``` 2. **兼容性处理**: Dubbo在升级过程中保持了向下兼容: ```java // Dubbo内部兼容逻辑 public class ExtensionLoader { private static String[] compatiblePaths() { return new String[]{ "META-INF/dubbo/internal/", "META-INF/dubbo/", "META-INF/services/" // 标准SPI路径 }; } private void loadFromClassLoader() { // 同时加载新旧包名路径 loadResource(com.alibaba.dubbo.rpc.Filter.class); loadResource(org.apache.dubbo.rpc.Filter.class); } } ``` 3. **实际开发建议**: - **新项目**:统一使用`org.apache.dubbo`包名 - **老项目迁移**: ```diff - META-INF/dubbo/com.alibaba.dubbo.rpc.Filter + META-INF/dubbo/org.apache.dubbo.rpc.Filter ``` #### 验证示例 **场景**:在Dubbo 2.7.15中同时存在新旧SPI文件 1. **旧版Filter实现**(兼容模式): `src/main/resources/META-INF/dubbo/com.alibaba.dubbo.rpc.Filter`: ```properties legacyFilter=com.example.LegacyFilterImpl ``` 2. **新版Filter实现**(推荐方式): `src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter`: ```properties modernFilter=com.example.ModernFilterImpl ``` 3. **Dubbo加载日志**: ``` [INFO] Loading legacy extension: Filter (com.alibaba.dubbo.rpc.Filter) [INFO] Loading extension: Filter (org.apache.dubbo.rpc.Filter) ``` #### 包名变更对照表 | 组件类型 | 旧版包名 (2.6.x) | 新版包名 (2.7.x+) | |---------|-----------------|------------------| | **Filter** | `com.alibaba.dubbo.rpc.Filter` | `org.apache.dubbo.rpc.Filter` | | **Router** | `com.alibaba.dubbo.rpc.cluster.Router` | `org.apache.dubbo.rpc.cluster.Router` | | **Protocol** | `com.alibaba.dubbo.rpc.Protocol` | `org.apache.dubbo.rpc.Protocol` | #### 如何正确选择SPI文件名? 1. **查看接口源码**: ```java package org.apache.dubbo.rpc.cluster; // 注意包名 public interface Router { // 接口定义 } ``` 根据实际使用的Dubbo版本中接口所在的包确定SPI文件名。 2. **标准命名公式**: ``` META-INF/dubbo/ + {接口全限定名} ``` #### 多版本兼容方案 若需要同时支持Dubbo 2.6和2.7: ```tree src/main/resources/ ├── META-INF │ ├── dubbo │ │ ├── com.alibaba.dubbo.rpc.Filter // 2.6兼容 │ │ └── org.apache.dubbo.rpc.Filter // 2.7+标准 ``` 在实现类中做版本适配: ```java public class CrossVersionFilter implements Filter { // 同时实现两个接口(编译需分别引入两个版本的dubbo) public Result invoke(Invoker<?> invoker, Invocation invocation) { // 业务逻辑 } } ``` #### 包名变更背后的设计演进 ```mermaid graph LR A[阿里巴巴开源时期] -->|包名| B(com.alibaba.dubbo) B --> C{捐赠给Apache} C -->|孵化期| D(org.apache.dubbo) D -->|毕业| E[Apache顶级项目] ``` --- ### §§相关问题§§: 1. Dubbo SPI 与 Java 标准 SPI 机制有何区别? 2. 如何实现一个同时兼容 Dubbo 2.6 和 2.7 的 SPI 扩展? 3. Dubbo 3.0 在扩展机制上有哪些重大改进? 4. 在 OSGi 环境下使用 Dubbo SPI 会遇到什么问题?</think>### Dubbo SPI 文件命名差异解析 #### 核心原因:Dubbo 的包名变更历史 Dubbo SPI 文件路径的差异源于 Dubbo 项目的发展历程: 1. **Alibaba 时期**(2011-2017): - 包名以 `com.alibaba.dubbo` 开头 - SPI 文件路径:`META-INF/dubbo/com.alibaba.dubbo.xxx` 2. **Apache 孵化期**(2017-2019): - 包名变更为 `org.apache.dubbo` - SPI 文件路径:`META-INF/dubbo/org.apache.dubbo.xxx` 3. **Apache 顶级项目**(2019至今): - 保持 `org.apache.dubbo` 包名 - 新扩展点使用新路径 #### 具体差异分析 | 组件 | 旧版路径 (Alibaba) | 新版路径 (Apache) | 使用建议 | |--------|-------------------------------------|---------------------------------------|--------------------------| | Filter | `com.alibaba.dubbo.rpc.Filter` | `org.apache.dubbo.rpc.Filter` | ❌ 已废弃 | | Router | - | `org.apache.dubbo.rpc.cluster.Router` | ✅ 推荐使用 | | 其他 | `com.alibaba.dubbo.common.extension`| `org.apache.dubbo.common.extension` | 根据版本选择 | #### 技术实现原理 Dubbo 的 `ExtensionLoader` 会同时扫描两种路径: ```java public class ExtensionLoader<T> { private static final String[] DEFAULT_DIRECTORIES = new String[] { "META-INF/dubbo/internal/", // 内部实现 "META-INF/dubbo/", // 用户扩展 "META-INF/services/" // Java SPI 兼容 }; private void loadDirectory(...) { // 同时加载新旧包名路径 loadResource(extensionClasses, classLoader, directory, "com/alibaba/dubbo"); loadResource(extensionClasses, classLoader, directory, "org/apache/dubbo"); } } ``` #### 版本兼容处理方案 **场景**:需要支持 Dubbo 2.6.x (Alibaba) 和 2.7.x+ (Apache) 1. **双路径注册**(推荐): ```bash # 项目资源目录 src/main/resources/ ├── META-INF │ ├── dubbo │ │ ├── com.alibaba.dubbo.rpc.Filter # 旧版兼容 │ │ └── org.apache.dubbo.rpc.Filter # 新版标准 ``` 2. **兼容性实现类**: ```java // 同时实现新旧接口 public class CompatibleFilter implements com.alibaba.dubbo.rpc.Filter, // 旧接口 org.apache.dubbo.rpc.Filter { // 新接口 @Override public Result invoke(Invoker<?> invoker, Invocation invocation) { // 统一实现逻辑 } } ``` 3. **SPI 文件内容**: `META-INF/dubbo/com.alibaba.dubbo.rpc.Filter`: ``` compatible=com.your.CompatibleFilter ``` `META-INF/dubbo/org.apache.dubbo.rpc.Filter`: ``` compatible=com.your.CompatibleFilter ``` #### 新版最佳实践(Dubbo 3.x+) ```java // 1. 使用新包名实现 @Activate(group = "consumer") public class AuthFilter implements org.apache.dubbo.rpc.Filter { // 实现逻辑 } // 2. 注册到新路径 // 文件位置: src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter // 内容: // authFilter=com.your.AuthFilter ``` #### 版本检测工具 ```java public class DubboVersionDetector { public static void main(String[] args) { try { // 检测核心类包名 Class.forName("org.apache.dubbo.common.extension.ExtensionLoader"); System.out.println("Apache Dubbo (2.7.x+)"); // 检查具体版本 String version = ExtensionLoader.class.getPackage().getImplementationVersion(); System.out.println("Version: " + version); } catch (ClassNotFoundException e) { System.out.println("Alibaba Dubbo (2.6.x or earlier)"); } } } ``` #### 包名变更图示 ```mermaid graph LR A[Alibaba Dubbo] -->|2011-2017| B[com.alibaba.dubbo] B -->|2017 捐赠 Apache| C[Apache Incubator] C -->|2019 毕业| D[org.apache.dubbo] D -->|2021+| E[Dubbo 3.x] ``` #### 迁移路线图 1. **阶段1**:双路径注册(兼容旧版) 2. **阶段2**:逐步替换旧路径引用 3. **阶段3**:移除 `com.alibaba` 依赖 4. **阶段4**:升级到 Dubbo 3.x 纯新版 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值