当前位置: 首页 > news >正文

第七节 ConfigurationClassParser 源码分析

tips: ConfigurationClassParser 是 Springframework 中的重要类。

本章主要是源码理解,有难度和深度,也枯燥乏味,可以根据实际情况选择阅读。

位置:org.springframework.context.annotation.ConfigurationClassParser

ConfigurationClassParser 它是解密 configuration 的关键。理解 ConfigurationClassParser 对理解整个 Spring 框架至关重要。

一、作用是什么

ConfigurationClassParser是一个非常重要的类,它主要用于解析带有@Configuration注解的类。

@Configuration注解表明该类用作配置类,其中可以定义bean和Spring容器应如何初始化和管理这些bean。

ConfigurationClassParser的作用可以从以下几个方面详细阐述:

  1. 解析导入的配置@Import注解允许一个配置类导入另一个配置类。ConfigurationClassParser解析这些@Import注解,确保所有导入的配置也被处理和应用。
  2. 处理属性注入:通过@PropertySource注解,可以指定一些属性文件,这些属性文件中的属性可以被注入到Spring管理的bean中。ConfigurationClassParser负责解析这些注解,并确保属性文件被加载且其值可用于注入。
  3. 处理@Conditional注解: Spring框架允许在bean的注册过程中使用条件逻辑,@Conditional注解及其派生注解(例如@ConditionalOnClass@ConditionalOnProperty等)使得只有在满足特定条件时,才会进行bean的注册。ConfigurationClassParser负责解析这些条件注解并应用其逻辑。
  4. processDeferredImportSelectors#processImports 处理扩展配置( Starter 能够被处理的核心分支)

二、触发时机

SpringBoot 应用启动过程中,通过后置处理器去触发 ConfigurationClassPostProcessor。 然后再调用 ConfigurationClassParser类解析

public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor,PriorityOrdered, ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware {....
}

处理如下:

下面我们将详细分析源码流程。

三、ConfigurationClassPostProcessor

下面是 ConfigurationClassPostProcessor 部分核心代码。

入口方法 processConfigBeanDefinitions(BeanDefinitionRegistry registry) 。开始分析这段代码。

注意这里有一个 do...while

	do {.....}while (!candidates.isEmpty());

它将逐一识别和解析配置类,然后将配置类中定义的Bean注册到Spring容器中。这个过程通过不断循环直到没有新的配置类候选者出现为止,确保了所有相关的配置都被完整地处理。

// 创建一个配置类解析器,用于解析和处理配置类信息
ConfigurationClassParser parser = new ConfigurationClassParser(this.metadataReaderFactory, this.problemReporter, this.environment,this.resourceLoader, this.componentScanBeanNameGenerator, registry);// 初始化一个集合,用于存储待处理的配置类候选者
Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
// 初始化一个集合,用于跟踪已经解析过的配置类,以避免重复解析
Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());// 循环处理,直到没有新的配置类候选者
do {// 解析当前候选者中的配置类parser.parse(candidates);// 对解析结果进行验证parser.validate();// 从解析器中获取已解析的配置类集合Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());// 移除已经处理过的,避免重复处理configClasses.removeAll(alreadyParsed);// 如果读取器未初始化,创建一个配置类Bean定义读取器if (this.reader == null) {this.reader = new ConfigurationClassBeanDefinitionReader(registry, this.sourceExtractor, this.resourceLoader, this.environment,this.importBeanNameGenerator, parser.getImportRegistry());}// 加载并注册配置类中定义的Beanthis.reader.loadBeanDefinitions(configClasses);// 将这批配置类标记为“已解析”alreadyParsed.addAll(configClasses);// 清空候选者集合,为下一轮寻找新候选者做准备candidates.clear();// 检查是否有新的Bean定义被注册(可能由@Configuration类引入)if (registry.getBeanDefinitionCount() > candidateNames.length) {// 重新获取所有Bean定义的名称String[] newCandidateNames = registry.getBeanDefinitionNames();// 创建一个旧候选名称的集合,用于辨识新的候选者Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));// 创建一个集合,用于跟踪已经解析的配置类的类名Set<String> alreadyParsedClasses = new HashSet<>();for (ConfigurationClass configurationClass : alreadyParsed) {alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());}// 遍历新的Bean定义名称,寻找新的配置类候选者for (String candidateName : newCandidateNames) {if (!oldCandidateNames.contains(candidateName)) {BeanDefinition bd = registry.getBeanDefinition(candidateName);if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&!alreadyParsedClasses.contains(bd.getBeanClassName())) {candidates.add(new BeanDefinitionHolder(bd, candidateName));}}}// 更新候选名称列表,以反映新的Bean定义candidateNames = newCandidateNames;}
// 如果还有未处理的候选者,继续循环
} while (!candidates.isEmpty());

特别说明,对于 Bean 的加载和实例化不在本范围了,不进行讲解。感兴趣可以阅读相关章节。

上面的这段代码是 ConfigurationClassPostProcessor 核心。解析来的重头戏。ConfigurationClassParser

四、ConfigurationClassParser

从这行代码开始入手parser.parse(candidates);

  1. parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
  2. processDeferredImportSelectors(); // 处理前面推迟的ImportSelector,一些二方包的导入类,将在这个方法中实现。 例如,我们配置在 Starter Spring.factories 中的自动导入类,将在这一环境被加载
parse方法入口
protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {// 如果这个配置类应该根据条件注解被跳过,则直接返回不进行处理if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {return;}// 尝试从已处理的配置类映射中获取这个配置类ConfigurationClass existingClass = this.configurationClasses.get(configClass);// 如果找到了已存在的配置类if (existingClass != null) {// 如果当前处理的是一个导入的配置类if (configClass.isImported()) {// 如果已存在的配置类也是导入的,则合并导入来源if (existingClass.isImported()) {existingClass.mergeImportedBy(configClass);}// 如果已存在的配置类不是导入的,则忽略当前导入的配置类,保留现有的非导入类return;}else {// 如果找到显式的bean定义,可能是意在替换一个导入的类。// 移除旧的配置类,采用新的配置类。this.configurationClasses.remove(configClass);this.knownSuperclasses.values().removeIf(configClass::equals);}}// 递归处理配置类及其超类层次结构SourceClass sourceClass = asSourceClass(configClass);do {// 处理当前配置类并更新sourceClass为配置类的超类,准备下一轮处理sourceClass = doProcessConfigurationClass(configClass, sourceClass);} while (sourceClass != null); // 如果sourceClass为null,表示超类已经处理完毕// 将处理完的配置类放入配置类映射中,标记为已处理this.configurationClasses.put(configClass, configClass);
}

上面可以理解,解析 MyApplication 类所在工程中的类。最终的解析由 doProcessConfigurationClass 实现

processDeferredImportSelectors

负责处理那些被延迟的特殊接口,使用它来按需动态地导入配置。

这些常常依赖于某些条件才被执行,所以被延迟处理。

// 定义处理延迟的ImportSelector的方法
private void processDeferredImportSelectors() {// 获取之前收集的所有延迟处理的ImportSelectorList<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;// 将引用置为null,表示开始处理过程,防止重复处理this.deferredImportSelectors = null;// 如果没有需要处理的延迟ImportSelector,则直接返回if (deferredImports == null) {return;}...... // 遍历所有的延迟ImportSelectorfor (DeferredImportSelectorHolder deferredImport : deferredImports) {// 获取与当前ImportSelector相关联的配置类ConfigurationClass configClass = deferredImport.getConfigurationClass();try {// 调用ImportSelector的selectImports方法,获取所有的导入类名String[] imports = deferredImport.getImportSelector().selectImports(configClass.getMetadata());// 处理这些导入的类,将它们作为配置类进行进一步的处理processImports(configClass, asSourceClass(configClass), asSourceClasses(imports), false);}}
}

这个方法的主要作用将是找出符合条件的 imports 类。最终还是由processImports() 处理。

到这里,spring.factories 符合条件的一些类将被加载。核心代码deferredImport.getImportSelector().selectImports(configClass.getMetadata());

到这里我们基本上了解了 Starter 是如何被引入进来的。

真正解析的方法 doProcessConfigurationClass

doProcessConfigurationClass

它递归地处理嵌套类、处理@PropertySource注解、处理@ComponentScan注解、处理@Import注解、处理@ImportResource注解、处理@Bean方法、处理接口上的默认方法,最后处理父类

  1. 处理成员类:方法首先递归地处理配置类中定义的任何成员类(嵌套类)。
  2. 处理@PropertySource注解:然后遍历配置类上的所有@PropertySource注解,这些注解用来指明属性文件的位置。如果当前的环境实现了ConfigurableEnvironment接口,则处理注解指定的属性源
  3. 处理@ComponentScan注解:接下来,处理配置类上的所有@ComponentScan注解,这些注解指示Spring扫描特定包下的组件(即带有@Component@Service等注解的类),并注册为Spring容器中的Bean。如果有条件注解指示在此阶段跳过处理,则不执行扫描。
  4. 处理@Import注解:处理配置类上的@Import注解,这些注解用来导入其他配置类或配置选择器,允许模块化地组织配置。
  5. 处理@ImportResource注解:处理配置类上的@ImportResource注解,这些注解用于导入XML配置文件。
  6. 处理@Bean方法:收集配置类中所有带有@Bean注解的方法的元数据,并将它们添加到配置类对象中。这些方法定义了应该由Spring容器管理的Bean。
  7. 处理接口上的默认方法:如果配置类实现了接口,并在这些接口上定义了默认方法,这些方法也会被处理。
  8. 处理父类:最后,如果配置类有超类,那么这个方法会检查超类是否也是一个配置类,不是Java内置类,并且还没有被处理过。如果满足条件,则递归地处理这个超类。

protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)throws IOException {// 首先,递归处理任何成员类(嵌套类)processMemberClasses(configClass, sourceClass);// 处理所有的@PropertySource注解for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(), PropertySources.class,org.springframework.context.annotation.PropertySource.class)) {// 如果环境实现了ConfigurableEnvironment接口if (this.environment instanceof ConfigurableEnvironment) {// 处理@PropertySource注解processPropertySource(propertySource);} else {// 如果环境没有实现ConfigurableEnvironment接口logger.warn("忽略了[" + sourceClass.getMetadata().getClassName() +"]上的@PropertySource注解。原因:环境必须实现ConfigurableEnvironment接口");}}// 处理所有的@ComponentScan注解Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);// 如果存在@ComponentScan注解,并且当前阶段不应该跳过if (!componentScans.isEmpty() &&!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {// 循环处理每个@ComponentScan注解for (AnnotationAttributes componentScan : componentScans) {// 配置类上存在@ComponentScan注解 -> 立即执行扫描Set<BeanDefinitionHolder> scannedBeanDefinitions =this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());// 检查扫描结果中的定义集合,如果有进一步的配置类,递归解析for (BeanDefinitionHolder holder : scannedBeanDefinitions) {if (ConfigurationClassUtils.checkConfigurationClassCandidate(holder.getBeanDefinition(), this.metadataReaderFactory)) {parse(holder.getBeanDefinition().getBeanClassName(), holder.getBeanName());}}}}// 处理所有的@Import注解processImports(configClass, sourceClass, getImports(sourceClass), true);// 处理所有的@ImportResource注解AnnotationAttributes importResource =AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);// 如果@ImportResource注解存在if (importResource != null) {// 获取资源位置String[] resources = importResource.getStringArray("locations");// 获取资源的阅读器类Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");// 循环处理每个资源for (String resource : resources) {// 解析资源位置中的占位符String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);// 把解析后的资源添加到配置类configClass.addImportedResource(resolvedResource, readerClass);}}// 处理单独的@Bean方法Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);// 循环处理每个@Bean方法for (MethodMetadata methodMetadata : beanMethods) {// 添加@Bean方法到配置类configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));}// 处理接口上的默认方法processInterfaces(configClass, sourceClass);// 处理父类,如果存在的话if (sourceClass.getMetadata().hasSuperClass()) {// 获取父类名称String superclass = sourceClass.getMetadata().getSuperClassName();// 如果父类存在,并且父类不是java.*开头,并且尚未处理过if (superclass != null && !superclass.startsWith("java") &&!this.knownSuperclasses.containsKey(superclass)) {// 记录已知的父类this.knownSuperclasses.put(superclass, configClass);// 找到父类,返回其注解元数据并递归处理return sourceClass.getSuperClass();}}// 没有父类 -> 处理完成return null;
}

到这里,大体流程我们已经清楚。不再继续针对注解的解析进行讲解,感兴趣可以自行下载源码阅读理解。

五、本章小结

本章是对整 ConfigurationClassParser 进行讲解,它是 Spring framework 中的最核心类。

到这里,Starter 的整个过程已经分析完成,但是针对条件装配,我们将在下一章进行讲解。

 已同步发布到公众号:面汤放盐 第七节 ConfigurationClassParser 源码分析 (qq.com)

掘金账号:第七节 ConfigurationClassParser 源码分析 - 掘金 (juejin.cn)

相关文章:

第七节 ConfigurationClassParser 源码分析

tips&#xff1a; ConfigurationClassParser 是 Springframework 中的重要类。 本章主要是源码理解&#xff0c;有难度和深度&#xff0c;也枯燥乏味&#xff0c;可以根据实际情况选择阅读。 位置&#xff1a;org.springframework.context.annotation.ConfigurationClassPars…...

零基础代码随想录【Day42】|| 1049. 最后一块石头的重量 II,494. 目标和,474.一和零

目录 DAY42 1049.最后一块石头的重量II 解题思路&代码 494.目标和 解题思路&代码 474.一和零 解题思路&代码 DAY42 1049.最后一块石头的重量II 力扣题目链接(opens new window) 题目难度&#xff1a;中等 有一堆石头&#xff0c;每块石头的重量都是正整…...

2024-5-24 石群电路-15

2024-5-24&#xff0c;星期五&#xff0c;22:15&#xff0c;天气&#xff1a;晴&#xff0c;心情&#xff1a;晴。今天最后一天上班&#xff0c;终于要放返校假啦&#xff0c;开心&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;不过放假也不能耽误…...

功能测试:核心原理、挑战以及解决之道

在软件开发生命周期中&#xff0c;功能测试占据了至关重要的位置。它是确保软件应用按照既定的要求和规格运行的关键测试阶段。功能测试的目的在于验证软件的功能、行为和用户界面等是否达到了业务需求的标准。本文将深入探讨功能测试的概念&#xff0c;执行过程中可能遇到的挑…...

跨境电商赛道,云手机到底能不能化繁为简?

当下国内电商背景&#xff1a; 从零售额的数据来看&#xff1a;随着互联网的普及和消费者购物习惯的改变&#xff0c;国内电商市场规模持续扩大。据相关数据显示&#xff0c;网络消费亮点纷呈&#xff0c;一季度全国网上零售额达到了3.3万亿元&#xff0c;同比增长12.4%。这表…...

linux:信号深入理解

文章目录 1.信号的概念1.1基本概念1.2信号的处理基本概念1.3信号的发送与保存基本概念 2.信号的产生2.1信号产生的五种方式2.2信号遗留问题(core,temp等) 3.信号的保存3.1 信号阻塞3.2 信号特有类型 sigset_t3.3 信号集操作函数3.4 信号集操作函数的使用 4.信号的处理4.1 信号的…...

Android系统的/etc/mkshrc文件

/etc/mkshrc 文件是用于配置 mksh&#xff08;MirBSD Korn Shell&#xff09;环境的启动脚本。mksh 是 Android 默认使用的 shell&#xff0c;在 shell 启动时会读取并执行这个文件中的配置。以下是关于 /etc/mkshrc 文件的详细信息及其用途。 /etc/mkshrc 文件的作用 环境配…...

LeetCode199二叉树的右视图

题目描述 给定一个二叉树的 根节点 root&#xff0c;想象自己站在它的右侧&#xff0c;按照从顶部到底部的顺序&#xff0c;返回从右侧所能看到的节点值。 解析 这一题的关键其实就是找到怎么去得到当前是哪一层级&#xff0c;可以利用队列对二叉树进行层次遍历&#xff0c;但…...

JavaScript 基础

一 JavaScript 的书写形式 1.1 行内式 <input type"button" value"点我一下" onclick"alert(hello akai);" > 注意,JS 中的字符串常量可以用单引号表示,也可以使用双引号表示.HTML 中推荐使用双引号,JS 中推荐使用单引号(使用双引号容易…...

DOS学习-目录与文件应用操作经典案例-type

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一.前言 二.使用 三.案例 1. 查看文本文件内容 2. 同时查看多个文本文件内容 3. 合并文…...

QT教程-一,初识QT

目录 一,QT是什么&#xff1f;能够使用它做什么&#xff1f; 二&#xff0c;Qt 能够使用的语言 三&#xff0c;Qt主要用于什么领域&#xff1f; 四&#xff0c;Qt开发的软件 一,QT是什么&#xff1f;能够使用它做什么&#xff1f; Qt是一个跨平台的 C 开发库&#xff0c;主…...

SpringBoot搭建Eureka注册中心

系列文章目录 文章目录 系列文章目录前言前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男女通用,看懂了就去分享给你的码吧。 1、Spring-Cloud Euraka介绍 Spring-Cloud Euraka是Spring Cloud集合中一…...

day 38 435.无重叠区间 763.划分字母区间 56. 合并区间 738.单调递增的数字 968.监控二叉树

435.无重叠区间 思路 为了使区间尽可能的重叠所以排序来使区间尽量的重叠&#xff0c;使用左边界排序来统计重叠区间的个数与452. 用最少数量的箭引爆气球恰好相反。 代码 class Solution {public int eraseOverlapIntervals(int[][] intervals) {Arrays.sort(intervals,(a,…...

ssm/springoot养老院问诊服务预约系统_96316老年人服务系统

2.管理员&#xff1a; &#xff08;1&#xff09;登入注册页面&#xff1a;管理员进行操作时需要是已注册登入的 &#xff08;2&#xff09;权限管理&#xff1a;管理员登入后可以运用权限进行相应的操作管理。 &#xff08;3&#xff09;用户管理&#xff1a;对用户进行删除、…...

WordPress插件优化对提升性能有多大影响?

WordPress插件优化对提升性能的影响可以是非常显著的。插件是WordPress平台的一个重要组成部分&#xff0c;它们可以增强网站的功能和定制性。然而&#xff0c;如果插件没有经过优化&#xff0c;它们可能会成为网站性能的瓶颈。 通过优化插件&#xff0c;可以减少对服务器资源…...

Servlet的response对象

目录 HTTP响应报文协议 reponse继承体系 reponse的方法 响应行 public void setStatus(int sc) 响应头 public void setHeader(String name, String value) 响应体 public java.io.PrintWriter getWriter() public ServletOutputStream getOutputStream() 请求重定…...

Unity射击游戏开发教程:(20)增加护盾强度

在本文中,我们将增强护盾,使其在受到超过 1 次攻击后才会被禁用。 Player 脚本具有 Shield PowerUp 方法,我们需要调整盾牌在被摧毁之前可以承受的数量,因此我们将声明一个 int 变量来设置盾牌可以承受的击中数量。...

初识C语言——第二十八天

代码练习1&#xff1a; 用函数的方式实现9*9乘法表 void print_table(int n) {int i 0;int j 0;for (i 1; i< n; i){for (j 1; j< i; j){printf("%d*%d%-3d ", i, j, i * j);}printf("\n");}}int main() {int n 0;scanf("%d", &a…...

Android NDK系列(三)输入事件分发到Native层的流程

在Android NDK系列(一)手动搭建Native Project 创建的Native工程中,是可以接收输入事件的,只需在android_main中注册输入事件的处理函数,当触摸屏幕后,handleInputEvent函数便会调用,代码如下。 static int32_t handleInputEvent(struct android_app* app, AInputEvent…...

Kafka之【生产消息】

消息&#xff08;Record&#xff09; 在kafka中传递的数据我们称之为消息&#xff08;message&#xff09;或记录(record)&#xff0c;所以Kafka发送数据前&#xff0c;需要将待发送的数据封装为指定的数据模型&#xff1a; 相关属性必须在构建数据模型时指定&#xff0c;其中…...

HTML 语义化

目录 HTML 语义化HTML5 新特性HTML 语义化的好处语义化标签的使用场景最佳实践 HTML 语义化 HTML5 新特性 标准答案&#xff1a; 语义化标签&#xff1a; <header>&#xff1a;页头<nav>&#xff1a;导航<main>&#xff1a;主要内容<article>&#x…...

Java 语言特性(面试系列2)

一、SQL 基础 1. 复杂查询 &#xff08;1&#xff09;连接查询&#xff08;JOIN&#xff09; 内连接&#xff08;INNER JOIN&#xff09;&#xff1a;返回两表匹配的记录。 SELECT e.name, d.dept_name FROM employees e INNER JOIN departments d ON e.dept_id d.dept_id; 左…...

【网络安全产品大调研系列】2. 体验漏洞扫描

前言 2023 年漏洞扫描服务市场规模预计为 3.06&#xff08;十亿美元&#xff09;。漏洞扫描服务市场行业预计将从 2024 年的 3.48&#xff08;十亿美元&#xff09;增长到 2032 年的 9.54&#xff08;十亿美元&#xff09;。预测期内漏洞扫描服务市场 CAGR&#xff08;增长率&…...

【论文阅读28】-CNN-BiLSTM-Attention-(2024)

本文把滑坡位移序列拆开、筛优质因子&#xff0c;再用 CNN-BiLSTM-Attention 来动态预测每个子序列&#xff0c;最后重构出总位移&#xff0c;预测效果超越传统模型。 文章目录 1 引言2 方法2.1 位移时间序列加性模型2.2 变分模态分解 (VMD) 具体步骤2.3.1 样本熵&#xff08;S…...

学习STC51单片机32(芯片为STC89C52RCRC)OLED显示屏2

每日一言 今天的每一份坚持&#xff0c;都是在为未来积攒底气。 案例&#xff1a;OLED显示一个A 这边观察到一个点&#xff0c;怎么雪花了就是都是乱七八糟的占满了屏幕。。 解释 &#xff1a; 如果代码里信号切换太快&#xff08;比如 SDA 刚变&#xff0c;SCL 立刻变&#…...

【分享】推荐一些办公小工具

1、PDF 在线转换 https://smallpdf.com/cn/pdf-tools 推荐理由&#xff1a;大部分的转换软件需要收费&#xff0c;要么功能不齐全&#xff0c;而开会员又用不了几次浪费钱&#xff0c;借用别人的又不安全。 这个网站它不需要登录或下载安装。而且提供的免费功能就能满足日常…...

Python Einops库:深度学习中的张量操作革命

Einops&#xff08;爱因斯坦操作库&#xff09;就像给张量操作戴上了一副"语义眼镜"——让你用人类能理解的方式告诉计算机如何操作多维数组。这个基于爱因斯坦求和约定的库&#xff0c;用类似自然语言的表达式替代了晦涩的API调用&#xff0c;彻底改变了深度学习工程…...

[大语言模型]在个人电脑上部署ollama 并进行管理,最后配置AI程序开发助手.

ollama官网: 下载 https://ollama.com/ 安装 查看可以使用的模型 https://ollama.com/search 例如 https://ollama.com/library/deepseek-r1/tags # deepseek-r1:7bollama pull deepseek-r1:7b改token数量为409622 16384 ollama命令说明 ollama serve #&#xff1a…...

Axure 下拉框联动

实现选省、选完省之后选对应省份下的市区...

如何通过git命令查看项目连接的仓库地址?

要通过 Git 命令查看项目连接的仓库地址&#xff0c;您可以使用以下几种方法&#xff1a; 1. 查看所有远程仓库地址 使用 git remote -v 命令&#xff0c;它会显示项目中配置的所有远程仓库及其对应的 URL&#xff1a; git remote -v输出示例&#xff1a; origin https://…...