搭建一套ASP.NET Core+Nacos+Spring Cloud Gateway專案

yi念之間發表於2020-07-03

前言

    伴隨著隨著微服務概念的不斷盛行,與之對應的各種解決方案也層出不窮。這畢竟是一個資訊大爆發的時代,各種程式語言大行其道,各有各的優勢。但是有一點未曾改變,那就是他們服務的方式,工作的時候各司其職,但是需要提供服務的時候必須要高度統一,這也是微服務的概念之一。日常的工作學習中,我個人更喜歡通用的解決方案,特別是能將不同程式語言亦或者不同程式設計框架整合到一起的那種,這種解決方案拉近了程式語言之間的距離,讓開發者能更清楚的意識到程式語言只是工具,解決問題才是王道。好了口遁到此結束,接下來我就搭建一套.Net體系結合Java體系的專案架構。

概念介紹

接下來我們用到的技術棧名詞主要涉及到ASP.NET Core、Nacos、Spring Cloud Gateway,接下來我們分別介紹所使用的的三種框架。

Nacos

Nacos是阿里巴巴開源的致力於服務發現、配置和管理微服務的框架。提供了一組簡單易用的特性集,幫助您快速實現動態服務發現、服務配置、服務後設資料及流量管理。一般用到的最多的就是當做配置中心和註冊中心。

  • 中文官網地址:https://nacos.io/zh-cn/
  • 官方GayHub GitHub地址:https://github.com/alibaba/nacos
  • 下載地址:https://github.com/alibaba/nacos/releases下載執行Nacos之前別忘了安裝JDK,如何安裝JDK請自行百度這裡就不再詳細介紹了。下載Nacos方式有兩種。第一種是直接下載打包好的檔案直接執行。第二種是下載原始碼自己編譯,還需要安裝maven,相對於第一個稍微複雜一些,我選擇的是第一種方式。

ASP.NET Core

ASP.NET Core是微軟開源跨平臺的Web開發框架,這個作為.Net開發者相信大家已經非常熟悉了,目前最新的正式版本是3.1.5,也是我們本次搭建框架的重頭戲,作為業務的真正執行者

Spring Cloud Gateway

Spring Cloud Gateway為Spring生態系統上的一個API閘道器元件,主要提供一種簡單而有效的方式路由對映到指定的API,併為他們提供安全性、監控和限流等等。最主要的是可以輕鬆整合已有的Spring各種全家桶,比如我們們這次使用的Nacos,搭建使用起來非常方便。

開始搭建

上面大致介紹了相關概念,相信大家也有了大致的瞭解。口說無憑,直接開幹。

執行Nacos

執行啟動Nacos,在瀏覽器輸入輸入http://localhost:8848/nacos/#會展示出如下介面。
本次我們主要是用Nacos作為註冊中心,所以我們只需要關注服務管理模組即可。

搭建ASP.NET Core專案

    ASP.NET Core專案是我們業務介面的真正提供者,這裡我搭建兩個專案用於模擬訂單系統和商品系統。用Visual Studio新建兩個Web空專案,分別是OrderApi和ProductApi。OrderApi呼叫ProductApi屬於內部之間呼叫,不走Gateway。由於我們使用Nacos作為註冊中心,所以我們在需要對接到Nacos上。Nacos有一套Open API的介面對接方式(官方文件)[https://nacos.io/zh-cn/docs/open-api.html]有詳細的介紹。自己寫終究還是比較麻煩的,好在隨著NET Core的日漸成熟,已經有大佬為我們實現了一套sdk基本上滿足我們的使用非常的方便,GitHub地址為https://github.com/catcherwong/nacos-sdk-csharp別忘了給大佬個Star???,將程式包分別引入OrderApi和ProductApi

<PackageReference Include="nacos-sdk-csharp-unofficial.AspNetCore" Version="0.2.6" />

在appsettings.json中配置,本次展示預設使用的OrderApi作為演示,ProductApi配置方式一致,只需更換註冊名稱即可

"nacos": {
    "ServerAddresses": [ "localhost:8848" ],//Nacos地址
    "DefaultTimeOut": 15000,
    "Namespace": "",
    "ListenInterval": 1000,
    "ServiceName": "orderservice" //註冊到Nacos上的服務名
  }

Startup中配置如下

public void ConfigureServices(IServiceCollection services)
{
    //註冊Nacos相關服務
    services.AddNacosAspNetCore(Configuration);

    services.AddScoped<NacosDiscoveryDelegatingHandler>();
    services.AddHttpClient(ServiceName.ProductService,client=> {
        //ServiceName是我為了方便定義的常量類用於承載我們可以使用到的服務名稱這裡即productservice
        client.BaseAddress = new Uri($"http://{ServiceName.ProductService}");
    }).AddHttpMessageHandler<NacosDiscoveryDelegatingHandler>();

    services.AddControllers();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    //新增Nacos相關中介軟體
    app.UseNacosAspNetCore();

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

上面我們提到過,OrderApi呼叫ProductApi屬於內部系統間呼叫,所以我引入了HttpClientFactory。由於我們使用的是Nacos作為註冊中心,所以我寫了一個NacosDiscoveryDelegatingHandler配合HttpClientFactory,能更優雅的使用註冊中心,如果對這種實現方式不熟悉的話可以參考我之前的博文.NET Core HttpClientFactory+Consul實現服務發現實現原理完全一致,具體程式碼如下

public class NacosDiscoveryDelegatingHandler: DelegatingHandler
{
    private readonly INacosServerManager _serverManager;
    private readonly ILogger<NacosDiscoveryDelegatingHandler> _logger;

    public NacosDiscoveryDelegatingHandler(INacosServerManager serverManager,ILogger<NacosDiscoveryDelegatingHandler> logger)
    {
        _serverManager = serverManager;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var current = request.RequestUri;
        try
        {
            //通過nacos sdk獲取註冊中心服務地址,內建了隨機負載均衡演算法,所以只返回一條資訊
            var baseUrl = await _serverManager.GetServerAsync(current.Host);
            request.RequestUri = new Uri($"{baseUrl}{current.PathAndQuery}");
            return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
        }
        catch (Exception e)
        {
            _logger?.LogDebug(e, "Exception during SendAsync()");
            throw;
        }
        finally
        {
            request.RequestUri = current;
        }
    }
}

由於我們需要模擬訂單介面,所以我新建了OrderController,大致程式碼如下

[Route("orderapi/[controller]")]
public class OrderController : ControllerBase
{
    private List<OrderDto> orderDtos = new List<OrderDto>();
    private readonly IHttpClientFactory _clientFactory;

    public OrderController(IHttpClientFactory clientFactory)
    {
        orderDtos.Add(new OrderDto { Id = 1,TotalMoney=222,Address="北京市",Addressee="me",From="淘寶",SendAddress="武漢" });
        orderDtos.Add(new OrderDto { Id = 2, TotalMoney = 111, Address = "北京市", Addressee = "yi", From = "京東", SendAddress = "北京" });
        orderDtos.Add(new OrderDto { Id = 3, TotalMoney = 333, Address = "北京市", Addressee = "yi念之間", From = "天貓", SendAddress = "杭州" });

        _clientFactory = clientFactory;
    }

    [HttpGet("get/{id}")]
    public OrderDto GetOrder(long id)
    {
        return orderDtos.FirstOrDefault(i => i.Id == id);
    }

    [HttpGet("getdetails/{id}")]
    public async Task<OrderDto> GetOrderDetailsAsync(long id)
    {
        OrderDto orderDto = GetOrder(id);
        if (orderDto != null)
        {
            OrderDetailDto orderDetailDto = new OrderDetailDto
            {
                Id = orderDto.Id,
                TotalMoney = orderDto.TotalMoney,
                Address = orderDto.Address,
                Addressee = orderDto.Addressee,
                From = orderDto.From,
                SendAddress = orderDto.SendAddress
            };

            //內部呼叫ProductApi,配合自定義的NacosDiscoveryDelegatingHandler可以更優雅的使用註冊中心方式
            var client = _clientFactory.CreateClient(ServiceName.ProductService);
            var response = await client.GetAsync($"/productapi/product/getall");
            var result = await response.Content.ReadAsStringAsync();

            orderDetailDto.Products = JsonConvert.DeserializeObject<List<OrderProductDto>>(result);
            return orderDetailDto;
        }
        return orderDto;
    }
}

ProductApi提供新建ProductController用於模擬提供商品資訊

[Route("productapi/[controller]")]
public class ProductController : ControllerBase
{
    private List<ProductDto> productDtos = new List<ProductDto>();
    public ProductController()
    {
        productDtos.Add(new ProductDto { Id = 1,Name="酒精",Price=22.5m });
        productDtos.Add(new ProductDto { Id = 2, Name = "84消毒液", Price = 19.9m });
        productDtos.Add(new ProductDto { Id = 3, Name = "醫用口罩", Price = 55 });
    }

    [HttpGet("get/{id}")]
    public ProductDto Get(long id)
    {
        return productDtos.FirstOrDefault(i => i.Id == id);
    }

    [HttpGet("getall")]
    public IEnumerable<ProductDto> GetAll()
    {
        return productDtos;
    }
}

分別啟動OrderApi和ProductApi,然後去Nacos上檢視,展示如下,說明服務註冊成功
我們每個服務只啟動了一個例項,每個服務可以啟動多個例項,實現高可用和負載均衡。到這裡ASP.NET Core相關的程式碼我們已經搭建完成了,以上只是展示了大致的流程,具體的實現可以去下載Demo檢視。

搭建Spring Cloud Gateway

如何搭建Spring Cloud Gateway網上有很多教程,IDEA搭建非常簡單,基本上就是起個名字,我們本專案名稱就叫apigateway,然後一直點下一步,由於本示例後端的業務系統都是對接到Nacos上的,所以需要在Gateway引入Nacos相關包,在pom.xml引入Nacos

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    <version>2.2.1.RELEASE</version>
</dependency>

然後再啟動類上加上@EnableDiscoveryClient註解

@SpringBootApplication
@EnableDiscoveryClient
public class ApiGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(ApiGatewayApplication.class, args);
    }
}

在application.yml中新增服務名稱和nacos相關配置

server:
  #閘道器啟動埠號
  port: 8080
spring:
  application:
    #閘道器服務名稱,也就是註冊到Nacos的名稱
    name: apigateway
  #Nacos相關配置
  cloud:
    nacos:
      discovery:
        #Nacos服務地址
        server-addr: localhost:8848
      gateway:
        discovery:
          locator:
            enabled: true
            lower-case-service-id: true

啟動閘道器專案apigateway,開啟Nacos管理介面,重新整理服務列表,如下圖所示,說明閘道器專案註冊成功
接下來我們要在閘道器配置轉發相關內容,讓apigateway可以轉發請求到我們具體的OrderApi和ProductApi。Spring Cloud Gateway預設支援兩種配置轉發的方式,一種是基於編碼的方式,另一種是通過配置的方式。我選用的是基於配置的方式,相對比較靈活一點。在application.yml中新增轉發相關配置,如下

server:
  port: 8080
spring:
  application:
    name: apigateway
  redis:
    host: localhost
    port: 6379
    database: 0
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
      gateway:
        discovery:
          locator:
            enabled: true
            lower-case-service-id: true
    #轉發相關配置
    gateway:
      routes:
        #唯一標識
        - id: productservice
          #服務在註冊中心的地址
          uri: lb://productservice
          #轉發匹配,即滿足/productapi/相關的路徑則轉發到productservice相關服務
          predicates:
            - Path=/productapi/**
        - id: orderservice
          uri: lb://orderservice
          predicates:
            - Path=/orderapi/**

到這裡閘道器相關的配置差不多先配置這麼多,當然閘道器還需要整合許多核心元件比如限流相關熔斷超時相關等等,Spring Cloud Gateway都是可以接入相關元件的比如阿里的Sentinel等等,在這裡我們就不做演示了。

測試呼叫

可以下載本文演示Demo啟動Postman進行測試,通過呼叫閘道器專案apigateway看看執行效果,首先呼叫OrderApi介面,OderDetails介面內部呼叫了ProductApi的介面。如下所示轉發成功
然後我們在去呼叫ProductApi的相關介面,如圖所示也是成功的
本文演示Demo下載

總結

    到這裡我們的相關示例也就差不多了,能有一套公共的解決方案,使用起來還是非常方便的。這裡我只是演示了非常基礎的一種模式,就是為了展示技術通用性給我們帶來的便利。我個人還是非常喜歡通用的解決方案的,這些方案能讓我更關注問題本身,而非某種特定的語言。比如現在容器技術大行其道,我們其實可以忽略原有的許多技術細節,而通過容器平臺本身的通用解決方案去解決,在我們使用的時候會非常方便。我也希望更多的開發者,能夠關注技術本身或者解決方案本身帶給我們的便利,而不是通過有色的眼光去看待這些。能解決的方案終究還是好的方案,它能指導我們的思想,提升我的思維方式,那我們為什麼不去接觸去學習呢?語言本身固然重要,但是解決問題的思維方式更是不可或缺。廢話不多說,本次就到這裡,歡迎大家評論區批評指導。

?歡迎掃碼關注我的公眾號? 搭建一套ASP.NET Core+Nacos+Spring Cloud Gateway專案

相關文章