【Spring Boot 源码学习】OnBeanCondition 详解

Spring Boot 源码学习系列

在这里插入图片描述

OnBeanCondition 详解

  • 引言
  • 往期内容
  • 主要内容
    • 1. getOutcomes 方法
    • 2. getMatchOutcome 方法
      • 2.1 ConditionalOnBean 注解处理
      • 2.2 ConditionalOnSingleCandidate 注解处理
      • 2.3 ConditionalOnMissingBean 注解处理
    • 3. getMatchingBeans 方法
  • 总结

引言

上篇博文带大家从 Spring Boot 源码深入详解了 OnClassCondition,那本篇也同样从源码入手,带大家深入了解 OnBeanCondition 的过滤匹配实现。

往期内容

在开始本篇的内容介绍之前,我们先来看看往期的系列文章【有需要的朋友,欢迎关注系列专栏】:

Spring Boot 源码学习
Spring Boot 项目介绍
Spring Boot 核心运行原理介绍
【Spring Boot 源码学习】@EnableAutoConfiguration 注解
【Spring Boot 源码学习】@SpringBootApplication 注解
【Spring Boot 源码学习】走近 AutoConfigurationImportSelector
【Spring Boot 源码学习】自动装配流程源码解析(上)
【Spring Boot 源码学习】自动装配流程源码解析(下)
【Spring Boot 源码学习】深入 FilteringSpringBootCondition
【Spring Boot 源码学习】OnClassCondition 详解

主要内容

话不多说,马上进入正题,我们开始本篇的内容,重点详解 OnBeanCondition 的实现。

在这里插入图片描述

1. getOutcomes 方法

OnBeanCondition 同样也是 FilteringSpringBootCondition 的子类,我们依旧是从 getOutcomes 方法源码来分析【Spring Boot 2.7.9】:

@Order(Ordered.LOWEST_PRECEDENCE)
class OnBeanCondition extends FilteringSpringBootCondition implements ConfigurationCondition {// ...@Overrideprotected final ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses,AutoConfigurationMetadata autoConfigurationMetadata) {ConditionOutcome[] outcomes = new ConditionOutcome[autoConfigurationClasses.length];for (int i = 0; i < outcomes.length; i++) {String autoConfigurationClass = autoConfigurationClasses[i];if (autoConfigurationClass != null) {Set<String> onBeanTypes = autoConfigurationMetadata.getSet(autoConfigurationClass, "ConditionalOnBean");outcomes[i] = getOutcome(onBeanTypes, ConditionalOnBean.class);if (outcomes[i] == null) {Set<String> onSingleCandidateTypes = autoConfigurationMetadata.getSet(autoConfigurationClass,"ConditionalOnSingleCandidate");outcomes[i] = getOutcome(onSingleCandidateTypes, ConditionalOnSingleCandidate.class);}}}return outcomes;}// ...
}

上述 getOutcomes 方法中针对 自动配置数据的循环处理逻辑,大致可总结为如下两种:

  • 通过调用 AutoConfigurationMetadata 接口的 get(String className, String key) 方法来获取与autoConfigurationClass 关联的名为 "ConditionalOnBean" 的条件属性值,可能含多个,存入 Set 集合 onBeanTypes 变量中;接着调用 getOutcome(Set<String> requiredBeanTypes, Class<? extends Annotation> annotation) 方法来获取过滤匹配结果,并赋值给 outcomes[i]

    我们以 RedisCacheConfiguration 为例,可以看到如下配置:
    在这里插入图片描述

  • 如果上述过滤匹配结果 outcomes[i]null,则通过调用 AutoConfigurationMetadata 接口的 get(String className, String key) 方法来获取与autoConfigurationClass 关联的名为 "ConditionalOnSingleCandidate" 的条件属性值,可能含多个,存入 Set 集合 onSingleCandidateTypes 变量中;接着调用 getOutcome(Set<String> requiredBeanTypes, Class<? extends Annotation> annotation) 方法来获取过滤匹配结果,并赋值给 outcomes[i]

    我们以 MongoDatabaseFactoryConfiguration 为例,可以看到如下配置:
    在这里插入图片描述

有关 AutoConfigurationMetadata 接口的 get(String className, String key) 方法的逻辑,请查看 Huazie 的 上一篇博文【Spring Boot 源码学习】OnClassCondition 详解,这里不再赘述。

下面我们继续查看 getOutcome(Set<String> requiredBeanTypes, Class<? extends Annotation> annotation) 方法的逻辑:

private ConditionOutcome getOutcome(Set<String> requiredBeanTypes, Class<? extends Annotation> annotation) {List<String> missing = filter(requiredBeanTypes, ClassNameFilter.MISSING, getBeanClassLoader());if (!missing.isEmpty()) {ConditionMessage message = ConditionMessage.forCondition(annotation).didNotFind("required type", "required types").items(Style.QUOTE, missing);return ConditionOutcome.noMatch(message);}return null;
}

进入 getOutcome 方法,可以看到:

  • 首先调用父类 FilteringSpringBootCondition 中的 filter 方法,来获取给定的类集合 requiredBeanTypes 中加载失败的类集合 missing【即当前类加载器中不存在的类集合】;
  • 如果 missing 不为空,说明存在加载失败的类,则返回 不满足过滤匹配的结果【即 ConditionOutcome.noMatch,其中没有找到 missing 中需要的类型】;
  • 如果 missing 为空,直接返回 null 即可。

2. getMatchOutcome 方法

OnClassCondition 一样,OnBeanCondition 同样实现了 FilteringSpringBootCondition 的父类 SpringBootCondition 中的抽象方法 getMatchOutcome 方法。

有关 SpringBootCondition 的介绍,这里不赘述了,请查看笔者的 【Spring Boot 源码学习】OnClassCondition 详解。

通过查看 getMatchOutcome 方法源码,可以看到针对 ConditionalOnBean 注解、ConditionalOnSingleCandidate 注解 和 ConditionalOnMissingBean 注解的三块处理逻辑,下面来一一讲解:

@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {ConditionMessage matchMessage = ConditionMessage.empty();MergedAnnotations annotations = metadata.getAnnotations();// ConditionalOnBean 注解处理// ConditionalOnSingleCandidate 注解处理// ConditionalOnMissingBean 注解处理return ConditionOutcome.match(matchMessage);
}

2.1 ConditionalOnBean 注解处理

我们来看看 ConditionalOnBean 注解处理逻辑的源码:

	if (annotations.isPresent(ConditionalOnBean.class)) {Spec<ConditionalOnBean> spec = new Spec<>(context, metadata, annotations, ConditionalOnBean.class);MatchResult matchResult = getMatchingBeans(context, spec);if (!matchResult.isAllMatched()) {String reason = createOnBeanNoMatchReason(matchResult);return ConditionOutcome.noMatch(spec.message().because(reason));}matchMessage = spec.message(matchMessage).found("bean", "beans").items(Style.QUOTE, matchResult.getNamesOfAllMatches());}

针对上述代码,且听分析如下:

  • 首先调用 MergedAnnotations 接口的 isPresent(Class<A> annotationType) 方法判断指定的注解类型是直接存在或者元存在【这里相当于调用 get(annotationType).isPresent()】,如果返回 true,表示存在指定的注解类型。
  • 如果存在 @ConditionalOnBean,则
    • 创建一个条件规范 Spec 对象,该类是从底层的注解中提取的搜索规范;
    • 接着,调用 getMatchingBeans 方法,并从上下文【context】中获取与条件规范【spec】匹配的 Spring Beans 的结果【MatchResult】;
    • 然后,检查匹配结果,如果不是所有的条件都匹配,则继续如下:
      • 调用 createOnBeanNoMatchReason 方法,创建一个描述条件不匹配原因的字符串并返回;
      • 返回一个表示未匹配条件的 ConditionOutcome 对象【其中包含了条件规范的消息以及不匹配的原因】;
    • 否则,更新匹配消息,并记录 找到了所有匹配的 Spring Beans

2.2 ConditionalOnSingleCandidate 注解处理

我们继续查看 ConditionalOnSingleCandidate 注解处理逻辑的源码:

	if (metadata.isAnnotated(ConditionalOnSingleCandidate.class.getName())) {Spec<ConditionalOnSingleCandidate> spec = new SingleCandidateSpec(context, metadata, annotations);MatchResult matchResult = getMatchingBeans(context, spec);if (!matchResult.isAllMatched()) {return ConditionOutcome.noMatch(spec.message().didNotFind("any beans").atAll());}Set<String> allBeans = matchResult.getNamesOfAllMatches();if (allBeans.size() == 1) {matchMessage = spec.message(matchMessage).found("a single bean").items(Style.QUOTE, allBeans);}else {List<String> primaryBeans = getPrimaryBeans(context.getBeanFactory(), allBeans,spec.getStrategy() == SearchStrategy.ALL);if (primaryBeans.isEmpty()) {return ConditionOutcome.noMatch(spec.message().didNotFind("a primary bean from beans").items(Style.QUOTE, allBeans));}if (primaryBeans.size() > 1) {return ConditionOutcome.noMatch(spec.message().found("multiple primary beans").items(Style.QUOTE, primaryBeans));}matchMessage = spec.message(matchMessage).found("a single primary bean '" + primaryBeans.get(0) + "' from beans").items(Style.QUOTE, allBeans);}}

同样针对上述代码,跟着 Huazie 来一步步分析下:

  • 首先调用 AnnotatedTypeMetadata 接口的 isAnnotated(String annotationName) 方法判断元数据中是否存在指定注解。如果返回 true,表示元数据中存在指定注解。
  • 如果元数据中存在 @ConditionalOnSingleCandidate 注解,则
    • 创建了一个 SingleCandidateSpec 的对象 spec ,并传入上下文 【context】、元数据 【metadata】 和注解信息 【annotations】 ,该类是专门针对 @ConditionalOnSingleCandidate 注解的条件规范。
    • 接着调用 getMatchingBeans 方法对 context 中的所有 bean 进行匹配,并将与条件规范【spec】匹配的 Spring Beans 的结果存储在 matchResult 变量中;
    • 如果没有匹配的 bean,则返回表示未匹配条件的 ConditionOutcome 对象【其中记录了 没有找到任何 bean 的信息】;
    • 否则,获取匹配的所有 bean 名称并存储在 allBeans 变量中。
      • 如果仅有一个匹配的 bean,则更新匹配消息,并记录找到了 单个 bean 的信息;
      • 否则,获取首选 bean 名称列表,并检查列表是否为空;
        • 如果列表为空,则返回表示未匹配条件的 ConditionOutcome 对象【其中记录了 一个首选 bean 也没有找到 的信息】;
        • 如果首选 bean 名称列表包含多个 bean,则返回表示未匹配条件的 ConditionOutcome 对象【其中记录了 找到了多个首选 bean 的信息】;
        • 否则,更新匹配消息,并记录 找到了首选 bean 的信息。

2.3 ConditionalOnMissingBean 注解处理

我们继续查看 ConditionalOnMissingBean 注解处理逻辑的源码:

	if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) {Spec<ConditionalOnMissingBean> spec = new Spec<>(context, metadata, annotations,ConditionalOnMissingBean.class);MatchResult matchResult = getMatchingBeans(context, spec);if (matchResult.isAnyMatched()) {String reason = createOnMissingBeanNoMatchReason(matchResult);return ConditionOutcome.noMatch(spec.message().because(reason));}matchMessage = spec.message(matchMessage).didNotFind("any beans").atAll();}

经过上述两种处理逻辑的分析,相信大家应该可以看懂第三种处理逻辑的分析:

  • 首先调用 AnnotatedTypeMetadata 接口的 isAnnotated(String annotationName) 方法判断元数据中是否存在指定注解。如果返回 true,表示元数据中存在指定注解。
  • 如果存在 @ConditionalOnMissingBean 注解,则
    • 创建一个条件规范 Spec 对象,该类是从底层的注解中提取的搜索规范;
    • 接着,调用 getMatchingBeans 方法,并从上下文【context】中获取与条件规范【spec】匹配的 Spring Beans 的结果【MatchResult】;
    • 如果存在任何一个匹配的 bean,则
      • 调用 createOnMissingBeanNoMatchReason 方法,创建一个描述条件不匹配原因的字符串并返回;
      • 返回一个表示未匹配条件的 ConditionOutcome 对象【其中包含了条件规范的消息以及不匹配的原因】;
    • 否则,更新匹配消息,并记录 找不到指定类型的 bean 的信息。

3. getMatchingBeans 方法

上述三种注解处理逻辑中,我们都看到了调用 getMatchingBeans 方法,下面重点来讲解一下:

protected final MatchResult getMatchingBeans(ConditionContext context, Spec<?> spec) {// ...
}

我们可以看到 getMatchingBeans 方法,有两个参数,它们分别是 上下文 【context】和 条件规范【spec】;

继续看 getMatchingBeans 方法内部逻辑:

	ClassLoader classLoader = context.getClassLoader();ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();

这里从上下文【context】中获取 ClassLoaderConfigurableListableBeanFactory

知识拓展:

  • ClassLoaderJava 中的一个接口,用于加载类。它是 Java 类加载机制的核心部分,负责将 .class 文件转换为 Java 类实例。ClassLoader 可以从不同的来源(如文件系统、网络、数据库等)加载类,也可以实现自定义的类加载逻辑。
  • ConfigurableListableBeanFactorySpring 框架中的一个核心接口,它扩展了ListableBeanFactory 接口,提供了更多的配置和扩展功能。它是一个 bean 工厂的抽象概念,用于管理 Spring 容器中的 bean 对象。ConfigurableListableBeanFactory 提供了添加、移除、注册和查找 bean 的方法,以及设置和获取 bean 属性值的功能。它还支持bean 的后处理和事件传播。
	boolean considerHierarchy = spec.getStrategy() != SearchStrategy.CURRENT;

这里根据 Spec 对象的 SearchStrategy 属性来确定是否考虑 bean 的层次结构。如果SearchStrategyCURRENT【】,则不考虑层次结构【即 considerHierarchy 为 false】;否则,考虑层次结构【即 considerHierarchy 为 true】。

	Set<Class<?>> parameterizedContainers = spec.getParameterizedContainers();

这里获取 Spec 对象的 parameterizedContainers 属性,这是一个包含参数化容器类型的集合

	if (spec.getStrategy() == SearchStrategy.ANCESTORS) {BeanFactory parent = beanFactory.getParentBeanFactory();Assert.isInstanceOf(ConfigurableListableBeanFactory.class, parent,"Unable to use SearchStrategy.ANCESTORS");beanFactory = (ConfigurableListableBeanFactory) parent;}

如果 Spec 对象的 SearchStrategy 属性是 SearchStrategy.ANCESTORS,则调用 getParentBeanFactory 方法获取其父工厂,并将其转换为 ConfigurableListableBeanFactory 类型。

	MatchResult result = new MatchResult();

新建一个 MatchResult 对象,用于存储匹配结果;

	Set<String> beansIgnoredByType = getNamesOfBeansIgnoredByType(classLoader, beanFactory, considerHierarchy,spec.getIgnoredTypes(), parameterizedContainers);

调用 getNamesOfBeansIgnoredByType 方法,获取被忽略类型的 bean 名称集合 beansIgnoredByType

	for (String type : spec.getTypes()) {Collection<String> typeMatches = getBeanNamesForType(classLoader, considerHierarchy, beanFactory, type,parameterizedContainers);Iterator<String> iterator = typeMatches.iterator();while (iterator.hasNext()) {String match = iterator.next();if (beansIgnoredByType.contains(match) || ScopedProxyUtils.isScopedTarget(match)) {iterator.remove();}}if (typeMatches.isEmpty()) {result.recordUnmatchedType(type);}else {result.recordMatchedType(type, typeMatches);}}

遍历 Spec 对象的 types 属性,它是一个 Set<String> 集合

  • 首先,针对每个类型 type,调用 getBeanNamesForType 方法获取匹配的 bean 名称集合 typeMatches
  • 然后,使用迭代器遍历这个集合,如果集合中的某个元素在被忽略类型的集合中,就将其从迭代器中移除。
  • 最后,如果 typeMatches 集合为空,则记录未匹配的类型;否则,记录匹配的类型。
	for (String annotation : spec.getAnnotations()) {Set<String> annotationMatches = getBeanNamesForAnnotation(classLoader, beanFactory, annotation,considerHierarchy);annotationMatches.removeAll(beansIgnoredByType);if (annotationMatches.isEmpty()) {result.recordUnmatchedAnnotation(annotation);}else {result.recordMatchedAnnotation(annotation, annotationMatches);}}

遍历 Spec 对象的 annotations 属性:

  • 首先,针对每个注解 annotation,调用 getBeanNamesForAnnotation 方法获取匹配的 bean 名称集合 annotationMatches
  • 然后,从 annotationMatches 集合中移除被忽略类型的集合。
  • 最后,如果 annotationMatches 集合为空,则记录未匹配的注解;否则,记录匹配的注解。
	for (String beanName : spec.getNames()) {if (!beansIgnoredByType.contains(beanName) && containsBean(beanFactory, beanName, considerHierarchy)) {result.recordMatchedName(beanName);}else {result.recordUnmatchedName(beanName);}}

遍历 Spec 对象的 names 属性,对于每个 bean 名称,如果它不在被忽略类型的集合中,并且它在 bean 工厂中存在,就记录匹配的名称;否则,记录未匹配的名称。

总结

本篇 Huazie 带大家介绍了自动配置过滤匹配子类 OnBeanCondition ,内容较多,感谢大家的支持;笔者接下来的博文还将详解 OnWebApplicationCondition 的实现,敬请期待!!!

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

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

相关文章

实战演练 | Navicat 常用功能之转储与运行 SQL 文件

数据库管理工作中&#xff0c;"转储 SQL 文件"和"运行 SQL 文件"是两个极为常见操作。一般来说&#xff0c;用户使用数据库管理工具或命令行工具来完成。Navicat 管理开发工具中的“转储 SQL 文件”和“运行 SQL 文件”功能具有直观易用的界面、多种文件格…

Linux内核源码分析 (B.x)Linux物理内存的初始化

Linux内核源码分析 (B.x)Linux物理内存的初始化 文章目录 Linux内核源码分析 (B.x)Linux物理内存的初始化一、DDR简介二、内存节点三、内存管理区域ZONE四、 struct zone五、 struct page六、mem_map数组七、伙伴系统简介八、迁移类型九、内存初始化十、总结 一、DDR简介 详细可…

threejs给3d模型中的物体换肤(修改材质)

变成这样 this.otherModel.traverse(function (child) {if (child instanceof THREE.Mesh && child.name Cylinder240) {// 导入纹理const textureLoader new THREE.TextureLoader();const floorColortextureLoader.load(require(../../../public/img/color.jpg));co…

5个GitHub热门算法岗面试攻略,附资源下载

最近听行内的大佬们讨论&#xff0c;说今年的秋招情况依旧卷的激烈&#xff0c;不知道大家有没有都拿到满意的offer&#xff1f; 已经拿到的同学我先给赞个&#xff0c;还没有获得心仪offer的同学也不要着急&#xff0c;当下最该做的就是抓紧时间提升自己的硬实力&#xff0c;…

openssl创建CA证书教程

配置生成CA证书 总示意图&#xff1a; (1)&#xff0c;通过openssl创建CA证书 第一步&#xff1a;创建一个秘钥&#xff0c;这个便是CA证书的根本&#xff0c;之后所有的东西都来自这个秘钥 # 通过rsa算法生成2048位长度的秘钥 openssl genrsa -out myCA.key 2048 第二步&#…

小谈设计模式(5)—开放封闭原则

小谈设计模式&#xff08;5&#xff09;—开放封闭原则 专栏介绍专栏地址专栏介绍 开放封闭原则核心思想关键词概括扩展封闭 解释抽象和接口多态 代码示例代码解释 优缺点优点可扩展性可维护性可复用性高内聚低耦合 缺点抽象设计的复杂性需要预留扩展点可能引入过度设计 总结 专…

uniapp 使用subNVue原生子窗体显示弹框或悬浮框

效果展示 在uniapp中&#xff0c;我们可以使用subNVue原生子窗体来解决web-view等原生页面中弹框无法显示的问题。 subNVue原生子窗体是uniapp提供的一种原生组件&#xff0c;可以在uniapp中嵌入原生页面&#xff0c;并且可以与uniapp页面进行通信。我们可以在原生页面中使用…

前后端分离的低代码快速开发框架

低代码开发正逐渐成为企业创新的关键工具。通过提高开发效率、降低成本、增强灵活性以及满足不同用户需求&#xff0c;低代码开发使企业能够快速响应市场需求&#xff0c;提供创新解决方案。选择合适的低代码平台&#xff0c;小成本组建一个专属于你的应用。 项目简介 这是一个…

Vue.js的服务器端渲染(SSR):为什么和如何

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

开户期权必看!期权有什么好的平台,优势在哪里?

有50W满足上述期权开户条件的可以选择在证券或者期货券商端口开通,不过都只能线下去柜台营业部开通。不满足可以选择第三方期权分仓开户,下文介绍开户期权必看&#xff01;期权有什么好的平台&#xff0c;优势在哪里&#xff1f;本文来自&#xff1a;期权酱 一、开户期权基础知…

js中如何实现一个简单的防抖函数?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 防抖函数⭐ 使用示例⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带你启航前端之旅 欢迎来到前端入门之旅&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏…

Linux——(第十一章)软件包管理

目录 一、RPM 1.概述 2.RPM查询指令 3.RPM卸载指令 4.RPM安装命令 二、YUM 1.概述 2.YUM常用命令 一、RPM 1.概述 RPM&#xff08;RedHat Package Manager&#xff09;&#xff0c;RedHat软件包管理工具&#xff0c;类似windows里面的setup.exe是Linux这系列操作系统里…

Hadoop初识及信息安全(大数据的分布式存储和计算平台)

目录 什么是Hadoop Hadoop的特点 Hadoop优点 Hadoop的缺点 Hadoop的重要组成 信息安全 什么是Hadoop Hadoop 是一个适合大数据的分布式存储和计算平台。 Hadoop的广义和狭义区分&#xff1a; 狭义的Hadoop:指的是一个框架&#xff0c;Hadoop是由三部分组成&#xff1a;H…

机器视觉-标定篇

3D结构光标定 结构光视觉的优点&#xff1a; 非接触、信息量大、测精度高、抗干扰能力强。 结构光视觉传感器参数的标定包括&#xff1a;摄像机参数标定、结构光平面参数标定。 结构光视觉测量原理图 我们不考虑镜头的畸变&#xff0c;将相机的成像模型简化为小孔成像模型…

ClickHouse面向列的数据库管理系统(原理简略理解)

目录 官网 什么是Clickhouse 什么是OLAP 面向列的数据库与面向行的数据库 特点 为什么面向列的数据库在OLAP场景中工作得更好 为什么ClickHouse这么快 真实的处理分析查询 OLAP场景的关键属性 引擎作用 ClickHouse引擎 输入/输出 CPU 官网 https://clickhouse.com…

Flink-CDC 抽取SQLServer问题总结

Flink-CDC 抽取SQLServer问题总结 背景 flink-cdc 抽取数据到kafka 中&#xff0c;使用flink-sql进行开发&#xff0c;相关问题总结flink-cdc 配置SQLServer cdc参数 1.创建CDC 使用的角色, 并授权给其查询待采集数据数据库 -- a.创建角色 create role flink_role;-- b.授权…

Direct3D融合技术

该技术能使我们将当前要进行光栅化的像素的颜色与先前已已光栅化并处于同一位置的像素的颜色进行合成&#xff0c;即将正在处理的图元颜色值与存储在后台缓存中的像素颜色值进行合成(混合)&#xff0c;利用该技术我们可得到各种各样的效果&#xff0c;尤其是透明效果。 在融合…

华为云云耀云服务器L实例评测|云耀云服务器L实例的购买及使用体验

华为云云耀云服务器L实例评测&#xff5c;云耀云服务器L实例的购买及使用体验 一、云耀云服务器L实例介绍1.1 云耀云服务器L实例简介1.2 云耀云服务器L实例特点1.3 云耀云服务器L实例使用场景 二、云耀云服务器L实例支持的镜像2.1 镜像类型2.2 系统镜像2.3 应用镜像 三、购买云…

docker学习:dockerfile和docker-compose

学习如何使用dockerfile 以下内容&#xff0c;部分来自gpt生成&#xff0c;里面的描述可能会出现问题&#xff0c;但代码部分&#xff0c;我都会进行测试。 1. 需求 对于一个docker&#xff0c;例如python&#xff0c;我们需要其在构建成容器时&#xff0c;就有np。有以下两种方…

Qt重写QTreeWidget实现拖拽

介绍 此文章记录QTreeWidget的重写进度&#xff0c;暂时停滞使用&#xff0c;重写了QTreeWidget的拖拽功能&#xff0c;和绘制功能&#xff0c;自定义了数据结构&#xff0c;增加复制&#xff0c;粘贴&#xff0c;删除&#xff0c;准备实现动态刷新数据支持千万数据动态刷新&a…