經(jīng)過(guò)一段時(shí)間的開(kāi)發(fā)與測(cè)試,終于發(fā)布了Lms框架的第一個(gè)正式版本(1.0.0版本),并給出了lms框架的樣例項(xiàng)目lms.samples。本文通過(guò)對(duì)lms.samples的介紹,簡(jiǎn)述如何通過(guò)lms框架快速的構(gòu)建一個(gè)微服務(wù)的業(yè)務(wù)框架,并進(jìn)行應(yīng)用開(kāi)發(fā)。
lms.sample項(xiàng)目由三個(gè)獨(dú)立的微服務(wù)應(yīng)用模塊組成:account、stock、order和一個(gè)網(wǎng)關(guān)項(xiàng)目gateway構(gòu)成。
每個(gè)獨(dú)立的微服務(wù)應(yīng)用采用模塊化設(shè)計(jì),主要由如下幾部分組成:
DTO
對(duì)象。應(yīng)用接口除了被該微服務(wù)應(yīng)用項(xiàng)目引用,并實(shí)現(xiàn)應(yīng)用服務(wù)之前,還可以被網(wǎng)關(guān)或是其他微服務(wù)模塊引用。網(wǎng)關(guān)或是其他微服務(wù)項(xiàng)目通過(guò)應(yīng)用接口生成的代理與該微服務(wù)模塊通過(guò)rpc進(jìn)行通信。SqlHelper
、或是ORM相關(guān)的類型等。在lms.samples中,通過(guò)efcore實(shí)現(xiàn)數(shù)據(jù)的讀寫(xiě)操作。lms框架不允許服務(wù)外部與微服務(wù)主機(jī)直接通信,應(yīng)用請(qǐng)求必須通過(guò)http請(qǐng)求到達(dá)網(wǎng)關(guān),網(wǎng)關(guān)通過(guò)lms提供的中間件解析到服務(wù)條目,并通過(guò)rpc與集群內(nèi)部的微服務(wù)進(jìn)行通信。所以,如果服務(wù)需要與集群外部進(jìn)行通信,那么,開(kāi)發(fā)者定義的網(wǎng)關(guān)必須要引用各個(gè)微服務(wù)模塊的應(yīng)用接口層;以及必須要使用lms相關(guān)的中間件。
通過(guò)lms框架創(chuàng)建一個(gè)業(yè)務(wù)模塊非常方便,只需要通過(guò)如下4個(gè)步驟,就可以輕松的創(chuàng)建一個(gè)lms應(yīng)用業(yè)務(wù)模塊。
1.創(chuàng)建項(xiàng)目
創(chuàng)建控制臺(tái)應(yīng)用(Console Application)項(xiàng)目,并且引用Silky.Lms.NormHost
包。
dotnet add package Silky.Lms.NormHost --version 1.0.0
2.應(yīng)用程序入口與主機(jī)構(gòu)建
在main
方法中,通用.net的主機(jī)Host
構(gòu)建并注冊(cè)lms微服務(wù)。在注冊(cè)lms微服務(wù)時(shí),需要指定lms啟動(dòng)的依賴模塊。
一般地,如果開(kāi)發(fā)者不需要額外依賴其他模塊,也無(wú)需在應(yīng)用啟動(dòng)或停止時(shí)執(zhí)行方法,那么您可以直接指定NormHostModule
模塊。
public class Program { public static async Task Main(string[] args) { await CreateHostBuilder(args).Build().RunAsync(); } private static IHostBuilder CreateHostBuilder(string[] args) { return Host.CreateDefaultBuilder(args) .RegisterLmsServicesNormHostModule>() ; } }
3.配置文件
lms框架支持yml
或是json
格式作為配置文件。通過(guò)appsettings.yml
對(duì)lms框架進(jìn)行統(tǒng)一配置,通過(guò)appsettings.${Environment}.yml
對(duì)不同環(huán)境變量下的配置項(xiàng)進(jìn)行設(shè)置。
開(kāi)發(fā)者如果直接通過(guò)項(xiàng)目的方式啟動(dòng)應(yīng)用,那么可以通過(guò)Properties/launchSettings.json
的environmentVariables.DOTNET_ENVIRONMENT
環(huán)境變量。如果通過(guò)docker-compose
的方式啟動(dòng)應(yīng)用,那么可以通過(guò).env
設(shè)置DOTNET_ENVIRONMENT
環(huán)境變量。
為保證配置文件有效,開(kāi)發(fā)者需要顯式的將配置文件拷貝到項(xiàng)目生成目錄下。
4.引用應(yīng)用服務(wù)層和數(shù)據(jù)訪問(wèn)層
一般地,主機(jī)項(xiàng)目需要引用該微服務(wù)模塊的應(yīng)用服務(wù)層和數(shù)據(jù)訪問(wèn)層。只有主機(jī)引用應(yīng)用服務(wù)層,主機(jī)在啟動(dòng)時(shí),才會(huì)生成服務(wù)條目的路由,并且將服務(wù)路由注冊(cè)到服務(wù)注冊(cè)中心。
一個(gè)典型的主機(jī)項(xiàng)目文件如下所示:
Project Sdk="Microsoft.NET.Sdk"> PropertyGroup> OutputType>Exe/OutputType> TargetFramework>net5.0/TargetFramework> /PropertyGroup> ItemGroup> PackageReference Include="Silky.Lms.NormHost" Version="$(LmsVersion)" /> /ItemGroup> ItemGroup> None Update="appsettings.yml"> CopyToOutputDirectory>Always/CopyToOutputDirectory> /None> None Update="appsettings.Production.yml"> CopyToOutputDirectory>Always/CopyToOutputDirectory> /None> None Update="appsettings.Development.yml"> CopyToOutputDirectory>Always/CopyToOutputDirectory> /None> /ItemGroup> ItemGroup> ProjectReference Include="..\Lms.Account.Application\Lms.Account.Application.csproj" /> ProjectReference Include="..\Lms.Account.EntityFrameworkCore\Lms.Account.EntityFrameworkCore.csproj" /> /ItemGroup> /Project>
一般地,一個(gè)微服務(wù)模塊的主機(jī)必須要配置:服務(wù)注冊(cè)中心、分布式鎖鏈接、分布式緩存地址、集群rpc通信token、數(shù)據(jù)庫(kù)鏈接地址等。
如果使用docker-compose來(lái)啟動(dòng)和調(diào)試應(yīng)用的話,那么,rpc配置節(jié)點(diǎn)下的的host和port可以缺省,因?yàn)樯傻拿總€(gè)容器的都有自己的地址和端口號(hào)。
如果直接通過(guò)項(xiàng)目的方式啟動(dòng)和調(diào)試應(yīng)用的話,那么,必須要配置rpc節(jié)點(diǎn)下的port,每個(gè)微服務(wù)模塊的主機(jī)應(yīng)用有自己的端口號(hào)。
lms框架的必要配置如下所示:
rpc: host: 0.0.0.0 rpcPort: 2201 token: ypjdYOzNd4FwENJiEARMLWwK0v7QUHPW registrycenter: connectionStrings: 127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183;127.0.0.1:2184,127.0.0.1:2185,127.0.0.1:2186 # 使用分號(hào);來(lái)區(qū)分不同的服務(wù)注冊(cè)中心 registryCenterType: Zookeeper distributedCache: redis: isEnabled: true configuration: 127.0.0.1:6379,defaultDatabase=0 lock: lockRedisConnection: 127.0.0.1:6379,defaultDatabase=1 connectionStrings: default: server=127.0.0.1;port=3306;database=account;uid=root;pwd=qwe!P4ss;
一般地,在應(yīng)用接口層開(kāi)發(fā)者需要安裝Silky.Lms.Rpc
包。如果該微服務(wù)模塊還涉及到分布式事務(wù),那么還需要安裝Silky.Lms.Transaction.Tcc
,當(dāng)然,您也可以選擇在應(yīng)用接口層安裝Silky.Lms.Transaction
包,在應(yīng)用服務(wù)層安裝Silky.Lms.Transaction.Tcc
包。
ServiceRouteAttribute
特性對(duì)應(yīng)用接口進(jìn)行直接即可。IXxxAppService
命名,這樣,服務(wù)條目生成的路由則會(huì)以api/xxx
形式生成。當(dāng)然這并不是強(qiáng)制的。[Governance(ProhibitExtranet = true)]
可以標(biāo)識(shí)一個(gè)方法禁止與集群外部進(jìn)行通信,通過(guò)網(wǎng)關(guān)也不會(huì)生成swagger文檔。HttpMethodAttribute
特性對(duì)某個(gè)方法進(jìn)行注解。一個(gè)典型的應(yīng)用接口的定義
/// summary> /// 賬號(hào)服務(wù) /// /summary> [ServiceRoute] public interface IAccountAppService { /// summary> /// 新增賬號(hào) /// /summary> /// param name="input">賬號(hào)信息/param> /// returns>/returns> TaskGetAccountOutput> Create(CreateAccountInput input); /// summary> /// 通過(guò)賬號(hào)名稱獲取賬號(hào) /// /summary> /// param name="name">賬號(hào)名稱/param> /// returns>/returns> [GetCachingIntercept("Account:Name:{0}")] [HttpGet("{name:string}")] TaskGetAccountOutput> GetAccountByName([CacheKey(0)] string name); /// summary> /// 通過(guò)Id獲取賬號(hào)信息 /// /summary> /// param name="id">賬號(hào)Id/param> /// returns>/returns> [GetCachingIntercept("Account:Id:{0}")] [HttpGet("{id:long}")] TaskGetAccountOutput> GetAccountById([CacheKey(0)] long id); /// summary> /// 更新賬號(hào)信息 /// /summary> /// param name="input">/param> /// returns>/returns> [UpdateCachingIntercept( "Account:Id:{0}")] TaskGetAccountOutput> Update(UpdateAccountInput input); /// summary> /// 刪除賬號(hào)信息 /// /summary> /// param name="id">賬號(hào)Id/param> /// returns>/returns> [RemoveCachingIntercept("GetAccountOutput","Account:Id:{0}")] [HttpDelete("{id:long}")] Task Delete([CacheKey(0)]long id); /// summary> /// 訂單扣款 /// /summary> /// param name="input">/param> /// returns>/returns> [Governance(ProhibitExtranet = true)] [RemoveCachingIntercept("GetAccountOutput","Account:Id:{0}")] [Transaction] Tasklong?> DeductBalance(DeductBalanceInput input); }
一個(gè)典型的應(yīng)用服務(wù)的實(shí)現(xiàn)如下所示:
public class AccountAppService : IAccountAppService { private readonly IAccountDomainService _accountDomainService; public AccountAppService(IAccountDomainService accountDomainService) { _accountDomainService = accountDomainService; } public async TaskGetAccountOutput> Create(CreateAccountInput input) { var account = input.MapToDomain.Accounts.Account>(); account = await _accountDomainService.Create(account); return account.MapToGetAccountOutput>(); } public async TaskGetAccountOutput> GetAccountByName(string name) { var account = await _accountDomainService.GetAccountByName(name); return account.MapToGetAccountOutput>(); } public async TaskGetAccountOutput> GetAccountById(long id) { var account = await _accountDomainService.GetAccountById(id); return account.MapToGetAccountOutput>(); } public async TaskGetAccountOutput> Update(UpdateAccountInput input) { var account = await _accountDomainService.Update(input); return account.MapToGetAccountOutput>(); } public Task Delete(long id) { return _accountDomainService.Delete(id); } [TccTransaction(ConfirmMethod = "DeductBalanceConfirm", CancelMethod = "DeductBalanceCancel")] public async Tasklong?> DeductBalance(DeductBalanceInput input) { var account = await _accountDomainService.GetAccountById(input.AccountId); if (input.OrderBalance > account.Balance) { throw new BusinessException("賬號(hào)余額不足"); } return await _accountDomainService.DeductBalance(input, TccMethodType.Try); } public Task DeductBalanceConfirm(DeductBalanceInput input) { return _accountDomainService.DeductBalance(input, TccMethodType.Confirm); } public Task DeductBalanceCancel(DeductBalanceInput input) { return _accountDomainService.DeductBalance(input, TccMethodType.Cancel); } }
ITransientDependency
接口,這樣,該領(lǐng)域服務(wù)才會(huì)被注入到ioc容器。一個(gè)典型的領(lǐng)域服務(wù)的實(shí)現(xiàn)如下所示:
public class AccountDomainService : IAccountDomainService { private readonly IRepository _repository; private readonly IDistributedCacheGetAccountOutput, string> _accountCache; public AccountDomainService(IRepository repository, IDistributedCacheGetAccountOutput, string> accountCache) { _repository = repository; _accountCache = accountCache; } public async TaskAccount> Create(Account account) { var exsitAccountCount = await _repository.GetCountAsyncAccount>(p => p.Name == account.Name); if (exsitAccountCount > 0) { throw new BusinessException($"已經(jīng)存在{account.Name}名稱的賬號(hào)"); } exsitAccountCount = await _repository.GetCountAsyncAccount>(p => p.Email == account.Email); if (exsitAccountCount > 0) { throw new BusinessException($"已經(jīng)存在{account.Email}Email的賬號(hào)"); } await _repository.InsertAsyncAccount>(account); return account; } public async TaskAccount> GetAccountByName(string name) { var accountEntry = _repository.GetQueryableAccount>().FirstOrDefault(p => p.Name == name); if (accountEntry == null) { throw new BusinessException($"不存在名稱為{name}的賬號(hào)"); } return accountEntry; } public async TaskAccount> GetAccountById(long id) { var accountEntry = _repository.GetQueryableAccount>().FirstOrDefault(p => p.Id == id); if (accountEntry == null) { throw new BusinessException($"不存在Id為{id}的賬號(hào)"); } return accountEntry; } public async TaskAccount> Update(UpdateAccountInput input) { var account = await GetAccountById(input.Id); if (!account.Email.Equals(input.Email)) { var exsitAccountCount = await _repository.GetCountAsyncAccount>(p => p.Email == input.Email); if (exsitAccountCount > 0) { throw new BusinessException($"系統(tǒng)中已經(jīng)存在Email為{input.Email}的賬號(hào)"); } } if (!account.Name.Equals(input.Name)) { var exsitAccountCount = await _repository.GetCountAsyncAccount>(p => p.Name == input.Name); if (exsitAccountCount > 0) { throw new BusinessException($"系統(tǒng)中已經(jīng)存在Name為{input.Name}的賬號(hào)"); } } await _accountCache.RemoveAsync($"Account:Name:{account.Name}"); account = input.MapTo(account); await _repository.UpdateAsync(account); return account; } public async Task Delete(long id) { var account = await GetAccountById(id); await _accountCache.RemoveAsync($"Account:Name:{account.Name}"); await _repository.DeleteAsync(account); } public async Tasklong?> DeductBalance(DeductBalanceInput input, TccMethodType tccMethodType) { var account = await GetAccountById(input.AccountId); var trans = await _repository.BeginTransactionAsync(); BalanceRecord balanceRecord = null; switch (tccMethodType) { case TccMethodType.Try: account.Balance -= input.OrderBalance; account.LockBalance += input.OrderBalance; balanceRecord = new BalanceRecord() { OrderBalance = input.OrderBalance, OrderId = input.OrderId, PayStatus = PayStatus.NoPay }; await _repository.InsertAsync(balanceRecord); RpcContext.GetContext().SetAttachment("balanceRecordId",balanceRecord.Id); break; case TccMethodType.Confirm: account.LockBalance -= input.OrderBalance; var balanceRecordId1 = RpcContext.GetContext().GetAttachment("orderBalanceId")?.Tolong>(); if (balanceRecordId1.HasValue) { balanceRecord = await _repository.GetByIdAsyncBalanceRecord>(balanceRecordId1.Value); balanceRecord.PayStatus = PayStatus.Payed; await _repository.UpdateAsync(balanceRecord); } break; case TccMethodType.Cancel: account.Balance += input.OrderBalance; account.LockBalance -= input.OrderBalance; var balanceRecordId2 = RpcContext.GetContext().GetAttachment("orderBalanceId")?.Tolong>(); if (balanceRecordId2.HasValue) { balanceRecord = await _repository.GetByIdAsyncBalanceRecord>(balanceRecordId2.Value); balanceRecord.PayStatus = PayStatus.Cancel; await _repository.UpdateAsync(balanceRecord); } break; } await _repository.UpdateAsync(account); await trans.CommitAsync(); await _accountCache.RemoveAsync($"Account:Name:{account.Name}"); return balanceRecord?.Id; } }
IConfigureService
,通過(guò)繼承該接口即可使用IServiceCollection
的實(shí)例指定數(shù)據(jù)上下文對(duì)象和注冊(cè)倉(cāng)庫(kù)服務(wù)。public class EfCoreConfigureService : IConfigureService { public void ConfigureServices(IServiceCollection services, IConfiguration configuration) { services.AddDbContextOrderDbContext>(opt => opt.UseMySql(configuration.GetConnectionString("Default"), ServerVersion.AutoDetect(configuration.GetConnectionString("Default")))) .AddGenericRepositoryOrderDbContext>(ServiceLifetime.Transient) ; } public int Order { get; } = 1; }
3.主機(jī)項(xiàng)目需要顯式的引用該項(xiàng)目,只有這樣,該項(xiàng)目的ConfigureServices
才會(huì)被調(diào)用。
4.數(shù)據(jù)遷移,請(qǐng)參考
1.使用git 克隆lms項(xiàng)目源代碼,lms.samples存放在samples
目錄下
# github git clone https://github.com/liuhll/lms.git # gitee git clone https://gitee.com/liuhll2/lms.git
zookeeper
redis
如果您電腦已經(jīng)安裝了docker以及docker-compose命令,那么您只需要進(jìn)入samples\docker-compose\infrastr
目錄下,打開(kāi)命令行工作,執(zhí)行如下命令就可以自動(dòng)安裝zookeeper
、redis
、mysql
等服務(wù):
docker-compose -f .\docker-compose.mysql.yml -f .\docker-compose.redis.yml -f .\docker-compose.zookeeper.yml up -d
需要分別進(jìn)入到各個(gè)微服務(wù)模塊下的EntityFrameworkCore
項(xiàng)目(例如:),執(zhí)行如下命令:
dotnet ef database update
例如: 需要遷移account模塊的數(shù)據(jù)庫(kù)如下所示:
order模塊和stock模塊與account模塊一致,在服務(wù)運(yùn)行前都需要通過(guò)數(shù)據(jù)庫(kù)遷移命令生成相關(guān)數(shù)據(jù)庫(kù)。
appsettings.Development.yml
中配置的,您可以通過(guò)修改該配置文件中的connectionStrings.default
配置項(xiàng)來(lái)指定自己的數(shù)據(jù)庫(kù)服務(wù)地址。dotnet ef
命令,則需要通過(guò)dotnet tool install --global dotnet-ef
安裝ef工具,請(qǐng)[參考] (https://docs.microsoft.com/zh-cn/ef/core/get-started/overview/install)使用visual studio作為開(kāi)發(fā)工具
進(jìn)入到samples目錄下,使用visual studio打開(kāi)lms.samples.sln
解決方案,將項(xiàng)目設(shè)置為多啟動(dòng)項(xiàng)目,并將網(wǎng)關(guān)和各個(gè)模塊的微服務(wù)主機(jī)設(shè)置為啟動(dòng)項(xiàng)目,如下圖:
設(shè)置完成后直接啟動(dòng)即可。
使用rider作為開(kāi)發(fā)工具進(jìn)入到samples目錄下,使用rider打開(kāi)lms.samples.sln
解決方案,打開(kāi)各個(gè)微服務(wù)模塊下的Properties/launchSettings.json
,點(diǎn)擊圖中綠色的箭頭即可啟動(dòng)項(xiàng)目。
啟動(dòng)網(wǎng)關(guān)項(xiàng)目后,可以看到應(yīng)用接口的服務(wù)條目生成的swagger api文檔 http://localhost:5000/swagger。
默認(rèn)的環(huán)境變量為: Development
,如果需要修改環(huán)境變量的話,可以通過(guò)Properties/launchSettings.json
下的environmentVariables
節(jié)點(diǎn)修改相關(guān)環(huán)境變量,請(qǐng)參考在 ASP.NET Core 中使用多個(gè)環(huán)境。
數(shù)據(jù)庫(kù)連接、服務(wù)注冊(cè)中心地址、以及redis緩存地址和分布式鎖連接等配置項(xiàng)可以通過(guò)修改appsettings.Development.yml
配置項(xiàng)自定義指定。
進(jìn)入到samples目錄下,使用visual studio打開(kāi)lms.samples.dockercompose.sln
解決方案,將docker-compose設(shè)置為啟動(dòng)項(xiàng)目,即可啟動(dòng)和調(diào)式。
應(yīng)用啟動(dòng)成功后,打開(kāi): http://127.0.0.1/swagger,即可看到swagger api文檔
以docker-compose的方式啟動(dòng)和調(diào)試,則指定的環(huán)境變量為:ContainerDev
數(shù)據(jù)庫(kù)連接、服務(wù)注冊(cè)中心地址、以及redis緩存地址和分布式鎖連接等配置項(xiàng)可以通過(guò)修改appsettings.ContainerDev.yml
配置項(xiàng)自定義指定,配置的服務(wù)連接地址不允許為: 127.0.0.1
或是localhost
服務(wù)啟動(dòng)成功后,您可以通過(guò)寫(xiě)入/api/account-post
接口和/api/product-post
新增賬號(hào)和產(chǎn)品,然后通過(guò)/api/order-post
接口進(jìn)行測(cè)試和調(diào)式。
github: https://github.com/liuhll/lms
gitee: https://gitee.com/liuhll2/lms
到此這篇關(guān)于通過(guò)lms.samples熟悉lms微服務(wù)框架的使用的文章就介紹到這了,更多相關(guān)lms微服務(wù)框架內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
標(biāo)簽:平頂山 鎮(zhèn)江 綿陽(yáng) 臺(tái)州 鶴崗 商丘 株洲 哈密
巨人網(wǎng)絡(luò)通訊聲明:本文標(biāo)題《通過(guò)lms.samples熟悉lms微服務(wù)框架的使用詳解》,本文關(guān)鍵詞 通過(guò),lms.samples,熟悉,lms,;如發(fā)現(xiàn)本文內(nèi)容存在版權(quán)問(wèn)題,煩請(qǐng)?zhí)峁┫嚓P(guān)信息告之我們,我們將及時(shí)溝通與處理。本站內(nèi)容系統(tǒng)采集于網(wǎng)絡(luò),涉及言論、版權(quán)與本站無(wú)關(guān)。