Dubbo SPI机制核心原理,你掌握了吗?|原创

news2025/1/13 7:57:57

这篇文章内容很干,做好心理准备!本文详细讲解了 Dubbo SPI 诞生原因以及它的用法,并且详细解读了核心类 ExtensionLoader 的关键属性,再根据demo 对 SPI 的加载原理进行详细解读。

文章较长,建议收藏!文末有原理图。

点击上方“后端开发技术”,选择“设为星标” ,优质资源及时送达

为什么不使用 JDK SPI

我们接着上次的文章讲,既然已经有了 JDK SPI 为什么还需要 Dubbo SPI 呢?

技术的出现通常都是为了解决现有问题,通过之前的 demo,不难发现 JDK SPI 机制就存在以下一些问题:

  1. 实现类会被全部遍历并且实例化,假如我们只需要使用其中的一个实现,这在实现类很多的情况下无疑是对机器资源巨大的浪费,

  2. 无法按需获取实现类,不够灵活,我们需要遍历一遍所有实现类才能找到指定实现。

Dubbo SPI 以 JDK SPI 为参考做出了改进设计,进行了性能优化以及功能增强,Dubbo SPI 机制的出现解决了上述问题。除此之外,Dubbo 的 SPI 还支持自适应扩展以及 IOC 和 AOP 等高级特性。

JDK SPI 原理,请移步这篇文章:

f511a8bb9f24c14744b348c499918590.jpeg

提到 Dubbo,为什么一定要谈SPI?|原创


Dubbo SPI配置

Dubbo SPI 的配置做出了改进,在 Dubbo 中有三种不同的目录可以存放 SPI 配置,用途也不同。

  • META-INF/services/ 目录:此目录配置文件用于兼容 JDK SPI 。

  • META-INF/dubbo/ 目录:此目录用于存放用户自定义 SPI 配置文件。

  • META-INF/dubbo/internal/ 目录:此目录用于存放 Dubbo 内部使用的 SPI 配置文件。

并且配置文件中可以配置成Key-Value 形式,key 为扩展名,value 为具体实现类。有了扩展名就可以动态根据名字来定位到具体的实现类,并且可以针对性的实例化需要的实现类。比如指定 zookeeper,就知道要使用 zookeeper 作为注册中心的实现。

zookeeper=org.apache.dubbo.registry.zookeeper.ZookeeperRegistryFactory

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

除了对应目录的配置,还需要 @SPI 注解的配合,被修饰的接口表示这是一个扩展接口,@SPI 的 value 表示这个扩展接口的默认扩展实现的扩展名,扩展名就是配置文件中对应实现类配置信息的 key。

//dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol
@SPI("dubbo")
public interface Protocol {
}

除此之外,还需要 @Adaptive注解配合,被他修饰的类会生成Dubbo的适配器,Adaptive注解比较复杂,关于这个注解我们后续文章会再详细讲解。

ExtensionLoader

之前的文章我们提到过,dubbo-common 模块中的 extension 包包含了 Dubbo SPI 的逻辑,而ExtensionLoader 就位于其中,Dubbo SPI 的核心逻辑几乎都封装在 ExtensionLoader 这个类里,其地位你可以类比为 JDK SPI 中的 java.util.ServiceLoader。

学习 ExtensionLoader 首先了解以下几个关键属性。

核心静态变量

1、EXTENSION_LOADERS

// 扩展接口和ExtensionLoader的关系
private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>(64);

这个字段保存了扩展接口和ExtensionLoader之间的映射关系,一个扩展接口对应一个ExtensionLoader。key 为接口,value 为 ExtensionLoader。

8237bfa597f620e90bf78f5b9de80e43.png

2、EXTENSION_INSTANCES

//扩展接口实现类和实例对象的关系
private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>(64);

这个字段保存了扩展接口扩展实现类和实例对象之间的关系,key 为扩展实现类,value为实例对象。

3、strategies 加载策略

//加载策略
private static volatile LoadingStrategy[] strategies = loadLoadingStrategies();
import static java.util.ServiceLoader.load;
private static LoadingStrategy[] loadLoadingStrategies() {
    return stream(load(LoadingStrategy.class).spliterator(), false)
           .sorted().toArray(LoadingStrategy[]::new);
}

strategies 对应的是Dubbo内部实现的三个加载策略,分别对应之前提到的三个不同的SPI配置目录,接口的继承关系如下。他们的实现中无非就是包含了两个信息:加载目录和优先级。

e267c313c3c4378fda1209a32b9f55b3.png

每个实现类都继承了优先级接口Prioritized,所以不同加载策略的加载优先级不同,对应的优先级是:

DubboInternalLoadingStrategy 大于>DubboLoadingStrategy 大于>ServicesLoadingStrateg

对应目录的优先级分别是:

META-INF/dubbo/internal/>META-INF/dubbo/>META-INF/services/

LoadingStrategy是如何加载的?

需要注意的是,LoadingStrategy的控制了Dubbo内部实现的加载策略,那他自身又是如何加载的呢?

其实根据上面的代码就可以发现LoadingStrategy正是依赖 JDK SPI 机制来加载的,在loadLoadingStrategies()方法中调用了ServiceLoader.load()方法,从META-INF/services文件夹下的org.apache.dubbo.common.extension.LoadingStrategy文件中读取到了具体实现类并且依次加载。所以一定程度上来说,Dubbo SPI 机制正是依赖于 JDK SPI 机制。

0c148bd3c479c6099a9d4fe6d96eaf9d.png

核心成员变量

1、type:与当前 ExtensionLoader 绑定的扩展接口类型。

2、cachedNames:存储了扩展实现类和扩展名的关系,key 为扩展实现类,value 为对应扩展名。

3、cachedClasses:存储了扩展名和扩展实现类之间的关系。key 为扩展名,value 为扩展实现类,这个 map 可以和 cachedNames 对应着理解,他们存贮内容是相反的关系,算是一种空间换时间的技巧。

4、cachedInstances:存储了扩展实现类类名与实现类的实例对象之间的关系,key 为扩展实现名,value 为实例对象。

5、cachedDefaultName:@SPI 注解配置的默认扩展名。

//扩展接口类型
private final Class<?> type;
//存储了扩展实现类和扩展名的关系
private final ConcurrentMap<Class<?>, String> cachedNames = new ConcurrentHashMap<>();
//存储了扩展名和扩展实现类之间的关系
private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
//存储了扩展实现类类名与实例对象之间的关系
private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
//扩展接口对@SPI注解配置的value
private String cachedDefaultName;

Dubbo SPI 加载原理

测试 demo

为了方便调试并且抓住核心原理,其实可以跟 JDK SPI 一样写一个 demo,我们直接复用上次的扩展接口和实现类,但是这次是用Dubbo SPI 的形式加载。配置文件需要重新写一份,写成key-value形式,并且放在META-INF/dubbo文件夹下。

/**
 * @author 后端开发技术
 */
@SPI
public interface MySPI {
    void say();
}
public class HelloMySPI implements MySPI{
    @Override
    public void say() {
        System.out.println("HelloMySPI say:hello");
    }
}
public class GoodbyeMySPI implements MySPI {
    @Override
    public void say() {
        System.out.println("GoodbyeMySPI say:Goodbye");
    }
}
public static void main(String[] args) {
    ExtensionLoader<MySPI> extensionLoader = ExtensionLoader.getExtensionLoader(MySPI.class);
    MySPI hello = extensionLoader.getExtension("hello");
    hello.say();
    MySPI goodbye = extensionLoader.getExtension("goodbye");
    goodbye.say();
}
//配置文件 META-INF/dubbo/org.daley.spi.demo.MySPI
goodbye=org.daley.spi.demo.GoodbyeMySPI
hello=org.daley.spi.demo.HelloMySPI
  
// 输出结果
HelloMySPI say:hello
GoodbyeMySPI say:Goodbye

一定记得给接口标记@SPI注解,否则会抛出以下错误。

Exception in thread "main" java.lang.IllegalArgumentException: Extension type (interface org.daley.spi.demo.MySPI) is not an extension, because it is NOT annotated with @SPI!

获得扩展实例

在上述 demo 中,我们首先通过 ExtensionLoader 的 getExtensionLoader 方法获取一个 ExtensionLoader 实例,然后再通过 ExtensionLoader 的 getExtension 方法获取拓展类对象。

在调用getExtensionLoader()时会首先尝试从EXTENSION_LOADERS中根据扩展点类型获得ExtensionLoader,如果未命中缓存,则会新创建一个ExtensionLoader,然后返回。

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
   ……省略校验逻辑
    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的构造方法中,会绑定扩展点接口类型,并且会绑定扩展对象工厂ExtensionFactory objectFactory,这也是一个扩展点,为了避免越套越深,这里不做深入追踪。

private ExtensionLoader(Class<?> type) {
    this.type = type;
    objectFactory =
            (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}

创建好ExtensionLoader之后,便可以调用getExtension()方法。核心在于获得扩展实现,首先检查缓存,缓存未命中则创建扩展对象,然后设置到 holder 中。

public T getExtension(String name) {
    return getExtension(name, true);
}

public T getExtension(String name, boolean wrap) {
    if (StringUtils.isEmpty(name)) {
        throw new IllegalArgumentException("Extension name == null");
    }
    if ("true".equals(name)) {
        // 获取默认的扩展实现类
        return getDefaultExtension();
    }
    //用于持有目标对象
    final Holder<Object> holder = getOrCreateHolder(name);
    Object instance = holder.get();
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
                //创建扩展实例
                instance = createExtension(name, wrap);
                //设置到holder中
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}

创建扩展实例方法createExtension()主要包含以下逻辑:

  1. 通过 getExtensionClasses() 获取所有的拓展类

  2. 通过反射创建拓展对象

  3. 向拓展对象中注入依赖(可以想象到,这里存在依赖其他SPI扩展点的情况,递归注入)

  4. 将拓展对象包裹在相应的 Wrapper 对象中

  5. 如果实现了Lifecycle接口,执行生命周期的初始化方法

步骤 1 是加载拓展类的关键,步骤 3 和 4 是 Dubbo IOC 与 AOP 的具体实现,注意 IOC 和 AOP 是一种思想,别和 Spring 的 IOC、AOP 混淆。

private T createExtension(String name, boolean wrap) {
    //关键代码!从配置文件中加载所有的拓展类,可得到“配置项名称”到“配置类”的映射关系表
    Class<?> clazz = getExtensionClasses().get(name);
    if (clazz == null || unacceptableExceptions.contains(name)) {
        throw findException(name);
    }
    try {
        // 通过反射创建实例
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.getDeclaredConstructor().newInstance());
            instance = (T) EXTENSION_INSTANCES.get(clazz);
        }
        // 向实例中注入依赖
        injectExtension(instance);


        if (wrap) {

            List<Class<?>> wrapperClassesList = new ArrayList<>();
            if (cachedWrapperClasses != null) {
                wrapperClassesList.addAll(cachedWrapperClasses);
                wrapperClassesList.sort(WrapperComparator.COMPARATOR);
                Collections.reverse(wrapperClassesList);
            }

            if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
                // 循环创建 Wrapper 实例
                for (Class<?> wrapperClass : wrapperClassesList) {
                    Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);
                    if (wrapper == null
                            || (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {
                        // 将当前 instance 作为参数传给 Wrapper 的构造方法,并通过反射创建 Wrapper 实例。
                        // 然后向 Wrapper 实例中注入依赖,最后将 Wrapper 实例再次赋值给 instance 变量
                        instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                    }
                }
            }
        }
    //执行生命周期初始化方法 initialize()
        initExtension(instance);
        return instance;
    } catch (Throwable t) {
        ……
    }
}


获得所有扩展类

我们在根据名称获得指定拓展类之前,首先需要根据配置文件解析出拓展项名称到拓展类的映射Map(Map<名称, 拓展类>),之后再根据拓展项名称从映射关系 Map 中根据 name 取出相应的拓展类即可。

在获得全部扩展名与扩展类关系classes时,如果还未创建,会有个双重检查的过程以防止并发加载。通过检查后会调用 loadExtensionClasses() 加载扩展类。相关过程的代码分析如下:

private Map<String, Class<?>> getExtensionClasses() {
    // 从缓存中获取已加载的拓展类
    Map<String, Class<?>> classes = cachedClasses.get();
    // DBC 双重检查
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                // 加载所有拓展类逻辑
                classes = loadExtensionClasses();
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}

loadExtensionClasses()主要做了两件事,一是解析@SPI注解设置默认扩展名,二是在这里会扫描 Dubbo 那三个配置文件夹下的所有配置文件,具体注释如下。

private Map<String, Class<?>> loadExtensionClasses() {
    cacheDefaultExtensionName();

    Map<String, Class<?>> extensionClasses = new HashMap<>();
    // 从三种加载策略中获得扩展实现类,对应加载指定文件夹下的配置文件,这里的加载策略已经根据优先级排好了顺序,数字越小越优先
    for (LoadingStrategy strategy : strategies) {
        loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(),
                strategy.overridden(), strategy.excludedPackages());
        // 此处为了兼容阿里巴巴到Apache的版本过度
        loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"),
                strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
    }

    return extensionClasses;
}

// 设置默认扩展名
private void cacheDefaultExtensionName() {
    // 获取 SPI 注解,这里的 type 变量是在调用 getExtensionLoader 方法时传入的
    final SPI defaultAnnotation = type.getAnnotation(SPI.class);
    if (defaultAnnotation == null) {
        return;
    }
    //从@SPI注解获得默认实现扩展实现名
    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));
        }
        if (names.length == 1) {
            // 设置默认名称,参考 getDefaultExtension 方法
            cachedDefaultName = names[0];
        }
    }
}

可以看出,关键的逻辑应该在loadDirectory()方法中。loadDirectory() 方法先通过 classLoader 获取所有资源链接,然后再通过 loadResource 方法加载资源。我们继续跟下去,看一下 loadResource 方法的实现。

private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader,
                          java.net.URL resourceURL, boolean overridden, String... excludedPackages) {
    try {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
            String line;
            String clazz = null;
            // 按行读取配置内容
            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();
                            clazz = line.substring(i + 1).trim();
                        } else {
                            clazz = line;
                        }
                        if (StringUtils.isNotEmpty(clazz) && !isExcluded(clazz, excludedPackages)) {
                            // 加载类,并通过 loadClass 方法对类进行缓存
                            loadClass(extensionClasses, resourceURL, Class.forName(clazz, true, classLoader), name, overridden);
                        }
                    } 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);
    }
}

loadResource 方法用于读取和解析配置文件,并通过反射加载类,最后调用 loadClass 方法进行其他操作。loadClass 方法用于主要用于操作缓存,该方法的逻辑如下:

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,
                       boolean overridden) 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.");
    }
    // 检测目标类上是否有 Adaptive 注解
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        // 设置 cachedAdaptiveClass缓存
        cacheAdaptiveClass(clazz, overridden);

        // 检测 clazz 是否是 Wrapper 类型
    } else if (isWrapperClass(clazz)) {
        // 存储 clazz 到 cachedWrapperClasses 缓存中
        cacheWrapperClass(clazz);

    } else {
        // 程序进入此分支,表明 clazz 是一个普通的拓展类
        clazz.getConstructor();
        // 检测 clazz 是否有默认的构造方法,如果没有,则抛出异常
        if (StringUtils.isEmpty(name)) {
            // 如果 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 (ArrayUtils.isNotEmpty(names)) {
            cacheActivateClass(clazz, names[0]);
            for (String n : names) {
                // 存储 Class 到名称的映射关系
                cacheName(clazz, n);
                // 存储名称到 Class 的映射关系
                saveInExtensionClass(extensionClasses, clazz, n, overridden);
            }
        }
    }
}

如上,loadClass 方法操作了不同的缓存,比如解析@Adaptive注解设置到cachedAdaptiveClass缓存;如果是wrapper类设置到cachedWrapperClasses 缓存;保存拓展类和拓展名到 cachedNames 缓存等等。

到这里 Dubbo SPI 加载机制的主要逻辑就讲完了,汇总了一下核心流程如下图:

da85521aa349d42dffa33f772efc65eb.png

总结

在我们了解了各种实现细节后,最后可以总结一下各种知识点。

  1. dubbo 的 ExtensionLoader 对应 JDK 的ServiceLoader,包含了加载所需要的各种上下文信息,一个扩展接口对应一个 ExtensionLoader,关系维护在一个 Map 中。

  2. 在尝试获取拓展类的时候,才开始实例化,按需加载,这种懒加载是一种对 JDK SPI 的优化。

  3. Dubbo SPI 会从三个配置目录按照优先级加载配置,加载策略是依赖 JDK SPI 加载的。

  4. 加载配置的时候会加载所有配置的Class类型,然后把拓展名和拓展类的类型维护一个映射(key-value),这也是按需加载,灵活使用的基础。

如果觉得对你有帮助,欢迎点赞、标🌟分享

update在MySQL中是怎样执行的,一张图牢记|原创

2022-11-19

514601c1f24250c755bfc844725d7ae8.jpeg

讲真,这篇最全HashMap你不能错过!|原创

2022-11-17

c1a95fdec7610ee5c904ee6abeae283a.jpeg

MySQL主从数据不一致,怎么办?

2022-11-11

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

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

相关文章

【BP回归预测】基于matlab随机蛙跳算法SFLA优化神经网络数据回归预测【含Matlab源码 2272期】

⛄一、蛙跳算法 1 改进的免疫蛙跳算法 免疫蛙跳算法具有混合蛙跳算法的全局优化与局部细致搜索优点, 可以优化连续问题和离散问题, 具有较强的鲁棒性;同时, 群体具有的免疫机制对群体进行控制和调节, 把目标函数和制约条件作为青蛙群体的抗原, 保证生成的青蛙群体直接与问题相…

亚马逊爆锤之下,中国跨境电商出路何在?

跨境电商服务行业已经发展了十几年了&#xff0c;加之新冠疫情的影响&#xff0c;众多卖家企业都将视线转向了跨境电商市场&#xff0c;行业里一直不断涌现着众多的服务平台&#xff0c;今年&#xff0c;许多像Starday这样的新生跨境电商服务平台在行业里势头猛进&#xff0c;而…

ImmunoChemistry丨ICT艾美捷一氧化氮合酶说明书

ImmunoChemistry艾美捷一氧化氮合酶测定为评估亚硝化应激抑制剂和活化剂的效力提供了一个很好的筛选选择&#xff0c;并将有助于确定氧化和亚硝化应激如何调节不同的细胞内途径。该试剂盒使用二氨基荧光素-2二乙酸酯&#xff08;DAF-2DA&#xff09;染料评估细胞内游离一氧化氮…

原生API编写简单富文本编辑器003

原生API编写简单富文本编辑器003 系列文章快速阅读&#xff1a; 富文本编辑器开发系列-1-基础概念 富文本编辑器开发系列2-document.execCommand 的API 富文本编辑器开发系列3-selection 富文本编辑器开发系列4——Range对象 富文本编辑器开发系列5——浏览器Selection API探究…

思科设备中STP生成树协议及其配置

目录 一、网络冗余存在的问题 1.广播风暴 2.MAC地址表震荡 二、STP简介 1.BPDU简介与字段含义 2.网桥ID 3.路径开销 4.端口ID 5.BPDU计时器 &#xff08;1&#xff09;Hello Time &#xff08;2&#xff09;Forward Delay转发延迟 &#xff08;3&#xff09;Max Ag…

[附源码]JAVA毕业设计小区物业管理系统录像展示.mp4(系统+LW)

[附源码]JAVA毕业设计小区物业管理系统录像展示.mp4&#xff08;系统LW&#xff09; 项目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09…

【Golang】切片的底层实现(关于slice调用append函数后分配新数组的问题)

问题描述 今天在写代码的时候遇到一个很奇怪的现象&#xff0c;先看下面两段代码 func push(a []int, v int) {a[1] 2a append(a, v) } func main() {a : []int{0, 1, 2}push(a, 3)fmt.Println(a) }结果&#xff1a;[0 2 2] func push(a []int, v int) {a append(a, v)a[…

宝塔下 nginx 支持图片放缩

要想通过nginx实现图片的放缩功能&#xff0c;首先需要对nginx添加http_image_filter_module模块的支持&#xff0c;首先查看安装的nginx是否已经支持了对应的模块 nginx -V 如图&#xff0c;如果返回的代码中没有包含 http_image_filter_module&#xff0c;则代表安装的nginx…

Docker学习笔记3(狂神)

可视化 这样我们就已经安装成功了。 我们一般选择本地的。 然后我们就可以看到这样的面板。 不过这个我们平时不会去使用&#xff0c;只是作为了解即可。 镜像分层的理解&#xff1a; 如何提交一个自己的镜像。 Commit镜像 实战测试 我们现在启动了tomcat。 我们进入了tomc…

Spring RestTemplate请求过程

文章目录前言1. RestTemplate请求整体过程2. httpRequest 创建3. doWithRequest4. execute5. http响应解析前言 目前Spring RestTemplate是常用的http请求工具类&#xff0c;本文简单Spring RestTemplate的请求过程。 1. RestTemplate请求整体过程 接下来以ResponseEntity e…

paddleOCR识别问题和解决方案

常见问题解答&#xff1a; 文本检测相关FAQ paddle ocr 常见问答 https://aistudio.baidu.com/aistudio/projectdetail/4491412 参数介绍 import argparse import os import sys import cv2 import numpy as np import paddle from PIL import Image, ImageDraw, ImageFont …

如何基于 APISIX 迭代数字智联平台

分享嘉宾&#xff1a;沈巍&#xff0c;浙大网新研发总监。 网新电气成立于 2011 年&#xff0c;属于浙大网新旗下&#xff0c;为绿色智慧交通系统解决方案的提供商。业务范围包括铁路、隧道、城市智能交通、航空、高速公路等行业。整个高铁信息化的业务分布占到了全国市场的 20…

Electron 麒麟 Linux 系统 root 账户报错

使用Electron打包成客户端在麒麟Linux 操作系统上运行&#xff0c;普通用户启动程序正常 使用root用户出现各种问题。总结问题如下&#xff1a; 1. Running as root without --no-sandbox is not supported。 解决方案&#xff1a; 在启动命令后面加入 --no-sandbox sudo …

为SSH远程配置固定的公网TCP端口地址【内网穿透】

由于使用免费的cpolar生成的公网地址&#xff0c;为随机临时地址&#xff0c;24小时内会发生变化&#xff0c;并且带宽较小&#xff0c;只有1M。对于需要长期SSH远程的用户来说&#xff0c;配置固定的公网TCP端口地址&#xff0c;提高带宽就很有必要。 1. 保留一个固定TCP端口地…

信息收集道道之外网信息收集

#信息收集道道之外网信息收集 从个人的角度去简单整理下打点前的信息收集那些事。从信息收集本质上来说多数内容都是大同小异&#xff0c;遇到坚壁时&#xff0c;不用死磕&#xff0c;毕竟条条大路通罗马&#xff08;大佬们也可以说说看法~向各位大佬学习&#xff01; 红队知…

业务数据LEFT JOIN 多表查询慢--优化操作

首先你会想到&#xff0c;给表加索引&#xff0c;那么mysql会给主键自动建立索引吗? 会的&#xff0c;当然会。 在我们查询的业务表操作的时候&#xff0c;表业务数据庞大起来的时候&#xff0c;以及left join多的时候&#xff0c;甚至多表关联到几十张表的时候&#xff0c;查…

【云原生】二进制部署k8s集群(中)搭建node节点

内容预知 连接上文 1. 部署 Worker Node 组件 1.1 work node 组件部署前需了解的节点注册机制 1.2 Worker Node 组件部署步骤 2. k8s的CNI网络插件模式 2.1 k8s的三种网络模式 K8S 中 Pod 网络通信&#xff1a; &#xff08;1&#xff09;Pod 内容器与容器之间的通信 &am…

2022年四川建筑八大员(标准员)考试试题及答案

百分百题库提供建筑八大员&#xff08;标准员&#xff09;考试试题、建筑八大员&#xff08;标准员&#xff09;考试真题、建筑八大员&#xff08;标准员&#xff09;证考试题库等,提供在线做题刷题&#xff0c;在线模拟考试&#xff0c;助你考试轻松过关。 1.施工项目管理目标…

[附源码]Python计算机毕业设计SSM基于框架的校园爱心公益平台的设计与实现(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

java基于Springboot的学生毕业离校系统-计算机毕业设计

项目介绍 学生毕业离校系统的开发过程中&#xff0c;采用B / S架构&#xff0c;主要使用Java技术进行开发&#xff0c;结合最新流行的springboot框架。中间件服务器是Tomcat服务器&#xff0c;使用Mysql数据库和Eclipse开发环境。该学生毕业离校系统包括管理员、学生和教师。其…