1. 程式人生 > >200行程式碼,7個物件——讓你瞭解ASP.NET Core框架的本質[3.x版]

200行程式碼,7個物件——讓你瞭解ASP.NET Core框架的本質[3.x版]

2019年1月19日,微軟技術(蘇州)俱樂部成立,我受邀在成立大會上作了一個名為《ASP.NET Core框架揭祕》的分享。在此次分享中,我按照ASP.NET Core自身的執行原理和設計思想建立了一個 “迷你版” 的ASP.NET Core框架,並且利用這個 “極簡” 的模擬框架闡述了ASP.NET Core框架最核心、最本質的東西。整個框架涉及到的核心程式碼不會超過200行,涉及到7個核心的物件。由於ASP.NET Core 3.X採用了不同的應用承載方式,所以我們將這個模擬框架升級到3.x版本。[本篇內容節選自即將出版的《ASP.NET Core 3框架解密》,感興趣的朋友可以通過《“ASP.NET Core 3框架揭祕”讀者群,歡迎加入》加入本書讀者群,以便及時瞭解本書的動態。原始碼從這裡下載。]

目錄
一、中介軟體委託鏈
     HttpContext
     中介軟體
     中介軟體管道的構建
二、伺服器
     IServer
     針對伺服器的適配
     HttpListenerServer
三、承載服務
     WebHostedService
     WebHostBuilder
     應用構建

一、中介軟體委託鏈

通過本篇文章,我將管道最核心的部分提取出來構建一個“迷你版”的ASP.NET Core框架。較之真正的ASP.NET Core框架,雖然重建的模擬框架要簡單很多,但是它們採用完全一致的設計。為了能夠在真實框架中找到對應物,在定義介面或者型別時會採用真實的名稱,但是在API的定義上會做最大限度的簡化。

HttpContext

一個HttpContext物件表示針對當前請求的上下文。要理解HttpContext上下文的本質,需要從請求處理管道的層面來講。對於由一個伺服器和多箇中間件構成的管道來說,面向傳輸層的伺服器負責請求的監聽、接收和最終的響應,當它接收到客戶端傳送的請求後,需要將請求分發給後續中介軟體進行處理。對於某個中介軟體來說,完成自身的請求處理任務之後,在大部分情況下需要將請求分發給後續的中介軟體。請求在伺服器與中介軟體之間,以及在中介軟體之間的分發是通過共享上下文的方式實現的。

如下圖所示,當伺服器接收到請求之後,會建立一個通過HttpContext表示的上下文物件,所有中介軟體都在這個上下文中完成針對請求的處理工作。那麼一個HttpContext物件究竟會攜帶什麼樣的上下文資訊?一個HTTP事務(Transaction)具有非常清晰的界定,如果從伺服器的角度來說就是始於請求的接收,而終於響應的回覆,所以請求和響應是兩個基本的要素,也是HttpContext承載的最核心的上下文資訊。

我們可以將請求和響應理解為一個Web應用的輸入與輸出,既然HttpContext上下文是針對請求和響應的封裝,那麼應用程式就可以利用這個上下文物件得到當前請求所有的輸入資訊,也可以利用它完成我們所需的所有輸出工作。所以,我們為ASP.NET Core模擬框架定義瞭如下這個極簡版本的HttpContext型別。

public class HttpListenerFeature : IHttpRequestFeature, IHttpResponseFeature
{
    private readonly HttpListenerContext _context;
    public HttpListenerFeature(HttpListenerContext context)=> _context = context;

    Uri IHttpRequestFeature.Url=> _context.Request.Url;
    NameValueCollection IHttpRequestFeature.Headers=> _context.Request.Headers;
    NameValueCollection IHttpResponseFeature.Headers=> _context.Response.Headers;
    Stream IHttpRequestFeature.Body=> _context.Request.InputStream;
    Stream IHttpResponseFeature.Body=> _context.Response.OutputStream;
    int IHttpResponseFeature.StatusCode
    {
        get => _context.Response.StatusCode;
        set => _context.Response.StatusCode = value;
    }
}

如上面的程式碼片段所示,我們可以利用HttpRequest物件得到當前請求的地址、請求訊息的報頭集合和主體內容。利用HttpResponse物件,我們不僅可以設定響應的狀態碼,還可以新增任意的響應報頭和寫入任意的主體內容。

中介軟體

HttpContext物件承載了所有與當前請求相關的上下文資訊,應用程式針對請求的響應也利用它來完成,所以可以利用一個Action<HttpContext>型別的委託物件來表示針對請求的處理,我們姑且將它稱為請求處理器(Handler)。但Action<HttpContext>僅僅是請求處理器針對“同步”程式設計模式的表現形式,對於面向Task的非同步程式設計模式,這個處理器應該表示成型別為Func<HttpContext,Task>的委託物件。

由於這個表示請求處理器的委託物件具有非常廣泛的應用,所以我們為它專門定義瞭如下這個RequestDelegate委託型別,可以看出它就是對Func<HttpContext,Task>委託的表達。一個RequestDelegate物件表示的是請求處理器,那麼中介軟體在模型中應如何表達?

public delegate Task RequestDelegate(HttpContext context);

作為請求處理管道核心組成部分的中介軟體可以表示成型別為Func<RequestDelegate, RequestDelegate>的委託物件。換句話說,中介軟體的輸入與輸出都是一個RequestDelegate物件。我們可以這樣來理解:對於管道中的某個中介軟體(下圖所示的第一個中介軟體)來說,後續中介軟體組成的管道體現為一個RequestDelegate物件,由於當前中介軟體在完成了自身的請求處理任務之後,往往需要將請求分發給後續中介軟體進行處理,所以它需要將後續中介軟體構成的RequestDelegate物件作為輸入。

當代表當前中介軟體的委託物件執行之後,如果將它自己“納入”這個管道,那麼代表新管道的RequestDelegate物件就成為該委託物件執行後的輸出結果,所以中介軟體自然就表示成輸入和輸出型別均為RequestDelegate的Func<RequestDelegate, RequestDelegate>物件。

中介軟體管道的構建

從事軟體行業10多年來,筆者對架構設計越來越具有這樣的認識:好的設計一定是“簡單”的設計。所以在設計某個開發框架時筆者的目標是再簡單點。上面介紹的請求處理管道的設計就具有“簡單”的特質:Pipeline = Server + Middlewares。但是“再簡單點”其實是可以的,我們可以將多箇中間件組成一個單一的請求處理器。請求處理器可以通過RequestDelegate物件來表示,所以整個請求處理管道將具有更加簡單的表達:Pipeline = Server + RequestDelegate(見下圖12)。

表示中介軟體的Func<RequestDelegate, RequestDelegate>物件向表示請求處理器的RequestDelegate物件之間的轉換是通過IApplicationBuilder物件來完成的。從介面命名可以看出,IApplicationBuilder物件是用來構建“應用程式”(Application)的,實際上,由所有註冊中介軟體構建的RequestDelegate物件就是對應用程式的表達,因為應用程式的意圖完全是由註冊的中介軟體達成的。

public interface IApplicationBuilder
{
    RequestDelegate Build();
    IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);
}

如上所示的程式碼片段是模擬框架對IApplicationBuilder介面的簡化定義。它的Use方法用來註冊中介軟體,而Build方法則將所有的中介軟體按照註冊的順序組裝成一個RequestDelegate物件。如下所示的程式碼片段中ApplicationBuilder型別是對該介面的預設實現。我們給出的程式碼片段還體現了這樣一個細節:當我們將註冊的中介軟體轉換成一個表示請求處理器的RequestDelegate物件時,會在管道的尾端新增一個處理器用來響應一個狀態碼為404的響應。這個細節意味著如果沒有註冊任何的中介軟體或者所有註冊的中介軟體都將請求分發給後續管道,那麼應用程式會回覆一個狀態碼為404的響應。

public class ApplicationBuilder : IApplicationBuilder
{
    private readonly IList<Func<RequestDelegate, RequestDelegate>> _middlewares = new List<Func<RequestDelegate, RequestDelegate>>();

    public RequestDelegate Build()
    {
        RequestDelegate next = context =>
        {
            context.Response.StatusCode = 404;
            return Task.CompletedTask;
        };                    
        foreach (var middleware in _middlewares.Reverse())
        {
            next = middleware.Invoke(next);
        }
        return next;
    }

    public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
    {
        _middlewares.Add(middleware);
        return this;
    }
}

二、伺服器

伺服器在管道中的職責非常明確:負責HTTP請求的監聽、接收和最終的響應。具體來說,啟動後的伺服器會繫結到指定的埠進行請求監聽。一旦有請求抵達,伺服器會根據該請求建立代表請求上下文的HttpContext物件,並將該上下文分發給註冊的中介軟體進行處理。當中間件管道完成了針對請求的處理之後,伺服器會將最終生成的響應回覆給客戶端。

IServer

在模擬的ASP.NET Core框架中,我們將伺服器定義成一個極度簡化的IServer介面。在如下所示的程式碼片段中,IServer介面具有唯一的StartAsync方法來啟動自身代表的伺服器。伺服器最終需要將接收的請求分發給註冊的中介軟體,而註冊的中介軟體最終會被IApplicationBuilder物件構建成一個代表請求處理器的RequestDelegate物件,StartAsync方法的引數handler代表的就是這樣一個物件。

public interface IServer
{
    Task StartAsync(RequestDelegate handler);
}

針對伺服器的適配

面向應用層的HttpContext物件是對請求和響應的抽象與封裝,但是請求最初是由面向傳輸層的伺服器接收的,最終的響應也會由伺服器回覆給客戶端。所有ASP.NET Core應用使用的都是同一個HttpContext型別,但是它們可以註冊不同型別的伺服器,應如何解決兩者之間的適配問題?計算機領域有這樣一句話:“任何問題都可以通過新增一個抽象層的方式來解決,如果解決不了,那就再加一層。”同一個HttpContext型別與不同伺服器型別之間的適配問題自然也可以通過新增一個抽象層來解決。我們將定義在該抽象層的物件稱為特性(Feature),特性可以視為對HttpContext某個方面的抽象化描述。

如上圖所示,我們可以定義一系列特性介面來為HttpContext提供某個方面的上下文資訊,具體的伺服器只需要實現這些Feature介面即可。對於所有用來定義特性的介面,最重要的是提供請求資訊的IRequestFeature介面和完成響應的IResponseFeature介面。

下面闡述用來適配不同伺服器型別的特性在程式碼層面的定義。如下面的程式碼片段所示,我們定義了一個IFeatureCollection介面來表示存放特性的集合。可以看出,這是一個以Type和Object作為Key和Value的字典,Key代表註冊Feature所採用的型別,而Value代表Feature物件本身,也就是說,我們提供的特性最終是以對應型別(一般為介面型別)進行註冊的。為了便於程式設計,我們定義了Set<T>方法和Get<T>方法來設定與獲取特性物件。

public interface IFeatureCollection : IDictionary<Type, object> { }
public class FeatureCollection : Dictionary<Type, object>, IFeatureCollection { }
public static partial class Extensions
{
    public static T Get<T>(this IFeatureCollection features)  => features.TryGetValue(typeof(T), out var value) ? (T)value : default(T);
    public static IFeatureCollection Set<T>(this IFeatureCollection features, T feature)
    {
        features[typeof(T)] = feature;
        return features;
    }
}

最核心的兩種特性型別就是分別用來表示請求和響應的特性,我們可以採用如下兩個介面來表示。可以看出,IHttpRequestFeature介面和IHttpResponseFeature介面具有與抽象型別HttpRequest和HttpResponse完全一致的成員定義。

public interface IHttpRequestFeature
{
    Uri Url { get; }
    NameValueCollection Headers { get; }
    Stream Body { get; }
}
public interface IHttpResponseFeature
{
    int StatusCode { get; set; }
    NameValueCollection Headers { get; }
    Stream Body { get; }
}

我們在前面給出了用於描述請求上下文的HttpContext型別的成員定義,下面介紹其具體實現。如下面的程式碼片段所示,表示請求和響應的HttpRequest與HttpResponse分別是由對應的特性(IHttpRequestFeature物件和IHttpResponseFeature物件)建立的。HttpContext物件本身則是通過一個表示特性集合的IFeatureCollection 物件來建立的,它會在初始化過程中從這個集合中提取出對應的特性來建立HttpRequest物件和HttpResponse物件。

public class HttpContext
{
    public HttpRequest Request { get; }
    public HttpResponse Response { get; }

    public HttpContext(IFeatureCollection features)
    {
        Request = new HttpRequest(features);
        Response = new HttpResponse(features);
    }
}

public class HttpRequest
{
    private readonly IHttpRequestFeature _feature;
    public Uri Url=> _feature.Url;
    public NameValueCollection Headers=> _feature.Headers;
    public Stream Body=> _feature.Body;
    public HttpRequest(IFeatureCollection features)=> _feature = features.Get<IHttpRequestFeature>();
}

public class HttpResponse
{
    private readonly IHttpResponseFeature _feature;

    public NameValueCollection Headers=> _feature.Headers;
    public Stream Body=> _feature.Body;
    public int StatusCode
    {
        get => _feature.StatusCode;
        set => _feature.StatusCode = value;
    }
    public HttpResponse(IFeatureCollection features)=> _feature = features.Get<IHttpResponseFeature>();
}

換句話說,我們利用HttpContext物件的Request屬性提取的請求資訊最初來源於IHttpRequestFeature物件,利用它的Response屬性針對響應所做的任意操作最終都會作用到IHttpResponseFeature物件上。這兩個物件最初是由註冊的伺服器提供的,這正是同一個ASP.NET Core應用可以自由地選擇不同伺服器型別的根源所在。

HttpListenerServer

在對伺服器的職責和它與HttpContext的適配原理有了清晰的認識之後,我們可以嘗試定義一個伺服器。我們將接下來定義的伺服器型別命名為HttpListenerServer,因為它對請求的監聽、接收和響應是由一個HttpListener物件來實現的。由於伺服器接收到請求之後需要藉助“特性”的適配來構建統一的請求上下文(即HttpContext物件),這也是中介軟體的執行上下文,所以提供針對性的特性實現是自定義服務型別的關鍵所在。

對HttpListener有所瞭解的讀者都知道,當它在接收到請求之後同樣會建立一個HttpListenerContext物件表示請求上下文。如果使用HttpListener物件作為ASP.NET Core應用的監聽器,就意味著不僅所有的請求資訊會來源於這個HttpListenerContext物件,我們針對請求的響應最終也需要利用這個上下文物件來完成。HttpListenerServer對應特性所起的作用實際上就是在HttpListenerContext和HttpContext這兩種上下文之間搭建起一座如下圖所示的橋樑。

上圖中用來在HttpListenerContext和HttpContext這兩個上下文型別之間完成適配的特性型別被命名為HttpListenerFeature。如下面的程式碼片段所示,HttpListenerFeature型別同時實現了針對請求和響應的特性介面IHttpRequestFeature與IHttpResponseFeature。

public class HttpListenerFeature : IHttpRequestFeature, IHttpResponseFeature
{
    private readonly HttpListenerContext _context;
    public HttpListenerFeature(HttpListenerContext context) => _context = context;
    Uri IHttpRequestFeature.Url => _context.Request.Url;
    NameValueCollection IHttpRequestFeature.Headers => _context.Request.Headers;
    NameValueCollection IHttpResponseFeature.Headers => _context.Response.Headers;
    Stream IHttpRequestFeature.Body => _context.Request.InputStream;
    Stream IHttpResponseFeature.Body => _context.Response.OutputStream;
    int IHttpResponseFeature.StatusCode
    {
        get => _context.Response.StatusCode;
        set => _context.Response.StatusCode = value;
    }
}

建立HttpListenerFeature物件時需要提供一個HttpListenerContext物件,IHttpRequestFeature介面的實現成員所提供的請求資訊全部來源於這個HttpListenerContext上下文,IHttpResponseFeature介面的實現成員針對響應的操作最終也轉移到這個HttpListenerContext上下文上。如下所示的程式碼片段是針對HttpListener的伺服器型別HttpListenerServer的完整定義。我們在建立HttpListenerServer物件的時候可以顯式提供一組監聽地址,如果沒有提供,監聽地址會預設設定“localhost:5000”。在實現的StartAsync方法中,我們啟動了在建構函式中建立的HttpListenerServer物件,並且在一個無限迴圈中通過呼叫其GetContextAsync方法實現了針對請求的監聽和接收。

public class HttpListenerServer : IServer
{
    private readonly HttpListener _httpListener;
    private readonly string[] _urls;
    public HttpListenerServer(params string[] urls)
    {
        _httpListener = new HttpListener();
        _urls = urls.Any() ? urls : new string[] { "http://localhost:5000/" };
    }

    public async Task StartAsync(RequestDelegate handler)
    {
        Array.ForEach(_urls, url => _httpListener.Prefixes.Add(url));
        _httpListener.Start();
        while (true)
        {
            var listenerContext = await _httpListener.GetContextAsync();
            var feature = new HttpListenerFeature(listenerContext);
            var features = new FeatureCollection()
                .Set<IHttpRequestFeature>(feature)
                .Set<IHttpResponseFeature>(feature);
            var httpContext = new HttpContext(features);
            await handler(httpContext);
            listenerContext.Response.Close();
        }
    }
}

當HttpListener監聽到抵達的請求後,我們會得到一個HttpListenerContext物件,此時只需要利用它建立一個HttpListenerFeature物件並且分別以IHttpRequestFeature介面和IHttpResponseFeature介面的形式註冊到建立的FeatureCollection集合上。我們最終利用這個FeatureCollection集合創建出代表請求上下文的HttpContext物件,當將它作為引數呼叫由所有註冊中介軟體共同構建的RequestDelegate物件時,中介軟體管道將接管並處理該請求。

三、承載服務

到目前為止,我們已經瞭解構成ASP.NET Core請求處理管道的兩個核心要素(伺服器和中介軟體),現在我們的目標是利用.NET Core承載服務系統來承載這一管道。毫無疑問,還需要通過實現IHostedService介面來定義對應的承載服務,為此我們定義了一個名為WebHostedService的承載服務。(關於.NET Core承載服務系統,請參閱我的系列文章《服務承載系統》)

WebHostedService

由於伺服器是整個請求處理管道的“龍頭”,所以從某種意義上來說,啟動一個ASP.NET Core應用就是為啟動伺服器,所以可以將服務的啟動在WebHostedService承載服務中實現。如下面的程式碼片段所示,建立一個WebHostedService物件時,需要提供伺服器物件和由所有註冊中介軟體構建的RequestDelegate物件。在實現的StartAsync方法中,我們只需要呼叫伺服器物件的StartAsync方法啟動它即可。

public class WebHostedService : IHostedService
{
    private readonly IServer _server;
    private readonly RequestDelegate _handler;
    public WebHostedService(IServer server, RequestDelegate handler)
    {
        _server = server;
        _handler = handler;
    }

    public Task StartAsync(CancellationToken cancellationToken) => _server.StartAsync(_handler);
    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

到目前為止,我們基本上已經完成了所有核心的工作,如果能夠將一個WebHostedService例項註冊到.NET Core的承載系統中,它就能夠幫助我們啟動一個ASP.NET Core應用。為了使這個過程在程式設計上變得更加便利和“優雅”,我們定義了一個輔助的WebHostBuilder型別。

WebHostBuilder

要建立一個WebHostedService物件,必需顯式地提供一個表示伺服器的IServer物件,以及由所有註冊中介軟體構建而成的RequestDelegate物件,WebHostBuilder提供了更加便利和“優雅”的伺服器與中介軟體註冊方式。如下面的程式碼片段所示,WebHostBuilder是對額外兩個Builder物件的封裝:一個是用來構建服務宿主的IHostBuilder物件,另一個是用來註冊中介軟體並最終幫助我們建立RequestDelegate物件的IApplicationBuilder物件。

public class WebHostBuilder
{   
    public IHostBuilder HostBuilder { get; }
    public IApplicationBuilder ApplicationBuilder { get; }
    public WebHostBuilder(IHostBuilder hostBuilder, IApplicationBuilder applicationBuilder)
    {
        HostBuilder = hostBuilder;
        ApplicationBuilder = applicationBuilder;
    }
}

我們為WebHostBuilder定義瞭如下兩個擴充套件方法:UseHttpListenerServer方法完成了針對自定義的伺服器型別HttpListenerServer的註冊;Configure方法提供了一個Action<IApplication
Builder>型別的引數,利用該引數來註冊任意中介軟體。

public static partial class Extensions
{
    public static WebHostBuilder UseHttpListenerServer(this WebHostBuilder builder, params string[] urls)
    {
        builder.HostBuilder.ConfigureServices(svcs => svcs.AddSingleton<IServer>(new HttpListenerServer(urls)));
        return builder;
    }

    public static WebHostBuilder Configure(this WebHostBuilder builder, Action<IApplicationBuilder> configure)
    {
        configure?.Invoke(builder.ApplicationBuilder);
        return builder;
    }
}

代表ASP.NET Core應用的請求處理管道最終是利用承載服務WebHostedService註冊到.NET Core的承載系統中的,針對WebHostedService服務的建立和註冊體現在為IHostBuilder介面定義的ConfigureWebHost擴充套件方法上。如下面的程式碼片段所示,ConfigureWebHost方法定義了一個Action<WebHostBuilder>型別的引數,利用該引數可以註冊伺服器、中介軟體及其他相關服務。

public static partial class Extensions
{
    public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action<WebHostBuilder> configure)
    {
        var webHostBuilder = new WebHostBuilder(builder, new ApplicationBuilder());
        configure?.Invoke(webHostBuilder);
        builder.ConfigureServices(svcs => svcs.AddSingleton<IHostedService>(provider => {
            var server = provider.GetRequiredService<IServer>();
            var handler = webHostBuilder.ApplicationBuilder.Build();
            return new WebHostedService(server, handler);
        }));
        return builder;
    }
}

在ConfigureWebHost方法中,我們建立了一個ApplicationBuilder物件,並利用它和當前的IHostBuilder物件建立了一個WebHostBuilder物件,然後將這個WebHostBuilder物件作為引數呼叫了指定的Action<WebHostBuilder>委託物件。在此之後,我們呼叫IHostBuilder介面的ConfigureServices方法在依賴注入框架中註冊了一個用於建立WebHostedService服務的工廠。對於由該工廠建立的WebHostedService物件來說,伺服器來源於註冊的服務,而作為請求處理器的RequestDelegate物件則由ApplicationBuilder物件根據註冊的中介軟體構建而成。

應用構建

到目前為止,這個用來模擬ASP.NET Core請求處理管道的“迷你版”框架已經構建完成,下面嘗試在它上面開發一個簡單的應用。如下面的程式碼片段所示,我們呼叫靜態型別Host的CreateDefaultBuilder方法建立了一個IHostBuilder物件,然後呼叫ConfigureWebHost方法並利用提供的Action<WebHostBuilder>物件註冊了HttpListenerServer伺服器和3箇中間件。在呼叫Build方法構建出作為服務宿主的IHost物件之後,我們呼叫其Run方法啟動所有承載的IHostedSerivce服務。

class Program
{
    static void Main()
    {
        Host.CreateDefaultBuilder()
            .ConfigureWebHost(builder => builder
                .UseHttpListenerServer()
                .Configure(app => app
                    .Use(FooMiddleware)
                    .Use(BarMiddleware)
                    .Use(BazMiddleware)))
            .Build()
            .Run();
    }

    public static RequestDelegate FooMiddleware(RequestDelegate next)
        => async context =>{
            await context.Response.WriteAsync("Foo=>");
            await next(context);
        };

    public static RequestDelegate BarMiddleware(RequestDelegate next)
        => async context =>{
            await context.Response.WriteAsync("Bar=>");
            await next(context);
        };

    public static RequestDelegate BazMiddleware(RequestDelegate next)
        => context => context.Response.WriteAsync("Baz");
}

由於中介軟體最終體現為一個型別為Func<RequestDelegate, RequestDelegate>的委託物件,所以可以利用與之匹配的方法來定義中介軟體。演示例項中定義的3箇中間件(FooMiddleware、BarMiddleware和BazMiddleware)對應的正是3個靜態方法,它們呼叫WriteAsync擴充套件方法在響應中寫了一段文字。

public static partial class Extensions
{    
    public static Task WriteAsync(this HttpResponse response, string contents)
    {
        var buffer = Encoding.UTF8.GetBytes(contents);
        return response.Body.WriteAsync(buffer, 0, buffer.Length);
    }
}

應用啟動之後,如果利用瀏覽器嚮應用程式採用的預設監聽地址(“http://localhost:5000”)傳送一個請求,得到的輸出結果如下圖所示。瀏覽器上呈現的文字正是註冊的3箇中間件寫入