基于Abp的WebApi容器
简述 对 Abp的动态web api的改造过程
注册
1. 首先通过反射将《服务类型》通过ApiControllerBuilder 构建成成 DynamicApiControllerInfo
2. 在DynamicApiControllerInfo中同时构建DynamicApiActionInfo
3. Ioc注入DynamicApiController<TService> Tservice就是最开始的《服务类型》
3. 最后将DynamicApiControllerInfo添加到DynamicApiControllerManager,通过ServiceName缓存
执行
1. AbpHttpControllerSelector 通过路由获取出“service” 这个参数即ServiceName
2. 通过ServiceName从DynamicApiControllerManager中获取DynamicApiControllerInfo 的信息
3. 将DynamicApiControllerInfo 放入HttpControllerDescriptor.Properties中,返回DynamicHttpControllerDescriptor给MVC处理流程
4. AbpControllerActivator 通过DynamicApiControllerInfor中的ControllerType激活Controller
5. AbpApiControllerActionSelector 获取HttpControllerDescriptor.Properties中的将DynamicApiControllerInfo 信息
6. AbpApiControllerActionSelector 通过 路由的{action}参数获取 方法名
7. AbpApiControllerActionSelector 在 DynamicApiControllerInfor通过方法名获取DynamicApiActionInfo 的信息
8. 最后返回DyanamicHttpActionDescriptor 给MVC处理流程
改造
Action执行的改造
实际在Abp中 DyanamicHttpActionDescriptor 的 ExecuteAsync 中实际是通过AOP拦截实现的.这里我做了修改
首先将DynamicController改为组合的方式注入IService来作为代理对象如下图
然后执行的时候采用获取IDynamicApiController 的ProxyObject 来使用反射执行
其中由于MVC并没有放出ReflectedHttpActionDescriptor.ActionExecutor 这个类型,所以用了点技巧。
支持重载
1. 首先在 DynamicApiControllerInfo 中增加属性 FullNameActions 类型和Actions 一致
2. 然后再初始化的时候同时初始化FullNameActions ,Action的key是Name,FullNameActions 是Method.ToString()[这种包含的信息更多,可作为唯一标识]
3. 最后在客户端调用的时候放到Header即可区分,实现函数重载
支持多个复杂参数
在ParameterBindingRules 中添加规则
//增加服务中多个参数的情况 ApiGlobalConfiguration.Configuration.ParameterBindingRules.Insert(0, descriptor => { if (descriptor.ActionDescriptor.SupportedHttpMethods.Contains(HttpMethod.Get) || descriptor.ActionDescriptor.SupportedHttpMethods.Contains(HttpMethod.Delete)) return null; if (descriptor.ActionDescriptor.GetParameters().Count(item => !item.ParameterType.IsSimpleUnderlyingType()) < 2) return null; if (descriptor.ParameterType.IsSimpleUnderlyingType()) return null; if (descriptor.ParameterType.GetCustomAttribute(typeof(ParameterBindingAttribute)) != null) return null; var config = descriptor.Configuration; IEnumerable<MediaTypeFormatter> formatters = config.Formatters; var validators = config.Services.GetBodyModelValidator(); return new MultiPostParameterBinding(descriptor, formatters, validators); });
2. 在MultiPostParameterBinding 代码如下
public class MultiPostParameterBinding : FormatterParameterBinding { // Magic key to pass cancellation token through the request property bag to maintain backward compat. private const string CancellationTokenKey = "MS_FormatterParameterBinding_CancellationToken"; public MultiPostParameterBinding(HttpParameterDescriptor descriptor, IEnumerable<MediaTypeFormatter> formatters, IBodyModelValidator bodyModelValidator) : base(descriptor, formatters, bodyModelValidator) { } public override bool WillReadBody => false; public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken) { var paramFromBody = Descriptor; var type = paramFromBody.ParameterType; var request = actionContext.ControllerContext.Request; IFormatterLogger formatterLogger = new ModelStateFormatterLogger(actionContext.ModelState, paramFromBody.ParameterName); var task = ExecuteBindingAsyncCore(metadataProvider, actionContext, paramFromBody, type, request, formatterLogger, cancellationToken); return task; } // Perf-sensitive - keeping the async method as small as possible private async Task ExecuteBindingAsyncCore(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, HttpParameterDescriptor paramFromBody, Type type, HttpRequestMessage request, IFormatterLogger formatterLogger, CancellationToken cancellationToken) { // pass the cancellation token through the request as we cannot call the ReadContentAsync overload that takes // CancellationToken for backword compatibility reasons. request.Properties[CancellationTokenKey] = cancellationToken; //todo 这里如果只是服务端使用需要要构造一个匿名对象去接受数据 Dictionary<string, object> allModels; if (actionContext.ActionArguments.ContainsKey("MultiDictionary")) { allModels = actionContext.ActionArguments["MultiDictionary"] as Dictionary<string, object>; } else { allModels = await ReadContentAsync(request, typeof(Dictionary<string, object>), Formatters, formatterLogger, cancellationToken) as Dictionary<string, object>; actionContext.ActionArguments["MultiDictionary"] = allModels; } if (allModels != null) { var model = JsonConvert.DeserializeObject(allModels[paramFromBody.ParameterName].ToString(), type); actionContext.ActionArguments[paramFromBody.ParameterName] = model; // validate the object graph. // null indicates we want no body parameter validation if (BodyModelValidator != null) BodyModelValidator.Validate(model, type, metadataProvider, actionContext, paramFromBody.ParameterName); } } }
原理实际是如果有两个复杂类型User user和Company company,那么客户端需要传入的是一个字典有两个key,user和company,分别对应两个参数即可
客户端代理
我这里使用Abp并不是为了基于界面做,而是为了做服务化,客户端通过接口,然后走http请求,最后由服务端的服务实现去执行结果最后返回,有点像以前webservice不过更加轻量级。
先来看看实现,至于作用会在后面的文章中陆续分享。
RealProxy
动态代理/真实代理,大部分情况下用来作为aop的实现,例如,日志,事务等,这里我们直接用来代理发送Http请求。实现如下
public class ClientProxy : RealProxy, IRemotingTypeInfo { private readonly Type _proxyType; private readonly IActionInvoker _actionInvoker; private List<string> _unProxyMethods = new List<string> { "InitContext", "Dispose", }; public ClientProxy(Type proxyType, IActionInvoker actionInvoker) : base(proxyType) { _proxyType = proxyType; _actionInvoker = actionInvoker; } public bool CanCastTo(Type fromType, object o) { return fromType == _proxyType || fromType.IsAssignableFrom(_proxyType); } public string TypeName { get { return _proxyType.Name; } set { } } private static ConcurrentDictionary<Type, List<MethodInfo>> _typeMethodCache = new ConcurrentDictionary<Type, List<MethodInfo>>(); private List<MethodInfo> GetMethods(Type type) { return _typeMethodCache.GetOrAdd(type, item => { List<MethodInfo> methods = new List<MethodInfo>(type.GetMethods()); foreach (Type interf in type.GetInterfaces()) { foreach (MethodInfo method in interf.GetMethods()) if (!methods.Contains(method)) methods.Add(method); } return methods; }); } public override IMessage Invoke(IMessage msg) { // Convert to a MethodCallMessage IMethodCallMessage methodMessage = new MethodCallMessageWrapper((IMethodCallMessage)msg); var methodInfo = GetMethods(_proxyType).FirstOrDefault(item => item.ToString() == methodMessage.MethodBase.ToString()); //var argumentTypes = TypeUtil.GetArgTypes(methodMessage.Args); //var methodInfo = _proxyType.GetMethod(methodMessage.MethodName, argumentTypes) ?? methodMessage.MethodBase as MethodInfo; object objReturnValue = null; if (methodMessage.MethodName.Equals("GetType") && (methodMessage.ArgCount == 0)) { objReturnValue = _proxyType; } else if (methodInfo != null) { if (methodInfo.Name.Equals("Equals") || methodInfo.Name.Equals("GetHashCode") || methodInfo.Name.Equals("ToString") || methodInfo.Name.Equals("GetType")) { throw CoralException.ThrowException<ClientProxyErrorCode>(item => item.UnValideMethod); } if (_unProxyMethods.All(item => item != methodInfo.Name)) { objReturnValue = _actionInvoker.Invoke(_proxyType, methodInfo, methodMessage.Args); } } // Create the return message (ReturnMessage) return new ReturnMessage(objReturnValue, methodMessage.Args, methodMessage.ArgCount, methodMessage.LogicalCallContext, methodMessage); }
IActionInvoker
方法调用者这里抽象成接口是因为我有两个实现,一个是基于WebApi一个是基于Hession,以后还可能有其他的,这样代理的逻辑可以复用,只是实现不同的请求转发就可以了。以RestActionInvoker为例(就是webApi)
public class RestActionInvoker : IActionInvoker { private readonly string _host; private readonly string _preFixString; public RestActionInvoker(string host, string preFixString = "") { _host = host; _preFixString = string.IsNullOrEmpty(preFixString) ? "api" : preFixString; } /// <summary> /// 执行方法 /// </summary> /// <param name="proxyType"></param> /// <param name="methodInfo"></param> /// <param name="parameters"></param> /// <returns></returns> public object Invoke(Type proxyType, MethodInfo methodInfo, params object[] parameters) { var url = TypeUtil.GetUrl(proxyType, methodInfo, _preFixString); var requestType = GetRequestType(methodInfo); ResultMessage result; switch (requestType) { case RequestType.Get: { var getParam = PrepareGetParams(methodInfo, parameters); result = AppComminutService.Get<ResultMessage>(_host, url, getParam, PrepareHeader(methodInfo)); break; } case RequestType.Delete: { var delParams = PrepareGetParams(methodInfo, parameters); result = AppComminutService.Delete<ResultMessage>(_host, url, delParams, PrepareHeader(methodInfo)); break; } case RequestType.Post: { var bodyParam = PrepareBodyParams(methodInfo, parameters); var simpaleParam = PrepareSampleParams(methodInfo, parameters); url = AppendGetParamToUrl(url, simpaleParam); result = AppComminutService.Post<object, ResultMessage>(_host, url, bodyParam.Count == 1 ? bodyParam.First().Value : bodyParam, PrepareHeader(methodInfo)); break; } case RequestType.Put: { var simpaleParam = PrepareSampleParams(methodInfo, parameters); url = AppendGetParamToUrl(url, simpaleParam); var putParam = PrepareBodyParams(methodInfo, parameters); result = AppComminutService.Put<object, ResultMessage>(_host, url, putParam.Count == 1 ? putParam.First().Value : putParam, PrepareHeader(methodInfo)); break; } default: throw new ArgumentOutOfRangeException(); } if (result.State == ResultState.Success) { if (result.Data != null) { try { return JsonConvert.DeserializeObject(result.Data.ToString(), methodInfo.ReturnType); } catch { return result.Data; } } return null; } throw CoralException.ThrowException<ClientProxyErrorCode>(item => item.UnknowError, result.Message); } private RequestType GetRequestType(MethodInfo methodInfo) { if (methodInfo.GetCustomAttribute(typeof(HttpGetAttribute)) != null) return RequestType.Get; else if (methodInfo.GetCustomAttribute(typeof(HttpDeleteAttribute)) != null) return RequestType.Delete; else if (methodInfo.GetCustomAttribute(typeof(HttpPostAttribute)) != null) return RequestType.Post; else if (methodInfo.GetCustomAttribute(typeof(HttpPutAttribute)) != null) return RequestType.Put; else if (methodInfo.Name.StartsWith("Get") && methodInfo.GetParameters().All(item => item.ParameterType.IsSimpleUnderlyingType())) return RequestType.Get; else if (methodInfo.Name.StartsWith("Delete") && methodInfo.GetParameters().All(item => item.ParameterType.IsSimpleUnderlyingType())) return RequestType.Delete; else if (methodInfo.Name.StartsWith("Remove") && methodInfo.GetParameters().All(item => item.ParameterType.IsSimpleUnderlyingType())) return RequestType.Delete; else if (methodInfo.Name.StartsWith("Update") && methodInfo.GetParameters().Any(item => !item.ParameterType.IsSimpleUnderlyingType())) return RequestType.Put; else if (methodInfo.Name.StartsWith("Modify") && methodInfo.GetParameters().Any(item => !item.ParameterType.IsSimpleUnderlyingType())) return RequestType.Put; return RequestType.Post; } /// <summary> /// 准备Header的数据 /// </summary> /// <returns></returns> private Dictionary<string, string> PrepareHeader(MethodInfo methodInfo) { var header = new Dictionary<string, string>(); if (UserContext.Current != null && UserContext.Current.User != null) { header.Add("UserId", UserContext.Current.User.Id.ToString()); header.Add("UserName", HttpUtility.UrlEncode(UserContext.Current.User.Name)); header.Add("UserToken", UserContext.Current.User.Token); header.Add("UserAccount", HttpUtility.UrlEncode(UserContext.Current.User.Account)); LoggerFactory.Instance.Info($"{methodInfo}存在认证信息,线程{Thread.CurrentThread.ManagedThreadId}"); LoggerFactory.Instance.Info($"用户信息为:{JsonConvert.SerializeObject(header)}"); } else { header.Add("IsAnonymous", "true"); LoggerFactory.Instance.Info($"{methodInfo}不存在认证信息,线程{Thread.CurrentThread.ManagedThreadId}"); } if (SessionContext.Current != null) header.Add("SessionId", HttpUtility.UrlEncode(SessionContext.Current.SessionKey)); if (PageContext.Current != null) header.Add("ConnectionId", HttpUtility.UrlEncode(PageContext.Current.PageKey)); if (methodInfo.DeclaringType != null && methodInfo.DeclaringType.GetMethods().Count(item => item.Name == methodInfo.Name) > 1) header.Add("ActionInfo", CoralSecurity.DesEncrypt(methodInfo.ToString())); return header; } /// <summary> /// 准备Url的请求数据 /// </summary> /// <param name="methodInfo"></param> /// <param name="parameters"></param> /// <returns></returns> private Dictionary<string, string> PrepareGetParams(MethodInfo methodInfo, params object[] parameters) { var paramInfos = methodInfo.GetParameters(); var dict = new Dictionary<string, string>(); for (int i = 0; i < paramInfos.Length; i++) { //todo 这里要支持嵌套 dict.Add(paramInfos[i].Name, parameters[i]?.ToString() ?? string.Empty); } return dict; } /// <summary> /// 准备Body的参数 /// </summary> /// <param name="methodInfo"></param> /// <param name="parameters"></param> /// <returns></returns> private Dictionary<string, object> PrepareBodyParams(MethodInfo methodInfo, params object[] parameters) { var paramInfos = methodInfo.GetParameters(); var dict = new Dictionary<string, object>(); for (var i = 0; i < paramInfos.Length; i++) { var item = paramInfos[i]; if (item.ParameterType.IsSimpleType()) continue; dict.Add(item.Name, parameters[i]); } return dict; } /// <summary> /// 准备Url的参数 /// </summary> /// <param name="methodInfo"></param> /// <param name="parameters"></param> /// <returns></returns> private Dictionary<string, string> PrepareSampleParams(MethodInfo methodInfo, params object[] parameters) { var paramInfos = methodInfo.GetParameters(); var dict = new Dictionary<string, string>(); for (var i = 0; i < paramInfos.Length; i++) { var item = paramInfos[i]; if (!item.ParameterType.IsSimpleType()) continue; dict.Add(item.Name, parameters[i]?.ToString() ?? string.Empty); } return dict; } /// <summary> /// 拼接url参数 /// </summary> /// <param name="url"></param> /// <param name="dict"></param> /// <returns></returns> private string AppendGetParamToUrl(string url, Dictionary<string, string> dict) { if (dict == null || dict.Count == 0) return url; if (url.Contains("?")) url += "&"; else url += "?"; url += string.Join("&", dict.Select(item => $"{item.Key}={item.Value ?? string.Empty}")); return url; } } internal enum RequestType { Get = 0, Post, Put, Delete, }
重点在于要结合服务端的实现考虑怎么得到请求,参数组织,认证信息组织等等, 代码逻辑应该还算清晰,可以从Invoke 方法开始看起来
客户端代理注入
注入代码比较简单,就是扫描所有的接口,然后利用动态代理注入Ioc容器即可
//代理工厂代码如下 public class ProxyFactory { public static TService GetService<TService>(ProxyType proxyType,string host,string preFixedString) { switch (proxyType) { case ProxyType.Rest: return (TService)new Core.ClientProxy(typeof(TService), new RestActionInvoker(host, preFixedString)).GetTransparentProxy(); case ProxyType.Hessian: return (TService)new Core.ClientProxy(typeof(TService), new HessianActionInvoker(host)).GetTransparentProxy(); default: return (TService)new Core.ClientProxy(typeof(TService), new RestActionInvoker(host, preFixedString)).GetTransparentProxy(); } } public static object GetService(Type serviceType, ProxyType proxyType, string host, string preFixedString) { switch (proxyType) { case ProxyType.Rest: return new Core.ClientProxy(serviceType, new RestActionInvoker(host, preFixedString)).GetTransparentProxy(); case ProxyType.Hessian: return new Core.ClientProxy(serviceType, new HessianActionInvoker(host)).GetTransparentProxy(); default: return new Core.ClientProxy(serviceType, new RestActionInvoker(host, preFixedString)).GetTransparentProxy(); } } } public enum ProxyType { Rest = 0, Hessian, } //利用Ioc注入代码如下 MetaDataManager.Type.GetAll().ForEach(type => { if (!type.IsInterface) return; if (type.GetInterface(typeof(IService).Name) == null) return; if (type.GetGenericArguments().Length != 0) return; //todo 这里最好放在特性里面判断 if (type.GetCustomAttribute(typeof(InjectAttribute)) != null) return; //Debug.WriteLine("客户端注册类型{0}", type); var obj = ProxyFactory.GetService(type, ProxyType.Rest, "http://localhost:28135", "apiservice"); UnityService.RegisterInstance(type, obj); //var obj = ProxyFactory.GetService(type, ProxyType.Hessian, "http://localhost:28135", ""); //UnityService.RegisterInstance(type, obj); //var interfaces = type.GetInterfaces(); //LinqExtensions.ForEach(interfaces, item => //{ // Debug.WriteLine(type.FullName + "-" + item.FullName); // UnityService.RegisterType(item, type); //}); });
应用
一个简单的Demo
工程结构如下
ClientProxy是客户端测试工程 ClientModule中做扫描注入
ServiceContractTest是契约:包含服务接口和DTO
ServiceHost :包含Abp的注册DynamicController和契约的实现
调用代码如下:
ITestService service = UnityService.Resolve<ITestService>();
var items = service.GetNames("123");
实现和契约比较简单就不贴代码了,这一篇贴了很多代码了。冏!_!
Api容器
有上面测试工程的结构以后,顺势就可以想象到一个Api容器的概念:
通过将Client和ServiceHost逻辑放到框架层面,客户端只需引用这两个dll,客户端只引用契约。
服务端将契约放到bin下面的Module(或者其他目录) 通过反射加载程序集即可。
整体来说Host可以为几种形态,IIs/WindowsServer/Console/....无论哪种只要放入dll即可实现Host
DDD和模块化
DDD典型六边形架构中,保证应用层和领域层的内聚性。简单来说以下几个dll:
1. Contract.XXX
2. App.XXX
3. Domain.XXX
4. Repository.XXX
如下几个dll组织成一个内聚的BC也可以理解为模块,这样你可以让任意一个模块切换部署方式IIS/Service/Console等。业务更加脱离于技术
架构的合与分
最开始的单体架构中如果我们以DDD的方式来组织业务,如上所述那几个dll是一组业务/模块/bc,那么我们多组业务在最开始业务复杂度不高的情况下以单体应用的方式来组成解决方案。
到后来随着业务复杂度增大,或者并发性能的因素影响需要对应用进行切割,只需要将解决方案进行重新组织,引入代理和Api容器即可实现几乎零修改的方式将一个单体的应用分割到不同的进程中进行调用。
微服务业务治理
1. 假设我们把一个Contract看成一个业务模块也就是一个服务
2. 将Contract上传到nuget中
3. 扩展nuget功能,提取Contract的注释作为元数据持久化
4. 提供搜索功能,搜索关键字即可获取相关的服务。然后下载dll实现
这里通过扩展nuget把所有的服务进行集中管理,可以进行分组,相当于构建了一个业务的仓库,所有的业务为都进行集中的存放,这样如果要引用某项服务只需要获取相关dll结合客户端代理即可开始开发对应的功能。
微服务技术
1. 因为开发人员接触到纯粹的业务,而且屏蔽掉了技术细节,那么架构上就留出了迭代的空间
2. 如果某个服务做集群,那么可以通过某个Contract和服务注册发现结合修改客户端代理实现。等等这些架构上的工作都可以按部就班的进行,并且业务上可以实现无影响的迁移
总结
基于这个Api容器和客户端代理的方式结合DDD可以让业务和技术并行几乎无影响分别进行迭代,避免因为技术架构的修改导致大量的业务代码的修改。
基于这个Api容器,自己后面做一些组件的时候也相对容易。下一篇文章将介绍一个作业调度模块的设计,就会用到这