Spring IOC @Configuration註解分析

来源:https://www.cnblogs.com/supportmyself/archive/2023/06/07/17464164.html
-Advertisement-
Play Games

# 引入 在使用SpringBoot開發時,最常用的註解有@Component、@Service、@Controller、@Configuration等。當類使用這些註解標記時,類會被Spring IOC容器管理,包括創建,填充屬性和實例化。 但是Spring容器如何發現並將這些類放到容器進行管理呢 ...


引入

在使用SpringBoot開發時,最常用的註解有@Component、@Service、@Controller、@Configuration等。當類使用這些註解標記時,類會被Spring IOC容器管理,包括創建,填充屬性和實例化。

但是Spring容器如何發現並將這些類放到容器進行管理呢?

今天這篇博客主要分析Spring如何處理@Configuration註解。

@Configuration定義

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
	@AliasFor(annotation = Component.class)
	String value() default "";
	
	//Since: 5.2
	boolean proxyBeanMethods() default true;

	//Since: 6.0
	boolean enforceUniqueMethods() default true;
}

看Configuration註解的定義,它本質也是一個@Component註解。

所以,標記有@Configuration註解的類在Spring註冊bean的第一階段就會被註冊到容器中,但是使用@Configuration註解的類內部的其他bean並沒有被註冊到容器中。

想知道@Configuration註解被如何處理,需要看invokeBeanFactoryPostProcessors 方法。

ConfigurationClassPostProcessor處理@Configuration註解

先大致說一下invokeBeanFactoryPostProcessors 方法的基本流程,因為這個方法會處理所有BeanDefinitionRegistryPostProcessor和BeanFactoryPostProcessor介面的子類:

  • 先處理方法入參:beanFactoryPostProcessors(如果只是分析AnnotationConfigApplicationContext,這個參數是空的。只有在分析SpringBoot的啟動過程時,這個參數才會有值)
  • 再處理BeanDefinitionRegistryPostProcessor的子類
    • 這裡最重要的一件事情是調用ConfigurationClassPostProcessor的processConfigBeanDefinitions方法。
  • 最後處理BeanFactoryPostProcessor的子類

invokeBeanFactoryPostProcessors 的部分代碼:

public static void invokeBeanFactoryPostProcessors(
		ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) {
	
	Set<String> processedBeans = new HashSet<>();
	if (beanFactory instanceof BeanDefinitionRegistry) {
		//省略部分代碼,處理入參:beanFactoryPostProcessors

        //開始處理BeanDefinitionRegistryPostProcessor介面的子類
        //保存所有符合的子類,即實現BeanDefinitionRegistryPostProcessor,同樣實現PriorityOrdered
		List<BeanDefinitionRegistryPostProcessor> currentRegistryProcessors = new ArrayList<>();
		String[] postProcessorNames =
				beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
		for (String ppName : postProcessorNames) {
			if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
				currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
				processedBeans.add(ppName);
			}
		}
		sortPostProcessors(currentRegistryProcessors, beanFactory);
		registryProcessors.addAll(currentRegistryProcessors);
        //調用currentRegistryProcessors中所有的BDRPP
		invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
		currentRegistryProcessors.clear();

		//省略部分代碼,同樣處理BeanDefinitionRegistryPostProcessor介面的子類
	}
	else {
		//省略部分代碼
	}
	//省略部分代碼 處理BeanFactoryPostProcessor介面的子類
}

以上是invokeBeanFactoryPostProcessors的部分代碼,接下來看ConfigurationClassPostProcessor的processConfigBeanDefinitions方法,在這個方法里開始正式地處理@Configruation註解:

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
	List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
	String[] candidateNames = registry.getBeanDefinitionNames();

	//遍歷所有的候選者
	for (String beanName : candidateNames) {
		BeanDefinition beanDef = registry.getBeanDefinition(beanName);
		if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
			if (logger.isDebugEnabled()) {
				logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
			}
		}
		//校驗候選者類上是否使用了@Configuration註解
		else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
			configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
		}
	}

	//如果configCandidates為空,則立即返回
	if (configCandidates.isEmpty()) {
		return;
	}

	configCandidates.sort((bd1, bd2) -> {
		int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
		int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
		return Integer.compare(i1, i2);
	});

	SingletonBeanRegistry sbr = null;
	if (registry instanceof SingletonBeanRegistry) {
		sbr = (SingletonBeanRegistry) registry;
		if (!this.localBeanNameGeneratorSet) {
			BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(
					AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR);
			if (generator != null) {
				this.componentScanBeanNameGenerator = generator;
				this.importBeanNameGenerator = generator;
			}
		}
	}

	if (this.environment == null) {
		this.environment = new StandardEnvironment();
	}

	// 創建一個解析@Configuration的解析器
	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 {
		StartupStep processConfig = this.applicationStartup.start("spring.context.config-classes.parse");
		//開始解析候選類
		parser.parse(candidates);

		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());
		}

		//註冊解析後的類到容器中
		this.reader.loadBeanDefinitions(configClasses);

		alreadyParsed.addAll(configClasses);
		processConfig.tag("classCount", () -> String.valueOf(configClasses.size())).end();
		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();
	}

}

這個方法的關鍵點:

  • 找到所有要解析的類
  • 創建一個解析者
  • 開始解析類
    • 記住,這裡只是解析,也就是確認所有需要處理的類或者方法,這些類或者方法最終也會註冊到容器中
  • 註冊解析後的類到容器中:this.reader.loadBeanDefinitions(configClasses);

解析

那麼實際上如何解析呢?

protected final SourceClass doProcessConfigurationClass(
		ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
		throws IOException {
	//1. 判斷當前類是否有內部類,如果有內部類是否使用了@Configuration註解,如果使用了該註解,那麼需要先處理內部類的配置
	//存在這種可能,但是實際開發中很少使用
	if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
		processMemberClasses(configClass, sourceClass, filter);
	}

	// 處理@PropertySource註解
	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");
		}
	}

	// 處理@ComponentScan註解
	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) {
			Set<BeanDefinitionHolder> scannedBeanDefinitions =
					this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
			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());
				}
			}
		}
	}

	// 處理@Import註解
	processImports(configClass, sourceClass, getImports(sourceClass), filter, true);

	// 處理@ImportResource註解
	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);
		}
	}

	// 處理使用了@Bean的方法
	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;
}
  • 確認當前類是否有內部類,內部類是否也使用了@Configuration註解
  • 處理@PropertySource註解
  • 處理@ComponentScan註解
  • 處理@Import註解
  • 處理@ImportResource註解
  • 處理@Bean註解

這裡以@Import和@Bean為例進行分析:

@Configuration
@Import(MyBeanFactoryPostProcessor.class)
public class JavaConfig {
    @Bean
    public Country country() {
        return new Country();
    }
}

JavaConfig類上使用了@Configuration註解和@Import註解,在@Import註解引入了MyBeanFactoryPostProcessor類:

public class MyBeanFactoryPostProcessor implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
        ImportBeanDefinitionRegistrar.super.registerBeanDefinitions(importingClassMetadata, registry, importBeanNameGenerator);
        RootBeanDefinition rootBeanDefinition = new RootBeanDefinition();
        rootBeanDefinition.setBeanClass(Person.class);

        registry.registerBeanDefinition("person", rootBeanDefinition);
    }

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        ImportBeanDefinitionRegistrar.super.registerBeanDefinitions(importingClassMetadata, registry);
    }
}

MyBeanFactoryPostProcessor實現了ImportBeanDefinitionRegistrar介面。

下麵分析一下如何處理@Import註解:

private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
		Collection<SourceClass> importCandidates, Predicate<String> exclusionFilter,
		boolean checkForCircularImports) {
	if (importCandidates.isEmpty()) {
		return;
	}
	if (checkForCircularImports && isChainedImportOnStack(configClass)) {
		this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
	}
	else {
		this.importStack.push(configClass);
		try {

			//@Import註解的參數可能不止一個類,可以有多個
			for (SourceClass candidate : importCandidates) {
				//candidate繼承了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) {
						this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
					}
					else {
						String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
						Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter);
						processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false);
					}
				}
				//candidate繼承了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);

					//將candidate放到configClass中,後續進行處理
					configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
				}
				//candidate沒有繼承上述兩個類,按照@Configuration處理
				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);
				}
			}
		}
		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();
		}
	}
}

processImports方法主要處理兩種情況:

  • 實現ImportSelector介面的類
  • 實現ImportBeanDefinitionRegistrar介面的類

但是需要註意,這個方法只是把這些類放到了集合中,但是還沒有開始處理。

下麵開始看如何處理使用@Bean的方法

private Set<MethodMetadata> retrieveBeanMethodMetadata(SourceClass sourceClass) {
		AnnotationMetadata original = sourceClass.getMetadata();
		//找到所有使用@Bean的方法
		Set<MethodMetadata> beanMethods = original.getAnnotatedMethods(Bean.class.getName());
		if (beanMethods.size() > 1 && original instanceof StandardAnnotationMetadata) {
			try {
				AnnotationMetadata asm =
						this.metadataReaderFactory.getMetadataReader(original.getClassName()).getAnnotationMetadata();
				Set<MethodMetadata> asmMethods = asm.getAnnotatedMethods(Bean.class.getName());
				if (asmMethods.size() >= beanMethods.size()) {
					Set<MethodMetadata> selectedMethods = new LinkedHashSet<>(asmMethods.size());
					for (MethodMetadata asmMethod : asmMethods) {
						for (MethodMetadata beanMethod : beanMethods) {
							if (beanMethod.getMethodName().equals(asmMethod.getMethodName())) {
								selectedMethods.add(beanMethod);
								break;
							}
						}
					}
					if (selectedMethods.size() == beanMethods.size()) {
						// All reflection-detected methods found in ASM method set -> proceed
						beanMethods = selectedMethods;
					}
				}
			}
			catch (IOException ex) {
				logger.debug("Failed to read class file via ASM for determining @Bean method order", ex);
				// No worries, let's continue with the reflection metadata we started with...
			}
		}
		return beanMethods;
	}

最後返回所有需要處理的方法。

註冊

private void loadBeanDefinitionsForConfigurationClass(
			ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {

		if (trackedConditionEvaluator.shouldSkip(configClass)) {
			String beanName = configClass.getBeanName();
			if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
				this.registry.removeBeanDefinition(beanName);
			}
			this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
			return;
		}

		if (configClass.isImported()) {
			registerBeanDefinitionForImportedConfigurationClass(configClass);
		}

		//註冊使用@Bean的方法
		for (BeanMethod beanMethod : configClass.getBeanMethods()) {
			loadBeanDefinitionsForBeanMethod(beanMethod);
		}

		loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());

		//註冊實現ImportBeanDefinitionRegistrar介面的類
		loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
	}

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • # 引擎下載地址 https://cocos2d-x.org/download/ 也可以在 github 下載 https://github.com/cocos2d/cocos2d-x/tags # 手冊地址 https://docs.cocos2d-x.org/cocos2d-x/v3/zh/ # ...
  • //個人學習筆記用 - 題目: 給定一個排序數組和一個目標值,在數組中找到目標值,並返回其索引。如果目標值不存在於數組中,返回它將會被按順序插入的位置。 請必須使用時間複雜度為 O(log n) 的演算法。 參考題解--代碼隨想錄 - 暴力解法: ~~~c++ class Solution { pub ...
  • # 數據結構 in Golang:Hash Tables(哈希表) ### 場景 - 水果店的價格表: - 蘋果 Apple:3元 - 香蕉 Banana:4元 - 桃子 Peach:2元 - 梨 Pear:3元 - 找到一種水果的價格: - 可以使用 binary search,通過名稱來查找,耗 ...
  • 某日二師兄參加XXX科技公司的C++工程師開發崗位第8面: > 面試官:C++中,函數的參數應該傳值還是傳引用? > > 二師兄:要看參數的用途。如果是出參,必須傳引用。如果是入參,主要考慮參數類型的大小,來決定傳值還是傳引用。 > > 面試官:為什麼不使用指針? > > 二師兄:傳指針也稱之為傳引 ...
  • **# Properties類** - **基本介紹** ![](https://img2023.cnblogs.com/blog/3008601/202306/3008601-20230604103622859-1793594469.png) 1. 專門用於讀寫配置文件的集合類 配置文件的格式: ...
  • ## SpringMVC如何接受請求參數(普通類型參數/對象類型參數/數組/json數據等) 1、普通類型參數 (1)在可以在方法參數上使用@RequestParam註解來綁定請求參數,此註解允許指定請求參數的名稱,以及是否是必須傳的參數。 ~~~java @RequestMapping("/exa ...
  • # 安裝 略 # hello world > 1. 文檔地址: [https://marketplace.visualstudio.com/items?itemName=humao.rest-client](https://marketplace.visualstudio.com/items?ite ...
  • 在數據結構中,我們已經學習到了簡單的靜態鏈表以及單鏈表和雙鏈表,它們各有優缺點,但是有個共同的問題是他們呢無法存儲不同的數據。下麵提供了一種方法,可以將不同節點的數據鏈接起來。 下麵的代碼都是基礎的C語言代碼,涉及到的知識點基本覆蓋到C語言學習的所有知識面,尤其是使用了巨集,減少了重覆的代碼。 無論是 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...