spring啟動流程 (3) BeanDefinition詳解

来源:https://www.cnblogs.com/xugf/archive/2023/07/04/17524707.html
-Advertisement-
Play Games

BeanDefinition在Spring初始化階段保存Bean的元數據信息,包括Class名稱、Scope、構造方法參數、屬性值等信息,本文將介紹一下BeanDefinition介面、重要的實現類,以及在Spring中的使用示例。 # BeanDefinition介面 用於描述了一個Bean實例, ...


BeanDefinition在Spring初始化階段保存Bean的元數據信息,包括Class名稱、Scope、構造方法參數、屬性值等信息,本文將介紹一下BeanDefinition介面、重要的實現類,以及在Spring中的使用示例。

BeanDefinition介面

用於描述了一個Bean實例,該Bean實例具有屬性、構造方法參數以及由具體實現提供的其他信息。

這是一個基礎介面:主要目的是允許BeanFactoryPostProcessor獲取和修改Bean實例屬性和其他元數據。

封裝以下信息:

  • ParentName - The name of the parent definition of this bean definition.
  • BeanClassName - The bean class name of this bean definition. The class name can be modified during bean factory post-processing, typically replacing the original class name with a parsed variant of it.
  • Scope - Override the target scope of this bean, specifying a new scope name.
  • isLazyInit - Whether this bean should be lazily initialized.
  • DependsOn - The names of the beans that this bean depends on being initialized. The bean factory will guarantee that these beans get initialized first.
  • AutowireCandidate - Whether this bean is a candidate for getting autowired into some other bean.
  • Primary - Whether this bean is a primary autowire candidate.
  • FactoryBeanName - The factory bean to use. This the name of the bean to call the specified factory method on.
  • FactoryMethodName - Specify a factory method, if any. This method will be invoked with constructor arguments, or with no arguments if none are specified. The method will be invoked on the specified factory bean, if any, or otherwise as a static method on the local bean class.
  • ConstructorArgumentValues - Constructor argument values for this bean.
  • PropertyValues - The property values to be applied to a new instance of the bean.
  • InitMethodName - The name of the initializer method.
  • DestroyMethodName - The name of the destroy method.
  • Role - The role hint for this BeanDefinition. The role hint provides the frameworks as well as tools an indication of the role and importance of a particular BeanDefinition.
  • ResolvableType - A resolvable type for this bean definition, based on the bean class or other specific metadata.
  • isSingleton - Whether this a Singleton, with a single, shared instance returned on all calls.
  • isPrototype - Whether this a Prototype, with an independent instance returned for each call.
  • isAbstract - Whether this bean is "abstract", that is, not meant to be instantiated.
  • OriginatingBeanDefinition - The originating BeanDefinition.

AbstractBeanDefinition類

實現了BeanDefinition介面,具體的、完整的BeanDefinition基類,抽取出GenericBeanDefinition、RootBeanDefinition和ChildBeanDefinition的公共屬性。

擴展的屬性:

  • AutowireMode - The autowire mode. This determines whether any automagical detection and setting of bean references will happen. Default is AUTOWIRE_NO which means there won't be convention-based autowiring by name or type (however, there may still be explicit annotation-driven autowiring).
    • AUTOWIRE_NO
    • AUTOWIRE_BY_NAME
    • AUTOWIRE_BY_TYPE
    • AUTOWIRE_CONSTRUCTOR
    • AUTOWIRE_AUTODETECT

RootBeanDefinition類

繼承AbstractBeanDefinition類。

RootBeanDefinition表示在運行時支持BeanFactory中指定Bean的合併BeanDefinition。它可能是由多個相互繼承的原始BeanDefinition創建的,通常註冊為GenericBeanDefinitions。RootBeanDefinition本質上是運行時的"統一"RootBeanDefinition視圖。

RootBeanDefinition也可以用於在配置階段註冊各個BeanDefinition。然而,自Spring2.5以來,以編程方式註冊BeanDefinition的首選方式是GenericBeanDefinition類。GenericBeanDefinition的優勢是允許動態定義父依賴項,而不是將角色硬編碼為RootBeanDefinition。

擴展的屬性:

  • DecoratedDefinition - Target definition that is being decorated by this bean definition.
  • QualifiedElement - Specify the AnnotatedElement defining qualifiers, to be used instead of the target class or factory method.
  • TargetType - Specify a generics-containing target type of this bean definition, if known in advance.
  • stale - Determines if the definition needs to be re-merged.
  • allowCaching
  • isFactoryBean

GenericBeanDefinition類

繼承AbstractBeanDefinition類。

GenericBeanDefinition是用於構建標準BeanDefinition的一站式組件。與其他BeanDefinition一樣,它允許指定一個類以及可選的構造方法參數和屬性。另外,從父BeanDefinition派生可以通過parentName屬性靈活配置。

通常,使用GenericBeanDefinition類來註冊用戶可見的BeanDefinition,後置處理器可能會對其進行操作,甚至可能重新配置parentName屬性。如果父子關係恰好是預先確定的,請使用RootBeanDefinition和ChildBeanDefinition。

AnnotatedBeanDefinition介面

繼承BeanDefinition介面。

擴展BeanDefinition介面,提供Bean的AnnotationMetadata,而不需要載入該類。

public interface AnnotatedBeanDefinition extends BeanDefinition {

	/**
	 * Obtain the annotation metadata (as well as basic class metadata)
	 * for this bean definition's bean class.
	 */
	AnnotationMetadata getMetadata();

	/**
	 * Obtain metadata for this bean definition's factory method, if any.
	 */
	MethodMetadata getFactoryMethodMetadata();
}

ScannedGenericBeanDefinition類

GenericBeanDefinition類的擴展,基於ASM ClassReader,實現了AnnotatedBeanDefinition介面,可以獲取註解元數據。

這個類不會提前載入Bean Class。它從.class文件檢索所有相關的元數據,並使用ASM ClassReader進行解析。

public class ScannedGenericBeanDefinition extends GenericBeanDefinition implements AnnotatedBeanDefinition {

	private final AnnotationMetadata metadata;

	/**
	 * Create a new ScannedGenericBeanDefinition for the class that the
	 * given MetadataReader describes.
	 * @param metadataReader the MetadataReader for the scanned target class
	 */
	public ScannedGenericBeanDefinition(MetadataReader metadataReader) {
		this.metadata = metadataReader.getAnnotationMetadata();
		setBeanClassName(this.metadata.getClassName());
		setResource(metadataReader.getResource());
	}

	@Override
	public final AnnotationMetadata getMetadata() {
		return this.metadata;
	}

	@Override
	public MethodMetadata getFactoryMethodMetadata() {
		return null;
	}
}

AnnotatedGenericBeanDefinition類

GenericBeanDefinition類的擴展,實現了AnnotatedBeanDefinition介面,可以獲取註解元數據。

public class AnnotatedGenericBeanDefinition 
		extends GenericBeanDefinition implements AnnotatedBeanDefinition {

	private final AnnotationMetadata metadata;

	private MethodMetadata factoryMethodMetadata;

	public AnnotatedGenericBeanDefinition(Class<?> beanClass) {
		setBeanClass(beanClass);
		this.metadata = AnnotationMetadata.introspect(beanClass);
	}

	public AnnotatedGenericBeanDefinition(AnnotationMetadata metadata) {
		if (metadata instanceof StandardAnnotationMetadata) {
			setBeanClass(((StandardAnnotationMetadata) metadata).getIntrospectedClass());
		} else {
			setBeanClassName(metadata.getClassName());
		}
		this.metadata = metadata;
	}

	public AnnotatedGenericBeanDefinition(
			AnnotationMetadata metadata,
			MethodMetadata factoryMethodMetadata) {
		this(metadata);
		setFactoryMethodName(factoryMethodMetadata.getMethodName());
		this.factoryMethodMetadata = factoryMethodMetadata;
	}


	@Override
	public final AnnotationMetadata getMetadata() {
		return this.metadata;
	}

	@Override
	public final MethodMetadata getFactoryMethodMetadata() {
		return this.factoryMethodMetadata;
	}
}

Spring中使用BeanDefinition示例

註冊componentClasses

AnnotationConfigApplicationContext啟動代碼:

AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.register(ServiceConfig.class);
applicationContext.refresh();

AnnotationConfigApplicationContext在啟動時可以使用register方法註冊@Configuration類,本小節將從這個方法入手看一個BeanDefinition的使用示例:

public void register(Class<?>... componentClasses) {
	Assert.notEmpty(componentClasses, "At least one component class must be specified");
	this.reader.register(componentClasses);
}

// reader.register(...)
public void register(Class<?>... componentClasses) {
	for (Class<?> componentClass : componentClasses) {
		registerBean(componentClass);
	}
}

private <T> void doRegisterBean(Class<T> beanClass, String name,
		Class<? extends Annotation>[] qualifiers, Supplier<T> supplier,
		BeanDefinitionCustomizer[] customizers) {

	// 構造方法中會解析AnnotationMetadata元數據
	AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass);
	// 判斷是否允許裝配
	if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) {
		return;
	}

	abd.setInstanceSupplier(supplier);
	ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd);
	abd.setScope(scopeMetadata.getScopeName());
	String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry));

	// 解析Lazy,Primary,DependsOn,Role等屬性
	AnnotationConfigUtils.processCommonDefinitionAnnotations(abd);
	if (qualifiers != null) {
		for (Class<? extends Annotation> qualifier : qualifiers) {
			if (Primary.class == qualifier) {
				abd.setPrimary(true);
			} else if (Lazy.class == qualifier) {
				abd.setLazyInit(true);
			} else {
				abd.addQualifier(new AutowireCandidateQualifier(qualifier));
			}
		}
	}
	if (customizers != null) {
		for (BeanDefinitionCustomizer customizer : customizers) {
			customizer.customize(abd);
		}
	}

	BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName);
	// 處理Scope的proxyMode
	definitionHolder = AnnotationConfigUtils
        .applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
	// 註冊到容器
	BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);
}

// BeanDefinitionReaderUtils.registerBeanDefinition(...)
public static void registerBeanDefinition(
		BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
		throws BeanDefinitionStoreException {

	// Register bean definition under primary name.
	String beanName = definitionHolder.getBeanName();
	// 將Bean註冊到BeanDefinitionRegistry
	registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());

	// Register aliases for bean name, if any.
	String[] aliases = definitionHolder.getAliases();
	if (aliases != null) {
		for (String alias : aliases) {
			registry.registerAlias(beanName, alias);
		}
	}
}

此處的registry是AnnotationConfigApplicationContext對象,registerBeanDefinition方法的實現在GenericApplicationContext類:

public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
		throws BeanDefinitionStoreException {
	this.beanFactory.registerBeanDefinition(beanName, beanDefinition);
}

// beanFactory.registerBeanDefinition(...)
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
		throws BeanDefinitionStoreException {

	if (beanDefinition instanceof AbstractBeanDefinition) {
		try {
			((AbstractBeanDefinition) beanDefinition).validate();
		} catch (BeanDefinitionValidationException ex) {
			throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
					"Validation of bean definition failed", ex);
		}
	}

	BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
	if (existingDefinition != null) {
		if (!isAllowBeanDefinitionOverriding()) {
			throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);
		}
		this.beanDefinitionMap.put(beanName, beanDefinition);
	} else {
		if (hasBeanCreationStarted()) {
			// Cannot modify startup-time collection elements anymore (for stable iteration)
			synchronized (this.beanDefinitionMap) {
				this.beanDefinitionMap.put(beanName, beanDefinition);
				List<String> updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1);
				updatedDefinitions.addAll(this.beanDefinitionNames);
				updatedDefinitions.add(beanName);
				this.beanDefinitionNames = updatedDefinitions;
				removeManualSingletonName(beanName);
			}
		} else {
			// Still in startup registration phase
			this.beanDefinitionMap.put(beanName, beanDefinition);
			this.beanDefinitionNames.add(beanName);
			removeManualSingletonName(beanName);
		}
		this.frozenBeanDefinitionNames = null;
	}

	if (existingDefinition != null || containsSingleton(beanName)) {
		resetBeanDefinition(beanName);
	} else if (isConfigurationFrozen()) {
		clearByTypeCache();
	}
}

@Bean註解

@Bean註解註入的Bean最終在ConfigurationClassBeanDefinitionReader的loadBeanDefinitionsForBeanMethod方法註冊BeanDefinition:

private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {
	ConfigurationClass configClass = beanMethod.getConfigurationClass();
	MethodMetadata metadata = beanMethod.getMetadata();
	String methodName = metadata.getMethodName();

	// Do we need to mark the bean as skipped by its condition?
	if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) {
		configClass.skippedBeanMethods.add(methodName);
		return;
	}
	if (configClass.skippedBeanMethods.contains(methodName)) {
		return;
	}

	AnnotationAttributes bean = AnnotationConfigUtils.attributesFor(metadata, Bean.class);

	// Consider name and any aliases
	List<String> names = new ArrayList<>(Arrays.asList(bean.getStringArray("name")));
	String beanName = (!names.isEmpty() ? names.remove(0) : methodName);

	// Register aliases even when overridden
	for (String alias : names) {
		this.registry.registerAlias(beanName, alias);
	}

	// Has this effectively been overridden before (e.g. via XML)?
	if (isOverriddenByExistingDefinition(beanMethod, beanName)) {
		return;
	}

	// 創建ConfigurationClassBeanDefinition
	// 是RootBeanDefinition的子類
	ConfigurationClassBeanDefinition beanDef = 
        new ConfigurationClassBeanDefinition(configClass, metadata, beanName);
	beanDef.setSource(this.sourceExtractor.extractSource(metadata, configClass.getResource()));

	if (metadata.isStatic()) {
		// static @Bean method
		if (configClass.getMetadata() instanceof StandardAnnotationMetadata) {
			beanDef.setBeanClass(
                ((StandardAnnotationMetadata) configClass.getMetadata()).getIntrospectedClass());
		} else {
			beanDef.setBeanClassName(configClass.getMetadata().getClassName());
		}
		beanDef.setUniqueFactoryMethodName(methodName);
	} else {
		// instance @Bean method
		beanDef.setFactoryBeanName(configClass.getBeanName());
		beanDef.setUniqueFactoryMethodName(methodName);
	}

	if (metadata instanceof StandardMethodMetadata) {
		beanDef.setResolvedFactoryMethod(((StandardMethodMetadata) metadata).getIntrospectedMethod());
	}

	beanDef.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR);
	beanDef.setAttribute(org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor.
			SKIP_REQUIRED_CHECK_ATTRIBUTE, Boolean.TRUE);

	AnnotationConfigUtils.processCommonDefinitionAnnotations(beanDef, metadata);

	Autowire autowire = bean.getEnum("autowire");
	if (autowire.isAutowire()) {
		beanDef.setAutowireMode(autowire.value());
	}

	boolean autowireCandidate = bean.getBoolean("autowireCandidate");
	if (!autowireCandidate) {
		beanDef.setAutowireCandidate(false);
	}

	String initMethodName = bean.getString("initMethod");
	if (StringUtils.hasText(initMethodName)) {
		beanDef.setInitMethodName(initMethodName);
	}

	String destroyMethodName = bean.getString("destroyMethod");
	beanDef.setDestroyMethodName(destroyMethodName);

	// Consider scoping
	ScopedProxyMode proxyMode = ScopedProxyMode.NO;
	AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(metadata, Scope.class);
	if (attributes != null) {
		beanDef.setScope(attributes.getString("value"));
		proxyMode = attributes.getEnum("proxyMode");
		if (proxyMode == ScopedProxyMode.DEFAULT) {
			proxyMode = ScopedProxyMode.NO;
		}
	}

	// Replace the original bean definition with the target one, if necessary
	BeanDefinition beanDefToRegister = beanDef;
	if (proxyMode != ScopedProxyMode.NO) {
		BeanDefinitionHolder proxyDef = ScopedProxyCreator.createScopedProxy(
				new BeanDefinitionHolder(beanDef, beanName), this.registry,
				proxyMode == ScopedProxyMode.TARGET_CLASS);
		beanDefToRegister = new ConfigurationClassBeanDefinition(
				(RootBeanDefinition) proxyDef.getBeanDefinition(), configClass, metadata, beanName);
	}

	this.registry.registerBeanDefinition(beanName, beanDefToRegister);
}

@ComponentScan註解

支持@ComponentScan註解的最終邏輯在ClassPathScanningCandidateComponentProvider類的scanCandidateComponents方法中:

private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
	Set<BeanDefinition> candidates = new LinkedHashSet<>();
	try {
		String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
				resolveBasePackage(basePackage) + '/' + this.resourcePattern;
		Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
		boolean traceEnabled = logger.isTraceEnabled();
		boolean debugEnabled = logger.isDebugEnabled();
		for (Resource resource : resources) {
			if (resource.isReadable()) {
				try {
					// 此處獲取到的是SimpleMetadataReader對象,
					// 內部使用ASM解析.class文件封裝AnnotationMetadata對象
					MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
					// 判斷是一個Component
					if (isCandidateComponent(metadataReader)) {
						// 創建ScannedGenericBeanDefinition對象
						ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
						sbd.setSource(resource);
						if (isCandidateComponent(sbd)) {
							candidates.add(sbd);
						}
					}
				} 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;
}

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

-Advertisement-
Play Games
更多相關文章
  • 併發指同一時間內進行了多個線程。併發問題是多個線程對同一資源進行操作時產生的問題。通過加鎖可以解決併發問題,ReentrantLock是鎖的一種。 ...
  • **在這篇文章中,我將手把手地教你如何從零開始部署一個使用Django框架的Python服務。無論你是一個剛開始接觸開發的新手,還是一個有經驗的開發者想要快速瞭解Django,這篇教程都會為你提供一條清晰的路徑。我們將從環境搭建開始,一步一步地創建一個可以處理GET和POST請求的服務,讓你能在實踐 ...
  • #### 近日公司有個需求,需要調研如何使用Java來讀取Windows日誌文件(類型:應用程式,安全,Setup,系統) ![](https://img2023.cnblogs.com/blog/1519440/202307/1519440-20230704100117681-1957523520 ...
  • ## 教程簡介 CakePHP是一個運用了諸如ActiveRecord、Association Data Mapping、Front Controller和MVC等著名設計模式的快速開發框架。該項目主要目標是提供一個可以讓各種層次的PHP開發人員快速地開發出健壯的Web應用,而 又不失靈活性。 Ca ...
  • 本文為大家整理彙總了常見的獲取Bean的方式,並提供一些優劣分析,方便大家在使用到時有更好的選擇。同時,也會為大家適當的普及和拓展一些相關知識。 ...
  • ## 教程簡介 DAX代表 Data Analysis Expressions. DAX是一種公式語言,是函數,運算符和常量的集合,可以在公式或表達式中用於計算和返回一個或多個值. DAX是與Excel Power Pivot的數據模型相關聯的公式語言. 它不是一種編程語言,而是一種允許用戶在計算列 ...
  • 學習記錄,不喜勿噴 什麼是OkHttp 一般在Java平臺上,我們會使用Apache HttpClient作為Http客戶端,用於發送 HTTP 請求,並對響應進行處理。比如可以使用http客戶端與第三方服務(如SSO服務)進行集成,當然還可以爬取網上的數據等。OKHttp與HttpClient類似 ...
  • > 作者:小牛呼嚕嚕 | [https://xiaoniuhululu.com](https://xiaoniuhululu.com/) > 電腦內功、源碼解析、科技故事、項目實戰、面試八股等更多硬核文章,首發於公眾號「[小牛呼嚕嚕](https://www.xiaoniuhululu.com/i ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...