SpringBoot 自动配置@EnableAutoConfiguration

news2024/9/25 17:20:08
自动配置vs自动装配

Spring有两个名字听起来相似的概念:一个是自动装配,一个是自动配置。他们两个只是听起来相似,实际根本不同。自动装配是autowire,自动配置是autoconfiguration,他们之间没有任何关系,概念也相差甚远。

Springboot的自动配置通过@EnableAutoConfiguration注解实现,@EnableAutoConfiguration是Springboot的注解,通过@EnableAutoConfiguration注解,Springboot实现了“约定大于配置”:系统猜测你需要哪些配置,从而帮助你自动实现了这些配置,所以程序员就不需要手动再进行配置了。

这个说起来非常简单的约定,实际上却大大简化了程序员的日常工作,让Spring项目变的非常简单。

@SpringBootApplication

学习SpringBoot,还是要从SpringBoot的入门注解@SpringBootApplication开始:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {

可以看到@SpringBootApplication是一个组合注解,由@SpringBootConfiguration+@EnableAutoConfiguration+@ComponentScan组成。

@SpringBootConfiguration的主要作用其实就是实现@Configuration注解的功能,告诉Spring当前类是一个配置类。

然后再加上@ComponentScan注解,告诉Spring容器包扫描路径,由于我们在@SpringBootApplication注解上其实并没有指定包扫描路径,所以,Spring容器会采用默认的包扫描路径:当前配置类所在的路径。因此才有了一个约定:SpringBoot的启动类(其实准确的叫法应该是配置类,配置类不一定必须是启动类,编写一个其他的配置类送给SpringApplication的run方法也是一样可以启动SpringBoot的)要放在项目的主路径下,因为不放在主路径下、而是放在特定包下的话,就可能会导致除这个特定包之外的其他包下的类不能被Spring容器扫描到。

然后最重要的是这个@EnableAutoConfiguration注解,@EnableAutoConfiguration也是一个组合注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

主要包含两个注解:@AutoConfigurationPackage和@Import,比较重要的是这个@Import注解。

其实我们可以看到,SpringBoot项目启动时的几个注解实际上都是由Spring注解组合生成的,所以说SpringBoot其实不算是什么新技术,只不过是Spring框架的一个应用,而已。

@EnableAutoConfiguration

接下来我们要重点分析一下@EnableAutoConfiguration注解,因为他是实现“约定大于配置”思想的关键。

前面已经看到了,@EnableAutoConfiguration注解中最重要的其实是@Import注解,所以我们先对@Import注解做一个简单的了解,之后再回来深入学习@EnableAutoConfiguration注解。

@Import注解

@Import注解和@Configuration注解一起作用,放在@Configuration配置类中,作用是通过该配置类引入Bean到Spring IoC容器中。

比如:

@Configuration
@Import(Person.class)
public class MyConfiguration(){
}

public class Person(){
   @Bean
   public Person createPerson(){
      return new Person();
   }
}

以上配置类通过@Import注解将Person对象注入到SpringIoC容器中。

@Import需要通过参数value指定引入类。

@Import是在Spring Ioc容器刷新的时候、通过BeanFactoryPostProcessors生效的。具体的调用路径:

Spring容器refresh方法 -> AbstractApplicationContext.refresh()->
invokeBeanFactoryPostProcessors()->
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors->
ConfigurationClassPostProcessor.processConfigBeanDefinitions ->
ConfigurationClassParser.parse()->
processConfigurationClass()->
doProcessConfigurationClass()

调用链路特别长,代码也特别多,所以我们今天也就没有办法分析全部代码。但是看到这个doProcessConfigurationClass就感觉快要找到核心代码了,因为Spring源码的特点,doxxxx方法是干实事的。

确实,这个ConfigurationClassParser类是专门用来处理配置类的,也就是加了@Configuration注解的类,@Import注解也是这个类负责解析,这也是@Import注解必须和@Configuration注解绑定才能生效的原因。

doProcessConfigurationClass方法源码也特别长,首先处理@Component、@PropertySource、@ComponentScan注解,之后调用processImports方法处理@Import注解。

processImports方法是正儿八经处理@Import注解的地方。

代码也比较长,我们还是简单一点,只关心比较重要的部分:

for (SourceClass candidate : importCandidates) {
                    //1.如果@Import的类实现了ImportSelector接口
					if (candidate.isAssignable(ImportSelector.class)) {
						// Candidate class is an ImportSelector -> delegate to it to determine imports
						Class<?> candidateClass = candidate.loadClass();
						ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
								this.environment, this.resourceLoader, this.registry);
						Predicate<String> selectorFilter = selector.getExclusionFilter();
						if (selectorFilter != null) {
							exclusionFilter = exclusionFilter.or(selectorFilter);
						}
						if (selector instanceof DeferredImportSelector deferredImportSelector) {
							this.deferredImportSelectorHandler.handle(configClass, deferredImportSelector);
						}
						else {
							String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
							Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter);
							processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false);
						}
					}
                    //2.如果@Import的类实现了ImportBeanDefinitionRegistrar接口
					else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
						// Candidate class is an ImportBeanDefinitionRegistrar ->
						// delegate to it to register additional bean definitions
						Class<?> candidateClass = candidate.loadClass();
						ImportBeanDefinitionRegistrar registrar =
								ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
										this.environment, this.resourceLoader, this.registry);
						configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
					}
                    //3.@Import引入的是普通类
					else {
						// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
						// process it as an @Configuration class
						this.importStack.registerImport(
								currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
						processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);
					}
				}

代码中加了注释,其实主要关注的是@Import注解支持引入三种类型的类:

  1. 第一种是实现了ImportSelector接口的类,这种情况下又判断是否实现了DeferredImportSelector接口,会有不同的处理逻辑,实现了DeferredImportSelector接口的类,最终会调用到其内部接口DeferredImportSelector.Group的process方法,否则,如果没有实现DeferredImportSelector接口,则会调用到ImportSelector的getImports方法。
  2. 第二种是实现了ImportBeanDefinitionRegistrar接口的类,通过将BeanDefinition注册到Spring容器中从而实现bean加载。
  3. 第三种是普通类,可以直接加载到SpringIoC容器中。

需要注意的是第一种情况中的DeferredImportSelector接口:

A variation of ImportSelector that runs after all @Configuration beans have been processed. This type of selector can be particularly useful when the selected imports are @Conditional.
Implementations can also extend the org.springframework.core.Ordered interface or use the org.springframework.core.annotation.Order annotation to indicate a precedence against other DeferredImportSelectors.
Implementations may also provide an import group which can provide additional sorting and filtering logic across different selectors.
Since:
4.0
Author:
Phillip Webb, Stephane Nicoll

DeferredImportSelector接口实现类的延迟导入,在所有的@Configuration配置类全部处理之后再运行。这个特性对于@Conditional注解下的条件配置很有用。

实现类也可以扩展Order接口实现对延迟导入配置类的排序,也可以提供一个导入的group实现排序(Group是DeferredImportSelector的子接口)。

以上是DeferredImportSelector类的javaDoc的解释,基本上说清楚了DeferredImportSelector接口的作用,我们今天理解到这个程度就OK了,不影响我们对SpringBoot自动配置原理的理解了。

好了,对于@Import注解的学习就到这里了,有了对@Import注解的基本认识,我们就可以继续深入分析@EnableAutoConfiguration注解了。

@EnableAutoConfiguration

继续研究@EnableAutoConfiguration注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

@Import注解引入了AutoConfigurationImportSelector类,这个类是SpringBoot自动配置的关键实现类:

public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
		ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {

发现AutoConfigurationImportSelector类实现了DeferredImportSelector接口,根据上面我们对@Import注解的分析,Spring容器的refresh方法最终会调用到DeferredImportSelector.Group的process方法,我们需要从AutoConfigurationImportSelector源码中找一下process方法的实现。

发现AutoConfigurationImportSelector类实现了一个AutoConfigurationGroup类,该类实现了eferredImportSelector.Group接口,我们找到它的process方法:

		@Override
		public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
			Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector,
					() -> String.format("Only %s implementations are supported, got %s",
							AutoConfigurationImportSelector.class.getSimpleName(),
							deferredImportSelector.getClass().getName()));
			AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
				.getAutoConfigurationEntry(annotationMetadata);
			this.autoConfigurationEntries.add(autoConfigurationEntry);
			for (String importClassName : autoConfigurationEntry.getConfigurations()) {
				this.entries.putIfAbsent(importClassName, annotationMetadata);
			}
		}

process方法强转参数deferredImportSelector为AutoConfigurationImportSelector后,调用getAutoConfigurationEntry方法,拿到需要实例化并放入SpringIoC容器中的类的全限定名之后,返回给Spring容器(我们肯定知道Bean的实例化不会在当前阶段完成、一定是Spring IoC容器统一完成的(还记得吗,入口方法是getBean))。

getAutoConfigurationEntry方法源码会揭露SpringBoot自动装配实现机制的真相:

	protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return EMPTY_ENTRY;
		}
		AnnotationAttributes attributes = getAttributes(annotationMetadata);
        //找到自动配置,获取需要自动配置的类
		List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
        //去重
		configurations = removeDuplicates(configurations);
        //根据排除配置,进行排除
		Set<String> exclusions = getExclusions(annotationMetadata, attributes);
		checkExcludedClasses(configurations, exclusions);
		configurations.removeAll(exclusions);
        //filter方法,实现条件配置,根据条件排除掉不必要的配置
		configurations = getConfigurationClassFilter().filter(configurations);
		fireAutoConfigurationImportEvents(configurations, exclusions);
		return new AutoConfigurationEntry(configurations, exclusions);
	}

上面加了备注的方法是关键方法,我们一个个看一下。

getCandidateConfigurations

从方法名上可以看出,是要获取到备选的配置项。

代码非常简单:

	protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
		List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
				getBeanClassLoader());
		Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
				+ "are using a custom packaging, make sure that file is correct.");
		return configurations;
	}

关键方法loadFactoryNames:

  public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
        ClassLoader classLoaderToUse = classLoader;
        if (classLoader == null) {
            classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
        }

        String factoryTypeName = factoryType.getName();
        return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
    }

又调用了方法loadSpringFactories:

private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
        Map<String, List<String>> result = (Map)cache.get(classLoader);
        if (result != null) {
            return result;
        } else {
            HashMap result = new HashMap();

            try {
                //读取配置文件META-INF/spring.factories
                Enumeration urls = classLoader.getResources("META-INF/spring.factories");

                while(urls.hasMoreElements()) {
                    URL url = (URL)urls.nextElement();
                    UrlResource resource = new UrlResource(url);
                    Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                    Iterator var6 = properties.entrySet().iterator();

                    while(var6.hasNext()) {
                        Entry<?, ?> entry = (Entry)var6.next();
                        String factoryTypeName = ((String)entry.getKey()).trim();
                        String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
                        String[] var10 = factoryImplementationNames;
                        int var11 = factoryImplementationNames.length;

                        for(int var12 = 0; var12 < var11; ++var12) {
                            String factoryImplementationName = var10[var12];
                            ((List)result.computeIfAbsent(factoryTypeName, (key) -> {
                                return new ArrayList();
                            })).add(factoryImplementationName.trim());
                        }
                    }
                }

                result.replaceAll((factoryType, implementations) -> {
                    return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
                });
                cache.put(classLoader, result);
                return result;
            } catch (IOException var14) {
                throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
            }
        }
    }

我们先有一个认识:类加载器可以在当前类加载上下文环境下找到指定位置的配置文件。

代码虽然比较长,但是实现的功能比较简单:通过类加载器读取配置文件“META-INF/spring.factories”,加工配置文件内容、放入Map之后返回。

我们简单看一眼META-INF/spring.factories文件的内容:
在这里插入图片描述

其中的org.springframework.boot.autoconfigure.AutoConfigurationImportFilter、以及org.springframework.boot.autoconfigure.EnableAutoConfiguration是key,后面的内容是value(是一个list),读取该文件的配置后最终以map的形式返回。

这样的话,getCandidateConfigurations方法就通过类加载器返回配置在META-INF/spring.factories文件中的内容并返回。

这段代码逻辑其实就是SpringBoot自动配置的核心内容,SpringBoot帮助程序员完成配置的机密就在这个META-INF/spring.factories文件中,其实如果没有META-INF/spring.factories文件,SpringBoot也不可能知道他究竟要自动加载哪些Bean!

getExclusions

getExclusions方法完成的功能比较简单,就是检查@Import注解是否有exclude的配置,根据配置对返回的自动配置结合进行排除。

getConfigurationClassFilter().filter

getConfigurationClassFilter().filter方法也非常关键,因为配置文件META-INF/spring.factories中的自动配置类非常多,但是我们的项目中不一定需要,比如RabbitMQ,在META-INF/spring.factories文件中就有关于RabbitMQ的配置,但是如果我们的项目不需要mq的话,SpringBoot就没有必要加载他。

SpringBoot就是通过filter方法来过滤上述不必要加载的组件的。过滤的方法是:

  1. SpringBoot通过META-INF/spring.factories配置文件指定了AutoConfigurationImportFilter过滤器
  2. filter方法中通过过滤器,以及各组件的自动配置类完成过滤

比如,RabbitMQ的自动配置类:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ RabbitTemplate.class, Channel.class })
@EnableConfigurationProperties(RabbitProperties.class)
@Import({ RabbitAnnotationDrivenConfiguration.class, RabbitStreamConfiguration.class })
public class RabbitAutoConfiguration {

其中@ConditionalOnClass注解,指定如果当前项目中有RabbitTemplate.class, 以及Channel.class,两个类存在的情况下就执行自动配置,否则,如果不存在的话,RabbitMQ的自动配置类RabbitAutoConfiguration就会被这个filter方法过滤掉,不会被Spring容器加载进来。

而这个“过滤掉”的具体工作,就是由AutoConfigurationImportFilter过滤器完成的。

OK,SpringBoot自动配置的实现原理就搞清楚了!

但是还是有必要提一下SpringBoot3.0,实现自动配置的逻辑和我们上面看到的有变化。

SpringBoot3.0

如果你的项目使用的是SpringBoot3.0,你会发现我上面写的这些内容是在胡说。

因为压根就没有META-INF/spring.factories这个东西!

META-INF/spring.factories其实是在SpringBoot2.7之后就被移除了。

看一下源码,从getCandidateConfigurations开始:

	protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
		List<String> configurations = ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader())
			.getCandidates();
		Assert.notEmpty(configurations,
				"No auto configuration classes found in "
						+ "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
						+ "are using a custom packaging, make sure that file is correct.");
		return configurations;
	}

关键是这个ImportCandidates.load方法:

	public static ImportCandidates load(Class<?> annotation, ClassLoader classLoader) {
		Assert.notNull(annotation, "'annotation' must not be null");
        //获取类加载器
		ClassLoader classLoaderToUse = decideClassloader(classLoader);
		String location = String.format(LOCATION, annotation.getName());
		//在类加载器的帮助下,获取到指定位置的配置
        Enumeration<URL> urls = findUrlsInClasspath(classLoaderToUse, location);
		List<String> importCandidates = new ArrayList<>();
		while (urls.hasMoreElements()) {
			URL url = urls.nextElement();
			importCandidates.addAll(readCandidateConfigurations(url));
		}
		return new ImportCandidates(importCandidates);
	}

要找到的就是location指定的文件,其中LOCATION定义为:

	private static final String LOCATION = "META-INF/spring/%s.imports";

发现配置文件META-INF/spring.factories被META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports取代!

这个变化对既有项目的影响很大!因为虽然SpringBoot自己的自动配置很容易就完成了修改,但是你项目中使用的那些第三方包不一定能够无缝支持!

有关第三方包的自动配置,我们下一篇文章分析。

上一篇 Spring MVC 十一:@EnableWebMvc

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

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

相关文章

ARM 版 OpenEuler 22.03 部署 KubeSphere v3.4.0 不完全指南

作者&#xff1a;运维有术 前言 知识点 定级&#xff1a;入门级KubeKey 安装部署 ARM 版 KubeSphere 和 KubernetesARM 版 KubeSphere 和 Kubernetes 常见问题 实战服务器配置 (个人云上测试服务器) 主机名IPCPU内存系统盘数据盘用途ks-master-1172.16.33.1661650200KubeSp…

翻页电子相册如何制作?看到就是赚到

在日常生活中&#xff0c;我们拿起手机随手一拍&#xff0c;即可记录美丽瞬间。但随着手机里的照片越来越多&#xff0c;这时该怎么办呢&#xff1f;相信很多小伙伴会选择把照片装订成相册&#xff0c;不过&#xff0c;时间一长也都成了压箱底&#xff0c;无人翻看。但除了这一…

sheng的学习笔记-【中】【吴恩达课后测验】Course 3 - 结构化机器学习项目 - 第一周测验

课程3_第1周_测验题 目录&#xff1a;目录 要解决的问题 ① 这个例子来源于实际项目&#xff0c;但是为了保护机密性&#xff0c;我们会对细节进行保护。 ② 现在你是和平之城的著名研究员&#xff0c;和平之城的人有一个共同的特点&#xff1a;他们害怕鸟类。 ③ 为了保护…

MyBatis-Plus 通过updateById更新日期null

date类型的字段为 一、需求&#xff1a; 有时候需要将页面日期重新赋值空&#xff0c;但是Mybatis Plus 默认情况下&#xff0c;baseMapper.updateById方法&#xff0c;当doman中字段值为null&#xff0c;后端并会不更新这个字段 解决方法&#xff1a; 对应的实体类的属性加…

浅谈智能制造

智能制造 如今&#xff0c;同一版本同一型号的手机&#xff0c;几乎是一模一样的。当我们说去选购商品&#xff0c;其实是在有限的型号中选择我们需要的那一款。可是&#xff0c;人的需求千变万化&#xff0c;为什么偏偏要归结到几个固定的型号上去呢&#xff1f;每个人不应该…

2023/10/25MySQL学习

外键约束 在子表添加外键后,不能在主表删除或更新记录,因为存在外键关联 删除外键,注意外键名称时我们添加外键时起的名称 使用cascade操作后,可以操作主表数据,并且子表的外键也会对应改变 set null的话,删除主表对应主键信息后,子表对应外键信息变为空 多表关系 创建中间表 可…

JavaScript进阶 第四天笔记——深浅拷贝、this绑定、防抖节流

JavaScript 进阶 - 第4天 深浅拷贝 浅拷贝 首先浅拷贝和深拷贝只针对引用类型 浅拷贝&#xff1a;拷贝的是地址 常见方法&#xff1a; 拷贝对象&#xff1a;Object.assgin() / 展开运算符 {…obj} 拷贝对象拷贝数组&#xff1a;Array.prototype.concat() 或者 […arr] 如…

NSSCTF做题第9页(3)

[GKCTF 2020]CheckIN 代码审计 这段代码定义了一个名为ClassName的类&#xff0c;并在脚本的最后创建了一个ClassName类的实例。 在ClassName类的构造函数中&#xff0c;首先通过调用$this->x()方法获取了请求参数$_REQUEST中的值&#xff0c;并将其赋值给$this->code属性…

Android [SPI,AutoSerivce,ServiceLoader]

记录一下在Android中使用SPI的过程。 1.项目gralde文件。 plugins {id kotlin-kapt } dependencies {implementation com.google.auto.service:auto-service:1.0-rc7 kapt "com.google.auto.service:auto-service:1.0-rc7" } 这个AutoServ…

【Qt】绘图与绘图设备

文章目录 绘图设备QPainter绘图实例案例1案例2-高级设置案例3&#xff1a;利用画家画资源图片 点击按钮移动图片 QtPaintDevice实例Pixmap绘图设备QImage 绘图设备QPicture 绘图设备 QPainter绘图 Qt 的绘图系统允许使用相同的 API 在屏幕和其它打印设备上进行绘制。整个绘图系…

外卖跑腿小程序开发如何满足不断变化的用户需求?

外卖跑腿小程序市场竞争激烈&#xff0c;用户需求不断演变。为了保持竞争力&#xff0c;开发团队需要不断适应变化&#xff0c;提供新功能和改进用户体验。本文将讨论如何通过技术手段来满足不断变化的用户需求。 1. 灵活的后端服务 后端服务是外卖跑腿小程序的核心&#xf…

Sui主网升级至V1.12.2版本

其他升级要点如下所示&#xff1a; #14305 使Sui能够验证zkLogin历史地址&#xff08;通过已填充的address_seed派生&#xff09;。升级协议版本至29&#xff0c;以启用对历史地址的验证。 #14100 修复了verify_zklogin_id函数中的错误&#xff0c;该函数按预期返回一个Ver…

Flutter extended_image库设置内存缓存区大小与缓存图片数

ExtendedImage ExtendedImage 是一个Flutter库&#xff0c;用于提供高级图片加载和显示功能。这个库使用了 image 包来进行图片的加载和缓存。如果你想修改缓存大小&#xff0c;你可以通过修改ImageCache的配置来实现。 1. 获取ImageCache实例: 你可以通过PaintingBinding…

Ansible简介

环境 控制节点&#xff1a;Ubuntu 22.04Ansible 2.10.8管理节点&#xff1a;CentOS 8 组成 Ansible环境主要由三部分组成&#xff1a; 控制节点&#xff08;Control node&#xff09;&#xff1a;安装Ansible的节点&#xff0c;在此节点上运行Ansible命令管理节点&#xff…

【剑指Offer】38.字符串的排列

题目 输入一个长度为 n 字符串&#xff0c;打印出该字符串中字符的所有排列&#xff0c;你可以以任意顺序返回这个字符串数组。 例如输入字符串ABC,则输出由字符A,B,C所能排列出来的所有字符串ABC,ACB,BAC,BCA,CBA和CAB。 数据范围&#xff1a;n<10 要求&#xff1a;空间复…

基于springboot实现篮球竞赛预约平台管理系统项目【项目源码+论文说明】

基于springboot实现篮球竞赛预约平台管理系统演示 摘要 随着信息化时代的到来&#xff0c;管理系统都趋向于智能化、系统化&#xff0c;篮球竞赛预约平台也不例外&#xff0c;但目前国内仍都使用人工管理&#xff0c;市场规模越来越大&#xff0c;同时信息量也越来越庞大&…

【图解数据结构】手把手教你如何实现顺序表(超详细)

&#x1f308;个人主页&#xff1a;聆风吟 &#x1f525;系列专栏&#xff1a;数据结构、算法模板、汇编语言 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 一. ⛳️线性表1.1 &#x1f514;线性表的定义1.2 &#x1f514;线性表的存储结构 二. ⛳️…

JavaScript进阶 第三天笔记

JavaScript 进阶 - 第3天笔记 了解构造函数原型对象的语法特征&#xff0c;掌握 JavaScript 中面向对象编程的实现方式&#xff0c;基于面向对象编程思想实现 DOM 操作的封装。 了解面向对象编程的一般特征掌握基于构造函数原型对象的逻辑封装掌握基于原型对象实现的继承理解什…

对比Vue2和Vue3的自定义指令

一、自定义指令简介 自定义指令是Vue提供的能力,用于注册自定义的指令,从而实现一些自定义的DOM操作。 二、Vue2中自定义指令 在Vue2中,自定义指令通过全局方法Vue.directive()进行注册: // 注册全局指令v-focus Vue.directive(focus, {inserted: function(el) {el.focus()…