為什么要改
最近公司在推廣SOA框架,第一次正經接觸這種技術(之前也有但還是忽略掉吧),感覺挺好,就想自己也折騰一下,實現一個簡單的SOA框架
用過mvc進行開發,印象之中WebApi和Mvc好像是一樣的,帶着這樣的預設開始玩WebApi,然后被虐得找不到着北。
被虐的原因,是Mvc和WebApi在細節上差別還是有點大,例如:
- 在Mvc中,一個Controller中的所有公共方法一般情況下可以響應POST方法,而WebApi中不行
- 在Mvc中,一個Action方法中的參數即可來自Url,也可以來自Form,而WebApi中不是這樣,具體的規則好像是除非你在參數中加了[FromBody],否則這個參數永遠也無法從Form中獲取
這是這兩種技術我知道的最大的差別,其他的沒發現或者說是沒注意,也有可能這些差別是因為我不會用,畢竟接觸WebApi時間不長。如果我有些地方說錯了,請指正。
就這兩個不同點,我查了很多資料,也沒有辦法解決,第一個還好,加個特性就行了,第二個的話好像就算加了[FromBody]也還是不行,感覺就是一堆限制。接着,既然這么多讓我不爽的地方,那我就來改造它吧。
改造的目標,有以下幾個:
- 不再限制控制器必須以Controller結尾,其實這個並不是必須,只是被限制着確實不太舒服
- 所有方法可以響應所有的請求方法,如果存在方法名相同的方法,那么才需要特性來區分
- Action中的參數優先從Url中獲取,再從Body中獲取,從Body中獲取的時候,優先假設Body中的數據是表單參數,若不是則將Body中的數據當作json或xml數據進行獲取
定下了目標之后,感覺微軟為什么要這樣設計WebApi呢,或許它有它的道理。
目標好定,做起來真是頭大,一開始想參考公司的SOA框架的實現,但因為我用了OWIN技術來進行宿主,而看了公司的框架好像不是用的這個,總之就是看了半天沒看懂應該從哪個地方開始,反而是越看越糊,畢竟不是完全一樣的技術,所以還是自己弄吧。
OK,廢話了這么多,進入正題吧。首先來一個鏈接,沒了這個文章我就不可能改造成功:http://www.cnblogs.com/beginor/archive/2012/03/22/2411496.html
OWIN宿主
其實這個網上很多,我主要是為了貼代碼,不然的話下面幾小節寫不下去
-
[assembly: OwinStartup(typeof(Startup))]//這句是在IIS宿主的時候使用的,作用是.Net會查找Startup類來啟動整個服務
-
namespace Xinchen.SOA.Server
-
{
-
public class Startup
-
{
-
public void Configuration(IAppBuilder appBuilder)
-
{
-
HttpConfiguration config = new HttpConfiguration();
-
config.Routes.MapHttpRoute(
-
name: "DefaultApi",
-
routeTemplate: "{controller}/{action}"
-
);
-
config.Services.Add(typeof(ValueProviderFactory), new MyValueProviderFactory());//自定義參數查找,實現第三個目標
-
config.Services.Replace(typeof(IHttpControllerSelector), new ControllerSelector(config));//自定義控制器查找,實現第一個目標
-
config.Services.Replace(typeof(IHttpActionSelector), new HttpActionSelector());//自定義Action查找,實現第二個目標
-
appBuilder.UseWebApi(config);
-
}
-
}
-
}
省略了部分不太重要的代碼,Services.Add和Replace從字面就能明白是什么意思,但我沒有試過是否必須要像上面那樣寫才行
對控制器的限制
-
public class ControllerSelector : IHttpControllerSelector
-
{
-
HttpConfiguration _config;
-
IDictionary<string, HttpControllerDescriptor> _desriptors = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);
-
public ControllerSelector(HttpConfiguration config)
-
{
-
_config = config;
-
}
-
-
void InitControllers()
-
{
-
if (_desriptors.Count <= 0)
-
{
-
lock (_desriptors)
-
{
-
if (_desriptors.Count <= 0)
-
{
-
var assemblies = AppDomain.CurrentDomain.GetAssemblies().Where(x => !x.GlobalAssemblyCache && !x.IsDynamic);
-
var controllerTypes = new List<Type>();
-
foreach (var ass in assemblies)
-
{
-
controllerTypes.AddRange(ass.GetExportedTypes().Where(x => typeof(ApiController).IsAssignableFrom(x)));
-
}
-
var descriptors = new Dictionary<string, HttpControllerDescriptor>();
-
foreach (var controllerType in controllerTypes)
-
{
-
var descriptor = new HttpControllerDescriptor(_config, controllerType.Name, controllerType);
-
_desriptors.Add(descriptor.ControllerName, descriptor);
-
}
-
}
-
}
-
}
-
}
-
-
public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
-
{
-
InitControllers();
-
return _desriptors;
-
}
-
-
public System.Web.Http.Controllers.HttpControllerDescriptor SelectController(System.Net.Http.HttpRequestMessage request)
-
{
-
InitControllers();
-
var routeData = request.GetRouteData();
-
var controllerName = Convert.ToString(routeData.Values.Get("controller"));
-
if (string.IsNullOrWhiteSpace(controllerName))
-
{
-
throw new ArgumentException(string.Format("沒有在路由信息中找到controller"));
-
}
-
-
return _desriptors.Get(controllerName);
-
}
-
-
}
這個其實比較簡單,測試中WebApi好像沒調用GetControllerMapping方法,直接調用了SelectController方法,最后一個方法中有兩個Get方法調用,Get只是把從字典獲取值的TryGetValue功能給封裝了一下,InitControllers方法是從當前所有的程序集中找繼承了ApiController的類,找到之后緩存起來。這段代碼整體比較簡單。
對Action的限制
-
public class HttpActionSelector : IHttpActionSelector
-
{
-
public ILookup<string, HttpActionDescriptor> GetActionMapping(HttpControllerDescriptor controllerDescriptor)
-
{
-
var methods = controllerDescriptor.ControllerType.GetMethods();
-
var result = new List<HttpActionDescriptor>();
-
foreach (var method in methods)
-
{
-
var descriptor = new ReflectedHttpActionDescriptor(controllerDescriptor, method);
-
result.Add(descriptor);
-
}
-
return result.ToLookup(x => x.ActionName);
-
}
-
-
public HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
-
{
-
var actionDescriptor = new ReflectedHttpActionDescriptor();
-
var routeData = controllerContext.RouteData;
-
object action = string.Empty;
-
if (!routeData.Values.TryGetValue("action", out action))
-
{
-
throw new HttpResponseException(controllerContext.Request.CreateErrorResponse(System.Net.HttpStatusCode.NotFound, "在路由中未找到action"));
-
}
-
string actionName = action.ToString().ToLower();
-
var methods = controllerContext.ControllerDescriptor.ControllerType.GetMethods().Where(x => x.Name.ToLower() == actionName);
-
var count = methods.Count();
-
MethodInfo method = null;
-
switch (count)
-
{
-
case 0:
-
throw new HttpResponseException(controllerContext.Request.CreateErrorResponse(System.Net.HttpStatusCode.NotFound, "在控制器" + controllerContext.ControllerDescriptor.ControllerName + "中未找到名為" + actionName + "的方法"));
-
case 1:
-
method = methods.FirstOrDefault();
-
break;
-
default:
-
var httpMethod = controllerContext.Request.Method;
-
var filterdMethods = methods.Where(x =>
-
{
-
var verb = x.GetCustomAttribute<AcceptVerbsAttribute>();
-
if (verb == null)
-
{
-
throw new HttpResponseException(controllerContext.Request.CreateErrorResponse(System.Net.HttpStatusCode.NotFound, "在控制器" + controllerContext.ControllerDescriptor.ControllerName + "中找到多個名為" + actionName + "的方法,請考慮為這些方法加上AcceptVerbsAttribute特性"));
-
}
-
return verb.HttpMethods.Contains(httpMethod);
-
});
-
if (filterdMethods.Count() > 1)
-
{
-
throw new HttpResponseException(controllerContext.Request.CreateErrorResponse(System.Net.HttpStatusCode.NotFound, "在控制器" + controllerContext.ControllerDescriptor.ControllerName + "中找到多個名為" + actionName + "的方法,並且這些方法的AcceptVerbsAttribute都含有" + httpMethod.ToString() + ",發生重復"));
-
}
-
else if (filterdMethods.Count() <= 0)
-
{
-
throw new HttpResponseException(controllerContext.Request.CreateErrorResponse(System.Net.HttpStatusCode.NotFound, "在控制器" + controllerContext.ControllerDescriptor.ControllerName + "中找到多個名為" + actionName + "的方法,但沒有方法被配置為可以響應" + httpMethod.ToString() + "請求"));
-
}
-
method = filterdMethods.FirstOrDefault();
-
break;
-
}
-
return new ReflectedHttpActionDescriptor(controllerContext.ControllerDescriptor, method);
-
}
-
}
GetActionMapping方法很簡單,從控制器類型中找到所有的Action方法並返回
SelectAction方法相對復雜,其實就是第二個目標的邏輯,代碼看起來比較多其實並有很難的地方。
對Action的參數的限制
這一塊比較難,我試了很久才成功,而且還有坑
-
public class ActionValueBinder : DefaultActionValueBinder
-
{
-
protected override HttpParameterBinding GetParameterBinding(HttpParameterDescriptor parameter)
-
{
-
ParameterBindingAttribute parameterBinderAttribute = parameter.ParameterBinderAttribute;
-
if (parameterBinderAttribute == null)
-
{
-
ParameterBindingRulesCollection parameterBindingRules = parameter.Configuration.ParameterBindingRules;
-
if (parameterBindingRules != null)
-
{
-
HttpParameterBinding binding = parameterBindingRules.LookupBinding(parameter);
-
if (binding != null)
-
{
-
return binding;
-
}
-
}
-
if (TypeHelper.IsValueType(parameter.ParameterType))
-
{
-
return parameter.BindWithAttribute(new ValueProviderAttribute(typeof(MyValueProviderFactory)));
-
}
-
parameterBinderAttribute = new FromBodyAttribute();
-
}
-
return parameterBinderAttribute.GetBinding(parameter);
-
}
-
}
這個類其實就是把.Net的默認實現給改了一點點,也就是從第17行到第20行,現在的判斷邏輯是如果參數的類型為基礎類型的話,則從Url或Form表單中獲取,而這個邏輯是寫在MyValueProviderFactory中的,ValueProviderAttribute是.Net自帶的。其他並沒有改動,怕是也改不動吧,因為一時間看不懂這些代碼是什么意思。
-
public class MyValueProviderFactory : ValueProviderFactory
-
{
-
public override IValueProvider GetValueProvider(System.Web.Http.Controllers.HttpActionContext actionContext)
-
{
-
return new ValueProvider(actionContext);
-
}
-
}
這個很簡單,略過。
-
public class ValueProvider : IValueProvider
-
{
-
private IEnumerable<KeyValuePair<string, string>> _queryParameters;
-
private HttpContent _httpContent;
-
private HttpActionContext _context;
-
-
public ValueProvider(HttpActionContext context)
-
{
-
_context = context;
-
_httpContent = context.Request.Content;
-
_queryParameters = context.Request.GetQueryNameValuePairs();
-
}
-
public bool ContainsPrefix(string prefix)
-
{
-
return _queryParameters.Any(x => x.Key == prefix);
-
}
-
-
NameValueCollection _formDatas = (NameValueCollection)CallContext.LogicalGetData("$formDatas");
-
-
public ValueProviderResult GetValue(string key)
-
{
-
var value = _queryParameters.FirstOrDefault(x => x.Key == key).Value;
-
if (string.IsNullOrWhiteSpace(value))
-
{
-
if (_formDatas == null)
-
{
-
if (_httpContent.IsFormData())
-
{
-
if (_formDatas == null)
-
{
-
_formDatas = _httpContent.ReadAsFormDataAsync().Result;
-
CallContext.LogicalSetData("$formDatas", _formDatas);
-
}
-
}
-
else
-
{
-
throw new HttpResponseException(_context.Request.CreateErrorResponse(System.Net.HttpStatusCode.NotFound, string.Format("未在URL中找到名為{0}的參數,此時必須傳入表單參數或json或xml參數", key)));
-
}
-
}
-
value = _formDatas[key];
-
if (string.IsNullOrWhiteSpace(value))
-
{
-
throw new HttpResponseException(_context.Request.CreateErrorResponse(System.Net.HttpStatusCode.NotFound, string.Format("未在URL中找到名為{0}的參數,也未在表單中找到該參數", key)));
-
}
-
}
-
return new ValueProviderResult(value, value, CultureInfo.InvariantCulture);
-
}
-
}
核心是GetValue方法,首先從查詢字符串中取值,若沒有則判斷是否有緩存,若沒有的話再一次判斷Body中是否表單參數,是的話就直接讀取。這個地方其實一開始並沒有想用緩存,但如果不用的話就會出現一個問題,如果一個Action有多個參數,那么就掛了。
原因在於:
- WebApi在查找參數時,如果這個Action有N個參數,那么WebApi會調用ActionValueBinder的GetParameterBinding方法N次
- GetParameterBinding方法在被調用這N次的時候每次都會執行parameter.BindWithAttribute(new ValueProviderAttribute(typeof(MyValueProviderFactory)));
- BindWithAttribute方法每次都會實例化一個MyValueProviderFactory對象(是WebApi實例化的)並調用GetValueProvider方法
- 大家可以看到GetValueProvider每次都new了一個ValueProvider,但這個我是可以控制的,但我發現除非我弄成全局緩存,否則是沒用的,因為MyValueProviderFactory對象每次都會重新實例化。如果弄成全局緩存,那么就會影響其他的Api調用
- 然后ValueProvider又調用GetValue方法,然后就開始坑爹了
- 因為第一次GetValue的時候就會讀取Body流中的表單數據,讀取之后其實Body流就不能再讀了,再讀就成空了,所以就變成了有N個參數,就會調用N次GetValue方法,但其實從第二次調用的時候就已經不能讀了,所以才用了這個緩存。
接下來的邏輯其實都簡單了。