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

news2024/11/25 20:51:22

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

  • 基于 2.7.0 版本
  • 上一章:『Dubbo SPI源码分析』依赖注入机制分析
  1. 创建测试 demo
  • 首先创建一个接口,举例为 Car
package com.luban.dubbo_spi.api;

@SPI
public interface Car {
    public void getColor();
    
    @Adaptive
    public void getColorForUrl(URL url);
}
- 根据接口,先创建一个实现类
```java
package com.luban.dubbo_spi.impl;

public class RedCar implements Car {

    public void getColor() {
        System.out.println("red");
    }
    
    @Override
    public void getColorForUrl(URL url) {
    }
}
  • 再创建一个实现类
package com.luban.dubbo_spi.impl;

public class BlackCar implements Car {

    public void getColor() {
        System.out.println("black");
    }

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

    public static void main(String[] args) {

        ExtensionLoader<Car> carLoader = ExtensionLoader.getExtensionLoader(Car.class);
        Car adCar = carLoader.getAdaptiveExtension();

        Map<String, String> map = new HashMap<>();
        map.put("car", "black");
        URL url = new URL("","",1, map);
        adCar.getColorForUrl(url);
    }
}
  1. @Adaptive 除了可以标记在自定义的 Adaptive 类(即 com.luban.dubbo_spi.impl.AdaptiveCar 类)上面以外(『Dubbo SPI源码分析』依赖注入机制分析),还可以标记在接口的对应方法中,获取 dubbo 帮你实现的代理类,首先也是按照老套路,先执行 ExtensionLoader.getExtensionLoader() 扫描配置文件中实现了 com.luban.dubbo_spi.api.Car 的类以后,就可以从中获取到 Adaptive 扩展类
public class CarDemo {

    public static void main(String[] args) {
		// 1. 获取实现了 com.luban.dubbo_spi.api.Car 接口的扩展类
        ExtensionLoader<Car> carLoader = ExtensionLoader.getExtensionLoader(Car.class);
        // 2. 获取 Adaptive 扩展类
        Car adCar = carLoader.getAdaptiveExtension();
        ...
    }
}
  1. com.luban.dubbo_spi.api.CarExtensionLoader 先判断是否有过 Adaptive 实例对象,如果有则直接返回,没有就创建
public class ExtensionLoader<T> {
    private final Holder<Object> cachedAdaptiveInstance = new Holder<Object>();
    private volatile Throwable createAdaptiveInstanceError;
    ...
    public T getAdaptiveExtension() {
        // 一个接口只能有一个Adaptive实现
        Object instance = cachedAdaptiveInstance.get();
        if (instance == null) {
            if (createAdaptiveInstanceError == null) {
                synchronized (cachedAdaptiveInstance) {
                    // 1. 尝试从缓存中获取 Adaptive 对象
                    instance = cachedAdaptiveInstance.get();
                    if (instance == null) {
                        try {
                            // 2. 没有就创建
                            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. ExtensionLoader 会调用 getAdaptiveExtensionClass() 获取 Adaptive 扩展类,其中依赖注入就忽略了,因为配置的 case 没有依赖注入
public class ExtensionLoader<T> {
    ...
    @SuppressWarnings("unchecked")
    private T createAdaptiveExtension() {
        try {
            // 1. 获取 Adaptive 扩展类
            return injectExtension((T) getAdaptiveExtensionClass().newInstance());
        } catch (Exception e) {
            throw new IllegalStateException("Can not create adaptive extension " + type + ", cause: " + e.getMessage(), e);
        }
    }
}
  1. ExtensionLoader 继续调用 createAdaptiveExtensionClass() 方法,用于生成代理类
public class ExtensionLoader<T> {
    ...
    private Class<?> createAdaptiveExtensionClass() {
    	// 1. 生成代理类的代码
        String code = createAdaptiveExtensionClassCode();
		...
    }
}
  1. 首先遍历每个方法,判断存在一个方法有注释 @Adaptive,没有则抛异常。找到有注释的方法后,先找到存在 URL.class 入参的位置,生成校验 URL 入参代码。然后从 @Adaptive 的 value 属性中取值,没有的话,只能用类名小写代替,即为 “car”(这个参数是 URL 上的占位符 key,用作取数据)。然后判断方法参数列表是否有 org.apache.dubbo.rpc.Invocation 入参,有也生成相应的校验入参代码。最后根据 value 值,生成获取参数的代码,并返回
  • 重点
    • @Adaptive 注解中,如果填写了 value 值,代表指定占位符名称,没填写,则用类名小写
    • 一定要填入 URL.class 参数,除非自定义其他 protocol 协议
    • 没标注 @Adaptive 注解的方法,使用会抛异常
    • 可以再添加一个 org.apache.dubbo.rpc.Invocation 入参
  • 生成的代理代码如下
package com.luban.dubbo_spi.api;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class Car$Adaptive implements com.luban.dubbo_spi.api.Car {
    public void getColorForUrl(org.apache.dubbo.common.URL arg0) {
        if (arg0 == null) throw new IllegalArgumentException("url == null");
        org.apache.dubbo.common.URL url = arg0;
        String extName = url.getParameter("car");
        if(extName == null) throw new IllegalStateException("Fail to get extension(com.luban.dubbo_spi.api.Car) name from url(" + url.toString() + ") use keys([car])");
        com.luban.dubbo_spi.api.Car extension = (com.luban.dubbo_spi.api.Car)ExtensionLoader.getExtensionLoader(com.luban.dubbo_spi.api.Car.class).getExtension(extName);extension.getColorForUrl(arg0);
    }
    public void getColor() {throw new UnsupportedOperationException("method public abstract void com.luban.dubbo_spi.api.Car.getColor() of interface com.luban.dubbo_spi.api.Car is not adaptive method!");
    }
}
  • 源代码
public class ExtensionLoader<T> {
	private final Class<?> type;
	private String cachedDefaultName;
	...
    private String createAdaptiveExtensionClassCode() {
        StringBuilder codeBuilder = new StringBuilder();
        Method[] methods = type.getMethods();
        boolean hasAdaptiveAnnotation = false;
        // 1. 看是不是所有方法上都没有 Adaptive 注解,都没有则会抛异常
        for (Method m : methods) {
            if (m.isAnnotationPresent(Adaptive.class)) {
                hasAdaptiveAnnotation = true;
                break;
            }
        }
        // 没有 Adaptive 注解,就抛异常
        if (!hasAdaptiveAnnotation) {
            throw new IllegalStateException("No adaptive method on extension " + type.getName() + ", refuse to create the adaptive class!");
        }

        // 生成包,import, 类名
        codeBuilder.append("package ").append(type.getPackage().getName()).append(";");
        codeBuilder.append("\nimport ").append(ExtensionLoader.class.getName()).append(";");
        codeBuilder.append("\npublic class ").append(type.getSimpleName()).append("$Adaptive").append(" implements ").append(type.getCanonicalName()).append(" {");

        for (Method method : methods) {
            Class<?> rt = method.getReturnType();
            Class<?>[] pts = method.getParameterTypes();
            Class<?>[] ets = method.getExceptionTypes();

            Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
            StringBuilder code = new StringBuilder(512);
            // 2. 如果方法上没有 Adaptive 注解,则加上抛异常代码
            if (adaptiveAnnotation == null) {
                code.append("throw new UnsupportedOperationException(\"method ")
                        .append(method.toString()).append(" of interface ")
                        .append(type.getName()).append(" is not adaptive method!\");");
            } else {
                // 3. 寻找 URL 参数所在位置
                int urlTypeIndex = -1;
                for (int i = 0; i < pts.length; ++i) {
                    if (pts[i].equals(URL.class)) {
                        urlTypeIndex = i;
                        break;
                    }
                }
                
                // 4. 存在 URL 参数,就从 URL 中取数据,并判空
                if (urlTypeIndex != -1) {
                    // Null Point check
                    String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"url == null\");",
                            urlTypeIndex);
                    code.append(s);

                    s = String.format("\n%s url = arg%d;", URL.class.getName(), urlTypeIndex);
                    code.append(s);
                }
                else {
                    String attribMethod = null;
                    // 如果没有 URL 参数,就从其他参数中去找是不是有返回值为 URL 的 get 方法
                    LBL_PTS:
                    for (int i = 0; i < pts.length; ++i) {
                        Method[] ms = pts[i].getMethods();
                        for (Method m : ms) {
                            String name = m.getName();
                            if ((name.startsWith("get") || name.length() > 3)
                                    && Modifier.isPublic(m.getModifiers())
                                    && !Modifier.isStatic(m.getModifiers())
                                    && m.getParameterTypes().length == 0
                                    && m.getReturnType() == URL.class) {
                                urlTypeIndex = i;
                                attribMethod = name;
                                break LBL_PTS;
                            }
                        }
                    }
                    if (attribMethod == null) {
                        throw new IllegalStateException("fail to create adaptive class for interface " + type.getName()
                                + ": not found url parameter or url attribute in parameters of method " + method.getName());
                    }

                    // Null point check
                    String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"%s argument == null\");",
                            urlTypeIndex, pts[urlTypeIndex].getName());
                    code.append(s);
                    s = String.format("\nif (arg%d.%s() == null) throw new IllegalArgumentException(\"%s argument %s() == null\");",
                            urlTypeIndex, attribMethod, pts[urlTypeIndex].getName(), attribMethod);
                    code.append(s);

                    s = String.format("%s url = arg%d.%s();", URL.class.getName(), urlTypeIndex, attribMethod);
                    code.append(s);
                }
                // 5. 取 adaptive 注解的值,可以是个数组,如果没有值,则会取类名,LoadBalance->load.balance
                String[] value = adaptiveAnnotation.value();
                if (value.length == 0) {
                    String splitName = StringUtils.camelToSplitName(type.getSimpleName(), ".");
                    value = new String[]{splitName};
                }
                // 6. 判断参数列表中是否有 Invocation 对象
                boolean hasInvocation = false;
                for (int i = 0; i < pts.length; ++i) {
                    if (("org.apache.dubbo.rpc.Invocation").equals(pts[i].getName())) {
                        // Null Point check
                        String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"invocation == null\");", i);
                        code.append(s);
                        s = String.format("\nString methodName = arg%d.getMethodName();", i);
                        code.append(s);
                        hasInvocation = true;
                        break;
                    }
                }
                // 默认名是接口上的 Extension 注解上对于的值
                String defaultExtName = cachedDefaultName;
                String getNameCode = null;
                // 倒序循环 adaptive 注解的值
                for (int i = value.length - 1; i >= 0; --i) {
                    if (i == value.length - 1) {
                        if (null != defaultExtName) {
                            if (!"protocol".equals(value[i])) {
                                if (hasInvocation) {
                                    // url.getMethodParameter(methodName, value[i], defaultExtName)
                                    // methodName 是从 Invocation 对象中获取的
                                    getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                                } else {
                                    // url.getParameter(value[i], defaultExtName)
                                    getNameCode = String.format("url.getParameter(\"%s\", \"%s\")", value[i], defaultExtName);
                                }
                            } else {
                                getNameCode = String.format("( url.getProtocol() == null ? \"%s\" : url.getProtocol() )", defaultExtName);
                            }
                        } else {
                            if (!"protocol".equals(value[i])) {
                                if (hasInvocation) {
                                    getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                                } else {
                                     
                                    getNameCode = String.format("url.getParameter(\"%s\")", value[i]);
                                }
                            } else {
                                getNameCode = "url.getProtocol()";
                            }
                        }
                    } else {
                        if (!"protocol".equals(value[i])) {
                            if (hasInvocation) {
                                getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                            } else {
                                // 7. 生成取从 URL 取数据代码,其中 value[i] = car
                                getNameCode = String.format("url.getParameter(\"%s\", %s)", value[i], getNameCode);
                            }
                        } else {
                            getNameCode = String.format("url.getProtocol() == null ? (%s) : url.getProtocol()", getNameCode);
                        }
                    }
                }
                code.append("\nString extName = ").append(getNameCode).append(";");
                // check extName == null?
                String s = String.format("\nif(extName == null) " +
                                "throw new IllegalStateException(\"Fail to get extension(%s) name from url(\" + url.toString() + \") use keys(%s)\");",
                        type.getName(), Arrays.toString(value));
                code.append(s);

                // 根据取出来的扩展名获取扩展
                s = String.format("\n%s extension = (%<s)%s.getExtensionLoader(%s.class).getExtension(extName);",
                        type.getName(), ExtensionLoader.class.getSimpleName(), type.getName());
                code.append(s);

                // return statement
                if (!rt.equals(void.class)) {
                    code.append("\nreturn ");
                }
                // 执行扩展实现类的方法
                s = String.format("extension.%s(", method.getName());
                code.append(s);
                for (int i = 0; i < pts.length; i++) {
                    if (i != 0) {
                        code.append(", ");
                    }
                    code.append("arg").append(i);
                }
                code.append(");");
            }

            codeBuilder.append("\npublic ").append(rt.getCanonicalName()).append(" ").append(method.getName()).append("(");
            for (int i = 0; i < pts.length; i++) {
                if (i > 0) {
                    codeBuilder.append(", ");
                }
                codeBuilder.append(pts[i].getCanonicalName());
                codeBuilder.append(" ");
                codeBuilder.append("arg").append(i);
            }
            codeBuilder.append(")");
            if (ets.length > 0) {
                codeBuilder.append(" throws ");
                for (int i = 0; i < ets.length; i++) {
                    if (i > 0) {
                        codeBuilder.append(", ");
                    }
                    codeBuilder.append(ets[i].getCanonicalName());
                }
            }
            codeBuilder.append(" {");
            codeBuilder.append(code.toString());
            codeBuilder.append("\n}");
        }
        codeBuilder.append("\n}");
        if (logger.isDebugEnabled()) {
            logger.debug(codeBuilder.toString());
        }
        // 8. 返回生成的代码
        return codeBuilder.toString();
    }
}
  1. 当生成代理类代码后,通过获取加载器,以及编译器来生成对应代码,并返回
public class ExtensionLoader<T> {
    ...
    private Class<?> createAdaptiveExtensionClass() {
    	// 生成代理类的代码
        String code = createAdaptiveExtensionClassCode();
        // 1. 创建加载器
        ClassLoader classLoader = findClassLoader();
        // 2. 创建编译器
        org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
        // 3. 编译生成代码
        return compiler.compile(code, classLoader);
    }
}
  1. 代码生成之后,要传入 URL 参数,以获取实际执行的类,其中参数一定是 “car”,而 value 值是在 com.luban.dubbo_spi.api.Car 配置文件的类
public class CarDemo {

    public static void main(String[] args) {

        ExtensionLoader<Car> carLoader = ExtensionLoader.getExtensionLoader(Car.class);
        Car adCar = carLoader.getAdaptiveExtension();

        Map<String, String> map = new HashMap<>();
        map.put("car", "black");
        URL url = new URL("","",1, map);
        // 1. 传入 URL 参数,执行方法
        adCar.getColorForUrl(url);
    }
}
  1. 实际执行的时候,就是执行下面这个代理类了,其中会根据 URL 的参数,通过 getExtensionLoader() 获取指定的类并且实例化后执行 getColorForUrl() 方法
package com.luban.dubbo_spi.api;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class Car$Adaptive implements com.luban.dubbo_spi.api.Car {
    public void getColorForUrl(org.apache.dubbo.common.URL arg0) {
        if (arg0 == null) throw new IllegalArgumentException("url == null");
        org.apache.dubbo.common.URL url = arg0;
        String extName = url.getParameter("car");
        if(extName == null) throw new IllegalStateException("Fail to get extension(com.luban.dubbo_spi.api.Car) name from url(" + url.toString() + ") use keys([car])");
        com.luban.dubbo_spi.api.Car extension = (com.luban.dubbo_spi.api.Car)ExtensionLoader.getExtensionLoader(com.luban.dubbo_spi.api.Car.class).getExtension(extName);extension.getColorForUrl(arg0);
    }
    public void getColor() {throw new UnsupportedOperationException("method public abstract void com.luban.dubbo_spi.api.Car.getColor() of interface com.luban.dubbo_spi.api.Car is not adaptive method!");
    }
}
  1. 总结
    在这里插入图片描述

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

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

相关文章

pycharm中运行py文件时,报错:找不到自己编写的包等目录问题ModuleNotFoundError: No module named ‘xxx‘

【问题描述】&#xff1a;pycharm中运行py文件时&#xff0c;报错&#xff1a;找不到自己编写的包等目录问题 【报错】: ModuleNotFoundError: No module named ‘xxx’ ERROR: file not found 【问题定位】&#xff1a;运行的py文件和用到的包或者数据不在同一个文件目录下时…

【HanLP】--自然语言处理场景应用

目录 一、前言二、Springboot集成HanLP三、HanLP分词四、HanLP的关键字提取一、前言 HanLP 是由一系列模型与算法组成的工具包,主要功能包括分词、词性标注、关键词提取、自动摘要、依存句法分析、命名实体识别、短语提取、拼音转换、简繁转换等等。 下面将介绍HanLP如何本地…

PHP swoole从入门到精通(一、如何安装swoole及phpStorm服务端代码同步设置)

swoole安装直接在宝塔后台-》安装扩展 phpStorm服务端代码同步设置 https://www.cnblogs.com/yszr/p/9009961.html 软件下载地址 代码上传下载&#xff0c;鼠标右击任何一个文件或者文件夹&#xff0c;选择 Development->上传/下载/同步 点击工具栏Tools->Development…

前端个人年终工作总结精选7篇

一、工作总结的种类 1、按总结的时间分&#xff0c;有年度总结、半年总结、季度总结、学期总结。进行某项重大任务时&#xff0c;还要分期总结或叫阶段总结。 2、按总结的范围分&#xff0c;有单位总结、个人总结、综合性总结、专题总结等。 3、按总结的性质分&#xff0c;有…

科普 | OSI模型

本文简要地介绍 OSI 模型 1’ 2’ 3。 更新&#xff1a;2023 / 7 / 23 科普 | OSI模型 术语节点链路协议网络拓扑 概念作用结构应用层表示层会话层传输层网络层数据链路层物理层 数据如何流动OSI 和TCP/IP 的对应关系和协议参考链接 术语 节点 节点&#xff08; Node &#…

vue基本语法(个人学习笔记一)

目录 友情提醒第一章、vue概述1.1&#xff09;vuej介绍1.2&#xff09;Vue 周边库1.3&#xff09;MVVM的思想 第二章、借助CDN使用Vue2.1&#xff09;vue简单引入&#xff1a;引入js文件2.2&#xff09;简单的Vue程序 第三章、vue的基本语法3.1&#xff09;Vue的数据绑定&#…

在工作中如何对文件进行批量标记和编号

大家在日常工作中&#xff0c;是如何给文件该或文件夹批量重命名或添加编号&#xff0c;有没有怎么好的方法&#xff0c;可以一键批量操作。 经过小编多次操作经验&#xff0c;得到一个结论&#xff0c;可以通过《文件批量改名高手》对这些文件进行批量标记和编号&#xff0c;…

WebGL 概念和基础入门

WebGL 概念和基础入门 WebGL 是什么 对于 WebGL 百度百科给出的解释是 WebGL 是一种 3D 绘图协议&#xff0c;而对此维基百科给出的解释却是一种 JavaScript API。由于 WebGL 技术旨在帮助我们在不使用插件的情况下在任何兼容的网页浏览器中开发交互式 2D 和 3D 网页效果&…

win10 远程桌面连接如何打开?(Win10常用快捷键总结)

一、dos窗口下输入命令 mstsc 当然用向日葵还是更简单&#xff0c;更好用一些。 二、常用DOS命令 以下是一些常用的 DOS 命令&#xff1a; dir&#xff1a;列出当前目录中的文件和子目录。cd&#xff1a;改变当前目录。md&#xff1a;创建一个新的目录。rd&#xff1a;删除…

QT+VS2019 环境搭建

一、概述 一个PC-QT 的跨平台项目要维护。需要搭建一套环境。使用的是QTVS2019的环境。 QT使用v5.9.3的版本 (这个版本qalgorithms.h文件要替换成新的&#xff0c;源码附在文尾)vs插件使用qt-vsaddin-msvc2019-2.7.1.vsix 二、安装 无脑安装上文的QT5.9.3 和qt-vsaddin-msv…

LeetCode1143.Longest-Common-Subsequence<最长公共子序列>

题目&#xff1a; 思路&#xff1a; 我也不会,看出来了是动态规划,然后想着用sort试试. 看的题解.每一次扫描 双重for循环扫描字符(感觉这和一个个对比双指针区别不大了)..这动态规划也太笨重. 循环扫描数组。当字符相同的时候 1&#xff0c;否则等于它们之前较大的那一个。…

【阿里云试用计划】免费试用GPU

写在前面&#xff1a;本博客仅作记录学习之用&#xff0c;部分图片来自网络&#xff0c;如需引用请注明出处&#xff0c;同时如有侵犯您的权益&#xff0c;请联系删除&#xff01; 文章目录 前言试用步骤问题No CUDA GPUs are available无故被killed 致谢 前言 算力在深度学习…

ESP-EYE-使用记录

文章目录 1 测试平台2 相关网站3 搭建ESP-IDF环境4 HTML 文件修改 1 测试平台 2 相关网站 Gitee 平台下 《乐鑫开源 / esp-who》乐鑫官网ESP-EYE板子介绍Github 平台下 esp-whoGitee 平台 《ESP-EYE 入门指南》乐鑫官网ESP-EYE板子资料下载 3 搭建ESP-IDF环境 主要参考 《乐…

【OpenCV】windows环境下,java OpenCV环境搭建,java 也可以实现opencv的功能了!opencv自由了

目录 1. 下载opencv 2. 安装opencv 目录 1. 下载opencv 2. 安装opencv 3. dll文件的导入配置 dll文件的导入&#xff1a; &#xff08;C的类库文件&#xff09;&#xff0c;opencv是c开发的类库&#xff0c;java语言要调用其中的方法&#xff0c;所以依赖了dll文件 3.1…

oracle 使用笔记

1.查看用户信息 查看oracle用户信息&#xff1a;id oracle 2.查看、编辑、复制、创建文件 查看&#xff1a;cat /etc/sysctl.conf 编辑&#xff1a;vi /etc/sysctl.conf 复制&#xff1a; cp /home/oracle/database/response/* /home/oracle/etc/ 创建&#xff1a;mkdir /h…

我的踩坑记录!!!积累中......

bug记录&#xff1a; 解决 nodejs安装后&#xff0c;在安装目录下【nodejs】创建两个文件夹【node_global】及【node_cache】用来配置全局环境变量。 之后&#xff0c;打开cmd命令窗口&#xff0c;输入 npm config set prefix ”D:\Program Files\nodejs\node_global” npm con…

KubeVela篇07:terraform controller实现原理

terraform-controller是一个专门负责terraform一类的组件"安装"的Operator,通过打包成helm,再封装成kubevela的Addon,由kubevela安装到管控集群,为其它terraform provider插件提供模块定义支持。 从前面kubevela安装一个Application的原理我们了解到,当一个如a…

pytest自动化测试指定执行测试用例

1、在控制台执行 打开cmd,进入项目目录 指定执行某个模块 pytest testcases\Logistics\Platform\CarSource\test_CarSourceList.py 指定执行某个目录及其子目录的所有测试文件 pytest testcases\Logistics\Platform\CarSource 指定执行某个模块的某个类的某个测试用例 pyte…

C进阶:文件操作

C语言文件操作 什么是文件 磁盘上的数据是文件。 但是在程序设计中&#xff0c;我们一般谈的文件有两种&#xff1a;程序文件&#xff08;例如.c,.h这一类编译&#xff0c;链接过程中的文件&#xff09;&#xff0c;数据文件。 程序文件 包括源程序文件&#xff08;后缀为.c&…

【Three.js基础入门】:创建你的第一个3D场景

引言&#xff1a; Three.js是一种强大的JavaScript库&#xff0c;用于在Web浏览器中创建交互式的3D图形和动画。无需熟练的图形编程经验&#xff0c;你也可以通过Three.js轻松地构建令人惊叹的3D场景。 本文将带你逐步学习如何入门Three.js&#xff0c;从创建一个简单的3D场景开…