spring源码解析-(2)Bean的包扫描

包扫描的过程

测试代码:

// 扫描指定包下的所有类
BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry();
// 扫描指定包下的所有类
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry);
scanner.scan("com.test.entity");
for (String beanDefinitionName : registry.getBeanDefinitionNames()) {
    System.err.println(beanDefinitionName);
}

注意:下述源码在阅读时应尽量避免死磕,先梳理整体流程,理解其动作的含义,具体细节可以在看完整体之后再细致打磨。如果整个流程因为一些细节而卡住,那就丧失了看源码的意义,我们需要的是掌握流程和优秀的设计理念,而不是一字一句的抄下来。

scan方法详解

源码如下:翻译通过CodeGeeX进行自动生成并自己进行修改。

public int scan(String... basePackages) {
    // 获取扫描开始时的BeanDefinition数量
    int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
    // 执行扫描
    doScan(basePackages);
    // 如果需要,注册注解配置处理器
    if (this.includeAnnotationConfig) {
        AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
    }
    // 返回扫描结束时的BeanDefinition数量与扫描开始时的数量之差
    return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}

通过上面的代码,我们可以看到核心的方法为doScan,具体源码如下:

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
    // 确保至少传入一个基础包
    Assert.notEmpty(basePackages, "At least one base package must be specified");
    // 创建一个存放BeanDefinitionHolder的集合
    Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
    for (String basePackage : basePackages) {
        // 1找出候选的组件
        Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
        // 遍历候选的组件
        for (BeanDefinition candidate : candidates) {
            // 解析组件的作用域元数据
            ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
            // 设置组件的作用域
            candidate.setScope(scopeMetadata.getScopeName());
            // 2生成组件的名称
            String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
            // 如果候选的组件是抽象BeanDefinition的实例
            if (candidate instanceof AbstractBeanDefinition) {
                // 执行后处理器,对候选的组件进行处理
                postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
            }
            // 如果候选的组件是带有注解的BeanDefinition的实例
            if (candidate instanceof AnnotatedBeanDefinition) {
                // 3处理带有注解的BeanDefinition的公共定义
                AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
            }
            // 如果检查候选的组件
            if (checkCandidate(beanName, candidate)) {
                // 创建一个BeanDefinitionHolder,并将其添加到集合中
                BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
                // 应用代理模式
                definitionHolder =
                        AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
                beanDefinitions.add(definitionHolder);
                // 将BeanDefinition注册到容器中
                registerBeanDefinition(definitionHolder, this.registry);
            }
        }
    }
    // 返回存放BeanDefinitionHolder的集合
    return beanDefinitions;
}

现在我们一步步的使用debug查看整个doScan的大体过程

1. 通过传入的路径扫描并得到对应的BeanDefinition

截取的部分代码片段如下:

for (String basePackage : basePackages) {
    // 找出候选的组件
    Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
    // other
}

findCandidateComponents方法的源码:

从 Spring 5.0 开始新增了一个 @Indexed 注解(新特性,@Component 注解上面就添加了 @Indexed 注解), 这里不会去扫描指定路径下的 .class 文件,而是读取所有 META-INF/spring.components 文件中符合条件的类名,直接添加 .class 后缀就是编译文件,而不要去扫描。

public Set<BeanDefinition> findCandidateComponents(String basePackage) {
    // 如果componentsIndex不为空,并且indexSupportsIncludeFilters()为true
    if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
        // 从componentsIndex中添加候选组件
        return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
    }
    // 否则
    else {
        // 扫描候选组件
        return scanCandidateComponents(basePackage);
    }
}

我们着重看scanCandidateComponents方法,源码如下:

private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
    // 创建一个LinkedHashSet,用于存储扫描到的候选组件
    Set<BeanDefinition> candidates = new LinkedHashSet<>();
    try {
        // 1.1拼接包搜索路径
        String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
                resolveBasePackage(basePackage) + '/' + this.resourcePattern;
        // 1.2获取资源
        Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
        // 判断是否开启日志记录
        boolean traceEnabled = logger.isTraceEnabled();
        boolean debugEnabled = logger.isDebugEnabled();
        for (Resource resource : resources) {
            if (traceEnabled) {
                logger.trace("Scanning " + resource);
            }
            try {
                // 1.3获取元数据读取器
                MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
                // 判断是否是候选组件
                if (isCandidateComponent(metadataReader)) {
                    // 1.4创建一个ScannedGenericBeanDefinition,用于存储元数据读取器
                    ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
                    sbd.setSource(resource);
                    // 判断是否是候选组件
                    if (isCandidateComponent(sbd)) {
                        if (debugEnabled) {
                            logger.debug("Identified candidate component class: " + resource);
                        }
                        // 将候选组件添加到candidates中
                        candidates.add(sbd);
                    } else {
                        if (debugEnabled) {
                            logger.debug("Ignored because not a concrete top-level class: " + resource);
                        }
                    }
                } else {
                    if (traceEnabled) {
                        logger.trace("Ignored because not matching any filter: " + resource);
                    }
                }
            } catch (FileNotFoundException ex) {
                if (traceEnabled) {
                    logger.trace("Ignored non-readable " + resource + ": " + ex.getMessage());
                }
            } catch (Throwable ex) {
                throw new BeanDefinitionStoreException(
                        "Failed to read candidate component class: " + resource, ex);
            }
        }
    } catch (IOException ex) {
        throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
    }
    return candidates;
}

1.1 拼接搜索路径

源码如下:

String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
            resolveBasePackage(basePackage) + '/' + this.resourcePattern;

简单的字符串拼接,其代码本身没有任何技术含量,但debug的时候,可以通过其显示的值来帮助我们更好的理解项目。

在这里插入图片描述

debug的结果为:classpath*:com/test/entity/**/*.class

可以看到,是从根路径的com.test.enetity下开始寻找所有的字节码文件。

1.2 根据包扫描路径获取资源

Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);

具体方法:

public Resource[] getResources(String locationPattern) throws IOException {
    // 断言locationPattern不能为空
    Assert.notNull(locationPattern, "Location pattern must not be null");
    // 如果locationPattern以CLASSPATH_ALL_URL_PREFIX开头
    if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
        // 是一个类路径资源(同一个名称可能存在多个资源)
        if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
            // 是一个类路径资源模式
            return findPathMatchingResources(locationPattern);
        }
        else {
            // 所有具有给定名称的类路径资源
            return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
        }
    }
    else {
        // 通常只在后缀前加上前缀,
        // 并且在Tomcat中,只有在“war:”协议下,才会使用“*/”分隔符。
        int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :
                locationPattern.indexOf(':') + 1);
        if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
            // 是一个文件模式
            return findPathMatchingResources(locationPattern);
        }
        else {
            // 具有给定名称的单个资源
            return new Resource[] {getResourceLoader().getResource(locationPattern)};
        }
    }
}

接着我们进入到findPathMatchingResources的方法中,具体源码如下:

protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
    // 确定根目录路径
    String rootDirPath = determineRootDir(locationPattern);
    // 获取locationPattern中根目录路径之后的部分
    String subPattern = locationPattern.substring(rootDirPath.length());
    // 获取根目录资源
    Resource[] rootDirResources = getResources(rootDirPath);
    // 创建一个LinkedHashSet集合
    Set<Resource> result = new LinkedHashSet<>(16);
    // 遍历根目录资源
    for (Resource rootDirResource : rootDirResources) {
        // 解析根目录资源
        rootDirResource = resolveRootDirResource(rootDirResource);
        // 获取根目录资源的URL
        URL rootDirUrl = rootDirResource.getURL();
        // 如果存在equinoxResolveMethod且根目录URL的协议以"bundle"开头
        if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) {
            // 调用equinoxResolveMethod方法,获取解析后的URL
            URL resolvedUrl = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirUrl);
            // 如果解析后的URL不为空
            if (resolvedUrl != null) {
                // 将解析后的URL赋值给rootDirUrl
                rootDirUrl = resolvedUrl;
            }
            // 将解析后的URL封装为Resource
            rootDirResource = new UrlResource(rootDirUrl);
        }
        // 如果根目录URL的协议以"vfs"开头
        if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
            // 调用VfsResourceMatchingDelegate的findMatchingResources方法,获取匹配的资源
            result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
        }
        // 如果根目录URL是jar文件或者根目录资源是jar资源
        else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
            // 调用doFindPathMatchingJarResources方法,获取匹配的jar资源
            result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
        }
        // 否则
        else {
            // 调用doFindPathMatchingFileResources方法,获取匹配的文件资源
            result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
        }
    }
    // 如果存在日志日志功能
    if (logger.isTraceEnabled()) {
        // 打印日志
        logger.trace("Resolved location pattern [" + locationPattern + "] to resources " + result);
    }
    // 返回结果
    return result.toArray(new Resource[0]);
}

1.3 获取元数据

源码:可以看到通过1.2中获取到的资源将通过如下方法获取一个名为MetadataReader的对象,那么这个元数据读取器究竟是干什么的?

// getMetadataReaderFactory工厂模式构建MetadataReader
MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);

MetadataReader接口中只定义了三个方法,源码如下:

public interface MetadataReader {
	Resource getResource();
	ClassMetadata getClassMetadata();
	AnnotationMetadata getAnnotationMetadata();
}

根据debug追随到的源码,发现其进入到了MetadataReader的实现类SimpleMetadataReader中,其构造器如下:

SimpleMetadataReader(Resource resource, @Nullable ClassLoader classLoader) throws IOException {
    // 这里使用到了经典的访问者模式,可以自行了解一下
    SimpleAnnotationMetadataReadingVisitor visitor = new SimpleAnnotationMetadataReadingVisitor(classLoader);
    getClassReader(resource).accept(visitor, PARSING_OPTIONS);
    this.resource = resource;
    this.annotationMetadata = visitor.getMetadata();
}

感兴趣的可以着重看下getClassReader(resource).accept(visitor, PARSING_OPTIONS);这句话中大概做了那些事情。

具体源码就不展示,这里大概就是将class文件读取并操作二进制代码。(可以通过《深入了解Java虚拟机》这本书来了解一下java的字节码相关内容)

1.4 创建ScannedGenericBeanDefinition

通过构造器的方式将MetadataReader中读取到的内容放入BeanDefinition中。

public ScannedGenericBeanDefinition(MetadataReader metadataReader) {
    Assert.notNull(metadataReader, "MetadataReader must not be null");
    this.metadata = metadataReader.getAnnotationMetadata();
    setBeanClassName(this.metadata.getClassName());
    setResource(metadataReader.getResource());
}

2. 获取Bean的名称

String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);

我们可以看一下spring是如何获取bean的名称的

public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
    if (definition instanceof AnnotatedBeanDefinition) {
        String beanName = determineBeanNameFromAnnotation((AnnotatedBeanDefinition) definition);
        if (StringUtils.hasText(beanName)) {
            // Explicit bean name found.
            return beanName;
        }
    }
    // Fallback: generate a unique default bean name.
    return buildDefaultBeanName(definition, registry);
}

可以看到该方法是有两个分支,如果通过注解中可以拿到名字,则直接返回,具体方法如下:

// 确定基于注解的bean名称
protected String determineBeanNameFromAnnotation(AnnotatedBeanDefinition annotatedDef) {
    // 获取注解的元数据
    AnnotationMetadata amd = annotatedDef.getMetadata();
    // 获取注解的类型
    Set<String> types = amd.getAnnotationTypes();
    // 初始化bean名称
    String beanName = null;
    // 遍历注解类型
    for (String type : types) {
        // 获取注解的属性
        AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(amd, type);
        if (attributes != null) {
            // 获取注解的元注解类型
            Set<String> metaTypes = this.metaAnnotationTypesCache.computeIfAbsent(type, key -> {
                Set<String> result = amd.getMetaAnnotationTypes(key);
                return (result.isEmpty() ? Collections.emptySet() : result);
            });
            // 判断注解是否为stereotype注解,并且包含name和value属性
            if (isStereotypeWithNameValue(type, metaTypes, attributes)) {
                // 获取name属性的值
                Object value = attributes.get("value");
                if (value instanceof String) {
                    String strVal = (String) value;
                    // 判断字符串长度是否大于0
                    if (StringUtils.hasLength(strVal)) {
                        // 判断beanName是否为空,或者与strVal不相等
                        if (beanName != null && !strVal.equals(beanName)) {
                            // 抛出异常
                            throw new IllegalStateException("Stereotype annotations suggest inconsistent " +
                                    "component names: '" + beanName + "' versus '" + strVal + "'");
                        }
                        // 更新beanName
                        beanName = strVal;
                    }
                }
            }
        }
    }
    return beanName;
}

或者,根据类的class直接创建名称,代码如下:

protected String buildDefaultBeanName(BeanDefinition definition) {
    // 获取bean的类名
    String beanClassName = definition.getBeanClassName();
    // 断言bean的类名不能为空
    Assert.state(beanClassName != null, "No bean class name set");
    // 获取bean的简短名称
    String shortClassName = ClassUtils.getShortName(beanClassName);
    // 将简短名称的首字母转换为小写
    return Introspector.decapitalize(shortClassName);
}

3. 处理带有注解的BeanDefinition的公共定义

AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);

在此方法中,我们可以了解到,注册为Bean的类上可以添加的注解有几种,代码如下:

static void processCommonDefinitionAnnotations(AnnotatedBeanDefinition abd, AnnotatedTypeMetadata metadata) {
    AnnotationAttributes lazy = attributesFor(metadata, Lazy.class);
    if (lazy != null) {
        abd.setLazyInit(lazy.getBoolean("value"));
    }
    else if (abd.getMetadata() != metadata) {
        lazy = attributesFor(abd.getMetadata(), Lazy.class);
        if (lazy != null) {
            abd.setLazyInit(lazy.getBoolean("value"));
        }
    }

    if (metadata.isAnnotated(Primary.class.getName())) {
        abd.setPrimary(true);
    }
    AnnotationAttributes dependsOn = attributesFor(metadata, DependsOn.class);
    if (dependsOn != null) {
        abd.setDependsOn(dependsOn.getStringArray("value"));
    }

    AnnotationAttributes role = attributesFor(metadata, Role.class);
    if (role != null) {
        abd.setRole(role.getNumber("value").intValue());
    }
    AnnotationAttributes description = attributesFor(metadata, Description.class);
    if (description != null) {
        abd.setDescription(description.getString("value"));
    }
}

4 将定义好的BeanDefinition放入set,并返回set。

if (checkCandidate(beanName, candidate)) {
    // 创建一个BeanDefinitionHolder,并将其添加到集合中
    BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
    // 应用代理模式
    definitionHolder =
            AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
    beanDefinitions.add(definitionHolder);
    // 将BeanDefinition注册到容器中
    registerBeanDefinition(definitionHolder, this.registry);
}

思考总结:

1、可以看到,在spring中使用了很多设计模式,设计模式在解决一系列问题上非常有帮助,如创建Bean的Bean工厂,访问并根据字节码操作的访问者模式,需要理解其思想,在遇到问题的时候,往设计模式上想一想。后续可能要深入的学习一下设计模式。

2、关于数据类型的使用,其实本人在实际开发的过程中大部分时间都使用list,Map,其中也遇到过很多次内存溢出,在合理的选择数据类型上,有必要仔细斟酌。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

HTML静态网页成品作业(HTML+CSS)—— 家乡南宁介绍网页(2个页面)

&#x1f389;不定期分享源码&#xff0c;关注不丢失哦 文章目录 一、作品介绍二、作品演示三、代码目录四、网站代码HTML部分代码 五、源码获取 一、作品介绍 &#x1f3f7;️本套采用HTMLCSS&#xff0c;未使用Javacsript代码&#xff0c;共有2个页面。 二、作品演示 三、代…

实战 | YOLOv10 自定义数据集训练实现车牌检测 (数据集+训练+预测 保姆级教程)

导读 本文主要介绍如何使用YOLOv10在自定义数据集训练实现车牌检测 (数据集训练预测 保姆级教程)。 YOLOv10简介 YOLOv10是清华大学研究人员在Ultralytics Python包的基础上&#xff0c;引入了一种新的实时目标检测方法&#xff0c;解决了YOLO以前版本在后处理和模型架构方面…

大模型时代的具身智能系列专题(十一)

UMass Amherst 淦创团队 淦创是马萨诸塞大学阿默斯特分校的一名教员&#xff0c;也是麻省理工学院- ibm沃森人工智能实验室的研究经理。在麻省理工学院博士后期间&#xff0c;和Antonio Torralba教授、Daniela Rus教授和Josh Tenenbaum教授一起工作。在此之前&#xff0c;在清…

【面试官】知道synchronized锁升级吗

一座绵延在水上的美术馆——白鹭湾巧克力美术馆。它漂浮于绿水之上&#xff0c;宛如一条丝带轻盈地伸向远方 文章目录 可重入锁synchronized实现原理 synchronized缺点保存线程状态锁升级锁升级优缺点 1. 可重入锁 面试官&#xff1a;知道可重入锁有哪些吗? 可重入意味着获取…

解决Mac无法上网/网络异常的方法,重置网络

解放方法 1、前往文件夹&#xff1a;/Library/Preferences/SystemConfiguration 2 、在弹窗中输入上边的地址 3 、把文件夹中除了下图未选中的文件全部删掉&#xff0c;删除时需要输入密码 4 、重启mac 电脑就搞定了。

免费数据库同步软件

在信息化日益发展的今天&#xff0c;数据同步成为了企业和个人用户不可或缺的一部分。数据库同步软件作为数据同步的重要工具&#xff0c;能够帮助我们实现不同数据库系统之间的数据复制和同步&#xff0c;确保数据的一致性和完整性。本文将介绍几款免费数据库同步软件&#xf…

SpringBoot+Vue教师工作量管理系统(前后端分离)

技术栈 JavaSpringBootMavenMySQLMyBatisVueShiroElement-UI 角色对应功能 教师管理员 功能截图

iBeacon赋能AR导航:室内定位技术的原理与优势

室内定位导航对于大型商场、机场、医院等复杂室内环境至关重要&#xff0c;它帮助人们快速找到目的地&#xff0c;提高空间利用率。AR技术通过将虚拟信息叠加在现实世界&#xff0c;提供直观导航指引&#xff0c;正在成为室内导航的新趋势&#xff0c;增强用户互动体验&#xf…

一文读懂 Compose 支持 Accessibility 无障碍的原理

前言 众所周知&#xff0c;Compose 作为一种 UI 工具包&#xff0c;向开发者提供了实现 UI 的基本功能。但其实它还默默提供了很多其他能力&#xff0c;其中之一便是今天需要讨论的&#xff1a;Android 特色的 Accessibility 功能。 采用 Compose 搭建的界面&#xff0c;完美…

将二叉排序树转换成双向链表--c++【做题记录】

【问题描述】 编写程序在不增加结点的情况下&#xff0c;将二叉排序树转换成有序双向链表&#xff08;如下图&#xff09;。 链表创建结束后&#xff0c;按照从前往后的顺序输出链表中结点的内容。 【输入输出】 【输入形式】 第一行输入数字n&#xff0c;第二行输入n个整数…

车载诊断架构 - 引导诊断

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 屏蔽力是信息过载时代一个人的特殊竞争力,任何消耗你的人和事,多看一眼都是你的不对。非必要不费力证明自己,无利益不试图说服别人,是精神上的节…

八爪鱼现金流-018,持续打磨

八爪鱼,被动收入,财务自由,现金流,现金流游戏,各银行利率,money,资产负债表,财务自由,资产管理,个人理财,管理个人资产,理财,打造被动收入,躺着赚钱,让钱为我打工

Cell-在十字花科植物中年生和多次开花多年生开花行为的互相转化-文献精读21

Reciprocal conversion between annual and polycarpic perennial flowering behavior in the Brassicaceae 在十字花科植物中年生和多次开花多年生开花行为的互相转化 亮点 喜马拉雅须弥芥 和 内华达糖芥 是两个多年生植物模型 MADS-box 基因的剂量效应决定了一年生、二年生…

使用OpenCV dnn c++加载YOLOv8生成的onnx文件进行实例分割

在网上下载了60多幅包含西瓜和冬瓜的图像组成melon数据集&#xff0c;使用 EISeg 工具进行标注&#xff0c;然后使用 eiseg2yolov8 脚本将.json文件转换成YOLOv8支持的.txt文件&#xff0c;并自动生成YOLOv8支持的目录结构&#xff0c;包括melon.yaml文件&#xff0c;其内容如下…

【Python教程】1-注释、变量、标识符与基本操作

在整理自己的笔记的时候发现了当年学习python时候整理的笔记&#xff0c;稍微整理一下&#xff0c;分享出来&#xff0c;方便记录和查看吧。个人觉得如果想简单了解一名语言或者技术&#xff0c;最简单的方式就是通过菜鸟教程去学习一下。今后会从python开始重新更新&#xff0…

人工智能--教育领域的运用

文章目录 &#x1f40b;引言 &#x1f40b;个性化学习 &#x1f988;体现&#xff1a; &#x1f988;技术解析&#xff1a; &#x1f40b;智能辅导与虚拟助手 &#x1f988;体现&#xff1a; &#x1f988;技术解析&#xff1a; &#x1f40b;自动评分与评估 &#x1f…

AI大模型在广告领域的应用

深度对谈&#xff1a;广告创意领域中AIGC的应用_生成式 AI_Tina_InfoQ精选文章

【python】OpenCV GUI——Mouse(14.1)

参考学习来自 文章目录 背景知识cv2.setMouseCallback 介绍小试牛刀 背景知识 GUI&#xff08;Graphical User Interface&#xff0c;图形用户界面&#xff09; 是一种允许用户通过图形元素&#xff08;如窗口、图标、菜单和按钮&#xff09;与电子设备进行交互的界面。与传统…

【Mtk Camera开发学习】06 MTK 和 Qcom 平台支持通过 Camera 标准API 打开 USBCamera

本专栏内容针对 “知识星球”成员免费&#xff0c;欢迎关注公众号&#xff1a;小驰行动派&#xff0c;加入知识星球。 #MTK Camera开发学习系列 #小驰私房菜 Google 官方介绍文档&#xff1a; https://source.android.google.cn/docs/core/camera/external-usb-cameras?hlzh-…

【React】classnames 优化类名控制

1. 介绍 classnames是一个简单的JS库&#xff0c;可以非常方便的通过条件动态的控制class类名的显示 ClassNames是一个用于有条件处理classname字符串连接的库 简单来说就是动态地去操作类名&#xff0c;把符合条件的类名粘在一起 现在的问题&#xff1a;字符串的拼接方式不…