服务间的通讯-WebApiClient
声明式REST Api客户端能够帮助我们在日常开发中减少很多构建HttpClient处理请求的工作,减少对业务代码的侵入。前文介绍了工作中自己实现的REST Api代理,其实.net 体系也有一个比较好用的开源REST Client,是大大老九写的 WebApiClient。
比较了一下,自己实现的REST Api客户端和WebApiClient在使用上基本类似,不过WebApiClient确实要强大很多,考虑了更多情况的兼容和更好的扩展性。这里简单的介绍一下WebApiClient在.net core中的使用,再介绍下怎么和nacos结合做微服务间的api调用,并且解析一下WebApiClient的核心实现源码。
WebApiClient的基本使用
-
安装WebApiClient nuget包
WebApiClient有以前兼容.net framework和.net core的版本WebApiClient-JIT和现在.net core版本WebApiClientCore,.net Core下请安装 WebApiClientCore包Install-Package WebApiClientCore
-
定义代理接口
[HttpHost("http://localhost:5000")] public interface IService { [HttpGet("/WeatherForecast")] public ITask<string> GetAsync(); }
-
在startup类中进行注入
public void ConfigureServices(IServiceCollection services) { services.AddHttpApi<IService>(); }
这里通过HttpHost特性配置了同一个域名,当前代理接口下的方法只配置路由,则所有的方法都会请求这个域名下的地址。如果方法上的特性重新指定一个完整的地址,则以方法上特性指定的优先。
除了在代理接口上通过特性的方式标记接口地址之外,还可以在依赖注入的时候配置。
services.AddHttpApi<IService>(options => options.HttpHost = new Uri("http://localhost:5000"));
public interface IService
{
[HttpGet("/WeatherForecast")]
public ITask<string> GetAsync();
}
api请求结果有多种返回方式,如上面的代理接口中是以string的方式接收返回结果,当然我们大多数情况下还是以强类型的方式接收的,如下:
[HttpHost("http://localhost:5000")]
public interface IService
{
[HttpGet("/WeatherForecast")]
public ITask<string> GetAsync();
[HttpGet("/WeatherForecast")]
public ITask<IEnumerable<WeatherForecast>> Get2Async();
}
[HttpGet("Get")]
public async Task<IEnumerable<WeatherForecast>> RemoteGet()
{
var result = await _service.Get2Async();
return result;
}
WebApiClient提供了多种对请求参数,返回值进行处理或者校验的方式,这里就不细讲了,具体的可以参考老九大大的文章或者github上的文档。
结合nacos实现服务接口调用
由于微服务间的接口调用一般不经过网关,而是在服务内部自由调用,在一个服务有多个实例的情况下,往往会有负载均衡策略,所以微服务间通讯时Host地址往往是不固定的,需要在接口调用之前动态地去设置。
WebApiClient提供了动态Host的处理方式,可以在在请求还没有发出去之前的任何环节,对ApiRequestContext进行拦截,修改请求消息的RequestUri来实现动态目标。其中更是提供了ApiActionAttribute、ApiFilterAttribute等特性,大大方便了我们对动态host的实现。
首先定义一个ServiceNameAttribute继承ApiActionAttribute特性,在请求发送前进行拦截
public class ServiceNameAttribute : ApiActionAttribute
{
public ServiceNameAttribute(string name)
{
Name = name;
OrderIndex = int.MinValue;
}
public string Name { get; set; }
public override async Task OnRequestAsync(ApiRequestContext context)
{
IServiceProvider sp = context.HttpContext.ServiceProvider;
IHostProvider hostProvider = sp.GetRequiredService<IHostProvider>();
//服务名也可以在接口配置时挂在Properties中
string host = await hostProvider.ResolveService(this.Name);
HttpApiRequestMessage requestMessage = context.HttpContext.RequestMessage;
// 覆盖了RequestUri,完成了替换
requestMessage.RequestUri = requestMessage.MakeRequestUri(new Uri(host));
}
}
这里的IHostProvider是解析服务名称的接口,可以根据不同的服务注册中心提供实现,例如nacos如下:
public class NacosHostProvider : IHostProvider
{
private readonly INacosNamingService _nacosNamingService;
public NacosHostProvider(INacosNamingService nacosNamingService)
{
_nacosNamingService = nacosNamingService;
}
public async Task<string> ResolveService(string name)
{
var instance = await _nacosNamingService.SelectOneHealthyInstance(name);
if (instance != null)
{
var host = $"{instance.Ip}:{instance.Port}";
var baseUrl = instance.Metadata.TryGetValue("secure", out _)
? $"https://{host}"
: $"http://{host}";
return baseUrl;
}
throw new Exception($"{name}服务不可用!");
}
}
之后提供依赖注入注册的方式
public static class NacosProxyServiceCollectionExtensions
{
/// <summary>
/// 添加 Nacos Http 请求代理必要依赖
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddNacosHttpProxy(this IServiceCollection services)
{
var configuration = services.GetConfiguration();
services.AddNacosAspNet(configuration, "nacos");
services.AddSingleton<IHostProvider, NacosHostProvider>();
return services;
}
}
最后是使用,和WebApiClient正常使用没有太大区别,但是要注意不用在设置HttpHost,而是需要设置ServiceName特性,HttpHost特性的设置都会被服务发现获取到的地址替换掉。
[ServiceName("App1")]
public interface IService
{
[HttpGet("/Service")]
public ITask<string> GetAsync();
}
public async Task Should_Get_Response()
{
var serviceCollection = new ServiceCollection();
var builder = new ConfigurationBuilder();
builder.AddJsonFile("appsettings.json");
var configuration = builder.Build();
serviceCollection.AddSingleton<IConfiguration>(configuration);
serviceCollection.AddNacosHttpProxy();
serviceCollection.AddHttpApi<IService>();
var serviceProvider = serviceCollection.BuildServiceProvider();
var service = serviceProvider.GetRequiredService<IService>();
var host = await service.GetAsync();
Assert.Equal("http://172.20.64.1:5000", host);
}
WebApiClient核心实现源码研究
WebApiClient的核心在于接口代理类的动态实现,这一块的代码在于DefaultHttpApiActivator<THttpApi>
中,通过下面的源码可以看到,这里是通过反射动态编译一个类实现THttpApi,也就是我们定义的代理接口的。
/// <summary>
/// 运行时使用Emit动态创建THttpApi的代理类和代理类实例
/// </summary>
/// <typeparam name="THttpApi"></typeparam>
public class DefaultHttpApiActivator<THttpApi> : HttpApiActivator<THttpApi>
{
/// <summary>
/// IHttpApiInterceptor的Intercept方法
/// </summary>
private static readonly MethodInfo interceptMethod = typeof(IHttpApiInterceptor).GetMethod(nameof(IHttpApiInterceptor.Intercept)) ?? throw new MissingMethodException(nameof(IHttpApiInterceptor.Intercept));
/// <summary>
/// 代理类型的构造器的参数类型
/// (IHttpApiInterceptor interceptor,ApiActionInvoker[] actionInvokers)
/// </summary>
private static readonly Type[] proxyTypeCtorArgTypes = new Type[] { typeof(IHttpApiInterceptor), typeof(ApiActionInvoker[]) };
/// <summary>
/// 运行时使用Emit动态创建THttpApi的代理类和代理类实例
/// </summary>
/// <param name="apiActionDescriptorProvider"></param>
/// <param name="actionInvokerProvider"></param>
/// <exception cref="ArgumentException"></exception>
/// <exception cref="NotSupportedException"></exception>
public DefaultHttpApiActivator(IApiActionDescriptorProvider apiActionDescriptorProvider, IApiActionInvokerProvider actionInvokerProvider)
: base(apiActionDescriptorProvider, actionInvokerProvider)
{
}
/// <summary>
/// 创建实例工厂
/// </summary>
/// <exception cref="NotSupportedException"></exception>
/// <exception cref="ProxyTypeCreateException"></exception>
/// <returns></returns>
protected override Func<IHttpApiInterceptor, ApiActionInvoker[], THttpApi> CreateFactory()
{
var proxyType = BuildProxyType(typeof(THttpApi), this.ApiMethods);
return LambdaUtil.CreateCtorFunc<IHttpApiInterceptor, ApiActionInvoker[], THttpApi>(proxyType);
}
/// <summary>
/// 创建IHttpApi代理类的类型
/// </summary>
/// <param name="interfaceType">接口类型</param>
/// <param name="apiMethods">接口的方法</param>
/// <exception cref="NotSupportedException"></exception>
/// <exception cref="ProxyTypeCreateException"></exception>
/// <returns></returns>
private static Type BuildProxyType(Type interfaceType, MethodInfo[] apiMethods)
{
// 接口的实现在动态程序集里,所以接口必须为public修饰才可以创建代理类并实现此接口
if (interfaceType.IsVisible == false)
{
var message = Resx.required_PublicInterface.Format(interfaceType);
throw new NotSupportedException(message);
}
var moduleName = Guid.NewGuid().ToString();
var assemblyName = new AssemblyName(Guid.NewGuid().ToString());
var module = AssemblyBuilder
.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run)
.DefineDynamicModule(moduleName);
var typeName = interfaceType.FullName ?? Guid.NewGuid().ToString();
var builder = module.DefineType(typeName, System.Reflection.TypeAttributes.Class);
builder.AddInterfaceImplementation(interfaceType);
var fieldApiInterceptor = BuildField(builder, "<>apiInterceptor", typeof(IHttpApiInterceptor));
var fieldActionInvokers = BuildField(builder, "<>actionInvokers", typeof(ApiActionInvoker[]));
BuildCtor(builder, fieldApiInterceptor, fieldActionInvokers);
BuildMethods(builder, apiMethods, fieldApiInterceptor, fieldActionInvokers);
var proxyType = builder.CreateType();
return proxyType ?? throw new ProxyTypeCreateException(interfaceType);
}
/// <summary>
/// 生成代理类型的字段
/// </summary>
/// <param name="builder">类型生成器</param>
/// <param name="fieldName">字段名称</param>
/// <param name="fieldType">字段类型</param>
/// <returns></returns>
private static FieldBuilder BuildField(TypeBuilder builder, string fieldName, Type fieldType)
{
const FieldAttributes filedAttribute = FieldAttributes.Private | FieldAttributes.InitOnly;
return builder.DefineField(fieldName, fieldType, filedAttribute);
}
/// <summary>
/// 生成代理类型的构造器
/// </summary>
/// <param name="builder">类型生成器</param>
/// <param name="fieldApiInterceptor">拦截器字段</param>
/// <param name="fieldActionInvokers">action执行器字段</param>
/// <returns></returns>
private static void BuildCtor(TypeBuilder builder, FieldBuilder fieldApiInterceptor, FieldBuilder fieldActionInvokers)
{
// .ctor(IHttpApiInterceptor apiInterceptor, ApiActionInvoker[] actionInvokers)
var ctor = builder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, proxyTypeCtorArgTypes);
var il = ctor.GetILGenerator();
// this.apiInterceptor = 第一个参数
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Stfld, fieldApiInterceptor);
// this.actionInvokers = 第二个参数
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_2);
il.Emit(OpCodes.Stfld, fieldActionInvokers);
il.Emit(OpCodes.Ret);
}
/// <summary>
/// 生成代理类型的接口实现方法
/// </summary>
/// <param name="builder">类型生成器</param>
/// <param name="actionMethods">接口的方法</param>
/// <param name="fieldApiInterceptor">拦截器字段</param>
/// <param name="fieldActionInvokers">action执行器字段</param>
private static void BuildMethods(TypeBuilder builder, MethodInfo[] actionMethods, FieldBuilder fieldApiInterceptor, FieldBuilder fieldActionInvokers)
{
const MethodAttributes implementAttribute = MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final | MethodAttributes.NewSlot | MethodAttributes.HideBySig;
for (var i = 0; i < actionMethods.Length; i++)
{
var actionMethod = actionMethods[i];
var actionParameters = actionMethod.GetParameters();
var parameterTypes = actionParameters.Select(p => p.ParameterType).ToArray();
var iL = builder
.DefineMethod(actionMethod.Name, implementAttribute, CallingConventions.Standard, actionMethod.ReturnType, parameterTypes)
.GetILGenerator();
// this.apiInterceptor
iL.Emit(OpCodes.Ldarg_0);
iL.Emit(OpCodes.Ldfld, fieldApiInterceptor);
// this.actionInvokers[i]
iL.Emit(OpCodes.Ldarg_0);
iL.Emit(OpCodes.Ldfld, fieldActionInvokers);
iL.Emit(OpCodes.Ldc_I4, i);
iL.Emit(OpCodes.Ldelem_Ref);
// var arguments = new object[parameters.Length]
var arguments = iL.DeclareLocal(typeof(object[]));
iL.Emit(OpCodes.Ldc_I4, actionParameters.Length);
iL.Emit(OpCodes.Newarr, typeof(object));
iL.Emit(OpCodes.Stloc, arguments);
for (var j = 0; j < actionParameters.Length; j++)
{
iL.Emit(OpCodes.Ldloc, arguments);
iL.Emit(OpCodes.Ldc_I4, j);
iL.Emit(OpCodes.Ldarg, j + 1);
var parameterType = parameterTypes[j];
if (parameterType.IsValueType || parameterType.IsGenericParameter)
{
iL.Emit(OpCodes.Box, parameterType);
}
iL.Emit(OpCodes.Stelem_Ref);
}
// 加载arguments参数
iL.Emit(OpCodes.Ldloc, arguments);
// Intercep(actionInvoker, arguments)
iL.Emit(OpCodes.Callvirt, interceptMethod);
if (actionMethod.ReturnType == typeof(void))
{
iL.Emit(OpCodes.Pop);
}
iL.Emit(OpCodes.Castclass, actionMethod.ReturnType);
iL.Emit(OpCodes.Ret);
}
}
}
之后通过Lambda表达式动态创建了THttpApi实现类的构造函数委托,再在通过AddHttpApi<THttpApi>
()向容器注册的时候,调用HttpApiProvider<THttpApi>
的CreateHttpApi方法,创建THttpApi动态实现类。
/// <summary>
/// 添加HttpApi代理类到服务
/// </summary>
/// <typeparam name="THttpApi"></typeparam>
/// <param name="services"></param>
/// <returns></returns>
public static IHttpClientBuilder AddHttpApi<THttpApi>(this IServiceCollection services) where THttpApi : class
{
var name = HttpApi.GetName(typeof(THttpApi));
services.AddWebApiClient();
services.NamedHttpApiType(name, typeof(THttpApi));
services.TryAddSingleton(typeof(HttpApiProvider<>));
services.TryAddTransient(serviceProvider =>
{
var httiApiProvider = serviceProvider.GetRequiredService<HttpApiProvider<THttpApi>>();
return httiApiProvider.CreateHttpApi(serviceProvider, name);
});
return services.AddHttpClient(name);
}
而最终,具体的Api的调用执行是通过IHttpApiInterceptor实现类中的Intercept方法来实现的。
在DefaultApiActionInvoker<TResult>
中可以看到,Invoke方法中又是通过静态类ApiRequestExecuter对请求上下文和响应上下文进行处理,包括参数校验、过滤器执行等,通过静态类ApiRequestSender进行最终的Api的调用,最终也是通过HttpClient进行调用Api接口的。
/// <summary>
/// 执行Api方法
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
private async Task<TResult> InvokeAsync(ApiRequestContext request)
{
#nullable disable
var response = await ApiRequestExecuter.ExecuteAsync(request).ConfigureAwait(false);
if (response.ResultStatus == ResultStatus.HasResult)
{
return (TResult)response.Result;
}
if (response.ResultStatus == ResultStatus.HasException)
{
ExceptionDispatchInfo.Capture(response.Exception).Throw();
}
throw new ApiReturnNotSupportedExteption(response);
#nullable enable
}
static class ApiRequestExecuter
{
/// <summary>
/// 执行上下文
/// </summary>
/// <param name="request">请求上下文</param>
/// <returns></returns>
public static async Task<ApiResponseContext> ExecuteAsync(ApiRequestContext request)
{
await HandleRequestAsync(request).ConfigureAwait(false);
var response = await ApiRequestSender.SendAsync(request).ConfigureAwait(false);
await HandleResponseAsync(response).ConfigureAwait(false);
return response;
}
/// <summary>
/// 处理请求上下文
/// </summary>
/// <returns></returns>
private static async Task HandleRequestAsync(ApiRequestContext context)
{
// 参数验证
var validateProperty = context.HttpContext.HttpApiOptions.UseParameterPropertyValidate;
foreach (var parameter in context.ActionDescriptor.Parameters)
{
var parameterValue = context.Arguments[parameter.Index];
DataValidator.ValidateParameter(parameter, parameterValue, validateProperty);
}
// action特性请求前执行
foreach (var attr in context.ActionDescriptor.Attributes)
{
await attr.OnRequestAsync(context).ConfigureAwait(false);
}
// 参数特性请求前执行
foreach (var parameter in context.ActionDescriptor.Parameters)
{
var ctx = new ApiParameterContext(context, parameter);
foreach (var attr in parameter.Attributes)
{
await attr.OnRequestAsync(ctx).ConfigureAwait(false);
}
}
// Return特性请求前执行
foreach (var @return in context.ActionDescriptor.Return.Attributes)
{
await @return.OnRequestAsync(context).ConfigureAwait(false);
}
// GlobalFilter请求前执行
foreach (var filter in context.HttpContext.HttpApiOptions.GlobalFilters)
{
await filter.OnRequestAsync(context).ConfigureAwait(false);
}
// Filter请求前执行
foreach (var filter in context.ActionDescriptor.FilterAttributes)
{
await filter.OnRequestAsync(context).ConfigureAwait(false);
}
}
/// <summary>
/// 处理响应上下文
/// </summary>
/// <returns></returns>
private static async Task HandleResponseAsync(ApiResponseContext context)
{
// Return特性请求后执行
var returns = context.ActionDescriptor.Return.Attributes.GetEnumerator();
while (context.ResultStatus == ResultStatus.None && returns.MoveNext())
{
try
{
await returns.Current.OnResponseAsync(context).ConfigureAwait(false);
}
catch (Exception ex)
{
context.Exception = ex;
}
}
// 结果验证
if (context.ResultStatus == ResultStatus.HasResult &&
context.ActionDescriptor.Return.DataType.IsRawType == false &&
context.HttpContext.HttpApiOptions.UseReturnValuePropertyValidate)
{
try
{
DataValidator.ValidateReturnValue(context.Result);
}
catch (Exception ex)
{
context.Exception = ex;
}
}
// GlobalFilter请求后执行
foreach (var filter in context.HttpContext.HttpApiOptions.GlobalFilters)
{
await filter.OnResponseAsync(context).ConfigureAwait(false);
}
// Filter请求后执行
foreach (var filter in context.ActionDescriptor.FilterAttributes)
{
await filter.OnResponseAsync(context).ConfigureAwait(false);
}
}
}
/// <summary>
/// 提供http请求
/// </summary>
static class ApiRequestSender
{
/// <summary>
/// 发送http请求
/// </summary>
/// <param name="context"></param>
/// <exception cref="ApiInvalidConfigException"></exception>
/// <returns></returns>
public static async Task<ApiResponseContext> SendAsync(ApiRequestContext context)
{
if (context.HttpContext.RequestMessage.RequestUri == null)
{
throw new ApiInvalidConfigException(Resx.required_HttpHost);
}
try
{
await SendCoreAsync(context).ConfigureAwait(false);
return new ApiResponseContext(context);
}
catch (Exception ex)
{
return new ApiResponseContext(context) { Exception = ex };
}
}
/// <summary>
/// 发送http请求
/// </summary>
/// <param name="context"></param>
/// <exception cref="HttpRequestException"></exception>
/// <returns></returns>
private static async Task SendCoreAsync(ApiRequestContext context)
{
var actionCache = await context.GetCaheAsync().ConfigureAwait(false);
if (actionCache != null && actionCache.Value != null)
{
context.HttpContext.ResponseMessage = actionCache.Value;
}
else
{
var client = context.HttpContext.HttpClient;
var request = context.HttpContext.RequestMessage;
var completionOption = context.GetCompletionOption();
using var tokenLinker = new CancellationTokenLinker(context.HttpContext.CancellationTokens);
var response = await client.SendAsync(request, completionOption, tokenLinker.Token).ConfigureAwait(false);
context.HttpContext.ResponseMessage = response;
await context.SetCacheAsync(actionCache?.Key, response).ConfigureAwait(false);
}
}
/// <summary>
/// 获取响应的缓存
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private static async Task<ActionCache?> GetCaheAsync(this ApiRequestContext context)
{
var attribute = context.ActionDescriptor.CacheAttribute;
if (attribute == null)
{
return default;
}
if (attribute.GetReadPolicy(context) == CachePolicy.Ignore)
{
return default;
}
var cacheKey = await attribute.GetCacheKeyAsync(context).ConfigureAwait(false);
if (string.IsNullOrEmpty(cacheKey))
{
return default;
}
var provider = attribute.GetCacheProvider(context);
if (provider == null)
{
return default;
}
var responseCache = await provider.GetAsync(cacheKey).ConfigureAwait(false);
if (responseCache.HasValue == false || responseCache.Value == null)
{
return new ActionCache(cacheKey, null);
}
var request = context.HttpContext.RequestMessage;
var response = responseCache.Value.ToResponseMessage(request, provider.Name);
return new ActionCache(cacheKey, response);
}
/// <summary>
/// 更新响应到缓存
/// </summary>
/// <param name="context"></param>
/// <param name="cacheKey">缓存键</param>
/// <param name="response">响应消息</param>
/// <returns></returns>
private static async Task SetCacheAsync(this ApiRequestContext context, string? cacheKey, HttpResponseMessage? response)
{
var attribute = context.ActionDescriptor.CacheAttribute;
if (attribute == null)
{
return;
}
if (response == null)
{
return;
}
if (attribute.GetWritePolicy(context) == CachePolicy.Ignore)
{
return;
}
if (string.IsNullOrEmpty(cacheKey) == true)
{
cacheKey = await attribute.GetCacheKeyAsync(context).ConfigureAwait(false);
}
if (cacheKey == null)
{
return;
}
var provider = attribute.GetCacheProvider(context);
if (provider == null)
{
return;
}
var cacheEntry = await ResponseCacheEntry.FromResponseMessageAsync(response).ConfigureAwait(false);
await provider.SetAsync(cacheKey, cacheEntry, attribute.Expiration).ConfigureAwait(false);
}
/// <summary>
/// 表示Action缓存结果
/// </summary>
private class ActionCache
{
/// <summary>
/// 获取缓存的键
/// </summary>
public string Key { get; }
/// <summary>
/// 获取响应信息
/// </summary>
public HttpResponseMessage? Value { get; set; }
/// <summary>
/// 缓存结果
/// </summary>
/// <param name="key">缓存的键</param>
/// <param name="value">响应信息</param>
public ActionCache(string key, HttpResponseMessage? value)
{
this.Key = key;
this.Value = value;
}
}
/// <summary>
/// 表示CancellationToken链接器
/// </summary>
private struct CancellationTokenLinker : IDisposable
{
/// <summary>
/// 链接产生的tokenSource
/// </summary>
private readonly CancellationTokenSource? tokenSource;
/// <summary>
/// 获取token
/// </summary>
public CancellationToken Token { get; }
/// <summary>
/// CancellationToken链接器
/// </summary>
/// <param name="tokenList"></param>
public CancellationTokenLinker(IList<CancellationToken> tokenList)
{
if (IsNoneCancellationToken(tokenList) == true)
{
this.tokenSource = null;
this.Token = CancellationToken.None;
}
else
{
this.tokenSource = CancellationTokenSource.CreateLinkedTokenSource(tokenList.ToArray());
this.Token = this.tokenSource.Token;
}
}
/// <summary>
/// 是否为None的CancellationToken
/// </summary>
/// <param name="tokenList"></param>
/// <returns></returns>
private static bool IsNoneCancellationToken(IList<CancellationToken> tokenList)
{
if (tokenList.Count == 0)
{
return true;
}
if (tokenList.Count == 1 && tokenList[0] == CancellationToken.None)
{
return true;
}
return false;
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
if (this.tokenSource != null)
{
this.tokenSource.Dispose();
}
}
}
}
微服务系列文章:
上一篇: 服务间的通讯—工作实践
下一篇:配置中心—nacos配置中心