第 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
本作品采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。
歡迎轉載、使用、重新發布,但務必保留文章署名 鄭子銘 (包含鏈接: http://www.cnblogs.com/MingsonZheng/ ),不得用於商業目的,基於本文修改后的作品務必以相同的許可發布。
如有任何疑問,請與我聯系 (MingsonZheng@outlook.com) 。