『Dubbo SPI源码分析』SPI 机制分析

Dubbo SPI 机制分析

  • 基于 2.7.0 版本
  1. 创建测试 demo
  • 首先创建一个接口,举例为 Car
package com.luban.dubbo_spi.api;

@SPI
public interface Car {
    public void getColor();
}
  • 根据接口,先创建一个实现类
package com.luban.dubbo_spi.impl;

public class RedCar implements Car {

    public void getColor() {
        System.out.println("red");
    }
}
  • 然后在 resources 创建一个目录 META-INF.services 目录
  • 并在 META-INF.services 目录中创建文件 com.luban.dubbo_spi.api.Car 一定要与接口同名
red = com.luban.dubbo_spi.impl.RedCar
  • 创建主程序测试 @SPI
public class CarDemo {

    public static void main(String[] args) {
        ExtensionLoader<Car> extensionLoader =
                ExtensionLoader.getExtensionLoader(Car.class);


        Car redCar = extensionLoader.getExtension("red");
        redCar.getColor();
    }
}
  1. 首先通过 ExtensionLoader.getExtensionLoader() 获取加载器
public class CarDemo {

    public static void main(String[] args) {
        // 1. 获取加载器
        ExtensionLoader<Car> extensionLoader =
                ExtensionLoader.getExtensionLoader(Car.class);
       ...
    }
}
  1. 调用 ExtensionLoader.getExtensionLoader() 时,会先判断接口是有 @SPI 标注,然后通过传入 type(即 Car.class),通过 EXTENSION_LOADERS 属性缓存获取 ExtensionLoader,没有则新建一个
  • 重点
    • 必须加载的是接口
    • 必须有 @SPI 标注
public class ExtensionLoader<T> {
    // 缓存 ExtensionLoader,每个接口对应一个 ExtensionLoader
    private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>();
    ...
    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
        // type = interface com.luban.dubbo_spi.api.Car
        if (type == null) {
            throw new IllegalArgumentException("Extension type == null");
        }
        // 不能不是接口
        if (!type.isInterface()) { 
            throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
        }
         // 不能没有 SPI 标注
        if (!withExtensionAnnotation(type)) {
            throw new IllegalArgumentException("Extension type(" + type +
                    ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
        }

        // 缓存 ExtensionLoader,
        ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        if (loader == null) {
            // 1. key:Car.class value:创建工厂,进行一次创建
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type)); 
            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        }
        return loader;
    }
}
  1. 因为 ExtensionLoader 的构造是私有的,先把 type 赋值到 type 属性,同事创建一个 objectFactory 工厂,由于第一次创建。所以得通过 ExtensionLoader.getExtensionLoader 创建 ExtensionFactory.class 类,陷入一次循环中,
public class ExtensionLoader<T> {

  private final Class<?> type;
  // 用于依赖注入
  private final ExtensionFactory objectFactory;
    ...
    private ExtensionLoader(Class<?> type) {
        // 这次的 type 是 Car.class
        this.type = type;
        // 1. 拿出 ExtensionFactory 接口的所有实现类中的 Adaptive 实现
        objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
    }
}
  1. 再次进入到 getExtensionLoader() 方法,只不过这次创建的是 ExtensionFactory.class
public class ExtensionLoader<T> {
    // 缓存 ExtensionLoader,每个接口对应一个 ExtensionLoader
    private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>();
    ...
    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
        // 这次 type = ExtensionFactory.class
        ...
        // 缓存 ExtensionLoader
        ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        if (loader == null) {
            // 1. key:interface org.apache.dubbo.common.extension.ExtensionFactory value:创建工厂,进行一次创建
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type)); 
            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        }
        return loader;
    }
}
  1. 调用 ExtensionLoader() 方法时,这次的 objectFactory 为空,然后返回 ExtensionFactoryExtensionLoader 对象,同时 EXTENSION_LOADERS 属性包含一个 ExtensionFactory.class 的 key-value 对
public class ExtensionLoader<T> {

  private final Class<?> type;
  // 用于依赖注入
  private final ExtensionFactory objectFactory;
    ...
    private ExtensionLoader(Class<?> type) {
        // 这次的 type 是 ExtensionFactory.class
        this.type = type;
        // 这次肯定时 null
        objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
    }
}
  1. 回到 Car.classExtensionLoader 构建,这次得调用 ExtensionFactory.classExtensionLoader 执行 getAdaptiveExtension() 方法
public class ExtensionLoader<T> {

  private final Class<?> type;
  // 用于依赖注入
  private final ExtensionFactory objectFactory;
    ...
    private ExtensionLoader(Class<?> type) {
        // 这次的 type 是 Car.class
        this.type = type;
        // 1. 调用 ExtensionFactory.class 的 ExtensionLoader 执行 getAdaptiveExtension()
        objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
    }
}
  1. 首先判断从 cachedAdaptiveInstance 属性中获取自适应的代理对象,如果没有,则执行 createAdaptiveExtension() 方法来创建
public class ExtensionLoader<T> {
    private final Holder<Object> cachedAdaptiveInstance = new Holder<Object>();
    private volatile Throwable createAdaptiveInstanceError;
    ...
    public T getAdaptiveExtension() {
    
        Object instance = cachedAdaptiveInstance.get();
        if (instance == null) {
            if (createAdaptiveInstanceError == null) {
                synchronized (cachedAdaptiveInstance) {
                    instance = cachedAdaptiveInstance.get();
                    if (instance == null) {
                        try {
                            // 1. 创建自适应扩展对象
                            instance = createAdaptiveExtension(); 
                            cachedAdaptiveInstance.set(instance);
                        } catch (Throwable t) {
                            createAdaptiveInstanceError = t;
                            throw new IllegalStateException("fail to create adaptive instance: " + t.toString(), t);
                        }
                    }
                }
            } else {
                throw new IllegalStateException("fail to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
            }
        }

        return (T) instance;
    }
}
  1. ExtensionFactory.classExtensionLoader 调用 getAdaptiveExtensionClass() 方法创建代理对象,然后返回
public class ExtensionLoader<T> {
    ...
    private T createAdaptiveExtension() {
        try {
            // 1. 通过 getAdaptiveExtensionClass() 创建代理对象,先不关注 injectExtension 依赖注入部分
            return injectExtension((T) getAdaptiveExtensionClass().newInstance()); 
        } catch (Exception e) {
            throw new IllegalStateException("Can not create adaptive extension " + type + ", cause: " + e.getMessage(), e);
        }
    }
}
  1. getAdaptiveExtensionClass() 方法再调用 getExtensionClasses() 获取自适应代理对象,如果一个实现标注了 @Adaptive 注解就直接返回该 class
public class ExtensionLoader<T> {
    ...
    // Adaptive 类存在的意义就是在调用接口方法时,根据url参数去加载对应的实现类,这样不用提前加载
    // 对于一个接口你可以手动实现一个 Adaptive 类,比如 AdaptiveExtensionFactory,
    // 也可以有 Dubbo 默认给我们实现,在实现的时候会根据接口中的方法是否含有 Adaptive 注解,有注解的方法才会代理,没有注解的方法则不会代理,并且使用代理类调用的时候会抛异常
    private Class<?> getAdaptiveExtensionClass() {
        // 1. 获取自适应代理对象
        getExtensionClasses();
        // 2. 如果一个接口实现了一个 Adaptive 实现就直接用,如果没有就默认实现一个
        if (cachedAdaptiveClass != null) {
            return cachedAdaptiveClass;
        }
        return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }
}
  1. getExtensionClasses() 方法会从 cachedClasses 属性获取实现类,如果没有,就得去文件目录加载,即执行 loadExtensionClasses() 方法,然后存入 cachedClasses 返回
public class ExtensionLoader<T> {
   
    private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<Map<String, Class<?>>>();
    ...
     // 从所有文件里获取所有 type 接口的所有实现类并缓存
    private Map<String, Class<?>> getExtensionClasses() {
        Map<String, Class<?>> classes = cachedClasses.get();
        if (classes == null) {
            synchronized (cachedClasses) {
                classes = cachedClasses.get();
                if (classes == null) {
                    // 1. 加载扩展类
                    classes = loadExtensionClasses();
                    cachedClasses.set(classes);
                }
            }
        }
        return classes;
    }
}
  1. 执行 loadExtensionClasses() 方法时,如果 @SPI 指定了 value,会校验只能有 1 个值,同时先缓存起来,然后通过各个目录去加载与 ExtensionFactory.class 相关联的实现(和我们最初定义 Car.class 实现一样的)
  • 重点
    • 加载的目录只会在这些地方,同时越往后,优先级越高
      • META-INF/dubbo/
      • META-INF/dubbo/internal/
      • META-INF/services/
public class ExtensionLoader<T> {

	private String cachedDefaultName;
	private final Class<?> type;
    private static final String SERVICES_DIRECTORY = "META-INF/services/";
    private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";
    private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";
    ...
    private Map<String, Class<?>> loadExtensionClasses() {
        final SPI defaultAnnotation = type.getAnnotation(SPI.class); // interface org.apache.dubbo.common.extension.ExtensionFactory
        // SPI后面的值就是默认的实现的类,只能指定一个实现类
        if (defaultAnnotation != null) {
            String value = defaultAnnotation.value();// 一开始 interface org.apache.dubbo.common.extension.ExtensionFactory 的 SPI 没标注默认值,
            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];
                }
            }
        }

        // 是一个map,key是别名,value是具体的实现类
        // 会从不同的地方寻找接口的所有实现类,这就是扩展的实现
        // 主要会从三个地方找,1. dubbo内部提供的
        // META-INF/dubbo/
        // META-INF/dubbo/internal/
        // META-INF/services/
        Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
        // 1. 从目录中加载对应的配置实现文件
        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; // 找到 org.apache.dubbo.common.extension.ExtensionFactory 所有的地方的实现类
    }
}
  1. 执行 loadDirectory() 时,会先拼接资源定位符 URL,然后加载对应资源文件
public class ExtensionLoader<T> {
    ...
    private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type) {// type:org.apache.dubbo.common.extension.ExtensionFactory
        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()) {
                    // 单个资源的url
                    java.net.URL resourceURL = urls.nextElement(); 
                    //  1. 通过拼接的资源定位符,加载文件
                    loadResource(extensionClasses, classLoader, resourceURL);
                }
            }
        } catch (Throwable t) {
            logger.error("Exception when load extension class(interface: " +
                    type + ", description file: " + fileName + ").", t);
        }
    }
}
  1. 第一次执行 loadResource() 时,主要是获取 org.apache.dubbo.common.extension.ExtensionFactory 文件下的内容,遍历每一行类,进行加载
  • org.apache.dubbo.common.extension.ExtensionFactory 内容
adaptive=org.apache.dubbo.common.extension.factory.AdaptiveExtensionFactory
spi=org.apache.dubbo.common.extension.factory.SpiExtensionFactory
  • 代码实现
public class ExtensionLoader<T> {
    ...
    private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), "utf-8"));
            try {
                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) {
                                // 1. name 就是加载文件中的别名,line 就是实现类全名
                                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);
                        }
                    }
                }
            } finally {
                reader.close();
            }
        } catch (Throwable t) {
            logger.error("Exception when load extension class(interface: " +
                    type + ", class file: " + resourceURL + ") in " + resourceURL, t);
        }
    }
}
  1. 在加载 org.apache.dubbo.common.extension.ExtensionFactory 第一行内容时,标注的 org.apache.dubbo.common.extension.factory.AdaptiveExtensionFactory 是指定了 @Adaptive,所以会先存入到 cachedAdaptiveClass 属性。然后再遍历第二行,org.apache.dubbo.common.extension.factory.SpiExtensionFactory 就是个实现类,存到 cachedNames 属性中(其中 key 为 class,value 为 name,这不管标注了多少个 name,都只存一次),同时存入 extensionClasses 中(其中 key 为 name, value 为 class)
  • 重点
    • 在文件中写的类,必须实现了文件名的接口
    • 同一个类,可以指定多个别名
    • 对于每个接口 @Adaptive 只能指定一个实现类
public class ExtensionLoader<T> {

    private final Class<?> type;
    private volatile Class<?> cachedAdaptiveClass = null;
    ...
    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 when load extension class(interface: " +
                    type + ", class line: " + clazz.getName() + "), class "
                    + clazz.getName() + "is not subtype of interface.");
        }
        // 1. 实现类上是否有 Adaptive 注解,这段代码的意思就是一个接口的实现类中只有一个 Adaptive
        if (clazz.isAnnotationPresent(Adaptive.class)) {
            if (cachedAdaptiveClass == null) {
                cachedAdaptiveClass = clazz;
            } else if (!cachedAdaptiveClass.equals(clazz)) {
                throw new IllegalStateException("More than 1 adaptive class found: "
                        + cachedAdaptiveClass.getClass().getName()
                        + ", " + clazz.getClass().getName());
            }
        } else if (isWrapperClass(clazz)) { // 实现类是不是wrapper类,判断逻辑是这个实现类的构造方法的参数是不是有且仅有一个参数,且参数类型是接口类型
            // wrapper类可以有多个
            Set<Class<?>> wrappers = cachedWrapperClasses;
            if (wrappers == null) {
                cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
                wrappers = cachedWrapperClasses;
            }
            wrappers.add(clazz);
        } else {
            // 2. 获取构造函数
            clazz.getConstructor();
            if (StringUtils.isEmpty(name)) {
                // 如果在文件里没有配名字,可以去看实现类上是否有 Extension 注解,可以取这个注解的值作为名字
                // 如果没有 Extension 这个注解,则取实现类的simple名,并进行简化,比如接口为 CarService, 实现类为RedCarService, 那么名字则为 red
                name = findAnnotationName(clazz);
                if (name.length() == 0) {
                    throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
                }
            }
            // 3. 如果配置的名字使用逗号隔开的,可以配置多个别名
            String[] names = NAME_SEPARATOR.split(name);
            if (names != null && names.length > 0) {
                // 这里找的是 Activate 注解,不是Adaptive
                // Activate 表示激活,如果实现类上配置了 Activate 注解,这里会先缓存,getActivateExtension() 方法
                Activate activate = clazz.getAnnotation(Activate.class);
                if (activate != null) {
                    cachedActivates.put(names[0], activate);
                } else {
                    // support com.alibaba.dubbo.common.extension.Activate
                    com.alibaba.dubbo.common.extension.Activate oldActivate = clazz.getAnnotation(com.alibaba.dubbo.common.extension.Activate.class);
                    if (oldActivate != null) {
                        cachedActivates.put(names[0], oldActivate);
                    }
                }
                for (String n : names) {
                    // 4. 配了多个名字 cachedName 也只会缓存一个
                    if (!cachedNames.containsKey(clazz)) {
                        cachedNames.put(clazz, n);
                    }
                    // 5. 多个名字会产生多条记录
                    Class<?> c = extensionClasses.get(n);
                    if (c == null) {
                        extensionClasses.put(n, clazz);
                    } else if (c != clazz) {
                        throw new IllegalStateException("Duplicate extension " + type.getName() + " name " + n + " on " + c.getName() + " and " + clazz.getName());
                    }
                }
            }
        }
    }
}
  1. 由于 ExtensionFactory 接口下的 AdaptiveExtensionFactory 实现了 @Adaptive 注解,所以在 Car.classcreateAdaptiveExtension() 时,返回的是 AdaptiveExtensionFactory 进行实例化,这时候再次获取 ExtensionFactory.classExtensionLoader,并把之前存入到 cachedClasses 属性的 org.apache.dubbo.common.extension.factory.SpiExtensionFactory 元素,存入 factories 属性,并返回 AdaptiveExtensionFactory 这个实例
@Adaptive
public class AdaptiveExtensionFactory implements ExtensionFactory {
    
    private final List<ExtensionFactory> factories;
    ...
    public AdaptiveExtensionFactory() {
        // 1. 获取 ExtensionFactory.class** 的 **ExtensionLoader
        ExtensionLoader<ExtensionFactory> loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class);
        List<ExtensionFactory> list = new ArrayList<ExtensionFactory>();
        for (String name : loader.getSupportedExtensions()) {
            // 2. 获取到 org.apache.dubbo.common.extension.factory.SpiExtensionFactory 元素
            list.add(loader.getExtension(name));
        }
        factories = Collections.unmodifiableList(list);
    }
}
  1. 这时候就回到 Car.classExtensionLoader,并赋值 objectFactoryAdaptiveExtensionFactory
public class ExtensionLoader<T> {

  private final Class<?> type;
  // 用于依赖注入
  private final ExtensionFactory objectFactory;
    ...
    private ExtensionLoader(Class<?> type) {
        // 这次的 type 是 Car.class
        this.type = type;
        // 1. 获取到 AdaptiveExtensionFactory
        objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
    }
}
  1. 得到 Car.classExtensionLoader 之后,执行 getExtension() 获取扩展类
public class CarDemo {

    public static void main(String[] args) {
        ...
        Car redCar = extensionLoader.getExtension("red");
    }
}
  1. 执行 getExtension() 时,先判断 cachedInstances 属性是否已经有过创建的实例,没有的话,就执行 createExtension() 方法
  • 重点
    • 当执行 getExtension() 时,可以放入参数为 true,只是获取的是默认扩展类
public class ExtensionLoader<T> {

    // 缓存实例,每个接口的实现类对应一个实例
    private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<String, Holder<Object>>();
    ...
    public T getExtension(String name) {
        if (StringUtils.isEmpty(name)) {
            throw new IllegalArgumentException("Extension name == null");
        }
        // 如果设置为 true,那么从 SPI 中拿默认扩展名
        if ("true".equals(name)) {
            return getDefaultExtension();
        }
        Holder<Object> holder = cachedInstances.get(name);
        if (holder == null) {
            cachedInstances.putIfAbsent(name, new Holder<Object>());
            holder = cachedInstances.get(name);
        }
        Object instance = holder.get();
        if (instance == null) {
            synchronized (holder) {
                instance = holder.get();
                if (instance == null) {
                    // 1. 执行获取扩展类
                    instance = createExtension(name);
                    holder.set(instance);
                }
            }
        }
        return (T) instance;
    }
}
  1. 再次执行 getExtensionClasses() 扫描目录获取所有实现了 Car.class 接口的类,然后找到指定 name=red 的类,并且实例化对象,存入 EXTENSION_INSTANCES 属性中,并返回。最后就可以执行接口的方法了
public class ExtensionLoader<T> {

    // 实现类对应的实例
    private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<Class<?>, Object>();
    ...
    private T createExtension(String name) {
        // 1. 扫描目录,取出对应的实现类
        Class<?> clazz = getExtensionClasses().get(name); 
        if (clazz == null) {
            throw findException(name);
        }
        try {
            // 2. 生成实例并缓存
            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));
                }
            }
            // 3. 返回实例
            return instance;
        } catch (Throwable t) {
            throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
                    type + ")  could not be instantiated: " + t.getMessage(), t);
        }
    }
}
  1. 总结
    在这里插入图片描述
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值