@Import注解源码解析

news2025/4/26 21:29:15

文章目录

  • 一、简介
  • 二、@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 content
		if (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 classes
	if (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 first
		processMemberClasses(configClass, sourceClass);
	}

	// Process any @PropertySource annotations
	for (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 annotations
	Set<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 immediately
			Set<BeanDefinitionHolder> scannedBeanDefinitions =
					this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
			// Check the set of scanned definitions for any further config classes and parse recursively if needed
			for (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 annotations
	processImports(configClass, sourceClass, getImports(sourceClass), true);

	// Process any @ImportResource annotations
	AnnotationAttributes 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 methods
	Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
	for (MethodMetadata methodMetadata : beanMethods) {
		configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
	}

	// Process default methods on interfaces
	processInterfaces(configClass, sourceClass);

	// Process superclass, if any
	if (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 recurse
			return sourceClass.getSuperClass();
		}
	}

	// No superclass -> processing is complete
	return 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 imports
					Class<?> 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 definitions
					Class<?> 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 class
					this.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.coloradmin.cn/o/715966.html

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

相关文章

C++实现打包工具代码框架+多种设计模式以及C++特性(附源码)

C++常用功能源码系列 文章目录 C++常用功能源码系列前言一、打包工具二、packtool框架前言 本文是C/C++常用功能代码封装专栏的导航贴。部分来源于实战项目中的部分功能提炼,希望能够达到你在自己的项目中拿来就用的效果,这样更好的服务于工作实践。 专栏介绍:专栏讲本人近…

沟通漏斗模型

沟通漏斗模型 沟通漏斗理论|是指信息在用语言这种形式&#xff0c;传递在过程中&#xff0c;逐渐衰减的现象。 模型介绍 「沟通漏斗模型」很多时候你说出来的≠别人听到的&#xff01;对沟通者来说&#xff0c;如果心里想的是100%的信息&#xff0c;受限于语言表达的技巧&…

海康威视(Hikvision) 摄像头-CVE-2017-7921漏洞复现

漏洞描述 许多HikvisionIP摄像机包含一个后门&#xff0c;允许未经身份验证的模拟任何配置的用户帐户。 漏洞复现 访问漏洞url 检索用户与用户列表 ttp://your-ip/Security/users?authYWRtaW46MTEK 从下图可知该摄像只有admin一个账户 获取监控快照 http://your-ip/onvif…

北京阿里云代理商:阿里云CDN流量包的价格和流量消耗计算?

标题&#xff1a;阿里云CDN流量包的价格和流量消耗计算&#xff1f;成本控制策略 本文由阿里云代理商[上海聚搜信息技术有限公司] 撰写。 面对企业或个人用户在使用阿里云CDN进行内容分发时&#xff0c;常常会有一个疑问&#xff0c;那就是如何计算阿里云CDN流量包的价格以及流…

社区底商需要加入团购?社区团购的运营到底应该怎么做?

社区团购作为越来越多社区底商老板的“副业”的模式&#xff0c;社区团购为门店带来的巨大盈利增长点&#xff0c;但传统门店盈利单一、客流有限等问题依然严重&#xff0c;门店改牌也是迟早的事情。 对于传统门店来说&#xff0c;生死存亡下想要转型线上&#xff0c;往往面临开…

TCP之延时Nagle算法实验详解

TCP/IP协议中&#xff0c;无论发送多少数据&#xff0c;总是要在数据前面加上协议头&#xff0c;同时&#xff0c;对方接收到数据&#xff0c;也需要发送ACK表示确认。为了尽可能的利用网络带宽&#xff0c;TCP总是希望尽可能的发送足够大的数据。&#xff08;一个连接会设置MS…

系统盘崩溃挂盘进入救援模式选择continue无法进入

问题&#xff1a; 系统盘崩溃挂盘进入救援模式选择continue无法进入&#xff0c;始终读码或者黑屏。 原因&#xff1a; 出现这个界面一般是在选择 "1 Continue" 后&#xff0c;某些文件系统没有成功 mount导致 解决方法&#xff1a; 再次进入救援模式&#xff0c…

(五)人工智能应用--深度学习原理与实战--Linux系统Tensorflow平台搭建

作为使用最广泛的深度学习框架,TensorfLow支持Windows、Linux、MacOs等多种操作系统。Linux系统作为服务器部署环境十分常见&#xff0c;本章我们将学习在Linux操作系统下Tensorflow的安装及基于NVIDIA GPU的Cuda(GPU并行计算框架)、cudnn(深度学习加速平台)的安装配置。 主要…

CV多模态和AIGC的原理解析:从CLIP、BLIP到Stable Diffusion、Midjourney

前言 终于开写本CV多模态系列的核心主题&#xff1a;stable diffusion相关的了&#xff0c;为何执着于想写这个stable diffusion呢&#xff0c;源于三点 去年stable diffusion和midjourney很火的时候&#xff0c;就想写&#xff0c;因为经常被刷屏&#xff0c;但那会时间错不…

Apache组件POI,勾选导出Excel文件。

如果对 POI组件、Maven依赖不了解的小伙伴&#xff0c;在观看本篇文章之前可以先观看我上一篇文章&#xff1a; Apache组件POI&#xff0c;将图片下载到Excel文件中并导出。 。 本篇文章主要讲解Excel文件如何实现勾选导出。 我们先看一下勾选导出时的入参参数&#xff0c;其中…

【调试】vscode远程连接服务器

概述 windows远程连接linux服务器进行操作&#xff0c;有多种方式。可以借助ssh客户端&#xff0c;MobaXterm&#xff0c;finalshell等&#xff0c;这两个我觉得是不错的软件。当然如果需要在服务器编写代码&#xff0c;我还是建议使用vscode这个神奇的工具。怎么用vscode远程…

Spring Boot 中的 TCC 事务

Spring Boot 中的 TCC 事务 在分布式系统中&#xff0c;事务一直是一个棘手的问题。传统的 ACID 事务无法满足分布式系统的需求&#xff0c;因为它们需要强一致性、单点故障和网络延迟等问题。近年来&#xff0c;随着微服务架构的普及&#xff0c;TCC 事务成为了一种非常流行的…

SpringMVC入门篇4 --- SSM整合案例

目录 SSM整合小案例 源码 1.整合配置(config目录) Spring - SpringConfigMyBatis - MyBatisConfig、JdbcConfig、jdbc.propertiesSpringMVC - ServletConfig、SpringMvcConfig 2.功能模块 模型(domain目录)&#xff1a;Book.java数据层(dao目录)&#xff1a;.BookDao.ja…

windows11安装Linux子系统

个人博客地址: https://cxx001.gitee.io Windows在10之后自带了虚拟机子系统功能&#xff0c;可以和本地磁盘共享&#xff0c;默认挂载到/mnt/目录下面。比起传统的VM方便多了。下面是搭建流程&#xff1a; 一 开启子系统服务 控制面板 -> 程序 -> 程序与功能 -> 启动…

学会vue3,仅需这一篇

V3-组合式API setup/reactive/ref/computed/watch/生命周期/父子通信/模板引用/provide和inject数据传输 vue3的优势** 组合式 API (Composition API) 是一系列 API 的集合&#xff0c;使我们可以使用函数而不是声明选项的方式书写 Vue 组件。 使用create-vue搭建Vue3项目…

Apikit 自学日记:API 变更历史

系统会自动保存API的每一次编辑内容。进入API详情页面&#xff0c;点击 编辑历史 &#xff0c;在弹窗中会列出API的每次改动&#xff0c;您可以点击 详情 了解编辑的概况信息。 如果您想对比当前版本和历史的某个版本的差别&#xff0c;点击 对比 按钮&#xff0c;会在新窗口中…

springboot整合shiro实现认证和授权(非常详细)

Shiro和Spring Sercurity应该是我们比较常用的权限框架了&#xff0c;这篇文章教大家怎么通过springboot整合shiro从0开始搭建一个包含权限控制的后台管理系统。 第一步&#xff1a;创建一个springboot项目 创建springboot项目&#xff0c;这里项目就命名为shiro 第二步&#…

如何用一键抠图工具在线压缩PNG图片

我们会发现&#xff0c;现在用的很多图片格式都是PNG&#xff0c;它通常以无损压缩的方式存储图像。然而&#xff0c;有时候我们可能会遇到PNG文件过大的问题&#xff0c;这样会导致文件传输、存储和加载速度变慢。怎么才能解决这个问题&#xff0c;有什么好用的工具推荐么&…

Linux--多个源文件编译:gcc -o 目标文件 源文件1 源文件2 ...

分析编译过程&#xff1a; 我们编译&#xff0c;实际上是针对.c文件。这里是main.c和test.c。 编译器在预处理的时候会将.h拷贝进.c文件。这里是test.h拷贝到test.c中 .c文件编译后变成.o目标文件。这里main.c和test.c文件编译后变成main.o和test.o 生成的.o文件再和lib.so…

CAD版本转换器有哪些?这些工具轻松转换CAD版本

当我们共享CAD文件时&#xff0c;如果接收人使用的CAD软件版本与发送人不同&#xff0c;则需要将CAD文件转换为接收人能够打开的版本。例如&#xff0c;当您使用的CAD软件版本为AutoCAD 2019&#xff0c;而接收人使用的CAD软件版本为AutoCAD 2017时&#xff0c;您需要将CAD文件…