第 4 章 后端服务
现实中的服务不可能处于真空之中,大多数服务都需要与其他服务通信才能完成功能。
我们将这些支持性服务称为后端服务,接下来我们将通过创建一个新的服务并修改之前的团队服务与这个服务通信,以探索如何创建并消费后端服务。
微服务生态系统
后端服务是通过某种机制绑定到应用上的,而这种机制又可以由云设施(PaaS)管理。与打开一个文件不同,我们与泛化的存储服务通信。
资源绑定的概念其实是一种抽象,而具体的实现可能根据应用托管所在的云平台而有所差异。服务的绑定信息可能直接来自从平台注入的环境变量,或者来自外部的配置提供设施。
微服务是单一职责 SRP 和 里氏替换原则 LSP 的集中体现。对单个微服务的修改,不应该对任何其他服务产生任何影响。对服务内部模型的修改不应该破坏服务公开的 API 和外部模型。
开发位置服务
GitHub 链接:https://github.com/microservices-aspnetcore/locationservice.git
首先创建一个模型表示位置记录
public class LocationRecord {
public Guid ID { get; set; }
public float Latitude { get; set; }
public float Longitude { get; set; }
public float Altitude { get; set; }
public long Timestamp { get; set; }
public Guid MemberID { get; set; }
}
接下来,我们需要一个接口来表示位置信息仓储的契约
public interface ILocationRecordRepository {
LocationRecord Add(LocationRecord locationRecord);
LocationRecord Update(LocationRecord locationRecord);
LocationRecord Get(Guid memberId, Guid recordId);
LocationRecord Delete(Guid memberId, Guid recordId);
LocationRecord GetLatestForMember(Guid memberId);
ICollection<LocationRecord> AllForMember(Guid memberId);
}
控制器通过构造器注入的方式接收一个 ILocationRecordRepository 实例
[Route("locations/{memberId}")]
public class LocationRecordController : Controller {
private ILocationRecordRepository locationRepository;
public LocationRecordController(ILocationRecordRepository repository) {
this.locationRepository = repository;
}
[HttpPost]
public IActionResult AddLocation(Guid memberId, [FromBody]LocationRecord locationRecord) {
locationRepository.Add(locationRecord);
return this.Created($"/locations/{memberId}/{locationRecord.ID}", locationRecord);
}
[HttpGet]
public IActionResult GetLocationsForMember(Guid memberId) {
return this.Ok(locationRepository.AllForMember(memberId));
}
[HttpGet("latest")]
public IActionResult GetLatestForMember(Guid memberId) {
return this.Ok(locationRepository.GetLatestForMember(memberId));
}
}
要让依赖注入能够提供仓储,只需要在启动期间把它添加为具有特定生命周期的服务,在 Stattup.cs 中
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<ILocationRecordRepository, InMemoryLocationRecordRepository>();
services.AddMvc();
}
优化团队服务
我们希望在查询特定团队成员的详细信息时,要包含他们最新的位置以及签入时间。实现这一功能,有两个主要步骤:
将位置服务的 URL 绑定到团队的服务
使用 URL 消费位置服务
使用环境变量配置服务的 URL
这个过程中要记住最重要的一点就是这些信息必须来自运行环境,而不是签入的代码。
消费 RESTful 服务
由于需要对团队服务终端控制器方法进行单元测试,并且在测试过程中不发出 HTTP 请求,我们要先为位置服务的客户端创建接口
将 teamservice 的分支切换为 location
public interface ILocationClient
{
Task<LocationRecord> GetLatestForMember(Guid memberId);
}
位置服务客户端的实现
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using StatlerWaldorfCorp.TeamService.Models;
using Newtonsoft.Json;
using Microsoft.Extensions.Configuration;
using System.Text;
namespace StatlerWaldorfCorp.TeamService.LocationClient
{
public class HttpLocationClient : ILocationClient
{
public String URL { get; set; }
public HttpLocationClient(string url)
{
this.URL = url;
}
public async Task<LocationRecord> GetLatestForMember(Guid memberId)
{
LocationRecord locationRecord = null;
using (var httpClient = new HttpClient())
{
httpClient.BaseAddress = new Uri(this.URL);
httpClient.DefaultRequestHeaders.Accept.Clear();
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
HttpResponseMessage response = await httpClient.GetAsync(String.Format("/locations/{0}/latest", memberId));
if (response.IsSuccessStatusCode)
{
string json = await response.Content.ReadAsStringAsync();
locationRecord = JsonConvert.DeserializeObject<LocationRecord>(json);
}
}
return locationRecord;
}
public async Task<LocationRecord> AddLocation(Guid memberId, LocationRecord locationRecord)
{
using (var httpClient = new HttpClient())
{
httpClient.BaseAddress = new Uri(this.URL);
httpClient.DefaultRequestHeaders.Accept.Clear();
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var jsonString = JsonConvert.SerializeObject(locationRecord);
var uri = String.Format("/locations/{0}", memberId);
HttpResponseMessage response =
await httpClient.PostAsync(uri, new StringContent(jsonString, Encoding.UTF8, "application/json"));
if (response.IsSuccessStatusCode)
{
string json = await response.Content.ReadAsStringAsync();
}
}
return locationRecord;
}
}
}
接着,修改控制器 MembersController,调用位置服务客户端,将团队成员的最新位置添加到响应中
[HttpGet]
[Route("/teams/{teamId}/[controller]/{memberId}")]
public async virtual Task<IActionResult> GetMember(Guid teamID, Guid memberId)
{
Team team = repository.Get(teamID);
if(team == null) {
return this.NotFound();
} else {
var q = team.Members.Where(m => m.ID == memberId);
if(q.Count() < 1) {
return this.NotFound();
} else {
Member member = (Member)q.First();
return this.Ok(new LocatedMember {
ID = member.ID,
FirstName = member.FirstName,
LastName = member.LastName,
LastLocation = await this.locationClient.GetLatestForMember(member.ID)
});
}
}
}
我们用的 LocationRecord 模型类是团队服务所私有的。
团队服务和位置服务并不共用模型,团队服务一直只依赖于位置服务公共的 API, 而不依赖于内部实现。
接下来我们希望增加一种能力,为使用应用的每个人维护签到过的历史位置信息,创建一个位置服务用于单独管理位置数据,它公开一个方便的端点来检索团队成员的最新位置。
团队服务:microservices-aspnetcore/teamservice:location
位置服务:microservices-aspnetcore/locationservice:nodb
首先启动团队服务
$ docker run -p 5000:5000 -e PORT=5000 \
-e LOCATION__URL=http://localhost:5001 \
dotnetcoreservices/teamservice:location
这样就能在 5000 端口启动团队服务,把容器内的 5000 端口映射到 localhost 的 5000 端口,并让团队服务从 http://localhost:5001 调用位置服务
团队服务启动完成后再启动位置服务
$ docker run -p 5001:5001 -e PORT=5001 \
dotnetcoreservices/locationservice:nodb
两个服务启动完成后,可通过 docker ps 命令查看各个服务的 Docker 配置。
接下来,运行一系列命令确保一切工作正常。
创建一个新的团队
$ curl -H "Content-Type:application/json" -X POST -d \
'{"id":"e52baa63-d511-417e-9e54-7aab04286281", \
"name":"Team Zombie"}' http://localhost:5000/teams
通过向 /teams/{id}/members 资源发送 POST 请求添加新的成员
$ curl -H "Content-Type:application/json" -X POST -d \
'{"id":"63e7acf8-8fae-42ec-9349-3c8593ac8292", \
"firstName":"Al", \
"lastName":"Foo"}' \
http://localhost:5000/teams/e52baa63-d511-417e-9e54-7aab04286281/members
尝试查询团队详情,确保一切工作正常
$ curl http://localhost:5000/teams/e52baa63-d511-417e-9e54-7aab04286281
往位置服务添加新的位置信息
$ curl -H "Content-Type:application/json" -X POST -d \
'{"id":"64c3e69f-1580-4b2f-a9ff-2c5f3b8f0elf", \
"latitude":12.0,"longitude":12.0,"altitude":10.0, \
"timestamp":0, \
"memberId":"63e7acf8-8fae-42ec-9349-3c8593ac8292"}' \
http://localhost:5001/locations/63e7acf8-8fae-42ec-9349-3c8593ac8292
现在可以真正测试团队服务和位置服务之间的集成了,从 teams/{id}/members/{id} 资源查询团队成员的详细信息
$ curl http://localhost:5000/teams/e52baa63-d511-417e-9e54-7aab04286281 \
/members/63e7acf8-8fae-42ec-9349-3c8593ac8292