Dubbo SPI源码

文章目录

    • Dubbo SPI
    • 使用方式
    • AOP功能
    • 源码剖析
      • @SPI注解
      • 1.获取加载器
      • 2.获取拓展实例对象
      • 3.创建拓展类的实例对象

Dubbo SPI

Dubbo 的 SPI(Service Provider Interface)机制是一种强大的扩展机制,它允许开发者在运行时动态地替换或增加框架的功能。Dubbo 的 SPI 机制与 Java 原生的 SPI 机制有所不同,它提供了更多的灵活性和功能。

在这里插入图片描述SPI机制的核心组件包括:

  • 服务接口:接口定义了服务提供者需要实现的方法,应用程序将使用这个接口与具体的服务实现进行交互。

  • 服务实现:这是实现了服务接口的具体类,第三方可以为服务接口提供多个实现。

  • 服务提供者配置文件:这是一个位于META-INF/dubbo目录下的文件,文件名与服务接口的全限定名相同,该文件包含了服务实现类的全限定名,每行一个接口的具体实现类,在运行时就可以加载这些实现类。

  • ServiceLoader:用于加载服务实现,应用程序可以使用ServiceLoader来获取服务接口的所有具体实现类。

SPI的工作流程如下:

  • 定义服务接口。
  • 实现服务接口,创建具体的服务实现类。
  • 在META-INF/dubbo目录下创建服务提供者配置文件,列出所有服务实现类的全限定名。
  • 使用ServiceLoader加载服务具体实现类,并根据需要使用它们。

总结就是说SPI机制使得应用程序可以在运行时动态地选择和加载服务实现,从而提高了应用程序的可扩展性和灵活性。

使用方式

使用方式和 java的SPI类似,首先通过@SPI注解定义服务接口

@SPI
public interface Greeting {public void sayHello();
}

再定义服务实现类实现接口并重写接口中的方法

public class ChineseGreeting implements Greeting {@Overridepublic void sayHello() {System.out.println("你好,世界!");}
}
public class EnglishGreeting implements Greeting {@Overridepublic void sayHello() {System.out.println("Hello World!");}
}

在META-INF/dubbo目录下创建一个名为com.xydp.dubbo.Greeting的文件,用于存储自定义键名(这里与java的 SPI 不同,需要自定义key的名称,键名随意,主要是为了实现后面按需加载)与具体实现类的全限定名。文件内容如下:
在这里插入图片描述
文件内容

english=com.xydp.dubbo.EnglishGreeting
chinese=com.xydp.dubbo.ChineseGreeting

编写测试类

public class SpiDemo {public static void main(String[] args) {ExtensionLoader<Greeting> extensionLoader = ExtensionLoader.getExtensionLoader(Greeting.class);Greeting englishGreeting = extensionLoader.getExtension("english");englishGreeting.sayHello();System.out.println(englishGreeting.getClass());}
}

输出结果
在这里插入图片描述
Dubbo的 SPI 通过自定义键名的方式按需加载,可以在O(1)的时间复杂度获取具体的服务实现类,而java SPI 需要通过迭代器的方式全局遍历获取某个具体实现类,达到线性时间复杂度O(n)。

Dubbo SPI与Java SPI区别

  • 设计理念:Java SPI 主要关注于服务实现的加载,而 Dubbo SPI 更注重于框架的可扩展性和灵活性。

  • 功能丰富度:Dubbo SPI 提供了更多的功能,如自适应扩展、激活扩展和依赖注入等,而 Java SPI 功能相对有限。

  • 加载方式:Java SPI 采用全局加载的方式,加载特定的实现类时间复杂度达到O(n),性能差,而 Dubbo SPI 采用按需加载的方式,时间复杂度只需O(1),提高了性能。

  • 配置方式:Dubbo SPI 支持通过注解和 URL 参数进行动态配置,使得框架更加灵活;Java SPI 主要通过配置文件进行静态配置。

总之,Java SPI 和 Dubbo SPI 都是用于实现服务发现和实现加载的机制,但 Dubbo SPI 在设计理念、功能和用法上更加灵活和强大,Dubbo SPI 更适合用于构建复杂的分布式系统,而 Java SPI 更适合用于简单的服务加载场景。

AOP功能

在 Dubbo 中,实现 AOP 功能的方式是通过自定义 Wrapper 类实现的,Dubbo 要求实现 AOP 功能的类以 Wrapper 结尾,这是一种约定,以便于识别这些类是用于包装服务实现的,Wrapper 类是 Dubbo 框架的一部分,用于在运行时动态地为服务实现类添加额外的功能,原理和Spring AOP类似,将通知织入所要执行的目标方法前后。

public class GreetingWrapper1 implements  Greeting{private Greeting greeting;public GreetingWrapper1(Greeting greeting){this.greeting = greeting;}@Overridepublic void sayHello() {System.out.println("before do someting");this.greeting.sayHello();System.out.println("after do something");}
}

同时需要在配置文件中添加包装类的全限定名
在这里插入图片描述

测试类保持不变

public class SpiDemo {public static void main(String[] args) {ExtensionLoader<Greeting> extensionLoader = ExtensionLoader.getExtensionLoader(Greeting.class);Greeting englishGreeting = extensionLoader.getExtension("english");englishGreeting.sayHello();System.out.println(englishGreeting.getClass());}
}

输出结果
在这里插入图片描述
在这里插入图片描述

从结果可以看出最终执行的是GreetingWrapper1中greeting的sayHello()方法,GreetingWrapper1在这里充当EnglishGreeting的代理对象。

源码剖析

@SPI注解

在 Dubbo 的 SPI 机制中,@SPI 注解用于标记一个接口为可扩展的扩展点,@SPI 注解有两个可选参数:value 和 scope。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SPI {String value() default "";ExtensionScope scope() default ExtensionScope.APPLICATION;
}

value 参数用于指定扩展点的默认实现类,当没有其他扩展实现类被明确指定时,Dubbo 会使用 value 参数指定的key,表示从配置文件中查找对应的实现类。
scope 参数指定扩展实现类的作用域,有以下四种作用域

  • Constants.FRAMEWORK(框架作用域):在Dubbo框架内,实现类只会生成唯一实例,并在整个应用程序内共享。

  • Constants.APPLICATION(应用程序作用域):在应用程序上下文中,实现类仅会被实例化一次,并在整个应用程序中共享,是默认的作用域。

  • Constants.MODULE(模块作用域):在模块上下文中,该实现类将仅创建一个实例,并在该模块内共享。

  • Constants.SELF(自定义作用域):用户可定义实现类的作用范围,涵盖任意范围。

1.获取加载器

当执行获取拓展加载器这行代码时

 ExtensionLoader<Greeting> extensionLoader = ExtensionLoader.getExtensionLoader(Greeting.class);

源码如下

public <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {//检查拓展目录是否被删除this.checkDestroyed();//校验类型是否为nullif (type == null) {throw new IllegalArgumentException("Extension type == null");} else if (!type.isInterface()) {throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");} else if (!withExtensionAnnotation(type)) {throw new IllegalArgumentException("Extension type (" + type + ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");} else {//如果类型不为null,尝试从缓存中获取加载器loader以及作用域ExtensionLoader<T> loader = (ExtensionLoader)this.extensionLoadersMap.get(type);ExtensionScope scope = (ExtensionScope)this.extensionScopeMap.get(type);//如果缓存中不存在scope,就从SPI注解获取scope,再放入缓存if (scope == null) {SPI annotation = (SPI)type.getAnnotation(SPI.class);scope = annotation.scope();this.extensionScopeMap.put(type, scope);}//如果加载器为null且作用域是SELF,就直接创建loaderif (loader == null && scope == ExtensionScope.SELF) {loader = this.createExtensionLoader0(type);}//若lader为空,作用域不是SELF且父类加载器不为空,那么尝试从父类加载器去获取loaderif (loader == null && this.parent != null) {loader = this.parent.getExtensionLoader(type);}//若无法从父类加载器获取loader,那么自己实例化一个loader并放入缓存if (loader == null) {loader = this.createExtensionLoader(type);}//返回加载器return loader;}}
    private <T> ExtensionLoader<T> createExtensionLoader(Class<T> type) {ExtensionLoader<T> loader = null;//根据SPI注解的属性判断作用域是否等于默认作用域if (this.isScopeMatched(type)) {loader = this.createExtensionLoader0(type);}return loader;}
    private <T> ExtensionLoader<T> createExtensionLoader0(Class<T> type) {//检查拓展目录是否被删除this.checkDestroyed();//根据类型创建加载器并放入缓存this.extensionLoadersMap.putIfAbsent(type, new ExtensionLoader(type, this, this.scopeModel));//从缓存获取loaderExtensionLoader<T> loader = (ExtensionLoader)this.extensionLoadersMap.get(type);//返回loaderreturn loader;}

2.获取拓展实例对象

执行这行代码获取拓展类的实例对象

Greeting englishGreeting = extensionLoader.getExtension("english");

源码如下

    public T getExtension(String name) {//获取拓展实例对象T extension = this.getExtension(name, true);if (extension == null) {throw new IllegalArgumentException("Not find extension: " + name);} else {//获取到了返回return extension;}}
public T getExtension(String name, boolean wrap) {//检查拓展目录是否被删除this.checkDestroyed();//若参数为空,抛异常if (StringUtils.isEmpty(name)) {throw new IllegalArgumentException("Extension name == null");}//若拓展类名称为true,表示使用SPI注解中声明的默认拓展实现类 else if ("true".equals(name)) {return this.getDefaultExtension();} else {String cacheKey = name;if (!wrap) {cacheKey = name + "_origin";}//尝试从缓存中获取拓展类实例对象,如果获取不到cacheKey对应的holder//则会创建一个空的holder再返回Holder<Object> holder = this.getOrCreateHolder(cacheKey);Object instance = holder.get();//若获取不到,则采用双重检验的单例模式创建实例对象,代码第一次执行是获取不到对象的,//此时instance为null//第一层判断是为了提高执行速度,防止实例对象不为空时还去竞争锁if (instance == null) {synchronized(holder) {instance = holder.get();//第二层判断是为了避免创建重复的实例对象if (instance == null) {//获取拓展类实例对象instance = this.createExtension(name, wrap);//将实例对象放入缓存holder.set(instance);}}}//返回实例对象return instance;}}
    private Holder<Object> getOrCreateHolder(String name) {//根据推展类名称获取缓存对象holderHolder<Object> holder = (Holder)this.cachedInstances.get(name);//获取不到,则创建一个空的holder并放入缓存if (holder == null) {this.cachedInstances.putIfAbsent(name, new Holder());holder = (Holder)this.cachedInstances.get(name);}//返回holderreturn holder;}

Holder类是一个缓存对象,用于缓存自定义键名对应的拓展类实例对象

public class Holder<T> {
//volatil的作用是禁止指令重排序private volatile T value;public Holder() {}public void set(T value) {this.value = value;}public T get() {return this.value;}
}

可以看到缓存Holder类用volatile修饰变量,这样做是为了禁止指令重排序,避免返回一个未初始化完成的实例对象,创建一个对象分为3步

  1. 为对象分配内存空间
  2. 初始化对象
  3. 将对象指向1所分配的内存空间

若holder没有用volatile修饰,2和3的指令发生顺序颠倒,此时指令的执行顺序为1->3->2,当执行完3时,代码执行到第二层 if 判断,发现instance不为null,此时直接返回instance,返回的是还未初始化的对象(对象的属性未赋值)。

3.创建拓展类的实例对象

第一次执行时缓存是获取不到实例对象的,所以需要创建,之后就能从缓存中直接获取。

 private T createExtension(String name, boolean wrap) {//解析配置文件,先获取所有的拓展类,再根据类名获取对应的classClass<?> clazz = (Class)this.getExtensionClasses().get(name);if (clazz != null && !this.unacceptableExceptions.contains(name)) {try {//根据类对象从缓存获取类实例对象T instance = this.extensionInstances.get(clazz);//获取不到,则通过反射的方式创建实例对象并放入缓存if (instance == null) {this.extensionInstances.putIfAbsent(clazz, this.createExtensionInstance(clazz));instance = this.extensionInstances.get(clazz);//前置处理instance = this.postProcessBeforeInitialization(instance, name);//依赖注入this.injectExtension(instance);//后置处理instance = this.postProcessAfterInitialization(instance, name);}//包装类的处理,用于实现AOP功能if (wrap) {List<Class<?>> wrapperClassesList = new ArrayList();if (this.cachedWrapperClasses != null) {wrapperClassesList.addAll(this.cachedWrapperClasses);wrapperClassesList.sort(WrapperComparator.COMPARATOR);Collections.reverse(wrapperClassesList);}if (CollectionUtils.isNotEmpty(wrapperClassesList)) {Iterator var6 = wrapperClassesList.iterator();while(var6.hasNext()) {Class<?> wrapperClass = (Class)var6.next();Wrapper wrapper = (Wrapper)wrapperClass.getAnnotation(Wrapper.class);boolean match = wrapper == null || (ArrayUtils.isEmpty(wrapper.matches()) || ArrayUtils.contains(wrapper.matches(), name)) && !ArrayUtils.contains(wrapper.mismatches(), name);if (match) {instance = this.injectExtension(wrapperClass.getConstructor(this.type).newInstance(instance));instance = this.postProcessAfterInitialization(instance, name);}}}}//生命周期管理this.initExtension(instance);return instance;} catch (Throwable var10) {throw new IllegalStateException("Extension instance (name: " + name + ", class: " + this.type + ") couldn't be instantiated: " + var10.getMessage(), var10);}} else {throw this.findException(name);}}

(1)解析配置文件,获取拓展类

    private Map<String, Class<?>> getExtensionClasses() {//从缓存中获取拓展类Map<String, Class<?>> classes = (Map)this.cachedClasses.get();//缓存获取不到,通过双重检查锁的单例模式创建拓展类if (classes == null) {synchronized(this.cachedClasses) {classes = (Map)this.cachedClasses.get();if (classes == null) {try {//解析配置文件,获取类信息并放入缓存classes = this.loadExtensionClasses();} catch (InterruptedException var5) {logger.error("0-15", "", "", "Exception occurred when loading extension class (interface: " + this.type + ")", var5);throw new IllegalStateException("Exception occurred when loading extension class (interface: " + this.type + ")", var5);}this.cachedClasses.set(classes);}}}return classes;}private Map<String, Class<?>> loadExtensionClasses() throws InterruptedException {//检查拓展目录是否被销毁this.checkDestroyed();//判断SPI注解中的默认参数是否合法this.cacheDefaultExtensionName();Map<String, Class<?>> extensionClasses = new HashMap();LoadingStrategy[] var2 = strategies;int var3 = var2.length;//解析三个配置文件的信息,并将类信息放入缓存//三个配置文件 META-INF/dubbo/,META-INF/dubbo/internal/,META-INF/services/for(int var4 = 0; var4 < var3; ++var4) {LoadingStrategy strategy = var2[var4];this.loadDirectory(extensionClasses, strategy, this.type.getName());if (this.type == ExtensionInjector.class) {this.loadDirectory(extensionClasses, strategy, ExtensionFactory.class.getName());}}return extensionClasses;}

(2)实例化对象

 T instance = this.extensionInstances.get(clazz);if (instance == null) {this.extensionInstances.putIfAbsent(clazz, this.createExtensionInstance(clazz));instance = this.extensionInstances.get(clazz);}//通过反射的方式创建实例对象private Object createExtensionInstance(Class<?> type) throws ReflectiveOperationException {return this.instantiationStrategy.instantiate(type);}

(3)前置处理
前置处理和Spring的前置处理类似,可以在实例对象初始化之前执行一些自定义的初始化逻辑,例如检查实例对象是否满足某些条件,或者为实例对象添加一些额外的功能。

instance = this.postProcessBeforeInitialization(instance, name);private T postProcessBeforeInitialization(T instance, String name) throws Exception {ExtensionPostProcessor processor;if (this.extensionPostProcessors != null) {for(Iterator var3 = this.extensionPostProcessors.iterator(); var3.hasNext(); instance = processor.postProcessBeforeInitialization(instance, name)) {processor = (ExtensionPostProcessor)var3.next();}}return instance;}

(4)依赖注入
Dubbo的依赖注入只支持Setter方法级别的注入。

this.injectExtension(instance);private T injectExtension(T instance) {if (this.injector == null) {return instance;} else {try {Method[] var2 = instance.getClass().getMethods();int var3 = var2.length;for(int var4 = 0; var4 < var3; ++var4) {Method method = var2[var4];//方法是setter方法,方法不包含DisableInject注解且instance不是基本数据类型if (this.isSetter(method) && !method.isAnnotationPresent(DisableInject.class) && method.getDeclaringClass() != ScopeModelAware.class && (!(instance instanceof ScopeModelAware) && !(instance instanceof ExtensionAccessorAware) || !ignoredInjectMethodsDesc.contains(ReflectUtils.getDesc(method)))) {//获取方法的参数类型Class<?> pt = method.getParameterTypes()[0];if (!ReflectUtils.isPrimitives(pt)) {try {//获取参数值String property = this.getSetterProperty(method);//根据参数类型和参数值获取依赖对象Object object = this.injector.getInstance(pt, property);if (object != null) {//将依赖对象object注入instancemethod.invoke(instance, object);}} catch (Exception var9) {logger.error("0-15", "", "", "Failed to inject via method " + method.getName() + " of interface " + this.type.getName() + ": " + var9.getMessage(), var9);}}}}} catch (Exception var10) {logger.error("0-15", "", "", var10.getMessage(), var10);}return instance;}}

(5)后置处理
后置处理的思想与Spring的后置处理类似。

 instance = this.postProcessAfterInitialization(instance, name);private T postProcessAfterInitialization(T instance, String name) throws Exception {if (instance instanceof ExtensionAccessorAware) {((ExtensionAccessorAware)instance).setExtensionAccessor(this.extensionDirector);}ExtensionPostProcessor processor;if (this.extensionPostProcessors != null) {for(Iterator var3 = this.extensionPostProcessors.iterator(); var3.hasNext(); instance = processor.postProcessAfterInitialization(instance, name)) {processor = (ExtensionPostProcessor)var3.next();}}return instance;}

(6)包装类Wrapper处理
Dubbo的AOP功能就是通过Wrapper实现的。

 if (wrap) {List<Class<?>> wrapperClassesList = new ArrayList();//判断wrapper缓存包装类集合是否为空// 不为空则加入list集合中,然后排序之后再翻转if (this.cachedWrapperClasses != null) {wrapperClassesList.addAll(this.cachedWrapperClasses);wrapperClassesList.sort(WrapperComparator.COMPARATOR);Collections.reverse(wrapperClassesList);}//判断wrapper包装类集合不为空if (CollectionUtils.isNotEmpty(wrapperClassesList)) {Iterator var6 = wrapperClassesList.iterator();//遍历集合中的每一个包装类while(var6.hasNext()) {Class<?> wrapperClass = (Class)var6.next();//获取包装类的注解Wrapper wrapper = (Wrapper)wrapperClass.getAnnotation(Wrapper.class);//判断是否符合包装条件boolean match = wrapper == null || (ArrayUtils.isEmpty(wrapper.matches()) || ArrayUtils.contains(wrapper.matches(), name)) && !ArrayUtils.contains(wrapper.mismatches(), name);//符合包装条件,将当前实例对象添加到包装类中,并做一些后置处理if (match) {//将instance作为参数传给wrapper的构造方法,通过反射的方式创建wrapper实例,//再往wrapper实例注入依赖,然后将wrapper赋值给instance,再对instance做后置处理instance = this.injectExtension(wrapperClass.getConstructor(this.type).newInstance(instance));instance = this.postProcessAfterInitialization(instance, name);}}}}

(7)生命周期管理

  this.initExtension(instance);private void initExtension(T instance) {if (instance instanceof Lifecycle) {Lifecycle lifecycle = (Lifecycle)instance;lifecycle.initialize();}}

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

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

相关文章

SafaRi:弱监督引用表达式分割的自适应序列转换器

引用表达式分割(reference Expression Segmentation, RES)旨在提供文本所引用的图像(即引用表达式)中目标对象的分割掩码。 目前存在的挑战 1)现有的方法需要大规模的掩码注释。 2)此外&#xff0c;这种方法不能很好地推广到未见/零射击场景 改进 1&#xff09;提出了一个弱…

Cobbler 搭建方法

统信服务器操作系统行业版V20-1000c【Cobbler 搭建】手册 统信服务器操作系统行业版 V20版本上Cobbler 搭建方法 文章目录 功能概述一、使用范围二、cobbler工作流程1. Server 端2. Client 端三、 环境准备1. 测试环境告知,以提供配置时参考:2. 关闭防火墙、selinux:3. 注意…

优化深度学习模型训练过程:提升PASCAL VOC 2012数据集上Deeplabv3+模型训练效率的策略

创作不易&#xff0c;您的打赏、关注、点赞、收藏和转发是我坚持下去的动力&#xff01; 优化说明&#xff1a; 避免重复下载和解压数据集&#xff1a;将downloadTrue改为downloadFalse&#xff0c;防止每次运行代码都重新下载和解压数据集&#xff0c;从而节省时间。 使用pin…

【C++】stack 和 queue 以及 容器适配器

文章目录 一、stack1.1 stack的使用1.2 stack的模拟实现 二、queue2.1 queue的使用2.2 queue的模拟实现 三、优先级队列1.优先级队列的介绍2. priority_queue的使用的使用3.模拟实现优先级队列 四、 容器适配器1.STL标准库中stack和queue的底层结构2.deque&#xff08;双端对列…

OS:初识操作系统——邂逅与启航

✨ Blog’s 主页: 白乐天_ξ( ✿&#xff1e;◡❛) &#x1f308; 个人Motto&#xff1a;实践是检验真理的唯一标准&#xff01;&#xff01;&#xff01; &#x1f4ab; 欢迎来到我的学习笔记&#xff01; 前言 各位uu好&#xff0c;现在我们要开始一个新的篇章——操作…

Geneformer AI 模型,有限数据也能解锁基因网络

目录 类似于 BERT 的单单元数据参考模型 NVIDIA Clara 工具组合用于药物研发 用于疾病建模的基础 AI 模型 Geneformer 是最近推出的 和功能强大的 AI 模型&#xff0c;可以通过从大量单细胞转录组数据中进行迁移学习来学习基因网络动力学和相互作用。借助此工具&#xff0c;…

misc合集(1)

[Week3] 这是一个压缩包 有密码&#xff0c;提示QmFzZUNURj8/Pz8/P0ZUQ2VzYUI base64解密是BaseCTF??????FTCesaB 猜测这应该是⼀个轴对称的密码 python ⽣成了密码字典&#xff0c;再通过 ARCHPR 进⾏字典爆破 lowercase abcdefghijklmnopqrstuvwxyz uppercase l…

java写s7和plc通讯

pom.xml <dependency><groupId>com.github.s7connector</groupId><artifactId>s7connector</artifactId><version>2.1</version></dependency>maven下载不了的&#xff0c;下载包&#xff0c;评论或者私自内免费给 DB212 类&a…

5.1 溪降技术:个人装备

Content 5.1 个人装备概览设备概览视频电子书&#xff1a;个人装备安全装备非安全装备 峡谷探险个人安全装备个人安全装备视频*安全扣结构*峡谷探险个人非安全装备 湿峡谷湿峡谷装备视频个人安全装备个人非安全装备 干峡谷干峡谷装备视频个人安全装备个人非安全装备 团队装备&a…

安全区域边界等保测评

1.边界防护 应保证跨越边界的访问和数据流通过边界设备提供的受控接口进行通信。 [测评方法] 1)应核查在网络边界处是否部署访问控制设备;网闸和防火墙2)应核查设备配置信息是否指定端口进行跨越边界的网络通信,指定端口是否配置并启用了安全策略acl 3)应采用其他技术手…

【网盘外快】百度网盘SVIP充值使用说明,如何通过软件自动充值获取新用户优惠?这篇文章给你正确答案。

资源地址&#xff1a; 此软件需要 网盘ck 才可以使用。 雷电模拟器下载地址&#xff1a;https://www.ldmnq.com/ 软件下载地址&#xff1a;https://wwi.lanzoup.com/b01qdiavzg 密码:666 模拟器使用说明&#xff1a; 1、调整模拟器分辨率调整为&#xff1a;540 X 960。 2、…

每天练打字1:今日状况——击键5第1遍

前言 首先说明一点&#xff0c;这个每天练打打字系列不会每天更新。因为本来练打字就不是一件太大的事&#xff0c;没必要为了更新而更新&#xff0c;但还是会做到每周一更新。以便于跟踪进度&#xff0c;监控目标是否达成。 今日练习情况 一、跟打情况 常用字中五百&#…

RockyLinux-软件实现RAID5

一、背景 RAID&#xff08;Redundant Array of Independent Disks&#xff0c;独立磁盘冗余阵列&#xff09;是一种将多个物理硬盘驱动器组合成单一逻辑单元的技术&#xff0c;目的是提高存储性能、可靠性和/或数据冗余度。虽然早期的名字中包含“独立”&#xff08;Independen…

【资料分析】常见的坑

in 比较或计数类问题 差别大的基期比较&#xff0c;可以直接用现期进行比较 注意单位可能不同&#xff01; 注意顺序是从小到大还是从大到小 以及老问题&#xff0c;名字本身就叫XX增量&#xff0c;XX增加值&#xff0c;而非还要另外去算的东东 给出的图表可能是不完整的 2…

通过SQL语句判断奇偶数的几种方法

文章目录 1. 准备数据2. 使用 % 判断奇偶数3. 使用 MOD 判断奇偶数4. 使用按位与运算符 & 1. 准备数据 假设我们有一张测试表test_numbers -- 创建测试表 CREATE TABLE test_numbers (number_value INT );-- 插入测试数据 INSERT INTO test_numbers (number_value) VALUE…

Qt:饿汉单例(附带单例使用和内存管理)

前言 本文主要写饿汉单例以及单例的释放&#xff0c;网上很多教程只有单例的创建&#xff0c;但是并没有告诉我们单例的内存管理&#xff0c;这就很头疼。 正文 饿汉式单例 // SingletonClass.h #ifndef SINGLETONCLASS_H #define SINGLETONCLASS_H #include <QObject&g…

PCIe进阶之TL:First/Last DW Byte Enables Rules Traffic Class Field

1 First/Last DW Byte Enables Rules & Attributes Field 1.1 First/Last DW Byte Enables Rules Byte Enable 包含在 Memory、I/O 和 Configuration Request 中。本文定义了相应的规则。Byte Enable 位于 header 的 byte 7 。对于 TH 字段值为 1 的 Memory Read Request…

【stm32笔记】使用rtt-studio与stm32CubeMx联合创建项目

使用rtt-studio与stm32CubeMx联合创建项目 创建rt-thread项目 设置项目信息 在项目资源管理器中“右击“&#xff0c;创建RRT studio 项目 双击“RT-Thread 项目“。 选择MCU&#xff0c;设置UART&#xff0c;以及调试方式。添加项目名称&#xff0c;点击“完成“按钮。 …

长春自闭症寄宿学校:开启创造力与艺术之旅

长春自闭症寄宿学校的启示&#xff1a;在广州星贝育园&#xff0c;自闭症儿童开启创造力与艺术之旅 当我们谈及自闭症儿童的教育与成长时&#xff0c;总有一股力量在推动着我们不断探索与创新。虽然题目中提及了“长春自闭症寄宿学校”&#xff0c;但本文将聚焦于广州的星贝育…

使用jmeter做性能测试实践过程中需要注意什么

前言 在驾驭Apache JMeter进行性能测试之旅中&#xff0c;深刻理解其特性和限制是至关重要的。以下是提升JMeter效能的关键策略&#xff0c;旨在挖掘其潜力&#xff0c;克服局限&#xff0c;实现精准测试。 1.精确调控线程数 推荐阈值&#xff1a;将线程数控制在300以内&…