请你谈谈:BeanDefinition类作为Spring Bean的建模对象,与BeanFactoryPostProcessor之间的羁绊

那么,我们如何理解Spring Bean的建模对象呢?简而言之,它是指用于描述和配置Bean实例化过程的模型对象。有人可能会提出疑问,既然只需要Class(类)就可以实例化一个对象,Class作为类的元数据,可以视作对象的建模对象,为何Spring还需要其他机制来建立Bean呢?这其中的关键在于,Class本身虽然包含了类的定义和实例化的基础信息,但它无法涵盖Spring框架中Bean的所有特性和配置。例如,Bean的作用域(Scope)、注入模型(Injection Model)、是否采用懒加载(Lazy Initialization)等属性,这些都是在Spring容器级别进行配置和管理的,而这些信息无法通过简单的Class元数据来表达。

因此,为了更全面地描述和管理Bean,Spring引入了一个名为BeanDefinition的类。BeanDefinition类作为Spring Bean的建模对象,能够抽象出Bean的各种属性和配置信息,包括但不限于作用域、生命周期回调、初始化方法、销毁方法、依赖注入等。通过BeanDefinition,Spring能够精确地控制Bean的实例化过程,确保Bean能够按照预期的方式被创建、配置和管理。
在这里插入图片描述
针对上述图示的详细文字说明如下:在一个给定的场景中,我们假设磁盘上存有N个.java源文件。首先,我们需要通过Java编译器将这些源文件逐一编译成相应的.class字节码文件。接着,当Java虚拟机(JVM)启动时,它会按照特定的类加载机制,将这些.class文件从磁盘加载到JVM的内部内存中。一旦.class文件被成功加载到内存中,JVM便能根据这些文件中定义的类模板信息来执行后续的操作。当JVM的字节码执行器在执行过程中遇到new关键字时,它会根据该关键字所指向的类的模板信息,在JVM的堆内存中为该类实例化一个对象,并为其分配相应的内存空间。这一过程确保了对象在JVM中的正确创建和初始化。

但是spring的bean实例化过程和一个普通java对象的实例化过程还是有区别的。
在这里插入图片描述
前提:假设在你的项目或者磁盘上有X和Y两个类,X是被加了spring注解的,Y没有加spring的注解;也就是正常情况下当spring容器启动之后通过getBean(X)能正常返回X的bean,但是如果getBean(Y)则会出异常,因为Y不能被spring容器扫描到不能被正常实例化;

在Spring容器启动的过程中,ConfigurationClassPostProcessor这一Bean工厂的后置处理器会被调用,以完成特定配置的扫描和解析。这里的“扫描”实际上是指Spring从类路径中识别并读取类的元数据。然而,读取到的类的元数据,如类的类型、名称和构造方法等,并不会直接存储在Class对象中。尽管Class对象确实包含了这些基本信息,但Spring在实例化Bean时,还需要考虑更多的配置属性,如作用域(scope)、延迟加载(lazy)、依赖关系(dependsOn)等。

为了满足这些需求,Spring设计了一个名为BeanDefinition的类,用于存储和管理Bean的完整配置信息。因此,当Spring读取到类的元数据后,

①它会为每个类实例化一个BeanDefinition对象,通过该对象的各种set方法将相关信息(包括从Class对象获取的元数据以及额外的配置属性)存储起来。

②在扫描过程中,每当Spring遇到一个符合规则的类,它都会实例化一个新的BeanDefinition对象,并根据类的名称自动生成一个Bean的名称(例如,对于名为IndexService的类,Spring会按照其命名规则生成一个名为indexService的Bean名称)。当然,Spring也提供了灵活的命名策略,允许开发者自定义名称生成器以覆盖默认行为。

③随后,Spring会将这个BeanDefinition对象及其对应的Bean名称存储在一个Map中,其中键(key)为Bean名称,值(value)为BeanDefinition对象。这个过程确保了Bean的元数据和配置信息在Spring容器中的有序管理和高效检索。至此,上述流程中的第①、②、③步便完成了,为后续的Bean创建和依赖注入等操作奠定了基础。

这里需要说明的是spring启动的时候会做很多工作,不仅仅是完成扫描,在扫描之前spring还干了其他大量事情;比如实例化beanFacctory、比如实例化类扫描器等等。

Appconfig.java
@ComponentScan("com.luban.beanDefinition")
@Configuration
public class Appconfig {
}X.java
@Component
public class X {public X(){System.out.println("X Constructor");}
}Y.java
public class Y {
}Test.java
public class Test{public static void main(String[] args) {AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();ac.register(Appconfig.class);ac.refresh();}}

在上述代码示例中,存在两个类:X和Y。其中,X类被标注了某个注解,而Y类则未被标注。在X类中,存在一个构造方法,该方法在X类实例化时会输出"X Constructor"。根据Spring框架的工作原理,当Spring容器启动时,它会执行一系列的初始化步骤,其中包括对带有特定注解的类的扫描和解析。

具体来说,在Spring容器的初始化过程中,会调用org.springframework.context.support.AbstractApplicationContext#invokeBeanFactoryPostProcessors方法。这个方法负责执行Bean工厂的后置处理器,其中就包括了对带有特定注解的类的扫描和解析。为了验证这一过程,我在该方法上设置了一个断点。

在Spring框架中,invokeBeanFactoryPostProcessors 是一个非常重要的方法,它负责在Spring容器初始化过程中调用所有实现了 BeanFactoryPostProcessor 接口的bean。这些后处理器允许在bean的定义被加载到Spring的bean工厂(BeanFactory)之后,但在bean被实例化之前,对bean定义进行修改或添加。

当应用程序启动并触发Spring容器初始化时,会调用invokeBeanFactoryPostProcessors方法。在方法执行之前,检查Spring内部的beanDefinitionMap(或类似的存储结构),发现其中并没有键为"x"的元素,这说明此时X类尚未被扫描和解析。

随着invokeBeanFactoryPostProcessors方法的执行,Spring开始扫描并解析带有注解的类。当扫描到X类时,它会将X类的信息解析为一个BeanDefinition对象,并将该对象以键为"x"的形式存储到beanDefinitionMap中。

完成扫描和解析后,我再次检查了beanDefinitionMap,发现其中已经包含了键为"x"的元素,其对应的值正是一个BeanDefinition对象。然而,此时并未发现控制台输出"X Constructor",这说明尽管X类已经被扫描并解析为一个BeanDefinition对象,但X类的实例并未被创建。

这个例子清晰地展示了Spring框架在初始化过程中的一个关键步骤:首先扫描并解析带有注解的类,将其信息存储为BeanDefinition对象并放入beanDefinitionMap中;随后,在适当的时候(如依赖注入时),才会根据这些BeanDefinition对象来实例化相应的类。关于beanDefinitionMap的详细工作原理和用途,将在后续的文章中进一步探讨。在本文中,读者只需理解它是一个专门用于存储BeanDefinition对象的集合即可。

在这里插入图片描述
在这里插入图片描述

④在Spring将类所对应的BeanDefinition对象存储到Map之后,它会进一步调用程序员提供的Bean工厂后置处理器BeanFactoryPostProcessor。那么,什么是BeanFactoryPostProcessor呢?在Spring的架构中,BeanFactoryPostProcessor是一个接口,用于定义对BeanFactory进行额外处理或修改的逻辑。任何实现了该接口的类都可以被视为一个BeanFactoryPostProcessor。关于BeanFactoryPostProcessor接口的详细源码解析,我们将在后续的文章中深入探讨。在此,我们先简要概述其基本作用。

值得注意的是,Spring内部也实现了BeanFactoryPostProcessor接口,例如ConfigurationClassPostProcessor。该类在Spring源码中扮演着举足轻重的角色,它负责执行诸如扫描配置类、解析配置信息等重要功能。在我看来,ConfigurationClassPostProcessor是理解Spring源码过程中不可或缺的关键类之一。由于其功能复杂且广泛,我们将在后续的分析中逐一深入探讨。现在,让我们先来看一下这个类的类结构图,以便对其有一个初步的了解。
在这里插入图片描述
ConfigurationClassPostProcessor作为Spring框架中的一个核心组件,实现了多个接口,其中与本文直接相关的是BeanDefinitionRegistryPostProcessor和BeanFactoryPostProcessor。尽管BeanDefinitionRegistryPostProcessor继承了BeanFactoryPostProcessor,但为了更好地理解其角色和用途,我们将其视为两个独立的接口。

在Spring的启动过程中,ConfigurationClassPostProcessor发挥了关键作用。具体来说,当Spring执行①②③步骤时,它实际上是调用了ConfigurationClassPostProcessor中实现的BeanDefinitionRegistryPostProcessor接口的postProcessBeanDefinitionRegistry方法。这一步骤负责处理与BeanDefinition注册相关的逻辑。

进入第④步时,Spring会执行BeanFactoryPostProcessor接口的postProcessBeanFactory方法。这里需要明确的是,Spring首先会调用ConfigurationClassPostProcessor自身的postProcessBeanFactory方法,这是因为它本身也实现了该接口。之后,如果应用程序开发者提供了自定义的BeanFactoryPostProcessor实现,Spring也会依次调用这些自定义的postProcessBeanFactory方法。

为了更清晰地表达这一过程,我们注意到第④步在图中是以红色虚线表示的,这是因为这一步的执行依赖于开发者是否提供了自定义的BeanFactoryPostProcessor。然而,不论开发者是否提供了自定义实现,Spring都会执行其内置的BeanFactoryPostProcessor,即ConfigurationClassPostProcessor。因此,图表在描述第④步时确实可以进一步细化,以明确表示Spring对内置和自定义BeanFactoryPostProcessor的执行。

综上所述,Spring在启动过程中通过ConfigurationClassPostProcessor的BeanDefinitionRegistryPostProcessor和BeanFactoryPostProcessor接口实现了对BeanDefinition的注册和Bean工厂的后处理。无论是内置的还是自定义的BeanFactoryPostProcessor,都在Spring的启动流程中扮演着重要角色。

重点:我们用自己的话总结一下BeanFactoryPostProcessor的执行时机(不管内置的还是程序员提供):

BeanFactoryPostProcessor的执行时机是在Spring容器创建并初始化Bean定义之后,但在实例化任何Bean之前。这是一个关键的扩展点,允许开发者在Spring容器完成基本的Bean定义加载和解析之后,对BeanFactory的内容进行额外的处理或修改。

无论是Spring内置的BeanFactoryPostProcessor实现(如ConfigurationClassPostProcessor),还是程序员自己提供的自定义BeanFactoryPostProcessor实现,它们的postProcessBeanFactory方法都会在容器准备实例化Bean之前被调用。

具体来说,当Spring容器启动时,它会首先读取配置文件或注解信息,并将这些配置转化为内部的Bean定义(BeanDefinition)。完成这一步之后,Spring会查找所有实现了BeanFactoryPostProcessor接口的类,并依次调用它们的postProcessBeanFactory方法。这些处理器可以对BeanFactory中的Bean定义进行修改、添加或删除,从而影响最终的Bean实例化过程。

因此,BeanFactoryPostProcessor为开发者提供了一个在Spring容器启动过程中干预Bean定义的机会,使得开发者能够根据自己的需求对容器进行定制和扩展。

那么第④步当中提到的执行程序员提供的BeanFactoryPostProcessor到底有什么意义呢?程序员提供BeanFactoryPostProcessor的场景在哪里?有哪些主流框架这么干过呢?

首先回答第一个问题,意义在哪里?可以看一下这个接口的方法签名:

void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) 
throws BeansException;

其实讨论这个方法的意义就是讨论BeanFactoryPostProcessor的作用或者说意义,参考这个方法的一句:

Modify the application context’s internal bean factory after its standard initialization
在应用程序上下文的标准初始化之后修改内部的BeanFactory

在Spring框架中,BeanFactoryPostProcessor(此处特指直接实现此接口的后置处理器,不涉及BeanDefinitionRegistryPostProcessor)被视为一个关键的扩展点。Spring框架通过提供这样的扩展点,旨在赋予开发者对BeanFactory初始化过程进行干预的能力,这是深入学习Spring源码并对其进行二次开发或构建优雅插件的重要一环。需要特别强调的是,这里的核心焦点在于“初始化过程”,而非“实例化过程”。这两者在Spring框架的上下文中具有显著的区别,特别是在阅读Spring源码时,应特别留意这两个术语的使用。

深入Spring的源码,我们会发现整个容器初始化过程实质上是由各种后置处理器(PostProcessor)的调用序列所构成的。这些后置处理器大致可以分为两类:一类关注于bean的实例化过程,而另一类则关注于bean的初始化过程。这种分类并非主观臆想,而是基于Spring框架对初始化和实例化概念的明确区分。熟悉Spring后置处理器体系的开发者,从其命名规则中就能洞察到Spring对这两者之间的严格区分。

严谨地阐述,BeanFactoryPostProcessor的主要作用并不在于干预BeanFactory的实例化过程,即BeanFactory如何被创建(new)出来。然而,一旦BeanFactory实例化完成,BeanFactoryPostProcessor便能够介入其后的属性配置和修改阶段,也就是我们通常所说的“初始化”过程。

具体来说,BeanFactoryPostProcessor接口中定义的方法postProcessBeanFactory接受一个类型为ConfigurableListableBeanFactory的参数,这个参数代表了已经实例化并配置好的BeanFactory对象。由于Spring在调用postProcessBeanFactory方法时传递的是已经准备好的beanFactory实例,因此开发者可以在这个方法中对BeanFactory进行进一步的配置或修改。

这意味着,通过实现BeanFactoryPostProcessor接口并覆盖postProcessBeanFactory方法,开发者可以定制BeanFactory的初始化过程,例如添加、修改或删除bean定义,调整bean的属性等。然而,需要强调的是,在操作过程中应当遵循Spring框架的设计原则和最佳实践,以确保系统的稳定性和可维护性。

在处理BeanFactory对象时,仅仅进行简单的打印(如使用System.out.println)是远远不够的,这种做法并不能充分发挥BeanFactory的功能,更不能称之为“肆意妄为”。实际上,BeanFactory提供了丰富的功能和API供开发者使用,但我们必须先深入理解其特性和API,才能有效地利用它。

尽管BeanFactory本身具有复杂性,不在此展开详细讨论,但与本文内容紧密相关的是beanDefintionMap(存储BeanDefinition的集合),这个重要的数据结构就定义在BeanFactory中。BeanFactory也提供了相应的API供程序员操作这个Map,比如允许我们修改其中已存在的BeanDefinition对象,或者向其中添加新的BeanDefinition对象。这些操作都需要开发者对BeanFactory和BeanDefinition有深入的了解,以确保在操作时遵循最佳实践,并保持系统的稳定性和可维护性。

@Component
public class TestBeanFactoryPostPorcessor implements BeanFactoryPostProcessor {@Overridepublic void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {// 转换为子类,因为父类没有添加beanDefintion对象的apiDefaultListableBeanFactory defaultbf =
(DefaultListableBeanFactory) beanFactory;// new一个Y的beanDefinition对象,方便测试动态添加GenericBeanDefinition y= new GenericBeanDefinition();y.setBeanClass(Y.class);// 添加一个beanDefinition对象,原本这个Y没有被spring扫描到defaultbf.registerBeanDefinition("y", y);// 得到一个已经被扫描出来的beanDefintion对象x// 因为X本来就被扫描出来了,所以是直接从map中获取BeanDefinition x = defaultbf.getBeanDefinition("x");// 修改这个X的beanDefintion对象的class为Z// 原本这个x代表的class为X.class;现在为Z.classx.setBeanClassName("com.luban.beanDefinition.Z");}}

在项目中,我们定义了三个类:X、Y和Z。其中,仅有类X被标注了@Component注解。因此,在代码执行到特定方法时,Spring容器仅扫描并识别到了类X,导致BeanFactory中的beanDefinitionMap仅包含与类X相对应的BeanDefinition对象。

为了演示如何动态添加自定义的BeanDefinition对象,笔者首先手动创建了一个与类Y相对应的BeanDefinition实例,并通过调用registerBeanDefinition(“y”, yBeanDefinition)方法,将其注册到beanDefinitionMap中。这一步骤展示了如何向Spring容器中动态添加一个自行实例化的BeanDefinition对象。

随后,为了演示如何动态修改已扫描的BeanDefinition对象,笔者通过调用getBeanDefinition(“x”)方法获取了与类X相对应的已存在BeanDefinition对象。然后,通过调用xBeanDefinition.setBeanClassName(“Z”)方法,将原本与类X关联的BeanDefinition对象所指向的类更改为Z。这一步骤展示了如何在运行时动态修改Spring容器中已扫描和注册的BeanDefinition对象。

public static void main(String[] args) {AnnotationConfigApplicationContext ac =
new AnnotationConfigApplicationContext();ac.register(Appconfig.class);ac.refresh();//正常打印System.out.println(ac.getBean(Y.class));//正常打印System.out.println(ac.getBean(Z.class));//异常打印//虽然X加了注解,但是被偷梁换柱了,故而异常System.out.println(ac.getBean(X.class));}

总结上述图示内容,Spring实例化一个Bean的过程并非直接与你所提供的类相关,而是与特定的BeanDefinition对象所映射的类直接相关。通常情况下,一个BeanDefinition对象对应一个类,但特殊情况下也可能存在不同映射关系。为了更形象地解释这一点,可以借用一个比喻:假设读者你喜欢一位女性(小A),而小A则对笔者有所好感。然而,你不能因此就推断你也喜欢笔者,因为两者之间的喜好关系并不具有传递性。此外,需要强调的是,笔者在此仅作为比喻中的一个角色,与现实中的性别倾向无关,这一点应明确区分。

综上所述,BeanFactoryPostProcessor是Spring框架中一个强大的扩展点,允许程序员在Bean生命周期的特定阶段对BeanFactory进行深入的定制和修改。通过合理地利用这一特性,程序员可以构建出更加灵活、可维护的Spring应用。

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

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

相关文章

电气工程VR虚拟仿真实训平台以趣味化方式增强吸引力

在工业4.0时代和教育信息化的双重推动下,我们致力于推动实训课件的跨界合作与共创。VR实训课件不仅促进了不同领域、不同行业之间的紧密合作,更让学习变得生动直观。我们凭借3D技术生动、直观、形象的特点,开发了大量配套3D教材,让…

CSS 【实用教程】(2024最新版)

CSS 简介 CSS 是层叠样式表( Cascading Style Sheets ) 的简写,用于精确控制 HTML 页面的样式,以便更好地展示图文信息或产生炫酷/友好的交互体验。 没有必要让所有浏览器都显示得一模一样的,好的浏览器有更好的显示,糟糕的浏览器…

C\C++ 终端输出带有颜色的字符

终端显示带有颜色的字符 终端显示带有颜色的字符 终端显示带有颜色的字符背景:测试机器,win10系统, VS2022编写字体设置不同的颜色背景色光标移动 (这个用的估计不是很多)字体设置动态显示C cout 也可以测试代码准确的…

【C++】继承(二)

目录 5、继承与友元 6、继承与静态成员 7、复杂的菱形继承和菱形虚拟继承 8、继承的总结与反思 5、继承与友元 友元关系不能继承,也就是说父类的友元不能访问子类的私有或保护的成员 class Student; class Person { public:friend void Display(const Person&a…

.net C# 使用网易163邮箱搭建smtp服务,实现发送邮件功能

功能描述:使用邮箱验证实现用户注册激活和找回密码。邮箱选择网易163作为smtp服务器。 真实测试情况:第一种:大部分服务器运行商的25端口默认是封禁的,可以联系运营商进行25端口解封,解封之后可以使用25端口。第二种&…

【Pytorch】Conda环境下载慢换源/删源/恢复默认源

文章目录 背景临时换源永久换源打开conda配置condarc换源执行配置 命令行修改源添加源查看源 删源恢复默认源使用示范 背景 随着实验增多,需要分割创建环境的情况时有出现,在此情况下使用conda create --name xx python3.10 pytorch torchvision pytorc…

文件读写操作之c语言、c++、windows、MFC、Qt

目录 一、前言 二、c语言文件读写 1.写文件 2.读文件 三、c文件读写 1.写文件 2.读文件 四、windows api文件读写 1.写文件 2.读文件 五、MFC文件读写 1.写文件 2.读文件 六、Qt文件读写 1.写文件 2.读文件 七、总结 一、前言 我们在学习过程中&#xff0c…

OpenCV解决验证码(数字和字母)识别(Python)

文章目录 前言一、准备验证码图片 前言 OpenCV是一个基于Apache2.0许可(开源)发行的跨平台计算机视觉和机器学习软件库。它支持Windows、Linux、Mac OS、Android和iOS等多个操作系统,提供了丰富的图像处理和计算机视觉功能,包括但…

链路追踪系列-01.mac m1 安装zipkin

下载地址:https://hub.docker.com/r/openzipkin/zipkin jelexjelexxudeMacBook-Pro zipkin-server % pwd /Users/jelex/Documents/work/zipkin-server 先启动Es: 可能需要先删除 /Users/jelex/dockerV/es/plugins 目录下的.DS_Store 当端口占用时再次启动&#x…

Qt+ESP32+SQLite 智能大棚

环境简介 硬件环境 ESP32、光照传感器、温湿度传感器、继电器、蜂鸣器 基本工作流程 上位机先运行,下位机启动后尝试连接上位机连接成功后定时上报传感器数据到上位机,上位机将信息进行处理展示判断下位机传感器数据,如果超过设置的阈值&a…

【Wamp】局域网设备访问WampServer | 使用域名访问Wamp | Wamp配置HTTPS

局域网设备访问WampServer 参考&#xff1a;https://www.jianshu.com/p/d431a845e5cb 修改Apache的httpd.conf文件 D:\Academic\Wamp\program\bin\apache\apache2.4.54.2\conf\httpd.conf 搜索 Require local 和Require all denied&#xff0c;改为Require all granted <…

【排序算法】计数排序

目录 一.基本思想 二.缺陷及优化 三.代码实现 四.特性总结 1.可以排序负数 2.适合范围集中的整数 3.时间复杂度&#xff1a;O(Nrange) 4.空间复杂度&#xff1a;O(range) 5.稳定性&#xff1a;稳定 一.基本思想 根据待排序数组a创建一个新的数组count&#xff0c;该数组…

python--实验 11 模块

目录 知识点 模块基础 模块使用方式 自定义模块示例 模块的有条件执行 Python包结构 定义和导入包 常用第三方库及安装 实例代码 第三方库自动安装脚本 Python标准库介绍 PyInstaller 小结 实验 1.(基础题)制作文本进度条。 2.(基础题) 蒙特卡罗方法计算圆周率…

nginx正向代理、反向代理、负载均衡

nginx.conf nginx首要处理静态页面 反向代理 动态请求 全局模块 work processes 1; 设置成服务器内核数的两倍&#xff08;一般不不超过8个超过8个反而会降低性能一般4个 1-2个也可以&#xff09; netstat -antp | grep 80 查端口号 *1、events块&#xff1a;* 配置影响ngi…

深度学习基础:Numpy 数组包

数组基础 在使用导入 Numpy 时&#xff0c;通常给其一个别名 “np”&#xff0c;即 import numpy as np 。 数据类型 整数类型数组与浮点类型数组 为了克服列表的缺点&#xff0c;一个 Numpy 数组只容纳一种数据类型&#xff0c;以节约内存。为方便起见&#xff0c;可将 Nu…

Linux多线程编程-生产者与消费者模型详解与实现(C语言)

1.什么是生成者与消费者模型 生产者-消费者模型是并发编程中的经典问题&#xff0c;描述了多个线程&#xff08;或进程&#xff09;如何安全、有效地共享有限的缓冲区资源。在这个模型中&#xff0c;有两种角色&#xff1a; 生产者&#xff08;Producer&#xff09;&#xff1…

Docker 安装ros 使用rviz 等等图形化程序

Docker 安装ros 使用rviz 等等图形化程序 ubuntu 版本与ros 发行版本对应 如何安装其它版本ros 此时考虑使用docker 易于维护 地址&#xff1a; https://hub.docker.com/r/osrf/ros 我主机是 ubuntu22.04 使用这个标签 melodic-desktop-full 1 clone 镜像到本机 docker pu…

OpenCV:python图像旋转,cv2.getRotationMatrix2D 和 cv2.warpAffine 函数

前言 仅供个人学习用&#xff0c;如果对各位朋友有参考价值&#xff0c;给个赞或者收藏吧 ^_^ 一. cv2.getRotationMatrix2D(center, angle, scale) 1.1 参数说明 parameters center&#xff1a;旋转中心坐标&#xff0c;是一个元组参数(col, row) angle&#xff1a;旋转角度…

html(抽奖设计)

<!DOCTYPE html> <html><head><meta charset"UTF-8"><title>抽奖</title><style type"text/css">* {margin: 0;padding: 0;}.container {width: 800px;height: 800px;border: 1px dashed red;position: absolut…

<数据集>光伏板缺陷检测数据集<目标检测>

数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;2400张 标注数量(xml文件个数)&#xff1a;2400 标注数量(txt文件个数)&#xff1a;2400 标注类别数&#xff1a;4 标注类别名称&#xff1a;[Crack,Grid,Spot] 序号类别名称图片数框数1Crack8688922Grid8248843S…