服务间的通讯—DispatchProxy 实现 RESTful API 客户端
在微服务架构中,很多时候需要调用很多服务才能完成一项功能。服务之间如何互相调用就变成微服务架构中的一个关键问题。微服务内部间的调用有多种方式,包括最基本的 RESTful Api,远程过程调用 RPC、基于消息队列的事件驱动。
在工作实践中,我们微服务间调用最基本的方式还是 RESTful Api,而这时就会发现如果每次请求都需要自己构建 HttpClient,硬编码去调用 api,会带来非常多的重复性的工作,而且对业务代码的侵入也非常严重。这就需要一个声明式 REST 客户端来解决问题。
我们通过 DispatchProxy 动态代理的方式模仿 Feign 实现这样一个声明式 REST 客户端,结合服务发现,通过定义接口,打上特性的方式实现异步调用 Http api。
1. DispatchProxy
DispatchProxy
是一个抽象类,是 .Net Core下的一个动态代理的类。它提供了实例化代理对象和处理器方法分发的机制。DispatchProxy 类的典型用法如下:
var proxy = DispatchProxy.Create<IInterface, Proxy>();
DispatchProxy 的强大功能:它允许我们创建一个 Proxy 类型,该类型可以像 IInterface 一样被使用,且不需要真正"实现它"。
具体示例如下:
首先定义一个接口
public interface IService
{
public Task<string> GetAsync(string name);
}
通过继承 DispatchProxy 实现一个代理类
public class ServiceDispatchProxy : DispatchProxy
{
protected override object Invoke(MethodInfo targetMethod, object[] args)
{
return Task.FromResult($"{targetMethod.Name}: {args[0]}");
}
}
通过 DispatchProxy.Create<T, TProxy>() 创建代理
var service = DispatchProxy.Create<IService, ServiceDispatchProxy>();
var result = service.GetAsync("test").Result;
Console.WriteLine(result);
Console.ReadKey();
可以看到:
这里调用 IService 的 GetAsync 方法时,实际上是执行了 ServiceDispatchProxy 中的 Invoke,在没有实现IService 的情况下动态的实现了 GetAsync 中的逻辑。
2. 声明式 REST 客户端实现
- 代码解析
AbstactFeignProxy<T>
实现 DispatchProxy 抽象类,在 Invoke 中通过反射执行调用 api 接口的逻辑,这里因为需要兼容异步和同步方法,所以对于 Get 和 Post 都区分了两种方式(由于生产场景中 Rest api 调用只使用了Get和 Post 两种方式,这里只针对两种方式提供代理)。
protected override object Invoke(MethodInfo targetMethod, object[] args)
{
var method = GetOrCacheMethod(targetMethod);
//代理请求
if (method.Method == HttpMethod.Get)
{
var methodName = method.IsAsync ? "GetAsync" : "Get";
// 反射获取方法Get方法,注意这里需要指定AbstactHystrixFeignProxy的泛型类型
var getMethod = typeof(AbstactFeignProxy<>).MakeGenericType(typeof(T))
.GetMethod(methodName);
var curMethod = getMethod.MakeGenericMethod(method.ReturnType);
var result = curMethod.Invoke(this, new object[] { method, args });
return result;
}
else
{
var methodName = method.IsAsync ? "PostAsync" : "Post";
var postMethod = typeof(AbstactFeignProxy<>).MakeGenericType(typeof(T))
.GetMethod(methodName);
var curMethod = postMethod.MakeGenericMethod(method.ReturnType);
if (args != null && args.Length == 1)
{
var res = curMethod.Invoke(this, new object[] { method, args[0] });
return res;
}
var result = curMethod.Invoke(this, new object[] { method, null });
return result;
}
}
其中,GetOrCacheMethod 方法是对代理接口方法的解析,包括通过自定义的标签判断接口的代理方式,并且从中提取接口调用必须的参数
protected FeignMethodInfo GetOrCacheMethod(MethodInfo targetMethod)
{
// 同一个接口的不同参数进行缓存
return cachedMethod.GetOrAdd(targetMethod.Name + targetMethod.GetParameters()
.Select(p => p.ParameterType.Name).JoinAsString("_"),
() =>
{
var attr = targetMethod.GetCustomAttribute<MappingAttribute>(true);
if (attr == null)
{
throw new InvalidOperationException("请设置代理方式!");
}
var classAttr = typeof(T).GetCustomAttribute<FeignClientAttribute>();
var fallback = classAttr?.FallbackImpl;
var serviceName = classAttr?.Name;
var url = attr.Route;
var urlTemplate = "";
for (int i = 0; i < targetMethod.GetParameters().Length; i++)
{
urlTemplate += targetMethod.GetParameters()[i].Name + "={" + i + "}&";
}
urlTemplate = "?" + urlTemplate.Trim('&');
return new FeignMethodInfo()
{
Url = url,
ServiceName = serviceName,
Method = attr is PostMappingAttribute ? HttpMethod.Post : HttpMethod.Get,
ReturnType = targetMethod.ReturnType.BaseType == typeof(Task) ?
targetMethod.ReturnType.GetGenericArguments().FirstOrDefault()
: targetMethod.ReturnType,
IsAsync = targetMethod.ReturnType.BaseType == typeof(Task),
Fallback = fallback,
UrlTemplate = urlTemplate,
Parameters = targetMethod.GetParameters()
};
});
}
在 Get 和 Post 方法中,只进行了请求参数的统一处理,又通过抽象方法将具体的请求操作交给具体的实现类实现,如图中的 HttpClientProxy,这里是因为考虑到普通 api 和微服务间的 api 在请求时是有区别的,微服务间的 api 请求需要使用服务名称通过服务发现获取服务地址。同样的,对于 api 结果的处理也会不一样,这里提供了 IApiResultProcessor 接口,可以通过实现它,将默认结果处理器替换掉,自行提供结果处理逻辑。
public async Task<TResult> GetAsync<TResult>(FeignMethodInfo targetMethod, object[] args)
{
var urlParam = string.Format(targetMethod.UrlTemplate, args);
if (targetMethod.Parameters.Length == 1 && !targetMethod.Parameters[0].ParameterType.IsPrimitive
&& targetMethod.Parameters[0].ParameterType != typeof(string))
{
urlParam = ModelToUriParam(args[0]);
}
var url = targetMethod.Url + urlParam;
var httpRes = await GetRequestAsync(targetMethod.ServiceName, url, targetMethod);
return await ApiResultProcessor.Process<TResult>(httpRes);
}
而后,提供 IFeignProxyFactory 代理工厂接口,具体使用时可以通过替换工厂实现类,创建不同的代理类。工厂类创建代理的时候,需要接口一些自定义的标签,如 FeignClientAttribute,通过这些标签来标识哪些类是需要创建代理的。
public class DefaultFeignProxyFactory : IFeignProxyFactory
{
private IProxyConfiguration _proxyConfiguration;
public DefaultFeignProxyFactory(IProxyConfiguration proxyConfiguration)
{
_proxyConfiguration = proxyConfiguration;
}
/// <summary>
/// 创建代理
/// </summary>
/// <returns></returns>
public virtual T GetProxy<T>()
{
Type type = typeof(T);
if (type.GetCustomAttributes(typeof(FeignClientAttribute), true).Any())
{
var proxy = DispatchProxy.Create<T, HttpClientProxy<T>>();
(proxy as AbstactFeignProxy<T>).ProxyConfiguration = _proxyConfiguration;
return proxy;
}
return default;
}
/// <summary>
/// 创建断路器代理
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public virtual T GetHytrixProxy<T>()
{
Type type = typeof(T);
if (type.GetCustomAttributes(typeof(FeignClientAttribute), true).Any())
{
var proxy = DispatchProxy.Create<T, HttpClientHystrixProxy<T>>();
(proxy as AbstactFeignProxy<T>).ProxyConfiguration = _proxyConfiguration;
return proxy;
}
return default;
}
}
可以看到这里提供了两种代理类的创建方式,其中的一个代理类是普通的 api 代理,另一个代理类实现了熔断降级功能。
类图中没有反应出来的一个点是,REST 客户端通过容器扩展提供快捷注入的方式,不用调用工厂类自行进行代理类创建。
public static class HttpProxyServiceCollectionExtensions
{
/// <summary>
/// 添加 HttpClientProxy 必要依赖
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddHttpClientProxyFactory(this IServiceCollection services)
{
services.AddSingleton<IProxyConfiguration, ProxyConfiguration>();
services.AddSingleton<IFeignProxyFactory, DefaultFeignProxyFactory>();
services.AddSingleton<IApiResultProcessor, DefaultApiResultProcessor>();
return services;
}
/// <summary>
/// 注册 HttpClient 代理
/// </summary>
/// <typeparam name="T">代理接口类型</typeparam>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddHttpClientProxy<T>(this IServiceCollection services) where T : class
{
//注册fallback
var attr = typeof(T).GetCustomAttributes(typeof(FeignClientAttribute), true).FirstOrDefault();
if (attr != null && (attr as FeignClientAttribute).FallbackImpl != null)
{
services.AddSingleton((attr as FeignClientAttribute).FallbackImpl);
}
//注册代理
services.AddSingleton(serviceProvider =>
{
var proxyFactory = serviceProvider.GetService<IFeignProxyFactory>();
var proxy = proxyFactory.GetHytrixProxy<T>();
return proxy;
});
return services;
}
}
这里是默认的依赖注入,若是有和其他情况的代理方式需要实现,替换掉几个默认接口即可,如以下与 Nacos结合的服务间调用的代理。
public static class NacosProxyServiceCollectionExtensions
{
/// <summary>
/// 添加 Nacos Http 请求代理必要依赖
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddNacosHttpProxyFactory(this IServiceCollection services)
{
var configuration = services.GetConfiguration();
services.AddNacosAspNet(configuration, "nacos");
services.AddHttpClientProxyFactory();
services.Replace(new ServiceDescriptor(typeof(IFeignProxyFactory), typeof(NacosFeignProxyFactory), ServiceLifetime.Singleton));
services.Replace(new ServiceDescriptor(typeof(IApiResultProcessor), typeof(MicroServiceApiResultProcessor), ServiceLifetime.Singleton));
return services;
}
}
- 如何使用
声明式REST API客户端的使用方式如下:
(1)普通的 api 代理
1)编写代理接口
// api接口基地址
[FeignClient(Name = "http://loalhost:5000")]
public interface IService5
{
// api接口路由
[GetMapping(Route = "/service1")]
Task<string> GetAsync(string name);
}
2)向容器添加依赖注入
public void ConfigeService(IServiceCollection services)
{
services.AddHttpClientProxyFactory();
services.AddHttpClientProxy<IService5>();
}
3)在类中注入使用
public class HomeController
{
public HomeController(IService5 service)
{
…
}
}
(2)结合 Nacos 的服务间 api 代理
1)编写代理接口,注意需要使用 NacosFeignClient 特性
[NacosFeignClient(Name = "App1")]
public interface IService7
{
[GetMapping(Route = "/Service")]
Task<string> GetAsync();
}
2)向容器添加依赖注入
public void ConfigeService(IServiceCollection services)
{
services.AddNacosHttpProxyFactory();
services.AddHttpClientProxy<IService5>();
}
对于本文中响应式 REST Api 客户端实现的源码已经上传到 github,仓库地址如下:
https://github.com/25lonelyboy/HttpApiClient
欢迎有兴趣的朋友去看看,希望能给大家提供一些参考。
微服务系列文章:
上一篇:服务发现—Asp.net core 结合consul实现服务发现
下一篇:服务间的通讯-WebApiClient