搭建一套ASP.NET Core+Nacos+Spring Cloud Gateway项目

前言

    伴随着随着微服务概念的不断盛行,与之对应的各种解决方案也层出不穷。这毕竟是一个信息大爆发的时代,各种编程语言大行其道,各有各的优势。但是有一点未曾改变,那就是他们服务的方式,工作的时候各司其职,但是需要提供服务的时候必须要高度统一,这也是微服务的概念之一。日常的工作学习中,我个人更喜欢通用的解决方案,特别是能将不同编程语言亦或者不同编程框架整合到一起的那种,这种解决方案拉近了编程语言之间的距离,让开发者能更清楚的意识到编程语言只是工具,解决问题才是王道。好了口遁到此结束,接下来我就搭建一套.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下载

总结

    到这里我们的相关示例也就差不多了,能有一套公共的解决方案,使用起来还是非常方便的。这里我只是演示了非常基础的一种模式,就是为了展示技术通用性给我们带来的便利。我个人还是非常喜欢通用的解决方案的,这些方案能让我更关注问题本身,而非某种特定的语言。比如现在容器技术大行其道,我们其实可以忽略原有的许多技术细节,而通过容器平台本身的通用解决方案去解决,在我们使用的时候会非常方便。我也希望更多的开发者,能够关注技术本身或者解决方案本身带给我们的便利,而不是通过有色的眼光去看待这些。能解决的方案终究还是好的方案,它能指导我们的思想,提升我的思维方式,那我们为什么不去接触去学习呢?语言本身固然重要,但是解决问题的思维方式更是不可或缺。废话不多说,本次就到这里,欢迎大家评论区批评指导。

👇欢迎扫码关注我的公众号👇
posted @ 2020-07-03 17:32  yi念之间  阅读(787)  评论(0编辑  收藏