@Import注解源码解析

news/2024/4/19 19:18:17/

文章目录

  • 一、简介
  • 二、@Import注解的几种用法
  • 三、@Import注解源码解析
    • 1、ConfigurationClassPostProcessor
    • 2、ConfigurationClassPostProcessor 类的 postProcessBeanDefinitionRegistry 方法
    • 3、ConfigurationClassParser 类的 parse 方法
    • 4、处理 ImportBeanDefinitionRegistrar 接口实现类
    • 5、处理 ImportSelector 接口实现类
    • 6、处理 DeferredImportSelector 接口实现类

一、简介

本文将对 Spring 如何处理 @Import 注解的源码进行解析

二、@Import注解的几种用法

在@Import注解的参数里可以填写一个类的数组,而源码上已经告诉我们常用的几种导入类型

image-20230703111310107

  • 导入的类是一个标注了 @Configuration 的配置类:Spring容器实例化这个配置类
  • 导入的类是 ImportSelector 接口的实现类:如果导入的类是 ImportSelector 接口的实现类,实例化这个类之后,会执行其 selectImports 方法,如果实现的是 ImportSelector 接口的子类 DeferredImportSelector,会执行其 selectImports 方法,但是时机会延后,这个后面具体讲
  • 导入的类是 ImportBeanDefinitionRegistrar 接口的实现类:如果导入的类是 ImportBeanDefinitionRegistrar 接口的实现类,实例化这个类之后,会执行其 registerBeanDefinitions 方法

三、@Import注解源码解析

1、ConfigurationClassPostProcessor

下面通过源码来看 Spring 是怎么处理 @Import 注解的,关于@Import注解的处理过程,在 ConfigurationClassPostProcessor 这个类里,这个类是用来处理 @Configuration 注解相关逻辑的,它实现了 BeanDefinitionRegistryPostProcessor 接口,而 BeanDefinitionRegistryPostProcessor 接口又是 BeanFactoryPostProcessor 的子接口,熟悉 Spring 源码的同学会比较熟悉这两个类,这是Spring的后置处理器

  • BeanFactoryPostProcessor:用于修改Bean定义(BeanDefinition
  • BeanDefinitionRegistryPostProcessor:用来注册 BeanDefinition 到Spring容器里

具体这里就不展开了,想了解的可以去看看 Spring 的核心方法 refresh 方法里的 invokeBeanFactoryPostProcessors(beanFactory);,这里只需要知道,在 Spring 容器启动过程中,会调用 ConfigurationClassPostProcessor 类的 postProcessBeanDefinitionRegistry 方法,下面我们从这个方法的入口开始看

2、ConfigurationClassPostProcessor 类的 postProcessBeanDefinitionRegistry 方法

image-20230703135037237

前面那一段,是一个防重复处理的逻辑,不用关心,重点是 processConfigBeanDefinitions 方法,跟进去看下

//ConfigurationClassPostProcessor
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {//1、找到所有的配置类List<BeanDefinitionHolder> configCandidates = new ArrayList<>();String[] candidateNames = registry.getBeanDefinitionNames();for (String beanName : candidateNames) {BeanDefinition beanDef = registry.getBeanDefinition(beanName);if (ConfigurationClassUtils.isFullConfigurationClass(beanDef) ||ConfigurationClassUtils.isLiteConfigurationClass(beanDef)) {if (logger.isDebugEnabled()) {logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);}}else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));}}// Return immediately if no @Configuration classes were found//2、如果没找到配置类就直接返回if (configCandidates.isEmpty()) {return;}// Sort by previously determined @Order value, if applicable//3、为找到的配置类排序configCandidates.sort((bd1, bd2) -> {int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());return Integer.compare(i1, i2);});// Detect any custom bean name generation strategy supplied through the enclosing application context//4、如果registry是SingletonBeanRegistry类型,且包含这个CONFIGURATION_BEAN_NAME_GENERATOR实例,就设置两个属性SingletonBeanRegistry sbr = null;if (registry instanceof SingletonBeanRegistry) {sbr = (SingletonBeanRegistry) registry;if (!this.localBeanNameGeneratorSet) {BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(CONFIGURATION_BEAN_NAME_GENERATOR);if (generator != null) {this.componentScanBeanNameGenerator = generator;this.importBeanNameGenerator = generator;}}}if (this.environment == null) {this.environment = new StandardEnvironment();}// Parse each @Configuration class//5、实例化ConfigurationClassParser对象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 {//6、处理配置类parser.parse(candidates);//7、验证配置类的合法性和正确性parser.validate();Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());configClasses.removeAll(alreadyParsed);// Read the model and create bean definitions based on its contentif (this.reader == null) {this.reader = new ConfigurationClassBeanDefinitionReader(registry, this.sourceExtractor, this.resourceLoader, this.environment,this.importBeanNameGenerator, parser.getImportRegistry());}//加载Bean定义到Spring容器里this.reader.loadBeanDefinitions(configClasses);alreadyParsed.addAll(configClasses);candidates.clear();if (registry.getBeanDefinitionCount() > candidateNames.length) {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());}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));}}}candidateNames = newCandidateNames;}}while (!candidates.isEmpty());// Register the ImportRegistry as a bean in order to support ImportAware @Configuration classesif (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());}if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {// Clear cache in externally provided MetadataReaderFactory; this is a no-op// for a shared cache since it'll be cleared by the ApplicationContext.((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();}
}

这个方法可以简单的概括成以下几个步骤:

  1. 找到所有的配置类;
  2. 循环解析每个配置类;
  3. 把配置类加载到 Spring 容器里;

最重要的就是解析配置类,ConfigurationClassParser 类的 parse 方法,我们跟进去看下

3、ConfigurationClassParser 类的 parse 方法

//ConfigurationClassParser
public void parse(Set<BeanDefinitionHolder> configCandidates) {for (BeanDefinitionHolder holder : configCandidates) {BeanDefinition bd = holder.getBeanDefinition();try {if (bd instanceof AnnotatedBeanDefinition) {parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());}else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());}else {parse(bd.getBeanClassName(), holder.getBeanName());}}catch (BeanDefinitionStoreException ex) {throw ex;}catch (Throwable ex) {throw new BeanDefinitionStoreException("Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);}}this.deferredImportSelectorHandler.process();
}

不论是哪个 if 条件,最终都会走到 ConfigurationClassParser 类的 processConfigurationClass 方法里,而且这些方法会将 DeferredImportSelector 接口的实现类放到 deferredImportSelectorHandler 里,在这个方法最后,调用这些类

image-20230703151637688

这里先跳过,我们先看 ConfigurationClassParser 类的 processConfigurationClass 方法

//ConfigurationClassParser
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);}// Otherwise ignore new imported config class; existing non-imported class overrides it.return;}else {// Explicit bean definition found, probably replacing an import.// Let's remove the old one and go with the new one.this.configurationClasses.remove(configClass);this.knownSuperclasses.values().removeIf(configClass::equals);}}// Recursively process the configuration class and its superclass hierarchy.SourceClass sourceClass = asSourceClass(configClass);do {sourceClass = doProcessConfigurationClass(configClass, sourceClass);}while (sourceClass != null);this.configurationClasses.put(configClass, configClass);
}

重点是 doProcessConfigurationClass 方法,跟进

//ConfigurationClassParser
@Nullable
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)throws IOException {if (configClass.getMetadata().isAnnotated(Component.class.getName())) {// Recursively process any member (nested) classes firstprocessMemberClasses(configClass, sourceClass);}// Process any @PropertySource annotationsfor (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(), PropertySources.class,org.springframework.context.annotation.PropertySource.class)) {if (this.environment instanceof ConfigurableEnvironment) {processPropertySource(propertySource);}else {logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +"]. Reason: Environment must implement ConfigurableEnvironment");}}// Process any @ComponentScan annotationsSet<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);if (!componentScans.isEmpty() &&!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {for (AnnotationAttributes componentScan : componentScans) {// The config class is annotated with @ComponentScan -> perform the scan immediatelySet<BeanDefinitionHolder> scannedBeanDefinitions =this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());// Check the set of scanned definitions for any further config classes and parse recursively if neededfor (BeanDefinitionHolder holder : scannedBeanDefinitions) {BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();if (bdCand == null) {bdCand = holder.getBeanDefinition();}if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {parse(bdCand.getBeanClassName(), holder.getBeanName());}}}}// Process any @Import annotationsprocessImports(configClass, sourceClass, getImports(sourceClass), true);// Process any @ImportResource annotationsAnnotationAttributes importResource =AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);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);}}// Process individual @Bean methodsSet<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);for (MethodMetadata methodMetadata : beanMethods) {configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));}// Process default methods on interfacesprocessInterfaces(configClass, sourceClass);// Process superclass, if anyif (sourceClass.getMetadata().hasSuperClass()) {String superclass = sourceClass.getMetadata().getSuperClassName();if (superclass != null && !superclass.startsWith("java") &&!this.knownSuperclasses.containsKey(superclass)) {this.knownSuperclasses.put(superclass, configClass);// Superclass found, return its annotation metadata and recursereturn sourceClass.getSuperClass();}}// No superclass -> processing is completereturn null;
}

这个方法也是很长的一段,关于 @Import 注解的其实就在 processImports 方法里,我们跟进看下

//ConfigurationClassParser
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,Collection<SourceClass> importCandidates, boolean checkForCircularImports) {if (importCandidates.isEmpty()) {return;}if (checkForCircularImports && isChainedImportOnStack(configClass)) {this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));}else {this.importStack.push(configClass);try {for (SourceClass candidate : importCandidates) {if (candidate.isAssignable(ImportSelector.class)) {// Candidate class is an ImportSelector -> delegate to it to determine importsClass<?> candidateClass = candidate.loadClass();ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class);ParserStrategyUtils.invokeAwareMethods(selector, this.environment, this.resourceLoader, this.registry);if (selector instanceof DeferredImportSelector) {this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);}else {String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);processImports(configClass, currentSourceClass, importSourceClasses, false);}}else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {// Candidate class is an ImportBeanDefinitionRegistrar ->// delegate to it to register additional bean definitionsClass<?> candidateClass = candidate.loadClass();ImportBeanDefinitionRegistrar registrar =BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class);ParserStrategyUtils.invokeAwareMethods(registrar, this.environment, this.resourceLoader, this.registry);configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());}else {// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->// process it as an @Configuration classthis.importStack.registerImport(currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());processConfigurationClass(candidate.asConfigClass(configClass));}}}catch (BeanDefinitionStoreException ex) {throw ex;}catch (Throwable ex) {throw new BeanDefinitionStoreException("Failed to process import candidates for configuration class [" +configClass.getMetadata().getClassName() + "]", ex);}finally {this.importStack.pop();}}
}

可以看到核心代码就是对 @Import 注解的参数,那个 Class 数组做循环处理,而 if 条件是按我们之前所说的三种情况做区分的

image-20230703151309460

@Configuration 注解类型的就不讲了,就是再回去调用处理 @Configuration 注解的方法,下面重点看下 ImportBeanDefinitionRegistrar 接口和 ImportSelector 接口的场景

4、处理 ImportBeanDefinitionRegistrar 接口实现类

image-20230703154651578

先是实例化这个类,如果这个类实现了 Aware 相关的接口,就去设置下相关的属性,最后调用 addImportBeanDefinitionRegistrar 方法,将对象放到 importBeanDefinitionRegistrars 这个 map 里

image-20230703162607872

那么这个类的 registerBeanDefinitions 方法究竟在什么时候调用的呢?

上面我们讲到在解析完配置类之后,会调用 this.reader.loadBeanDefinitions(configClasses); 方法,我们跟进这个方法

image-20230703162836338

继续跟进 loadBeanDefinitionsForConfigurationClass 方法

image-20230703162909334

configClass.getImportBeanDefinitionRegistrars() 方法就是获取的上面所说的 importBeanDefinitionRegistrars 这个map,继续跟进 loadBeanDefinitionsFromRegistrars 方法

image-20230703162945514

可以看到循环map,调用了 registerBeanDefinitions 方法

5、处理 ImportSelector 接口实现类

image-20230703170552852

同样的,先实例化对象,然后处理 Aware 相关属性注入,然后开始区分了,如果是 DeferredImportSelector 接口的实现类,就封装成 DeferredImportSelectorHolder 对象,添加到 deferredImportSelectors 这个集合里,这个前面有说过,为了延迟调用,如果不是 DeferredImportSelector 接口的实现类,那就是 ImportSelector 接口的实现类,那么就先调用其 selectImports 方法,这个方法会返回 bean 的名字,然后转成 SourceClass 对象,再调用 processImports 方法处理,然后就看 selectImports 方法返回的 bean 是什么类型,再判断处理

总结一下,就是

  • 如果实现了 DeferredImportSelector 接口,那么就存到 deferredImportSelectors 集合里,等待后续处理
  • 如果没有实现 DeferredImportSelector 接口,那么是实现了 ImportSelector 接口,直接调用其 selectImports 方法

所以,实现了 DeferredImportSelector 接口的类的 selectImports 方法,在什么时候调用的呢?

6、处理 DeferredImportSelector 接口实现类

我们回到 ConfigurationClassParser 类的 parse 方法

image-20230703180344801

可以看到最后一步,就是处理所有 DeferredImportSelector 接口的实现类(后面简称deferredImports)的方法,我们跟进去

image-20230703180437893

这个方法里,先把所有的 deferredImports 排序,然后调用 DeferredImportSelectorGroupingHandlerregister 方法,把这些类进行分组,最后再调用 processGroupImports 方法处理,跟进这个方法

image-20230703180836187

可以看到对分组后的map,获取其内容,然后进行循环,获取每个分组中的 deferredImports 循环,调用 processImports 方法,我们看下 grouping.getImports() 返回的是什么

image-20230703181212745

可以看到,正是调用 selectImports 方法的返回,所以处理 DeferredImportSelector 接口的实现类,和处理 ImportSelector 接口的实现类,大致是差不多的,只不过 DeferredImportSelector 要延迟到最后处理,而且会根据不同的顺序和分组,分批处理。
dlerregister方法,把这些类进行分组,最后再调用processGroupImports` 方法处理,跟进这个方法

image-20230703180836187

可以看到对分组后的map,获取其内容,然后进行循环,获取每个分组中的 deferredImports 循环,调用 processImports 方法,我们看下 grouping.getImports() 返回的是什么

image-20230703181212745

可以看到,正是调用 selectImports 方法的返回,所以处理 DeferredImportSelector 接口的实现类,和处理 ImportSelector 接口的实现类,大致是差不多的,只不过 DeferredImportSelector 要延迟到最后处理,而且会根据不同的顺序和分组,分批处理。


http://www.ppmy.cn/news/716653.html

相关文章

nginx配置https加密

以下操作版本为Ubuntu2004&#xff0c;文件位置可能略有不同 https 功能 Web网站的登录页面通常都会使用https加密传输的&#xff0c;加密数据以保障数据的安全&#xff0c;HTTPS能够加密信息&#xff0c;以免敏感信息被第三方获取&#xff0c;所以很多银行网站或电子邮箱等等…

强化学习王者荣耀项目修改

修改模型参数中的名字 chkpt torch.load(weights/model_weights_StateJudgmentPre, map_location"cuda:0")chkpt[fn_layer.weight] chkpt.pop(图转.weight) chkpt[fn_layer.bias] chkpt.pop(图转.bias) chkpt[evaluate.weight] chkpt.pop(评价.weight) chkpt[eva…

修改定位工具相关的app

修改定位工具相关 修改定位工具1&#xff08;修改经纬度&#xff09;&#xff1a;110_45b1627a34040c58f39d02036c0a030c.apk修改定位工具2&#xff08;修改经纬度&#xff09;&#xff1a;商店搜索 小鹏定位助手 工具3&#xff1a;根据经纬度去查询工具3&#xff1a;可以利用…

逍遥模拟器过检测_王者荣耀怎么修改定位变更荣耀战区提升排位--逍遥模拟器电脑版更换荣耀战区...

王者荣耀怎么修改定位变更荣耀战区提升排位呢?我看了很多的答案,要么就是要就加微信号的假冒或者骗子信息,要么就是使用收费的软件,然后无法保证效果。真的没有简单好用还免费的方法吗? 下面我就将目前最简单方便修改定位变更荣耀战区提升排位的方法分享给大家,简单三步就…

iOS/Android 王者荣耀更换战区

玩王者的小伙伴&#xff0c;有时候人不在当地&#xff0c;但是需要将排位战区设置到指定地区&#xff0c;因为方便英雄排名&#xff0c;玩的爽&#xff0c;这个时候就需要通过手机虚拟定位&#xff0c;将战区修改到指定区域了。 注意&#xff1a;王者只支持 每周一 修改或设置…

王者该服务器未获取角色信息,王者荣耀荣耀榜尚未获取定位信息是怎么回事

王者荣耀游戏中出现一种情况&#xff0c;会显示荣耀榜尚未获取定位信息&#xff0c;很玩家并不知道为什么出现这种情况&#xff0c;那么王者荣耀荣耀榜尚未获取定位信息是怎么回事?下面就是针对荣耀榜尚未获取定位信息情况的详细解答&#xff0c;一起看看吧。 在目前游戏中荣耀…

安卓车机root改流浪地球_教你王者荣耀改战区

王者荣耀怎么改荣耀战区呢?今天小编给大家带来的是王者荣耀改荣耀战区方法哦!想知道的小伙伴就和小编一起来看看吧! 1、首先玩家需要下载一个多开器&#xff0c;保证能够多开王者荣耀&#xff0c;多开器安卓的可以直接市场里面找&#xff0c;苹果的在App Store也能找到。 2、接…

王者该服务器未获取角色信息,王者荣耀荣耀榜尚未获取定位信息是怎么回事[多图]...

王者荣耀游戏中出现一种情况&#xff0c;会显示荣耀榜尚未获取定位信息&#xff0c;很玩家并不知道为什么出现这种情况&#xff0c;那么王者荣耀荣耀榜尚未获取定位信息是怎么回事?下面就是针对荣耀榜尚未获取定位信息情况的详细解答&#xff0c;一起看看吧。 在目前游戏中荣耀…

王者荣耀java修改_王者荣耀战区怎么改到其他地方 王者荣耀战区修改教程最新...

王者荣耀战区修改教程是游戏战区玩法&#xff0c;玩家们想知道如何修改喔&#xff0c;那么王者荣耀战区怎么改到其他地方、王者荣耀战区修改教程最新呢&#xff0c;跑跑车手游网为大家带来了介绍。 *王者荣耀战区怎么改到其他地方&#xff1f; 王者荣耀中有一个战区系统&#x…

荣耀电脑怎样更改计算机名称,王者荣耀荣耀战区怎么修改别的地区?手机电脑改荣耀战区不封号操作方法[多图]...

王者荣耀荣耀战区在哪里改&#xff1f;荣耀战区的位置是可以更改的&#xff0c;但是很多玩家都不清楚这个位置应该如何修改&#xff0c;想要将荣耀战区改到别的地区的小伙伴&#xff0c;可以直接参考下面小编提供的教程哦&#xff0c;更方便大家了解操作的步骤。 王者荣耀改荣耀…

王者荣耀如何修改服务器信息,王者荣耀定位怎么更改设置 修改定位方法推荐...

王者荣耀定位是决定我们的荣耀战区的&#xff0c;所以有些玩家小伙伴还是比较看重的&#xff01;那么现在小编要给你们推荐带来的就是王者荣耀定位怎么更改设置的攻略&#xff0c;小伙伴们想知道那就一起来看一下吧&#xff01; 最近火热版本&#xff1a;王者荣耀云游戏 定位怎…

mysql中什么是表?列?行?什么是主键和外键?什么是索引?为什么要使用索引?

mysql中什么是表&#xff1f;列&#xff1f;行&#xff1f; 在关系数据库中&#xff0c;表&#xff08;Table&#xff09;是数据的主要组织单元。它是由一组命名的列和行组成&#xff0c;用于存储和组织数据。 列&#xff08;Column&#xff09;是表中的一个字段&#xff0c;…

GitHub 组织是什么?您应该使用一个吗?

GitHub 作为一个平台,被个人程序员和大型组织所使用。无论您与多少人一起工作,“GitHub Organizations”都为管理多个项目的人员提供了一些不错的工具。 GitHub 组织是什么? GitHub Organizations 是 GitHub 的一项功能,允许您创建一个中心位置,团队成员可以在其中访问和…

科大讯飞成为北京 2022 年冬奥会和冬残奥会官方自动语音转换与翻译独家供应商

2019 年 9 月 16 日&#xff0c;北京 2022 年冬奥会和冬残奥会官方自动语音转换与翻译独家供应商发布会在北京冬奥组委园区举行。科大讯飞股份有限公司正式成为北京 2022 年冬奥会和冬残奥会官方自动语音转换与翻译独家供应商。 北京冬奥组委专职副主席、秘书长韩子荣&#x…

到の 飞飞第三方孙丹菲

建立了快捷三大浪费建立了快捷三大浪费圣灵丹方剂了大家法律山东uhdf哦idfsdkfskd看似简单开发开始开始凯仕达上课的凯仕达思考覅偶尔夫人我饿拉倒三闾大夫欧威风哦三林渡口圣灵丹方剂欧文发掘历史的风景欧威风欧文欧文欧文欧文额金额均认为欧文较为解放三路口附近搜地方叫欧文…

云扩科技RPA机器人|助力零售行业数字转型新未来

目前零售行业极为关注数字技术创新对产品研发及消费者服务的推动作用&#xff0c;品牌发展旨在拥抱科技&#xff0c;创变未来。云扩科技RPA机器人在零售领域的实际应用案例和智能流程自动化解决方案&#xff0c;展示了零售RPA机器人赋能零售企业数字化转型的敏捷创新能力。 RP…

智能客服在2022:从成本中心奔向价值中心

​ 智能客服二十年&#xff0c;体现的不仅仅是技术、服务的演进&#xff0c;更是其从成本中心奔向价值中心行业价值重构。 作者|斗斗 编辑|皮爷 出品|产业家 智能客服正在迎来新的风口。 随着AI、5G等新兴技术的普及&#xff0c;以及新冠疫情的爆发&#xff0c;智能客服市场…

百度飞桨成为北京市首个AI产业方向创新应用平台

1月20日&#xff0c;北京市经济和信息化局正式授予百度公司"北京市人工智能产业创新应用平台&#xff08;百度飞桨&#xff09;"。当前&#xff0c;北京市正在创建国家人工智能创新应用先导区&#xff0c;人工智能作为新科技革命和产业变革前沿领域&#xff0c;是北京…

python免费的实时语音交互(讯飞语音识别+青云客Robot)

须知&#xff1a; py文件中的库&#xff0c;需要自己去pip&#xff0c; 其中有个pyaudio的库也许会安装失败&#xff0c;老是报错。我刚开始也费了半天时间才安装好的。 安装报错的话&#xff0c;可以去官网下载一个wheel文件&#xff0c;找到对应的版本下载&#xff0c;下载之…

【Python自动化Excel】pandas操作Excel的“分分合合”

话说Excel数据表&#xff0c;分久必合、合久必分。Excel数据表的“分”与“合”是日常办公中常见的操作。手动操作并不困难&#xff0c;但数据量大了之后&#xff0c;重复性操作往往会令人崩溃。利用Python的Pandas库&#xff0c;便可以自动实现Excel数据表的“分分合合”。下面…