9.1.3 .net framework通过业务逻辑层自动生成WebApi的做法
首先需要说明的是这是.net framework的一个组件,而不是针对.net core的。目前工作比较忙,因此.net core的转换正在编写过程中,有了实现会第一时间贴出来。
接下来进入正题。对于大型的分层系统,会有一个应用程序层,应用程序层的主要作用是封装业务领域层的业务逻辑层,并对界面展示层提供服务。界面展示层例如有Web网站、移动应用、WPF等等,例如下图。
很多情况下,业务领域层中间的业务逻辑层方法和应用服务层的服务接口几乎是一致的。在业务逻辑方法编写完成后,编程人员,也会重复性的编写应用服务层。该层难度不大,但是属于重复性劳动并且工作量不小。对于一个有敬业精神的程序员来说,问题就来了,写一大堆不加思考的、工作量大的代码,还不如写一个框架自动通过业务逻辑层生成WebApi。
为了简化编程人员的工作量,减少错误的出现,我们编写了这个框架,就是通过业务逻辑层的方法自动生成应用服务层的服务。
其实ABP也有这个东西,但是发现ABP中的实现与Castle Windsor结合太紧密了,用了些Castle Windsor的特性。我等用Unity作为DI/IoC的无法借鉴。
要了解这个自动生成WebApi的框架,我们得简要的讲解下.net framework下webapi的请求处理过程。
Web API是微软的主导的一种面向服务的实现方式,已经集成在visual studio的模板中,是一种比较成熟的SOA数据服务方式。Web API的服务提供方式实现过程由三个步骤组成:路由匹配阶段;控制器选择和构建阶段;执行器选择和执行阶段。
缺省情况下,一个mvc项目包含webapi,会在app_start目录下有一个webapiconfig.cs文件。这个文件是webapi路由的设置,我们假设按照controller和action的名称进行路由,写法见下(尤其是routetemplate行):
1 public static void Register(HttpConfiguration config) 2 { 3 // Web API 配置和服务 4 5 // Web API 路由 6 config.MapHttpAttributeRoutes(); 7 8 config.Routes.MapHttpRoute( 9 name: "DefaultApi", 10 routeTemplate: "api/{controller}/{action}/{id}", 11 defaults: new { id = RouteParameter.Optional } 12 ); 13 }
系统运行过程中,如果框架发现了URI的一个匹配,它会创建一个包含了每个占位符适用的值的字典集合。键是不包含大括号的占位符名称,例如controller、action。值是提取自URI路径或者表单提交的数据。该字典被存储在IHttpRouteData对象中。
在路由匹配阶段,"{controller}"和"{action}"占位符会被像其他占位符一样对待。它们被同其他值一起简单地存储在字典中。http://[server]/[appName]/api/user/get/1,路由字典将包含:
- Controller: user
- Action: get
- Id: 1
控制器选择和构建阶段
在一个路径匹配路由规则后,可以获得到Controller和Action,并存放与路由字典中。Web API的消息处理管道由一组HttpMessageHandler经过"首尾相连"而成。WebApi 最终会引导到默认的HttpControllerDispatcher处理,其中HttpControllerDispatcher实现了HttpMessageHandler接口。HttpControllerDispatcher实现了目标HttpController对象的激活、执行。
- HttpControllerDispatcher接收请求之后,会获取IHttpControllerSelector的实现(缺省是DefaultHttpControllerSelector),然后调用SelectController方法,创建HttpController的描述类HttpControllerDescriptor。
- HttpControllerDispatcher接下来调用HttpControllerDescriptor对象的CreateController方法得到激活的HttpController对象。对于这个HttpControllerDescriptor对象来说,当它的CreateController方法被调用之后,它会获取注册的IHttpControllerActivator对象(缺省是DefaultHttpControllerActivator),并调用其Create方法实现针对目标HttpController对象的激活并将激活的对象返回。
- DefaultHttpControllerActivator对象根据HttpController类型去获取代表目标HttpController实例的对象。如果后者返回一个具体的HttpController对象,该对象将直接作为方法的返回值,否则DefaultHttpControllerActivator直接采用反射的形式创建目标HttpController对象并返回。
执行器选择和调用阶段
ApiController是HttpController的基类。ApiController中的ExecuteAsync方法实现了Action的选择和执行。
- 首先执行GetActionSelector()方法,获取IHttpActionSelector的实现ApiControllerActionSelector,ApiControllerActionSelector调用SelectAction方法,构建用来描述HttpAction的HttpActionDescriptor..
- 然后创建Action的上下文,HttpActionContext。
- 最后执行GetActionInvoker方法,获取IHttpActionInvoker。紧接着调用ActionInvoker的InvokeActionAsync方法,执行Action并反馈HttpResponseMessage格式的数据。
至此,整个从路由到执行并返回结果的整个流程就结束了。
上面洋洋洒洒介绍了整个WeApi的路由和执行过程,下面我们就根据这个流程来确定如何加入我们修改的内容,以通过业务逻辑层的方法自动实现WebApi。业务逻辑层是一个个的逻辑实现类,类中包含了一个个的业务逻辑方法,例如UserService类包含了GetUser方法。我们最终的实现就是生成http://[server]/[appName]/User/GetUser的WebApi调用方式。做法是:
- 替换IHttpControllerSelector的实现DefaultHttpControllerSelector,将Controller的查找重定向到逻辑层的逻辑类
- 替换IHttpActionSelector的实现ApiControllerActionSelector,将Action的查找重定向到逻辑类的方法
- 替换IHttpActionInvoker的实现ApiControllerActionInvoker,将Action执行重定向到方法的执行
在替换之前,我们预先做了部分处理,在所有程序集中查找所有以AppService结尾的逻辑类,并将逻辑类生成为DynamicApiControllerInfo,注册进DynamicApiControllerManager中,逻辑类中的GetUser等方法生成为DynamicApiActionInfo,存放在DynamicApiControllerInfo的Actions属性中。该方式可以避免每次查找Controller和Action都要做反射的做法,提高系统执行效率。
这里,我们约定所有以AppService结尾的逻辑类都要自动生成WebApi,也可以根据情况写成继承IApplicationService接口的类自动生成WebApi。
1 public static class DynamicApiBuilder 2 { 3 public static void Build() 4 { 5 IEnumerable<Type> types = ReflectionHelper.GetSubTypes<object>().Where(type => !type.IsAbstract && type.IsPublic && type.FullName.EndsWith("AppService")); 6 7 foreach (Type type in types) 8 { 9 DynamicApiControllerInfo controllerInfo = GenerateApiControllerInfo(type); 10 11 DynamicApiControllerManager.Register(controllerInfo); 12 } 13 } 14 15 private static DynamicApiController GenerateApiController() 16 { 17 DynamicApiController controller = new DynamicApiController(); 18 19 return controller; 20 } 21 22 private static DynamicApiControllerInfo GenerateApiControllerInfo(Type type) 23 { 24 //10位是AppService 25 DynamicApiControllerInfo controllerInfo = new DynamicApiControllerInfo(type); 26 27 foreach (MethodInfo methodInfo in GetMethodsOfType(type)) 28 { 29 DynamicApiActionInfo actionInfo = new DynamicApiActionInfo(methodInfo.Name, GetNormalizedVerb(methodInfo), methodInfo); 30 31 controllerInfo.Actions.Add(actionInfo.ActionName, actionInfo); 32 } 33 34 return controllerInfo; 35 } 36 37 private static IEnumerable<MethodInfo> GetMethodsOfType(Type type) 38 { 39 var allMethods = new List<MethodInfo>(); 40 41 FillMethodsRecursively(type, BindingFlags.Public | BindingFlags.Instance, allMethods); 42 //method.DeclaringType != typeof(ApplicationService) && 43 return allMethods.Where(method => method.DeclaringType != typeof(object) && !IsPropertyAccessor(method)); 44 } 45 46 private static void FillMethodsRecursively(Type type, BindingFlags flags, List<MethodInfo> members) 47 { 48 members.AddRange(type.GetMethods(flags).Where(m => !members.Exists(mm => m.Name == mm.Name))); 49 50 foreach (var interfaceType in type.GetInterfaces()) 51 { 52 FillMethodsRecursively(interfaceType, flags, members); 53 } 54 } 55 56 private static bool IsPropertyAccessor(MethodInfo method) 57 { 58 return method.IsSpecialName && (method.Attributes & MethodAttributes.HideBySig) != 0; 59 } 60 }
接下来实现DynamicApiControllerSelector,替换缺省的DefaultHttpControllerSelector。这个程序的主要做法是从路由信息RouteData中获取当前的Controller和Action等信息。根据Controller信息从DynamicApiControllerManager中获取已注册的DynamicApiControllerInfo,创建Controller的描述类DynamicApiControllerDescriptor。DynamicApiControllerManager就是预处理部分,将DynamicApiControllerInfo注册的类。
1 public class DynamicApiControllerSelector : DefaultHttpControllerSelector 2 { 3 private readonly HttpConfiguration _Configuration; 4 5 public DynamicApiControllerSelector(HttpConfiguration configuration) 6 : base(configuration) 7 { 8 _Configuration = configuration; 9 } 10 11 public override HttpControllerDescriptor SelectController(HttpRequestMessage request) 12 { 13 if (request == null) 14 { 15 return base.SelectController(null); 16 } 17 18 var routeData = request.GetRouteData(); 19 if (routeData == null) 20 { 21 return base.SelectController(request); 22 } 23 24 //Get Area/Controller/Action from route 25 object objArea; 26 if (!routeData.Values.TryGetValue("area", out objArea)) 27 { 28 return base.SelectController(request); 29 } 30 object objController; 31 if (!routeData.Values.TryGetValue("controller", out objController)) 32 { 33 return base.SelectController(request); 34 } 35 object objAction; 36 if (!routeData.Values.TryGetValue("action", out objAction)) 37 { 38 return base.SelectController(request); 39 } 40 41 string areaName = (string)objArea; 42 string controllerName = (string)objController; 43 string actionName = (string)objAction; 44 45 //Normalize serviceNameWithAction 46 if (actionName.EndsWith("/")) 47 { 48 actionName = actionName.Substring(0, actionName.Length - 1); 49 routeData.Values["action"] = actionName; 50 } 51 52 DynamicApiControllerInfo controllerInfo = DynamicApiControllerManager.Get(areaName, controllerName); 53 54 //Create the controller descriptor 55 var controllerDescriptor = new DynamicApiControllerDescriptor(_Configuration, controllerInfo.AreaName, controllerInfo.ControllerName); 56 controllerDescriptor.Properties["__MicroLibraryDynamicApiControllerInfo"] = controllerInfo; 57 return controllerDescriptor; 58 } 59 }
下一步就是实现DynamicApiActionSelector,替换缺省的ApiControllerActionSelector。DynamicApiActionSelector重写了SelectAction方法,主要做法是Controller的上下文HttpControllerContext中获取当前的DynamicApiControllerInfo,创建Action的描述类DynamicApiActionDescriptor。
1 public class DynamicApiActionSelector : ApiControllerActionSelector 2 { 3 public override HttpActionDescriptor SelectAction(HttpControllerContext controllerContext) 4 { 5 object controllerInfoObj; 6 if (!controllerContext.ControllerDescriptor.Properties.TryGetValue("__MicroLibraryDynamicApiControllerInfo", out controllerInfoObj)) 7 { 8 return GetDefaultActionDescriptor(controllerContext); 9 } 10 11 //Get controller information which is selected by HttpControllerSelector. 12 var controllerInfo = controllerInfoObj as DynamicApiControllerInfo; 13 MicroLibraryExceptionHelper.IsNull(controllerInfo, this.GetType().FullName, TraceLogType.Error, "DynamicApiControllerInfo in ControllerDescriptor.Properties is not a " + typeof(DynamicApiControllerInfo).FullName + " class."); 14 15 string areaName = (string)controllerContext.RouteData.Values["area"]; 16 string controllerName = (string)controllerContext.RouteData.Values["controller"]; 17 string actionName = (string)controllerContext.RouteData.Values["action"]; 18 19 return GetActionDescriptorByActionName(controllerContext, controllerInfo, actionName); 20 } 21 22 private HttpActionDescriptor GetActionDescriptorByActionName(HttpControllerContext controllerContext, DynamicApiControllerInfo controllerInfo, string actionName) 23 { 24 //Get action information by action name 25 DynamicApiActionInfo actionInfo; 26 MicroLibraryExceptionHelper.FalseThrow(controllerInfo.Actions.TryGetValue(actionName, out actionInfo), this.GetType().FullName, TraceLogType.Error, "在Api Controller:" + controllerInfo.ControllerName + "中,没有对应的Action:" + actionName); 27 MicroLibraryExceptionHelper.FalseThrow(actionInfo.Verb.IsEqualTo(controllerContext.Request.Method), this.GetType().FullName, TraceLogType.Error, "在Api Controller:" + controllerInfo.ControllerName + "中的Action:" + actionName + "的HttpVerb不正确,应该为:" + actionInfo.Verb); 28 29 return new DynamicApiActionDescriptor(controllerContext.ControllerDescriptor, actionInfo.Method); 30 } 31 32 private HttpActionDescriptor GetDefaultActionDescriptor(HttpControllerContext controllerContext) 33 { 34 return base.SelectAction(controllerContext); 35 } 36 }
需要说明的是,我们只是按照ActionName来确定最终的Action。还可以按照HttpVeb来确定Actin的方式,我们没有使用该方式,因此没有实现,可以参照如下代码实现:
1 private HttpVerb GetNormalizedVerb(MethodInfo methodInfo) 2 { 3 if (methodInfo.IsDefined(typeof(HttpGetAttribute))) 4 { 5 return HttpVerb.Get; 6 } 7 8 if (methodInfo.IsDefined(typeof(HttpPostAttribute))) 9 { 10 return HttpVerb.Post; 11 } 12 13 if (methodInfo.IsDefined(typeof(HttpPutAttribute))) 14 { 15 return HttpVerb.Put; 16 } 17 18 if (methodInfo.IsDefined(typeof(HttpDeleteAttribute))) 19 { 20 return HttpVerb.Delete; 21 } 22 23 if (methodInfo.IsDefined(typeof(HttpOptionsAttribute))) 24 { 25 return HttpVerb.Options; 26 } 27 28 if (methodInfo.IsDefined(typeof(HttpHeadAttribute))) 29 { 30 return HttpVerb.Head; 31 } 32 33 return HttpVerb.Get; 34 } 35 36 private HttpActionDescriptor GetActionDescriptorByCurrentHttpVerb(HttpControllerContext controllerContext, DynamicApiControllerInfo controllerInfo) 37 { 38 //Check if there is only one action with the current http verb 39 var actionsByVerb = controllerInfo.Actions.Values.Where(action => action.Verb.IsEqualTo(controllerContext.Request.Method)); 40 41 MicroLibraryExceptionHelper.TrueThrow(actionsByVerb.Count() == 0, this.GetType().FullName, TraceLogType.Error, "在Api Controller:" + controllerInfo.ServiceName + "中,没有HttpVerb: " + controllerContext.Request.Method + "的Action"); 42 MicroLibraryExceptionHelper.TrueThrow(actionsByVerb.Count() > 1, this.GetType().FullName, TraceLogType.Error, "在Api Controller:" + controllerInfo.ServiceName + "中,HttpVerb: " + controllerContext.Request.Method + "的Action有多个"); 43 44 //Return the single action by the current http verb 45 return new DynamicApiActionDescriptor(controllerContext.ControllerDescriptor, actionsByVerb.First().Method); 46 }
再下一步就是实现DynamicApiActionInvoker,替换缺省的ApiControllerActionInvoker。DynamicApiActionInvoker实现了InvokeActionAsync方法,主要做法是获取Controller和Action的描述类DynamicApiControllerDescriptor、DynamicApiActionDescriptor。根据Controller的描述类获取DynamicApiControllerInfo,进一步获取ControllerInfo的ServiceType,这就是逻辑类的类型信息。通过我们自己的Ioc管理器IocManager,从DI容器中获取SerivceType的具体实现obj对象。然后从Action描述中获取MethodInfo,执行方法。返回结果信息,返回前在头部增加Access-Control-Allow-Origin等信息。
还有一点说明的是需要在Web.config中进行修改,modules中移除WebDavModule,handlers中移除WebDav和OPTIONSVerbHandler。
1 public class DynamicApiActionInvoker : IHttpActionInvoker 2 { 3 private bool authenticatedFlag = DynamicApiConfiguration.GetConfig().AuthenticatedFlag; 4 5 public Task<HttpResponseMessage> InvokeActionAsync(HttpActionContext actionContext, CancellationToken cancellationToken) 6 { 7 DynamicApiActionDescriptor actionDescriptor = actionContext.ActionDescriptor as DynamicApiActionDescriptor; 8 DynamicApiControllerDescriptor controllerDescriptor = actionContext.ControllerContext.ControllerDescriptor as DynamicApiControllerDescriptor; 9 10 DynamicApiControllerInfo controllerInfo = controllerDescriptor.Properties["__MicroLibraryDynamicApiControllerInfo"] as DynamicApiControllerInfo; 11 12 object obj = IocManager.Instance.Resolve(controllerInfo.ServiceType); 13 14 MicroLibraryExceptionHelper.FalseThrow(Verify(actionContext), this.GetType().FullName, TraceLogType.Error, "Token验证不通过!"); 15 16 //todo: 异常处理以及参数顺序问题 17 object result = actionDescriptor.MethodInfo.Invoke(obj, actionContext.ActionArguments.Where(kvp => !string.Equals(kvp.Key, "sign", StringComparison.OrdinalIgnoreCase) || !string.Equals(kvp.Key, "appKey", StringComparison.OrdinalIgnoreCase)).Select(kvp => kvp.Value).ToArray()); 18 19 return Task.Run<HttpResponseMessage>(() => 20 { 21 string strResult; 22 if (result is String || result is Char) 23 { 24 strResult = obj.ToString(); 25 } 26 else 27 { 28 strResult = SerializerHelper.ToJson(result); 29 } 30 31 var response = new HttpResponseMessage() 32 { 33 Content = new StringContent(strResult, Encoding.GetEncoding("UTF-8"), "application/json") 34 }; 35 36 //需要在web.config中,system.webServer---modules---remove name="WebDavModule" 37 //---handlers---remove name="WebDav"和remove name="OPTIONSVerbHandler" 38 response.Headers.Add("Access-Control-Allow-Origin", "*"); 39 response.Headers.Add("Access-Control-Allow-Headers", "X-Requested-With"); 40 if (actionContext.Request.Method.Method == "Options") 41 { 42 response.Headers.Add("Access-Control-Allow-Methods", "*"); 43 response.Headers.Add("Access-Control-Allow-Headers", "*"); 44 } 45 return response; 46 }); 47 } 48 49 private bool Verify(HttpActionContext actionContext) 50 { 51 if (!authenticatedFlag) return true; 52 53 var attrs = actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>(); 54 if (attrs.Any()) return true; 55 56 string appKey = (string)actionContext.ActionArguments["appCode"]; 57 58 return DynamicApiHelper.ValidAppSecretSign(actionContext.ActionArguments, appKey); 59 } 60 }
大家可能还注意到我们有Verify方法。这个方法是判断对WebApi是否有权限访问。在这种Rest形式的服务中,服务接口对外部暴露出来,因此对用户调用的授权尤为重要。在Dynamic WebAPI的安全实现过程中,使用了JWT(JsonWebToken)技术,这里就不做赘述了。