動手寫一個簡版 asp.net core

来源:https://www.cnblogs.com/weihanli/archive/2020/05/22/mini-aspnetcore.html

動手寫一個簡版 asp.net core Intro 之前看到過蔣金楠老師的一篇 200 行代碼帶你瞭解 asp.net core 框架,最近參考蔣老師和 Edison 的文章和代碼,結合自己對 asp.net core 的理解 ,最近自己寫了一個 MiniAspNetCore ,寫篇文章總結一下。 ...


動手寫一個簡版 asp.net core

Intro

之前看到過蔣金楠老師的一篇 200 行代碼帶你瞭解 asp.net core 框架,最近參考蔣老師和 Edison 的文章和代碼,結合自己對 asp.net core 的理解 ,最近自己寫了一個 MiniAspNetCore ,寫篇文章總結一下。

HttpContext

HttpContext 可能是最為常用的一個類了,HttpContext 是請求上下文,包含了所有的請求信息以及響應信息,以及一些自定義的用於在不同中間件中傳輸數據的信息

來看一下 HttpContext 的定義:

public class HttpContext
{
    public IServiceProvider RequestServices { get; set; }

    public HttpRequest Request { get; set; }

    public HttpResponse Response { get; set; }

    public IFeatureCollection Features { get; set; }

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

HttpRequest 即為請求信息對象,包含了所有請求相關的信息,

HttpResponse 為響應信息對象,包含了請求對應的響應信息

RequestServices 為 asp.net core 里的RequestServices,代表當前請求的服務提供者,可以使用它來獲取具體的服務實例

Features 為 asp.net core 里引入的對象,可以用來在不同中間件中傳遞信息和用來解耦合

,下麵我們就來看下 HttpRequestHttpResponse 是怎麼實現的

HttpRequest:

public class HttpRequest
{
    private readonly IRequestFeature _requestFeature;

    public HttpRequest(IFeatureCollection featureCollection)
    {
        _requestFeature = featureCollection.Get<IRequestFeature>();
    }

    public Uri Url => _requestFeature.Url;

    public NameValueCollection Headers => _requestFeature.Headers;

    public string Method => _requestFeature.Method;

    public string Host => _requestFeature.Url.Host;

    public Stream Body => _requestFeature.Body;
}

HttpResponse:

public class HttpResponse
{
    private readonly IResponseFeature _responseFeature;

    public HttpResponse(IFeatureCollection featureCollection)
    {
        _responseFeature = featureCollection.Get<IResponseFeature>();
    }

    public bool ResponseStarted => _responseFeature.Body.Length > 0;

    public int StatusCode
    {
        get => _responseFeature.StatusCode;
        set => _responseFeature.StatusCode = value;
    }

    public async Task WriteAsync(byte[] responseBytes)
    {
        if (_responseFeature.StatusCode <= 0)
        {
            _responseFeature.StatusCode = 200;
        }
        if (responseBytes != null && responseBytes.Length > 0)
        {
            await _responseFeature.Body.WriteAsync(responseBytes);
        }
    }
}

Features

上面我們提供我們可以使用 Features 在不同中間件中傳遞信息和解耦合

由上面 HttpRequest/HttpResponse 的代碼我們可以看出來,HttpRequestHttpResponse 其實就是在 IRequestFeatureIResponseFeature 的基礎上封裝了一層,真正的核心其實是 IRequestFeature/IResponseFeature ,而這裡使用介面就很好的實現瞭解耦,可以根據不同的 WebServer 使用不同的 RequestFeature/ResponseFeature,來看下 IRequestFeature/IResponseFeature 的實現

public interface IRequestFeature
{
    Uri Url { get; }

    string Method { get; }

    NameValueCollection Headers { get; }

    Stream Body { get; }
}

public interface IResponseFeature
{
    public int StatusCode { get; set; }

    NameValueCollection Headers { get; set; }

    public Stream Body { get; }
}

這裡的實現和 asp.net core 的實際的實現方式應該不同,asp.net core 里 Headers 同一個 Header 允許有多個值,asp.net core 里是 StringValues 來實現的,這裡簡單處理了,使用了一個 NameValueCollection 對象

上面提到的 Features 是一個 IFeatureCollection 對象,相當於是一系列的 Feature 對象組成的,來看下 FeatureCollection 的定義:

public interface IFeatureCollection : IDictionary<Type, object> { }

public class FeatureCollection : Dictionary<Type, object>, IFeatureCollection
{
}

這裡 IFeatureCollection 直接實現 IDictionary<Type, object> ,通過一個字典 Feature 類型為 Key,Feature 對象為 Value 的字典來保存

為了方便使用,可以定義兩個擴展方法來方便的Get/Set

public static class FeatureExtensions
{
    public static IFeatureCollection Set<TFeature>(this IFeatureCollection featureCollection, TFeature feature)
    {
        featureCollection[typeof(TFeature)] = feature;
        return featureCollection;
    }

    public static TFeature Get<TFeature>(this IFeatureCollection featureCollection)
    {
        var featureType = typeof(TFeature);
        return featureCollection.ContainsKey(featureType) ? (TFeature)featureCollection[featureType] : default(TFeature);
    }
}

Web伺服器

上面我們已經提到了 Web 伺服器通過 IRequestFeature/IResponseFeature 來實現不同 web 伺服器和應用程式的解耦,web 伺服器只需要提供自己的 RequestFeature/ResponseFeature 即可

為了抽象不同的 Web 伺服器,我們需要定義一個 IServer 的抽象介面,定義如下:

public interface IServer
{
    Task StartAsync(Func<HttpContext, Task> requestHandler, CancellationToken cancellationToken = default);
}

IServer 定義了一個 StartAsync 方法,用來啟動 Web伺服器,

StartAsync 方法有兩個參數,一個是 requestHandler,是一個用來處理請求的委托,另一個是取消令牌用來停止 web 伺服器

示例使用了 HttpListener 來實現了一個簡單 Web 伺服器,HttpListenerServer 定義如下:

public class HttpListenerServer : IServer
{
    private readonly HttpListener _listener;
    private readonly IServiceProvider _serviceProvider;

    public HttpListenerServer(IServiceProvider serviceProvider, IConfiguration configuration)
    {
        _listener = new HttpListener();
        var urls = configuration.GetAppSetting("ASPNETCORE_URLS")?.Split(';');
        if (urls != null && urls.Length > 0)
        {
            foreach (var url in urls
                     .Where(u => u.IsNotNullOrEmpty())
                     .Select(u => u.Trim())
                     .Distinct()
                    )
            {
                // Prefixes must end in a forward slash ("/")
                // https://stackoverflow.com/questions/26157475/use-of-httplistener
                _listener.Prefixes.Add(url.EndsWith("/") ? url : $"{url}/");
            }
        }
        else
        {
            _listener.Prefixes.Add("http://localhost:5100/");
        }

        _serviceProvider = serviceProvider;
    }

    public async Task StartAsync(Func<HttpContext, Task> requestHandler, CancellationToken cancellationToken = default)
    {
        _listener.Start();
        if (_listener.IsListening)
        {
            Console.WriteLine("the server is listening on ");
            Console.WriteLine(_listener.Prefixes.StringJoin(","));
        }
        while (!cancellationToken.IsCancellationRequested)
        {
            var listenerContext = await _listener.GetContextAsync();

            var featureCollection = new FeatureCollection();
            featureCollection.Set(listenerContext.GetRequestFeature());
            featureCollection.Set(listenerContext.GetResponseFeature());

            using (var scope = _serviceProvider.CreateScope())
            {
                var httpContext = new HttpContext(featureCollection)
                {
                    RequestServices = scope.ServiceProvider,
                };

                await requestHandler(httpContext);
            }
            listenerContext.Response.Close();
        }
        _listener.Stop();
    }
}

HttpListenerServer 實現的 RequestFeature/ResponseFeatue

public class HttpListenerRequestFeature : IRequestFeature
{
    private readonly HttpListenerRequest _request;

    public HttpListenerRequestFeature(HttpListenerContext listenerContext)
    {
        _request = listenerContext.Request;
    }

    public Uri Url => _request.Url;
    public string Method => _request.HttpMethod;
    public NameValueCollection Headers => _request.Headers;
    public Stream Body => _request.InputStream;
}

public class HttpListenerResponseFeature : IResponseFeature
{
    private readonly HttpListenerResponse _response;

    public HttpListenerResponseFeature(HttpListenerContext httpListenerContext)
    {
        _response = httpListenerContext.Response;
    }

    public int StatusCode { get => _response.StatusCode; set => _response.StatusCode = value; }

    public NameValueCollection Headers
    {
        get => _response.Headers;
        set
        {
            _response.Headers = new WebHeaderCollection();
            foreach (var key in value.AllKeys)
                _response.Headers.Add(key, value[key]);
        }
    }

    public Stream Body => _response.OutputStream;
}

為了方便使用,為 HttpListenerContext 定義了兩個擴展方法,就是上面 HttpListenerServer 中的 GetRequestFeature/GetResponseFeature

public static class HttpListenerContextExtensions
{
    public static IRequestFeature GetRequestFeature(this HttpListenerContext context)
    {
        return new HttpListenerRequestFeature(context);
    }

    public static IResponseFeature GetResponseFeature(this HttpListenerContext context)
    {
        return new HttpListenerResponseFeature(context);
    }
}

RequestDelegate

在上面的 IServer 定義里有一個 requestHandler 的 對象,在 asp.net core 里是一個名稱為 RequestDelegate 的對象,而用來構建這個委托的在 asp.net core 里是 IApplicationBuilder,這些在蔣老師和 Edison 的文章和代碼里都可以看到,這裡我們只是簡單介紹下,我在 MiniAspNetCore 的示例中沒有使用這些對象,而是使用了自己抽象的 PipelineBuilder 和原始委托實現的

asp.net core 里 RequestDelegate 定義:

public delegate Task RequestDelegate(HttpContext context);

其實和我們上面定義用的 Func<HttpContext, Task> 是等價的

IApplicationBuilder 定義:

/// <summary>
/// Defines a class that provides the mechanisms to configure an application's request pipeline.
/// </summary>
public interface IApplicationBuilder
{
    /// <summary>
    /// Gets or sets the <see cref="T:System.IServiceProvider" /> that provides access to the application's service container.
    /// </summary>
    IServiceProvider ApplicationServices { get; set; }

    /// <summary>
    /// Gets the set of HTTP features the application's server provides.
    /// </summary>
    IFeatureCollection ServerFeatures { get; }

    /// <summary>
    /// Gets a key/value collection that can be used to share data between middleware.
    /// </summary>
    IDictionary<string, object> Properties { get; }

    /// <summary>
    /// Adds a middleware delegate to the application's request pipeline.
    /// </summary>
    /// <param name="middleware">The middleware delegate.</param>
    /// <returns>The <see cref="T:Microsoft.AspNetCore.Builder.IApplicationBuilder" />.</returns>
    IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);

    /// <summary>
    /// Creates a new <see cref="T:Microsoft.AspNetCore.Builder.IApplicationBuilder" /> that shares the <see cref="P:Microsoft.AspNetCore.Builder.IApplicationBuilder.Properties" /> of this
    /// <see cref="T:Microsoft.AspNetCore.Builder.IApplicationBuilder" />.
    /// </summary>
    /// <returns>The new <see cref="T:Microsoft.AspNetCore.Builder.IApplicationBuilder" />.</returns>
    IApplicationBuilder New();

    /// <summary>
    /// Builds the delegate used by this application to process HTTP requests.
    /// </summary>
    /// <returns>The request handling delegate.</returns>
    RequestDelegate Build();
}

我們這裡沒有定義 IApplicationBuilder,使用了簡化抽象的 IAsyncPipelineBuilder,定義如下:

public interface IAsyncPipelineBuilder<TContext>
{
    IAsyncPipelineBuilder<TContext> Use(Func<Func<TContext, Task>, Func<TContext, Task>> middleware);

    Func<TContext, Task> Build();

    IAsyncPipelineBuilder<TContext> New();
}

對於 asp.net core 的中間件來說 ,上面的 TContext 就是 HttpContext,替換之後也就是下麵這樣的:

public interface IAsyncPipelineBuilder<HttpContext>
{
    IAsyncPipelineBuilder<HttpContext> Use(Func<Func<HttpContext, Task>, Func<HttpContext, Task>> middleware);

    Func<HttpContext, Task> Build();

    IAsyncPipelineBuilder<HttpContext> New();
}

是不是和 IApplicationBuilder 很像,如果不像可以進一步把 Func<HttpContext, Task> 使用 RequestDelegate 替換

public interface IAsyncPipelineBuilder<HttpContext>
{
    IAsyncPipelineBuilder<HttpContext> Use(Func<RequestDelegate, RequestDelegate> middleware);

    RequestDelegate Build();

    IAsyncPipelineBuilder<HttpContext> New();
}

最後再將介面名稱替換一下:

public interface IApplicationBuilder1
{
    IApplicationBuilder1 Use(Func<RequestDelegate, RequestDelegate> middleware);

    RequestDelegate Build();

    IApplicationBuilder1 New();
}

至此,就完全可以看出來了,這 IAsyncPipelineBuilder<HttpContext> 就是一個簡版的 IApplicationBuilder

IAsyncPipelineBuilderIApplicationBuilder 的作用是將註冊的多個中間件構建成一個請求處理的委托

中間件處理流程:

更多關於 PipelineBuilder 構建中間件的信息可以查看 讓 .NET 輕鬆構建中間件模式代碼 瞭解更多

WebHost

通過除了 Web 伺服器之外,還有一個 Web Host 的概念,可以簡單的這樣理解,一個 Web 伺服器上可以有多個 Web Host,就像 IIS/nginx (Web Server) 可以 host 多個站點

可以說 WebHost 離我們的應用更近,所以我們還需要 IHost 來托管應用

public interface IHost
{
    Task RunAsync(CancellationToken cancellationToken = default);
}

WebHost 定義:

public class WebHost : IHost
{
    private readonly Func<HttpContext, Task> _requestDelegate;
    private readonly IServer _server;

    public WebHost(IServiceProvider serviceProvider, Func<HttpContext, Task> requestDelegate)
    {
        _requestDelegate = requestDelegate;
        _server = serviceProvider.GetRequiredService<IServer>();
    }

    public async Task RunAsync(CancellationToken cancellationToken = default)
    {
        await _server.StartAsync(_requestDelegate, cancellationToken).ConfigureAwait(false);
    }
}

為了方便的構建 Host對象,引入了 HostBuilder 來方便的構建一個 Host,定義如下:

public interface IHostBuilder
{
    IHostBuilder ConfigureConfiguration(Action<IConfigurationBuilder> configAction);

    IHostBuilder ConfigureServices(Action<IConfiguration, IServiceCollection> configureAction);

    IHostBuilder Initialize(Action<IConfiguration, IServiceProvider> initAction);

    IHostBuilder ConfigureApplication(Action<IConfiguration, IAsyncPipelineBuilder<HttpContext>> configureAction);

    IHost Build();
}

WebHostBuilder

public class WebHostBuilder : IHostBuilder
{
    private readonly IConfigurationBuilder _configurationBuilder = new ConfigurationBuilder();
    private readonly IServiceCollection _serviceCollection = new ServiceCollection();

    private Action<IConfiguration, IServiceProvider> _initAction = null;

    private readonly IAsyncPipelineBuilder<HttpContext> _requestPipeline = PipelineBuilder.CreateAsync<HttpContext>(context =>
    {
        context.Response.StatusCode = 404;
        return Task.CompletedTask;
    });

    public IHostBuilder ConfigureConfiguration(Action<IConfigurationBuilder> configAction)
    {
        configAction?.Invoke(_configurationBuilder);
        return this;
    }

    public IHostBuilder ConfigureServices(Action<IConfiguration, IServiceCollection> configureAction)
    {
        if (null != configureAction)
        {
            var configuration = _configurationBuilder.Build();
            configureAction.Invoke(configuration, _serviceCollection);
        }

        return this;
    }

    public IHostBuilder ConfigureApplication(Action<IConfiguration, IAsyncPipelineBuilder<HttpContext>> configureAction)
    {
        if (null != configureAction)
        {
            var configuration = _configurationBuilder.Build();
            configureAction.Invoke(configuration, _requestPipeline);
        }
        return this;
    }

    public IHostBuilder Initialize(Action<IConfiguration, IServiceProvider> initAction)
    {
        if (null != initAction)
        {
            _initAction = initAction;
        }

        return this;
    }

    public IHost Build()
    {
        var configuration = _configurationBuilder.Build();
        _serviceCollection.AddSingleton<IConfiguration>(configuration);
        var serviceProvider = _serviceCollection.BuildServiceProvider();

        _initAction?.Invoke(configuration, serviceProvider);

        return new WebHost(serviceProvider, _requestPipeline.Build());
    }

    public static WebHostBuilder CreateDefault(string[] args)
    {
        var webHostBuilder = new WebHostBuilder();
        webHostBuilder
            .ConfigureConfiguration(builder => builder.AddJsonFile("appsettings.json", true, true))
            .UseHttpListenerServer()
            ;

        return webHostBuilder;
    }
}

這裡的示例我在 IHostBuilder 里增加了一個 Initialize 的方法來做一些初始化的操作,我覺得有些數據初始化配置初始化等操作應該在這裡操作,而不應該在 StartupConfigure 方法里處理,這樣 Configure 方法可以更純粹一些,只配置 asp.net core 的請求管道,這純屬個人意見,沒有對錯之分

這裡 Host 的實現和 asp.net core 的實現不同,有需要的可以深究源碼,在 asp.net core 2.x 的版本里是有一個 IWebHost 的,在 asp.net core 3.x 以及 .net 5 里是沒有 IWebHost 的取而代之的是通用主機 IHost, 通過實現了一個 IHostedService 來實現 WebHost

Run

運行示例代碼:

public class Program
{
    private static readonly CancellationTokenSource Cts = new CancellationTokenSource();

    public static async Task Main(string[] args)
    {
        Console.CancelKeyPress += OnExit;

        var host = WebHostBuilder.CreateDefault(args)
            .ConfigureServices((configuration, services) =>
            {
            })
            .ConfigureApplication((configuration, app) =>
            {
                app.When(context => context.Request.Url.PathAndQuery.StartsWith("/favicon.ico"), pipeline => { });

                app.When(context => context.Request.Url.PathAndQuery.Contains("test"),
                    p => { p.Run(context => context.Response.WriteAsync("test")); });
                app
                    .Use(async (context, next) =>
                    {
                        await context.Response.WriteLineAsync($"middleware1, requestPath:{context.Request.Url.AbsolutePath}");
                        await next();
                    })
                    .Use(async (context, next) =>
                    {
                        await context.Response.WriteLineAsync($"middleware2, requestPath:{context.Request.Url.AbsolutePath}");
                        await next();
                    })
                    .Use(async (context, next) =>
                    {
                        await context.Response.WriteLineAsync($"middleware3, requestPath:{context.Request.Url.AbsolutePath}");
                        await next();
                    })
                    ;
                app.Run(context => context.Response.WriteAsync("Hello Mini Asp.Net Core"));
            })
            .Initialize((configuration, services) =>
            {
            })
            .Build();
        await host.RunAsync(Cts.Token);
    }

    private static void OnExit(object sender, EventArgs e)
    {
        Console.WriteLine("exiting ...");
        Cts.Cancel();
    }
}

在示例項目目錄下執行 dotnet run,並訪問 http://localhost:5100/:

仔細觀察瀏覽器 consolenetwork 的話,會發現還有一個請求,瀏覽器會預設請求 /favicon.ico 獲取網站的圖標

因為我們針對這個請求沒有任何中間件的處理,所以直接返回了 404

在訪問 /test,可以看到和剛纔的輸出完全不同,因為這個請求走了另外一個分支,相當於 asp.net core 里 Map/MapWhen 的效果,另外 Run 代表裡中間件的中斷,不會執行後續的中間件

More

上面的實現只是我在嘗試寫一個簡版的 asp.net core 框架時的實現,和 asp.net core 的實現並不完全一樣,如果需要請參考源碼,上面的實現僅供參考,上面實現的源碼可以在 Github 上獲取 https://github.com/WeihanLi/SamplesInPractice/tree/master/MiniAspNetCore

asp.net core 源碼:https://github.com/dotnet/aspnetcore

Reference


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

更多相關文章
  • 環境 雖說就發郵件這麼個小事,很容易相容Python2, Python3, 但是大家還是擁抱Python3吧, 我這裡沒有做python2的相容寫法,所以需要python3以上。 很多人學習python,不知道從何學起。很多人學習python,掌握了基本語法過後,不知道在哪裡尋找案例上手。很多已經做 ...
  • 我的LeetCode:https://leetcode cn.com/u/ituring/ 我的LeetCode刷題源碼[GitHub]:https://github.com/izhoujie/Algorithmcii LeetCode 面試題55 I. 二叉樹的深度 與以下題目相同 前往:Leet ...
  • 我的LeetCode:https://leetcode cn.com/u/ituring/ 我的LeetCode刷題源碼[GitHub]:https://github.com/izhoujie/Algorithmcii LeetCode 104. 二叉樹的最大深度 題目 給定一個二叉樹,找出其最大深 ...
  • 0. 前言 前言,暫時揮別NHibernate(雖然我突然發現這玩意還挺有意思的,不過看得人不多)。大步進入了有很多小伙伴向我安利的SQLSugar,嗯,我一直叫SugarSQL,好像是這個吧? 這是一個由國內開發者開發的ORM框架,是一個輕量級框架(最新版的sqlSugarCore大概只有290k ...
  • 需求:資料庫數據都是縱向的,呈現的時候要求是橫向(列轉行)同時看到行頭(行轉列)。 分析:很多報表呈現都要求要這樣顯示,用類是可以實現,但是代碼多,又需要建很多dto。發下Excel有轉置功能,但又不想牽扯Excel這一套組件。就使用DataTable來實現,代碼也不多。 先看看示例數據3列10行: ...
  • 同步版本示例: namespace SyncSample { class MyDownloadString { Stopwatch sw = new Stopwatch(); public void DoRun() { const int LargeNumber = 6000000; sw.Star ...
  • 在Linux上搭建基於開源技術的nuget私人保密倉庫 前言 在Linux上搭建nuget私人倉庫一直是一個老大難的問題,主要涉及到以下難點: nuget.org官方使用的Nuget.Server基於.NET Framework的ASP.NET,而不是ASP.NET Core,因此是Windows ...
  • 網上有很多Socket框架,但是我想,C#既然有Socket類,難道不是給人用的嗎? 寫了一個SocketServerHelper和SocketClientHelper,分別隻有5、6百行代碼,比不上大神寫的,和業務代碼耦合也比較重,但對新手非常友好,容易看懂。 支持返回值或回調,支持不定長度的數據 ...
一周排行
  • 一:背景 1. 講故事 曾今在項目中發現有同事自定義結構體的時候,居然沒有重寫Equals方法,比如下麵這段代碼: static void Main(string[] args) { var list = Enumerable.Range(0, 1000).Select(m => new Point ...
  • 最近一個朋友有個關於素數的小東西要寫一下,素數是什麼呢?除了1和他本身不能被其他數整除,那麼這個數就是素數,1除外哦。我們知道概念那就很簡單了,直接代碼擼起。 ...
  • 前言 在開發編程中,我們經常會遇到功能非常相似的功能模塊,只是他們的處理的數據不一樣,所以我們會分別採用多個方法來處理不同的數據類型。但是這個時候,我們就會想一個問題,有沒有辦法實現利用同一個方法來傳遞不同種類型的參數呢? 這個時候,泛型也就因運而生,專門來解決這個問題的。 泛型是在C 2.0就推出 ...
  • 本文章主要用於介紹在Asp.Net Mvc(C#)中使用Fleck製作一個Html5的即時聊天室,含有完整代碼和演示Demo。 ...
  • 出庫單的功能。能學習了出庫單管理之後,WMS的 主體功能算是完成了。當然一個成熟的WMS還包括了盤點,報表,策略規則,移庫功能及與其他系統(ERP、TMS等)的介面,實現無縫集成,打破信息孤島,讓數據實時、準確和同步。 ...
  • Data StructureThere're two types of variables in C#, reference type and value type.Enum:enum Color{Red=0,Green=1}//equals to enum Color{Red,//start fr... ...
  • 0. 前言 該項目使用Maven進行管理和構建,所以需要預先配置好Maven。嗯,在這個系列里就不做過多的介紹了。 1. 創建項目 先創建一個pom.xml 文件,添加以下內容: <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http: ...
  • API 概述 API(Application Programming Interface),應用程式編程介面。 Java API是一本程式員的 字典 ,是JDK中提供給我們使用的類的說明文檔。 這些類將底層的代碼實現封裝了起來,我們不需要關心這些類是如何實現的,只需要學習這些類如何使用即可。 所以我 ...
  • 女程式員是這麼徵婚的: SELECT * FROM 男人們 WHERE 未婚=true and 同性戀=false and 有房=true and 有車=true and 條件 in (帥氣,紳士,大度,氣質,智慧,溫柔,體貼,會浪漫,活潑,可愛,最好還能帶孩子) and 年齡 between(24 ...
  • 有很多剛學習軟體測試的小伙伴,都會在網路上找尋各種學習資料,去提升自己的專業技能水平。因此,我決定定期分享我整理收集的一些軟體測試的測試工具下載、面試寶典、視頻教學合集。都整理好了,有需要的可以關註我(獲取方式在文末) 軟體測試的學習,不止是基礎理論,還需要學習測試工具的用法,如介面工具Postma ...