前情提要

假设你已经知道Dubbo SPI的使用方式,不知道的请出门左转:

Dubbo源码(一) - SPI使用

Dubbo源码地址:

apache/dubbo

本文使用版本:2.6.x

测试Demo

  1. 新建SPI测试接口以及实现类

    package com.javaedit.spi;
    import com.alibaba.dubbo.common.URL; // 定义SPI接口
    @SPI
    public interface Robot {
    void sayHello(URL url);
    } // 自动注入演示
    public class IocRobotImpl implements Robot {
    private Robot robot;
    public void setRobot(Robot robot) {
    this.robot = robot;
    } @Override
    public void sayHello(URL url) {
    System.out.println("ioc start");
    robot.sayHello(url);
    }
    } // 自适应代理类
    @Adaptive
    public class AdaptiveRobot implements Robot {
    @Override
    public void sayHello(URL url) {
    System.out.println("标注在类上的自适应代理类,类名:" + this.getClass().getSimpleName());
    }
    } // 包装类
    public class RobotWrapper implements Robot { private Robot robot; // 带Robot参数的构造方法,这是包装类的重点
    public RobotWrapper(Robot robot) {
    this.robot = robot;
    } @Override
    public void sayHello(URL url) {
    System.out.println("wrapper before...");
    this.robot.sayHello(url);
    System.out.println("wrapper after...");
    }
    } // 测试方法
    public static void main(String[] args) {
    ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class);
    Robot robot = extensionLoader.getExtension("iocRobot");
    robot.sayHello(null);
    }
  2. resources/META-INF/services目录添加com.javaedit.spi.Robot文件

    iocRobot = com.javaedit.spi.IocRobotImpl
    wrapper = com.javaedit.spi.RobotWrapper
    adaptiveRobot = com.javaedit.spi.AdaptiveRobot

源码解析

获取所有的拓展类

ExtensionLoader的加载

Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,获取ExtensionLoader的方法是getExtensionLoader

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
// 判空
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<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对象,则通过new ExtensionLoader构造方法创建。继续看构造方法

private ExtensionLoader(Class<?> type) {
// 本文中type就是Robot接口
this.type = type;
// 此处先忽略,后面将自适应拓展的时候再回来
objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}

构造方法就是给type赋值,以及创建objectFactory,这个涉及自适应拓展,此时先略过。

配置文件的读取

ExtensionLoader已经获取到,继续看看extensionLoader.getExtension("norRobot");做了什么

public T getExtension(String name) {
if (name == null || name.length() == 0)
throw new IllegalArgumentException("Extension name == null");
if ("true".equals(name)) {
// 获取默认的拓展实现类
return getDefaultExtension();
}
// Holder,顾名思义,用于持有目标对象
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) {
// 创建拓展对象
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}

此方法先是校验,然后尝试从缓存获取对象,没有再创建拓展对象。下面继续看如何创建拓展对象

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);
}
// 向实例中注入依赖(setter方法相关)
injectExtension(instance);
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
// 循环创建 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("Extension instance(name: " + name + ", class: " +
type + ") could not be instantiated: " + t.getMessage(), t);
}
}

此方法做了几个操作:

  1. 加载配置文件
  2. 创建拓展对象(java反射创建)
  3. 向拓展对象中注入依赖(IOC操作,后面讲解)
  4. 将拓展对象包裹在相应的 Wrapper 对象中(后面讲解)

本小节加载配置文件是重点,看看getExtensionClasses()是如何加载所有的类信息的

/**
* 获取所有拓展类(返回值不包含包装类和自适应拓展类)
*/
private Map<String, Class<?>> getExtensionClasses() {
// 从缓存中获取已加载的拓展类
Map<String, Class<?>> classes = cachedClasses.get();
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
// 加载拓展类
classes = loadExtensionClasses();
cachedClasses.set(classes);
}
}
}
return classes;
}

此方法就是一些缓存逻辑和加锁,重点在loadExtensionClasses()方法

这里要注意一点:返回的map中不包含包装类和自适应拓展类

private Map<String, Class<?>> loadExtensionClasses() {
// 获取 SPI 注解,这里的 type 变量是在调用 getExtensionLoader 方法时传入的
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
if (defaultAnnotation != null) {
String value = defaultAnnotation.value();
if ((value = value.trim()).length() > 0) {
// 对 SPI 注解内容进行切分
String[] names = NAME_SEPARATOR.split(value);
// 检测 SPI 注解内容是否合法,不合法则抛出异常
if (names.length > 1) {
throw new IllegalStateException("more than 1 default extension name on extension " + type.getName()
+ ": " + Arrays.toString(names));
}
// 设置默认名称,参考 getDefaultExtension 方法
if (names.length == 1) cachedDefaultName = names[0];
}
} Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
// 加载指定文件夹下的配置文件
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY); // META-INF/dubbo/internal
loadDirectory(extensionClasses, DUBBO_DIRECTORY); // META-INF/dubbo
loadDirectory(extensionClasses, SERVICES_DIRECTORY); // META-INF/services
return extensionClasses;
}

前半部分逻辑是SPI注解的value值的处理,不是重点,略过。

loadDirectory方法就是加载指定文件夹下的配置文件,并将其存入extensionClasses中。

从代码中可以看到,读取了3个文件夹下的配置,分别是:

META-INF/dubbo/internal

META-INF/dubbo

META-INF/services

如何解析配置文件不是重点,略过。直接跳到类信息的解析。

路径:loadDirectory()loadResource()loadClass()

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.");
}
// 检测目标类上是否有 Adaptive 注解
if (clazz.isAnnotationPresent(Adaptive.class)) {
if (cachedAdaptiveClass == null) {
// 设置 cachedAdaptiveClass缓存
cachedAdaptiveClass = clazz;
} else if (!cachedAdaptiveClass.equals(clazz)) {
throw new IllegalStateException("More than 1 adaptive class found: "
+ cachedAdaptiveClass.getClass().getName()
+ ", " + clazz.getClass().getName());
}
// 检测 clazz 是否是 Wrapper 类型
} else if (isWrapperClass(clazz)) {
Set<Class<?>> wrappers = cachedWrapperClasses;
if (wrappers == null) {
cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
wrappers = cachedWrapperClasses;
}
// 存储 clazz 到 cachedWrapperClasses 缓存中
wrappers.add(clazz);
// 程序进入此分支,表明 clazz 是一个普通的拓展类
} else {
// 检测 clazz 是否有默认的构造方法,如果没有,则抛出异常
clazz.getConstructor();
if (name == null || name.length() == 0) {
// 如果 name 为空,则尝试从 Extension 注解中获取 name,或使用小写的类名作为 name
name = findAnnotationName(clazz);
if (name.length() == 0) {
throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
}
}
// 切分 name
String[] names = NAME_SEPARATOR.split(name);
if (names != null && names.length > 0) {
Activate activate = clazz.getAnnotation(Activate.class);
if (activate != null) {
// 如果类上有 Activate 注解,则使用 names 数组的第一个元素作为键,
// 存储 name 到 Activate 注解对象的映射关系
cachedActivates.put(names[0], activate);
}
for (String n : names) {
if (!cachedNames.containsKey(clazz)) {
cachedNames.put(clazz, n);
}
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());
}
}
}
}
}

此方法就是根据条件,将class放入不同的缓存(cachedAdaptiveClass自适应拓展类缓存、cachedWrapperClasses包装类缓存、cachedNames普通缓存)

注入依赖(IOC)

前面讲到在createExtension方法中,加载了所有配置文件以及反射生成了拓展对象。

而向实例中注入依赖,是通过injectExtension方法实现的。

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())) {
// 如果不需要自动注入,则在方法上添加@DisableInject注解
if (method.getAnnotation(DisableInject.class) != null) {
continue;
}
// 获取 setter 方法参数类型
Class<?> pt = method.getParameterTypes()[0];
try {
// 获取属性名,比如 setRobot 方法对应属性名 robot
String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
method.invoke(instance, object);
}
} catch (Exception e) {
logger.error("fail to inject via method " + method.getName()
+ " of interface " + type.getName() + ": " + e.getMessage(), e);
}
}
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return instance;
}

满足自动注入需要符合4个条件:

方法已set开头

方法仅有一个参数

方法为public修饰

方法上没有@DisableInject注解

此方法就是处理通过set方法名,获取需要注入的对象名,并通过objectFactory.getExtension获取到需要注入的对象,反射注入。

objectFactory 变量的类型为 AdaptiveExtensionFactory,涉及到自适应拓展,此处略过,下面讲。

包装类

继续回到createExtension方法中,看包装类相关代码

private T createExtension(String name) {
。。。
try {
。。。
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
// 循环创建 Wrapper 实例
for (Class<?> wrapperClass : wrapperClasses) {
// 将当前 instance 作为参数传给 Wrapper 的构造方法,并通过反射创建 Wrapper 实例。
// 然后向 Wrapper 实例中注入依赖,最后将 Wrapper 实例再次赋值给 instance 变量
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
} catch (Throwable t) {
。。。
}
}

可以看到,包装类就是将当前生成的拓展类通过构造方法注入

自适应拓展

自适应拓展是通过extensionLoader.getAdaptiveExtension()方法获取的,下面我们来分析这个方法

public T getAdaptiveExtension() {
// 从缓存中获取自适应拓展
Object instance = cachedAdaptiveInstance.get();
if (instance == null) {
if (createAdaptiveInstanceError == null) {
synchronized (cachedAdaptiveInstance) {
instance = cachedAdaptiveInstance.get();
if (instance == null) {
try {
// 创建自适应拓展
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;
}

一些加锁以及缓存操作,重点在createAdaptiveExtension方法,继续分析

private T createAdaptiveExtension() {
try {
// 获取自适应拓展类,并通过反射实例化,调用 injectExtension 方法向拓展实例中注入依赖
return injectExtension((T) getAdaptiveExtensionClass().newInstance());
} catch (Exception e) {
throw new IllegalStateException("Can not create adaptive extension " + type + ", cause: " + e.getMessage(), e);
}
}

injectExtension在注入依赖小节已经讲过,直接看getAdaptiveExtensionClass方法

private Class<?> getAdaptiveExtensionClass() {
// 通过 SPI 获取所有的拓展类,例子中指所有 Robot 接口实现类
getExtensionClasses();
// 如果某个实现类被 Adaptive 注解修饰了,那么该类就会被赋值给 cachedAdaptiveClass 变量。
// 那么这里直接返回,不创建自适应拓展类
// 这也就是为什么说被 @Adaptive 注解修饰在类上和方法上不一样
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
}
// 创建自适应拓展类
return cachedAdaptiveClass = createAdaptiveExtensionClass();
}

cachedAdaptiveClass参数的处理逻辑在loadClass方法中,前面讲过了。也就是当@Adaptive注解修饰类时,直接返回该类的字节码对象。

如果@Adaptive修饰的是方法,则会进入到createAdaptiveExtensionClass方法逻辑中

private Class<?> createAdaptiveExtensionClass() {
// 通过反射检测接口方法是否包含 Adaptive 注解
// 构建自适应拓展代码
String code = createAdaptiveExtensionClassCode();
ClassLoader classLoader = findClassLoader();
// 获取编译器实现类
com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
// 编译代码,生成 Class
return compiler.compile(code, classLoader);
}

createAdaptiveExtensionClassCode方法会根据SPI接口,生成相应的自适应类代码。例如:

// 如果接口长这样
@SPI
public interface Robot {
@Adaptive("robotAda")
void sayHello(URL url);
} // 返回的自适应类代码,也就是code变量,长这样
package com.javaedit.spi; import com.alibaba.dubbo.common.extension.ExtensionLoader; public class Robot$Adaptive implements com.javaedit.spi.Robot {
public void sayHello(com.alibaba.dubbo.common.URL arg0) {
if (arg0 == null) throw new IllegalArgumentException("url == null");
com.alibaba.dubbo.common.URL url = arg0;
String extName = url.getParameter("robotAda");
if (extName == null)
throw new IllegalStateException("Fail to get extension(com.javaedit.spi.Robot) name from url(" + url.toString() + ") use keys([robot])");
com.javaedit.spi.Robot extension = (com.javaedit.spi.Robot) ExtensionLoader.getExtensionLoader(com.javaedit.spi.Robot.class).getExtension(extName);
extension.sayHello(arg0);
}
}

简单讲下createAdaptiveExtensionClassCode的逻辑:

  1. 判断sayHello是否有参数类型为URL,或者有参数提供了getUrl方法
  2. 从URL中获取实际要调用的拓展类名
  3. 获取实际的拓展类并调用

createAdaptiveExtensionClassCode方法的逻辑略复杂,有兴趣的可自行查看源码,本文略。

下面继续回到createAdaptiveExtensionClass方法中,compiler也是一个自适应拓展对象,也调用了getAdaptiveExtension方法,妥妥的套娃。

不妥Compiler接口的实现类AdaptiveCompiler是使用@Adaptive修饰类的,所以会直接返回cachedAdaptiveClass,不会进入到createAdaptiveExtensionClass,套娃结束。

下面继续看看compiler.compile(code, classLoader);做了啥

@Adaptive
public class AdaptiveCompiler implements Compiler { private static volatile String DEFAULT_COMPILER; public static void setDefaultCompiler(String compiler) {
DEFAULT_COMPILER = compiler;
} @Override
public Class<?> compile(String code, ClassLoader classLoader) {
Compiler compiler;
ExtensionLoader<Compiler> loader = ExtensionLoader.getExtensionLoader(Compiler.class);
String name = DEFAULT_COMPILER; // copy reference
if (name != null && name.length() > 0) {
compiler = loader.getExtension(name);
} else {
// 获取默认编译器
compiler = loader.getDefaultExtension();
}
return compiler.compile(code, classLoader);
} }

compile方法逻辑也很简单,有自定义编译器,就使用,否则使用默认编译器。默认编译器是JavassistCompiler,这点在Compiler接口中已经定义了

@SPI("javassist")
public interface Compiler{}

前面忽略的部分

objectFactory的生成

objectFactory的生成是在ExtensionLoader的构造函数中

private ExtensionLoader(Class<?> type) {
this.type = type;
// 此处先忽略,后面将自适应拓展的时候再回来
objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}

此时type是Robot,所以进入到ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension())中,

getAdaptiveExtension是获取自适应扩展的方法,所以看ExtensionFactory接口的实现类

@Adaptive
public class AdaptiveExtensionFactory implements ExtensionFactory { private final List<ExtensionFactory> factories; public AdaptiveExtensionFactory() {
// 获取ExtensionFactory的拓展类
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);
} @Override
public <T> T getExtension(Class<T> type, String name) {
for (ExtensionFactory factory : factories) {
T extension = factory.getExtension(type, name);
if (extension != null) {
return extension;
}
}
return null;
} }

这段代码逻辑也简单,就是获取ExtensionFactory的拓展类,并调用拓展类的getExtension方法。

那么factories集合中就包含了SpiExtensionFactorySpringExtensionFactory

而前面将自动注入时,注入对象就是factory.getExtension生成的。也就是自动注入支持Dubbo SPI对象以及spring bean

总结

Dubbo SPI机制涉及到Dubbo源码的方方面面,需要优先掌握才好开始阅读其他部分的源码。


参考资料

心灵蚂蚁-Dubbo 源码解析(一)Dubbo SPI

Dubbo开发指南

Dubbo源码(二) - SPI源码的更多相关文章

  1. dubbo源码解析-spi(二)

    前言 上一篇简单的介绍了spi的基本一些概念,在末尾也提到了,dubbo对jdk的spi进行了一些改进,具体改进了什么,来看看文档的描述 JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩 ...

  2. Dubbo 源码分析 - SPI 机制

    1.简介 SPI 全称为 Service Provider Interface,是 Java 提供的一种服务发现机制.SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加 ...

  3. dubbo源码解析-spi(3)

    前言 在上一篇的末尾,我们提到了dubbo的spi中增加了IoC和AOP的功能.那么本篇就讲一下这个增加的IoC,spi部分预计会有四篇,因为这东西实在是太重要了.温故而知新,我们先来回顾一下,我们之 ...

  4. dubbo源码解析-spi(一)

    前言 虽然标题是dubbo源码解析,但是本篇并不会出现dubbo的源码,本篇和之前的dubbo源码解析-简单原理.与spring融合一样,为dubbo源码解析专题的知识预热篇. 插播面试题 你是否了解 ...

  5. Dubbo SPI源码解析①

    目录 0.Java SPI示例 1.Dubbo SPI示例 2.Dubbo SPI源码分析 ​ SPI英文全称为Service Provider Interface.它的作用就是将接口实现类的全限定名 ...

  6. Dubbo2.7源码分析-SPI的应用

    SPI简介 SPI是Service Provider Interface的缩写,即服务提供接口(翻译出来好绕口,还是不翻译的好),实质上是接口,作用是对外提供服务. SPI是Java的一种插件机制,可 ...

  7. 34 网络相关函数(二)——live555源码阅读(四)网络

    34 网络相关函数(二)——live555源码阅读(四)网络 34 网络相关函数(二)——live555源码阅读(四)网络 2)socketErr 套接口错误 3)groupsockPriv函数 4) ...

  8. 【转】Android手机客户端关于二维码扫描的源码--不错

    原文网址:https://github.com/SkillCollege/QrCodeScan QrCodeScan 这是Android手机客户端关于二维码扫描的源码,使用了高效的ZBar解码库,并修 ...

  9. 安卓图表引擎AChartEngine(二) - 示例源码概述和分析

    首先看一下示例中类之间的关系: 1. ChartDemo这个类是整个应用程序的入口,运行之后的效果显示一个list. 2. IDemoChart接口,这个接口定义了三个方法, getName()返回值 ...

随机推荐

  1. Java中的JVM和Redis,你了解的透彻么?

    招聘在前不久已经渐渐拉下帷幕了,看到最近技术群一个问题,引起了我的思考:"今年面试为什么那么难?" 想必大家都知道程序员要涨薪主要靠跳槽来完成!但是无论是考试,还是求职,这个难度, ...

  2. Django学习——图书相关表关系建立、基于双下划线的跨表查询、聚合查询、分组查询、F查询、Q查询、admin的使用、使用脚本调用Django、Django查看源生sql

    0 图书相关表关系建立 1.5个表 2.书籍表,作者表,作者详情表(垂直分表),出版社表,书籍和作者表(多对多关系) 一对一 多对多 本质都是一对多 外键关系 3.一对一的关系,关联字段可以写在任意一 ...

  3. HIVE 数据分析

    题目要求: 具体操作: ①hive路径下建表:sale create table sale (day_id String, sale_nbr String, buy_nbr String, cnt S ...

  4. Property or method "xxx" is not defined on the instance but referenced during render

    是xxx中的data写成date了,因此报错. 这个错误属于粗心

  5. CF1580E Railway Construction

    CF1580E Railway Construction 铁路系统中有 \(n\) 个车站和 \(m\) 条双向边,有边权,无重边.这些双向边使得任意两个车站互相可达. 你现在要加一些单向边 \((u ...

  6. quasar + uni-app混合打包APP

    写几个关键点,作为备忘录. 和所有框架一样,现在本地run build quasar的cli是 quasar build 然后记住打包好以后的静态文件 目录 uni-app新建一个5+App的默认模板 ...

  7. yum源更换/新

    参考:https://www.cnblogs.com/opsprobe/p/10673031.html

  8. 如何优化PlantUML流程图(时序图)

    这篇文章用来介绍,如何画出好看的流程图. 1. 选择合适的组件 1.1 plantuml官方提供的组件 1.2 加载图片 1.2.1 加载本地图片 1.2.2 加载网络图片 1.2.3 图片资源 2. ...

  9. 使用 DartPad 制作代码实践教程

    DartPad 是一个开源的.在浏览器中体验和运行 Dart 编程语言的线上编辑器,目标是为了帮助开发者更好地了解 Dart 编程语言以及 Flutter 应用开发. DartPad 项目起始于 20 ...

  10. Node.js精进(2)——异步编程

    虽然 Node.js 是单线程的,但是在融合了libuv后,使其有能力非常简单地就构建出高性能和可扩展的网络应用程序. 下图是 Node.js 的简单架构图,基于 V8 和 libuv,其中 Node ...