ASP.NET MVC Controller的激活

来源:http://www.cnblogs.com/neverstop/archive/2016/04/20/5408814.html
-Advertisement-
Play Games

最近抽空看了一下ASP.NET MVC的部分源碼,順帶寫篇文章做個筆記以便日後查看。 在UrlRoutingModule模塊中,將請求處理程式映射到了MvcHandler中,因此,說起Controller的激活,首先要從MvcHandler入手,MvcHandler實現了三個介面:IHttpAsyn ...


最近抽空看了一下ASP.NET MVC的部分源碼,順帶寫篇文章做個筆記以便日後查看。

在UrlRoutingModule模塊中,將請求處理程式映射到了MvcHandler中,因此,說起Controller的激活,首先要從MvcHandler入手,MvcHandler實現了三個介面:IHttpAsyncHandler, IHttpHandler, IRequiresSessionState。 其處理邏輯主要實現在同步和非同步的ProcessRequest方法中,總的來說,該方法在執行的時候,大致經歷以下幾個步驟:

  1. 預處理(在響應頭中添加版本信息並去除未賦值的可選路由參數)
  2. 通過ControllerBuilder獲取ControlerFactory,並使用Controller工廠創建Controller
  3. 根據是否是非同步處理,調用Controller中相應的方法(ExecuteCore或BeginExecute)
  4. 釋放Controller

其中第一步在ProcessRequestInit方法中進行處理,本文主要是分析第兩步中的controller是如何創建出來的。

Controller的創建是通過ControllerFactory實現的,而ControllerFactory的創建又是在ControllerBuilder中完成的,因此我們先瞭解一下ControllerBuilder的工作原理。

ControllerBuilder

從源碼中可以看出,在ControllerBuilder類中,並沒有直接實現對controller工廠的創建,ControllerFactory的創建實際上是委托給一個繼承自IResolver介面的SingleServiceResolver類的實例來實現的,這一點從GetControllerFactory方法中可以看出,它是通過調用SingleServiceResolver對象的Current屬性來完成controller工廠的創建的。

public IControllerFactory GetControllerFactory()
{
    return _serviceResolver.Current;  //依賴IResolver介面創建工廠
}

並且在源碼中還發現,SingleServiceResolver類是internal級別的,這意味著外部無法直接訪問,那麼ControllerBuilder是如何藉助SingleServiceResolver來實現工廠的註冊呢?繼續看代碼,ControllerBuilder類和SingleServiceResolver類都有一個Func<IControllerFactory>類型的委托欄位,我們姑且稱為工廠委托,

//ControllerBuilder.cs
private Func<IControllerFactory> _factoryThunk = () => null;  //工廠委托
//SingleServiceResolver.cs
private Func<TService> _currentValueThunk;  //工廠委托

該委托實現了工廠的創建,而通過SetControllerFactory方法僅僅是更改了ControllerBuilder類的工廠委托欄位,並沒有更改SingleServiceResolver類的工廠委托欄位,

public void SetControllerFactory(IControllerFactory controllerFactory)
{
    if (controllerFactory == null)
    {
        throw new ArgumentNullException("controllerFactory");
    }

    _factoryThunk = () => controllerFactory;  //更改ControllerBuilder的工廠委托欄位
}

因此必須將相應的更改應用到SingleServiceResolver類中才能實現真正的註冊,我們知道,如果是單純的引用賦值,那麼更改一個引用並不會對另外一個引用造成改變,比如:

Func<object> f1 = ()=>null;
Func<object> f2 = f1;  //f1與f2指向同一個對象
object o = new object();
f1 = ()=>o;  //更改f1後,f2仍然指向之前的對象
bool b1 = f1() == o;   //true
bool b2 = f2() == null;  //true,  f1()!=f2()

所以,ControllerBuilder在實例化SingleServiceResolver對象的時候,並沒有將自身的工廠委托欄位直接賦值給SingleServiceResolver對象的對應欄位(因為這樣的話SetControllerFactory方法註冊的委托無法應用到SingleServiceResolver對象中),而是通過委托來進行了包裝,這樣就會形成一個閉包,在閉包中進行引用,如下所示:

Func<object> f1 = ()=>null;
Func<object> f2 = ()=>f1();  //通過委托包裝f1,形成閉包
object o = new object();
f1 = ()=>o;  //更改f1後,f2與f1保持同步
bool b1 = f1() == o;  //true
bool b2 = f2() == o;  //true,  f1()==f2()

//ControllerBuilder.cs
internal ControllerBuilder(IResolver<IControllerFactory> serviceResolver)
{
    _serviceResolver = serviceResolver ?? new SingleServiceResolver<IControllerFactory>(
                                              () => _factoryThunk(),  //封裝委托,閉包引用
                                              new DefaultControllerFactory { ControllerBuilder = this },
                                              "ControllerBuilder.GetControllerFactory");
}

這樣SingleServiceResolver對象中的工廠委托就會與ControllerBuilder對象中的對應欄位保持同步了,SetControllerFactory方法也就達到了替換預設工廠的目的。

閉包引用測試代碼:

using System;

class Program
{
    public static void Main(string[] args)
    {
        Func<object> f1 = ()=>null;
        Func<object> f2 = f1;  //f1與f2指向同一個對象
        object o = new object();
        f1 = ()=>o;  //更改f1後,f2仍然指向之前的對象
        bool b1 = f1() == o;   //true
        bool b2 = f2() == null;  //true,  f1()!=f2()

        Print("直接賦值:");
        Print(f1(),"f1() == {0}");
        Print(f2(),"f2() == {0}");
        Print(f1() == f2(),"f1() == f2() ? {0}");

        Func<object> ff1 = ()=>null;
        Func<object> ff2 = ()=>ff1();  //通過委托包裝f1,形成閉包
        object oo = new object();
        ff1 = ()=>oo;  //更改f1後,f2與f1保持同步
        bool bb1 = ff1() == oo;  //true
        bool bb2 = ff2() == oo;  //true,  f1()==f2()

        Print("委托賦值:");
        Print(ff1(),"ff1() == {0}");
        Print(ff2(),"ff2() == {0}");
        Print(ff1() == ff2(),"ff1() == ff2() ? {0}");
        
        Console.ReadLine();
    }

    static void Print(object mess,string format = "{0}")
    {
        string message = mess == null ? "null" : mess.ToString();
        Console.WriteLine(string.Format(format,message));
    }
}

下麵看一下SingleServiceResolver類是如何實現對象的創建的,該類是個泛型類,這意味著可以構造任何類型的對象,不僅限於ControllerFactory,實際上在MVC中,該類在很多地方都得到了應用,例如:ControllerBuilder、DefaultControllerFactory、BuildManagerViewEngine等,實現了對多種對象的創建。

SingleServiceResolver

該類實現了IResolver介面,主要用來提供指定類型的實例,在SingleServiceResolver類中有三種方式來創建對象:

1、private Lazy<TService> _currentValueFromResolver;  //內部調用_resolverThunk
2、private Func<TService> _currentValueThunk;  //委托方式
3、private TService _defaultValue;   //預設值方式

private Func<IDependencyResolver> _resolverThunk;  //IDependencyResolver方式

從Current方法中可以看出他們的優先順序:

public TService Current
{
    get { return _currentValueFromResolver.Value ?? _currentValueThunk() ?? _defaultValue; }
}

_currentValueFromResolver實際上是對_resolverThunk的封裝,內部還是調用_resolverThunk來實現對象的構造,所以優先順序是:_resolverThunk > _currentValueThunk > _defaultValue,即:IDependencyResolver方式 > 委托方式 > 預設值方式。

SingleServiceResolver在構造函數中預設實現了一個DefaultDependencyResolver對象封裝到委托欄位_resolverThunk中,該預設的Resolver是以Activator.CreateInstance(type)的方式創建對象的,但是有個前提,指定的type不能是介面或者抽象類,否則直接返回null。
在ControllerBuilder類中實例化SingleServiceResolver對象的時候指定的是IControllerFactory介面類型,所以其內部的SingleServiceResolver對象無法通過IDependencyResolver方式創建對象,那麼創建ControllerFactory對象的職責就落到了_currentValueThunk(委托方式)和_defaultValue(預設值方式)這兩個方式上,前面說過,SingleServiceResolver類中的委托欄位實際上是通過閉包引用ControllerBuilder類中的相應委托來創建對象的,而在ControllerBuilder類中,這個對應的委托預設是返回null,

private Func<IControllerFactory> _factoryThunk = () => null;

因此,預設情況下SingleServiceResolver類的第二種方式也失效了,那麼此時也只能依靠預設值方式來提供對象了,在ControllerBuilder類中這個預設值是DefaultControllerFactory:

internal ControllerBuilder(IResolver<IControllerFactory> serviceResolver)
{
    _serviceResolver = serviceResolver ?? new SingleServiceResolver<IControllerFactory>(
                                              () => _factoryThunk(),
                                              new DefaultControllerFactory { ControllerBuilder = this }, //預設值
                                              "ControllerBuilder.GetControllerFactory");
}

所以,在預設情況下是使用DefaultControllerFactory類來構造Controller的。
在創建SingleServiceResolver對象的時候,可以從三個地方判斷出真正創建對象的方法是哪種:

new SingleServiceResolver<IControllerFactory>(   //1、看泛型介面,如果為介面或抽象類,則IDependencyResolver方式失效
    () => _factoryThunk(),  //2、看_factoryThunk()是否返回null,如果是則委托方式失效
    new DefaultControllerFactory { ControllerBuilder = this },  //3、以上兩種都失效,則使用該預設值
    "ControllerBuilder.GetControllerFactory");

通過以上創建對象的過程可以得知,有兩種方式可以替換預設的對象提供器:

  1. 替換預設的DependencyResolver,可以通過DependencyResolver類的靜態方法SetResolver方法來實現:

    CustomDependencyResolver customResolver = new  CustomDependencyResolver();
    DependencyResolver.SetResolver(customResolver);

    將以上語句放在程式啟動的地方,例如:Application_Start

  2. 通過前面介紹的ControllerBuilder類的SetControllerFactory方法

註:第一種方式的優先順序更高。

ControllerFactory

通過ControllerBuilder創建出ControllerFactory對象後,下麵就要利用該對象完成具體Controller的創建,ControllerFactory都實現了IControllerFactory介面,通過實現CreateController方法完成對Controller的實例化,CreateController的內部邏輯非常簡單,就兩步:獲取Controller類型,然後創建Controller對象。

獲取Controller類型

根據控制器名稱獲取控制器Type的過程,有必要深入瞭解一下,以便於我們在日後遇到相關問題的時候能夠更好的進行錯誤定位。獲取類型的邏輯都封裝在GetControllerType方法中,該過程根據路由數據中是否含有命名空間信息,分為三個階段進行類型搜索:

  • 首先,如果當前路由數據中存在命名空間信息,則在緩存中根據控制器名稱和命名空間搜索對應的類型,如果找到唯一一個類型,則返回該類型,找到多個直接拋異常
  • 其次,如果當前路由數據中不存在命名空間信息,或在第一階段的搜索沒有找到對應的類型,並且UseNamespaceFallback==true,此時會獲取ControllerBuilder中設置的命名空間信息,利用該信息和控制器名稱在緩存中進行類型搜索,如果找到唯一一個類型,則返回該類型,找到多個直接拋異常
  • 最後,如果路由數據和ControllerBuilder中都沒有命名空間信息,或者在以上兩個階段都沒有搜索到對應的Controller類型,那麼會忽略命名空間,在緩存中僅按照控制器名稱進行類型搜索,如果找到唯一一個類型,則返回該類型,找到多個直接拋異常

因此,命名空間的優先順序是:RouteData > ControllerBuilder

在緩存中搜索類型的時候,如果是第一次查找,會調用ControllerTypeCache.EnsureInitialized方法將保存在硬碟中的Xml緩存文件載入到一個字典類型的記憶體緩存中。如果該緩存文件不存在,則會遍歷當前應用引用的所有程式集,找出所有public許可權的Controller類型(判斷條件:實現IController介面、非抽象類、類名以Controller結尾),然後將這些類型信息進行xml序列化,生成緩存文件保存在硬碟中,以便於下次直接從緩存文件中載入,同時將類型信息分組以字典的形式緩存在記憶體中,提高搜索效率,字典的key為ControllerName(不帶命名空間)。

Controller類型搜索流程如下圖所示:

獲取Controller類型

創建Controller對象

獲取Controller類型以後,接下來就要進行Controller對象的創建。在DefaultControllerFactory類的源碼中可以看到,同ControllerBuilder類似,該類的構造函數中也實例化了一個SingleServiceResolver對象,按照之前介紹的方法,我們一眼就可以看出,該對象是利用預設值的方式提供了一個DefaultControllerActivator對象。

_activatorResolver = activatorResolver ?? new SingleServiceResolver<IControllerActivator>(  //1、泛型為介面,IDependencyResolver方式失效
                     () => null,  //2、返回了null,委托方式失效
                     new DefaultControllerActivator(dependencyResolver),  //3、以上兩種方式均失效,則使用該提供方式
                     "DefaultControllerFactory constructor");

實際上DefaultControllerFactory類僅實現了類型的搜索,對象的真正創建過程需要由DefaultControllerActivator類來完成,預設情況下,DefaultControllerActivator創建Controller的過程是很簡單的,因為它實際上使用的是一個叫做DefaultDependencyResolver的類來進行Controller創建的,在該類內部直接調用Activator.CreateInstance(serviceType)方法完成對象的實例化。

從DefaultControllerFactory和DefaultControllerActivator這兩個類的創建過程可以發現,MVC提供了多種方式(IDependencyResolver方式、委托方式 、預設值方式)來提供對象,因此在對MVC相關模塊進行擴展的時候,也有多種方式可以採用。

Controller中的數據容器

Controller中涉及到幾個給view傳值的數據容器:TempData、ViewData和ViewBag。前兩者的不同之處在於TempData僅存儲臨時數據,裡面的數據在第一次讀取之後會被移除,即:只能被讀取一次;ViewData和ViewBag保存的是同一份數據,只不過ViewBag是動態對象,對ViewData進行了封裝。

public dynamic ViewBag
{
    get
    {
        if (_dynamicViewDataDictionary == null)
        {
            _dynamicViewDataDictionary = new DynamicViewDataDictionary(() => ViewData); //封裝ViewData
        }
        return _dynamicViewDataDictionary;
    }
}  

下麵簡單說一下TempData的實現原理。

TempData

首先看下MSDN上是如何解釋的:

你可以按使用 ViewDataDictionary 對象的相同方式使用 TempDataDictionary 對象傳遞數據。 但是,TempDataDictionary 對象中的數據僅從一個請求保持到下一個請求,除非你使用 Keep 方法將一個或多個鍵標記為需保留。 如果鍵已標記為需保留,則會為下一個請求保留該鍵。
TempDataDictionary 對象的典型用法是,在數據重定向到一個操作方法時從另一個操作方法傳遞數據。 例如,操作方法可能會在調用 RedirectToAction 方法之前,將有關錯誤的信息存儲在控制器的 TempData 屬性(該屬性返回 TempDataDictionary 對象)中。 然後,下一個操作方法可以處理錯誤並呈現顯示錯誤消息的視圖。

TempData的特性就是可以在兩個Action之間傳遞數據,它會保存一份數據到下一個Action,並隨著再下一個Action的到來而失效。所以它被用在兩個Action之間來保存數據,比如,這樣一個場景,你的一個Action接受一些post的數據,然後交給另一個Action來處理,並顯示到頁面,這時就可以使用TempData來傳遞這份數據。

TempData實現了IDictionary介面,同時內部含有一個IDictionary類型的私有欄位,並添加了相關方法對字典欄位的操作進行了控制,這明顯是代理模式的一個應用。因為TempData需要在Action之間傳遞數據,因此要求其能夠對自身的數據進行保存,TempData依賴ITempDataProvider介面實現了數據的載入與保存,預設情況下是使用SessionStateTempDataProvider對象將TempData中的數據存放在Session中。

下麵看一下TempData是如何控制數據操作的,TempDataDictionary源碼中有這樣一段定義:

internal const string TempDataSerializationKey = "__tempData";

private Dictionary<string, object> _data;
private HashSet<string> _initialKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private HashSet<string> _retainedKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

私有字典欄位_data是真正存放數據的地方,哈希集合_initialKeys和_retainedKeys用來標記數據,_initialKeys中存放尚未被讀取的數據key,_retainedKeys存放可以被多次訪問的key。
TempDataDictionary對數據操作的控制行為主要體現在在讀取數據的時候並不會立即從_data中刪除對應的數據,而是通過_initialKeys和_retainedKeys這兩個hashset標記每條數據的狀態,最後在通過ITempDataProvider進行保存的時候再根據之前標記的狀態對數據進行過濾,這時才去除已訪問過的數據。

相關的控制方法有:TryGetValue、Add、Keep、Peek、Remove、Clear

1、TryGetValue

public bool TryGetValue(string key, out object value)
{
    _initialKeys.Remove(key);
    return _data.TryGetValue(key, out value);
}

該方法在讀取數據的時候,會從_initialKeys集合中移除對應的key,前面說過,因為_initialKeys是用來標記數據未訪問狀態的,從該集合中刪除了key,之後在通過ITempDataProvider保存的時候就會將數據從_data字典中刪除,下一次請求就無法再從TempData訪問該key對應的數據了,即:數據只能在一次請求中使用。

2、Add

public void Add(string key, object value)
{
    _data.Add(key, value);
    _initialKeys.Add(key);
}

添加數據的時候在_initialKeys中打上標記,表明該key對應的數據可以被訪問。

3、Keep

public void Keep(string key)
{
    _retainedKeys.Add(key);
} 

調用Keep方法的時候,會將key添加到_retainedKeys中,表明該條記錄可以被多次訪問,為什麼可以被多次訪問呢,可以從Save方法中找到原因:

public void Save(ControllerContext controllerContext, ITempDataProvider tempDataProvider)
{
    // Frequently called so ensure delegate is stateless
    _data.RemoveFromDictionary((KeyValuePair<string, object> entry, TempDataDictionary tempData) =>
        {
            string key = entry.Key;
            return !tempData._initialKeys.Contains(key) 
                && !tempData._retainedKeys.Contains(key);
        }, this);

    tempDataProvider.SaveTempData(controllerContext, _data);
}

可以看出,在保存的時候,會從_data中取出每一條數據,判斷該數據的key是否存在於_initialKeys和_retainedKeys中,如果都不存在才會從_data中移除,所以keep方法將key添加到_retainedKeys後,該數據就不會被刪除了,即:可以在多個請求中被訪問了。

4、Peek

public object Peek(string key)
{
    object value;
    _data.TryGetValue(key, out value);
    return value;
}

從代碼中可以看出,該方法在讀取數據的時候,僅僅是從_data中進行了獲取,並沒有移除_initialKeys集合中對應的key,因此通過該方法讀取數據不影響數據的狀態,該條數據依然可以在下一次請求中被使用。

5、Remove 與 Clear

public bool Remove(string key)
{
    _retainedKeys.Remove(key);
    _initialKeys.Remove(key);
    return _data.Remove(key);
}

public void Clear()
{
    _data.Clear();
    _retainedKeys.Clear();
    _initialKeys.Clear();
}

這兩個方法沒什麼多說的,只是在刪除數據的時候同時刪除其對應的狀態。


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

-Advertisement-
Play Games
更多相關文章
  • 1. NET 4.0 Tasks 使用 ThreadPool 可設置最大併發級別。 2. 多個WebClient多線程下載受System.Net.ServicePointManager.DefaultConnectionLimit屬性顯示,此值預設為2,伺服器系統預設為10。 3. 在發佈WebAP ...
  • 在C#2.0引入匿名方法之前,聲明委托的唯一方法就是使用命名方法,C#2.0之後的C#3.0中開始引入了Lambda表達式取代了匿名方法。 匿名方法 要說Lambda必然離不開匿名方法,實際上,Lambda的本質就是一個匿名方法,上代碼 上述代碼中,首先定義了一個委托SayHello,註冊到改委托的 ...
  • 前面準備:下載spring.net並解壓 下載地址:spring.net下載地址 Ioc:控制反轉 DI:依賴註入 一、IOC(控制反轉) 1.新建一個控制台程式springTest,引用dll。 Spring.NET > bin > net > 4.0 > release下找到 Comon.Log ...
  • 什麼是ORM? ORM的全稱是Object Relational Mapping,即對象關係映射。它的實現思想就是將關係資料庫中表的數據映射成為對象,以對象的形式展現,這樣開發人員就可以把對資料庫的操作轉化為對這些對象的操作。因此它的目的是為了方便開發人員以面向對象的思想來實現對資料庫的操作。 OR ...
  • DataGridView是在用C#做windows界面程式時常用到的控制項,DataGridView的功能非常多,用起來也非常複雜,下麵我就為DataGridView提供一個擴展. 實現目標: 1: DataGridView的顯示內容使用文本配置,不使用編碼,可以簡化編碼.2: 為DatgaGridV ...
  • 1.絕對路徑轉相對路徑 絕對轉相對似乎C#沒有提供實現,需要自己寫,這裡摘選了一位博友的實現方法: string RelativePath(string absolutePath, string relativeTo) { //from - www.cnphp6.com string[] absol ...
  • 異常處理彙總-開發工具 http://www.cnblogs.com/dunitian/p/4522988.html ...
  • 項目前景 由於之前的列印是客戶端程式,也就是winform做的,現在需要改版成網頁版,其他功能都能夠很好的實現,就是在列印上遇到一些難點。由於第一次做列印功能,剛開始照搬winform中調用word文檔實現列印,在本地運行時都很正常,可是發佈到IIS之後,怎麼也實現不了,經過研究學習,發現客戶端不可 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...