文章目錄
- 簡介
Feign是Java裏的一個聲明式的http api請求庫,可以通過註解(類似.Net的特性)來快速並優雅的封裝對http的調用,並且方便理解和後續的維護,已經廣泛的在Spring Cloud的解決方案中應用。
基於這些優點,我也爲.Net封裝了一個類似的類庫:Beinet.Feign,下面簡單介紹一下使用方法。
注1:該庫基於Framework4.0開發(可以支持WinXP系統),並依賴如下2個庫:
LinFu.DynamicProxy.OfficialRelease 1.0.5以上
Newtonsoft.Json 12.0.3以上
注2:完整的調用Demo代碼已上傳到Git,Beinet.Feign庫源代碼參考 調用Demo代碼參考.
- QuickStart 常規調用代碼
1、接口DTO對象定義:
// DTO對象,屬性可以跟響應的大小寫 不一樣
public class FeignDtoDemo
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime AddTime { get; set; }
public Work[] Works { get; set; }
public string Url { get; set; } // api支持,調用的完整url
public string Post { get; set; } // api支持,調用的完整Form數據,比如a=1&b=2
public string Stream { get; set; }// api支持,調用的完整Stream流數據,比如json
public Dictionary<string, string> Headers { get; set; }// api支持,請求的完整Header
}
public class Work
{
public int Id { get; set; }
public string Company { get; set; }
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
}
2、HTTP API接口聲明:
[FeignClient("", Url = "https://47.107.125.247")]
public interface FeignTestQuick
{
// http無參接口 無返回值
[GetMapping("test/api.aspx?flg=1")]
void Get();
// http無參接口,返回數值
[GetMapping("test/api.aspx?flg=1")]
int GetMs();
// http有參接口返回數值,通過RequestParam把參數拼接到url裏
[GetMapping("test/api.aspx?flg=2")]
int GetAdd([RequestParam]int n1, [RequestParam("n2")]int second2);
// http有參接口,POST返回數值,通過佔位符把參數拼接到url裏
[PostMapping("test/api.aspx?flg=2&n1={num1}&n2={num2}")]
int PostAdd([RequestNone]int num1, [RequestNone]int num2);
// http無參接口返回json字符串,不需要反序列化,想自行處理可以用
[GetMapping("test/api.aspx")]
string GetDtoStr();
// http無參接口返回dto對象
[GetMapping("test/api.aspx")]
FeignDtoDemo GetDtoObj();
// POST有參,返回dto對象,通過RequestParam把參數拼接到url裏
[PostMapping("test/api.aspx")]
FeignDtoDemo PostDtoObj([RequestParam]int id, [RequestParam]string name);
// POST參數爲對象,並自定義url參數名爲urlPara,返回dto對象
[PostMapping("test/api.aspx")]
FeignDtoDemo PostDtoObj(FeignDtoDemo dto, [RequestParam("urlPara")]string arg2);
// 返回類型爲object,等效於返回string
[GetMapping("test/api.aspx")]
object GetObj();
}
3、發起Http調用的代碼:
static void TestQuick()
{
FeignTestQuick feign = ProxyLoader.GetProxy<FeignTestQuick>();
feign.Get();
int ret1 = feign.GetMs();
WriteMsg(ret1);
int ret2 = feign.GetAdd(12, 34);
WriteMsg(ret2);
int ret3 = feign.PostAdd(56, 78);
WriteMsg(ret3);
string json = feign.GetDtoStr();
WriteMsg(json);
FeignDtoDemo dto1 = feign.GetDtoObj();
WriteMsg(JsonConvert.SerializeObject(dto1));
FeignDtoDemo dto2 = feign.PostDtoObj(11, "fankuai");
WriteMsg(JsonConvert.SerializeObject(dto2));
FeignDtoDemo dto3 = feign.PostDtoObj(dto2, "xxx");
WriteMsg(JsonConvert.SerializeObject(dto3));
object obj = feign.GetObj();
WriteMsg($"返回類型:{dto3.GetType()}");
WriteMsg(JsonConvert.SerializeObject(obj));
}
private static int _idx = 0;
public static void WriteMsg(object msg)
{
var ret = Interlocked.Increment(ref _idx);
Console.WriteLine($"{ret.ToString()}: {msg}\r\n");
}
- URL或路由從配置讀取的Demo代碼
1、接口DTO對象定義參考上面的定義;
2、在App.Config或Web.Config裏添加如下配置:
<configuration>
<appSettings>
<add key="env" value="prod"/>
<add key="ConfigKey" value="123456"/>
3、HTTP API接口聲明如下:
// {env} 從app.config文件中讀取配置,也可以整個Url讀取配置,如 Url="{env}"
[FeignClient("", Url = "https://47.107.125.247/{env}/cc")]
public interface FeignTestPlace
{
// 佔位符 num1和num2從方法參數讀取,
// 佔位符 ConfigKey從app.config文件中讀取配置
[GetMapping("test/api.aspx?n1={num1}&n2={num2}&securekey={ConfigKey}")]
FeignDtoDemo GetDtoObj([RequestNone]int num1, [RequestNone]int num2);
}
4、發起Http調用的代碼,最終url,經過讀取配置和參數組合後,是: https://47.107.125.247/prod/cc/test/api.aspx?n1=12&n2=45&securekey=123456
static void TestPlace()
{
FeignTestPlace feign = ProxyLoader.GetProxy<FeignTestPlace>();
// 如下代碼發起的HTTP請求,最終的url是: https://47.107.125.247/cc/test/api.aspx?n1=12&n2=45&securekey=123456
FeignDtoDemo dto1 = feign.GetDtoObj(12, 45);
WriteMsg(JsonConvert.SerializeObject(dto1));
}
- 給請求添加Header的Demo代碼
1、接口DTO對象定義參考上面的定義;
2、HTTP API接口聲明如下:
[FeignClient("", Url = "https://47.107.125.247")]
public interface FeignTestHeader
{
// 在方法特性裏增加header
[GetMapping("test/api.aspx", Headers = new string[] { "headerName=headerValue", "user-agent=beinet feign1234" })]
FeignDtoDemo GetDtoObj();
// 在參數特性裏增加header,一個使用參數名作爲header name,一個使用自定義header name
[GetMapping("test/api.aspx")]
FeignDtoDemo GetDtoObj([RequestHeader]string headerName, [RequestHeader("RealHeaderName")]string arg2);
}
3、發起Http調用的代碼:
static void TestHeader()
{
FeignTestHeader feign = ProxyLoader.GetProxy<FeignTestHeader>();
// http調用前,會添加header:"User-Agent":"beinet feign1234", "headerName":"headerValue"
FeignDtoDemo dto1 = feign.GetDtoObj();
WriteMsg(JsonConvert.SerializeObject(dto1));
// http調用前,會添加header:"headerName":"header1","RealHeaderName":"header2"
FeignDtoDemo dto2 = feign.GetDtoObj("header1", "header2");
WriteMsg(JsonConvert.SerializeObject(dto2));
}
- 使用System.Uri類型參數,修改方法發起請求的url
1、接口DTO對象定義參考上面的定義;
2、HTTP API接口聲明如下:
[FeignClient("", Url = "https://47.107.125.247")]
public interface FeignTestURI
{
// 參數中存在URI類型,且不爲空時,會忽略FeignClient的Url配置
[GetMapping("test/api.aspx")]
FeignDtoDemo GetDtoObj(Uri uri);
// 參數中存在URI類型,且不爲空時,會忽略FeignClient的Url配置
[GetMapping("test/api.aspx")]
FeignDtoDemo GetDtoObj(string arg1, Uri uri);
}
3、發起Http調用的代碼:
// 參數中存在URI類型,且不爲空時,會忽略FeignClient的Url配置
static void TestURI()
{
FeignTestURI feign = ProxyLoader.GetProxy<FeignTestURI>();
Uri uri = new Uri("https://47.107.125.247/cc");
// 請求爲 GET https://47.107.125.247/cc/test/api.aspx
FeignDtoDemo dto1 = feign.GetDtoObj(uri);
WriteMsg(JsonConvert.SerializeObject(dto1));
// 請求爲 POST https://47.107.125.247/cc/test/api.aspx Stream爲abc
FeignDtoDemo dto2 = feign.GetDtoObj("abc", uri);
WriteMsg(JsonConvert.SerializeObject(dto2));
// uri參數傳空,使用類定義的url,即 GET https://47.107.125.247/test/api.aspx
FeignDtoDemo dto3 = feign.GetDtoObj(null);
WriteMsg(JsonConvert.SerializeObject(dto3));
}
- 使用Type類型參數,修改方法返回數據類型
1、接口DTO對象定義參考上面的定義;
2、HTTP API接口聲明如下:
[FeignClient("", Url = "https://47.107.125.247")]
public interface FeignTestArgType
{
// 參數中存在Type類型,且不爲空時,會把返回值反序列化爲該Type,注意type必須是返回類型的子類
[GetMapping("test/api.aspx")]
object GetDtoObj(Type type);
// 參數中存在URI類型,且不爲空時,會忽略FeignClient的Url配置
[GetMapping("test/api.aspx")]
object GetDtoObj(string arg1, Type type);
// 參數中存在Type類型,且Type不是返回類型的子類時,會拋異常
[GetMapping("test/api.aspx")]
FeignDtoDemo GetErr(Type type);
}
3、發起Http調用的代碼:
static void TestArgType()
{
FeignTestArgType feign = ProxyLoader.GetProxy<FeignTestArgType>();
Type type = typeof(FeignDtoDemo);
object dto1 = feign.GetDtoObj(type);
WriteMsg($"返回類型:{dto1.GetType()}");
WriteMsg(JsonConvert.SerializeObject(dto1));
object dto2 = feign.GetDtoObj("123", type);
WriteMsg($"返回類型:{dto2.GetType()}");
WriteMsg(JsonConvert.SerializeObject(dto2));
object dto3 = feign.GetDtoObj(null);
WriteMsg($"返回類型:{dto3.GetType()}");
WriteMsg(JsonConvert.SerializeObject(dto3));
try
{
feign.GetErr(typeof(object));
}
catch (Exception exp)
{
WriteMsg(exp);
}
}
- 自定義配置:攔截請求,自定義序列化和自定義異常處理等
1、接口DTO對象定義參考上面的定義;
2、添加自定義配置類,繼承自FeignDefaultConfig(也可以從 IFeignConfig 接口繼承),定義如下:
public class FeignConfigDeom : FeignDefaultConfig
{
// 返回HTTP請求前後的攔截器
public override List<IRequestInterceptor> GetInterceptor()
{
return new List<IRequestInterceptor>()
{
new RequestInterceptDemo()
};
}
// 如果要對post數據,自定義序列化器,可以重寫此方法
public override string Encoding(object arg)
{
return base.Encoding(arg);
}
// 如果要對api返回的數據,自定義反序列化器,可以重寫此方法
public override object Decoding(string str, Type returnType)
{
// 注意:返回的object必須是returnType類型
return base.Decoding(str, returnType);
}
// 如果要自行處理http請求返回的異常,重寫此方法,返回null將不拋出異常
public override Exception ErrorHandle(Exception exp)
{
return base.ErrorHandle(exp);
}
}
public class RequestInterceptDemo : IRequestInterceptor
{
private DateTime _beginTime;
// 需要對發起請求的url進行處理時,在這裏操作
public Uri OnCreate(Uri url)
{
if(url.ToString().EndsWith("xxx"))
return new Uri("https://www.beinet.com/xxx"); // 返回一個錯誤地址用於測試
return url;
}
// 在HttpWebRequest.GetResponse之前執行的方法,比如記錄日誌,添加統一header
public void BeforeRequest(HttpWebRequest request)
{
request.Headers.Add("aaa", "bbb");
request.UserAgent = "bbbbb";
request.Timeout = 1000;
Console.WriteLine(request.Method + " " + request.RequestUri);
Console.WriteLine(request.Headers);
_beginTime = DateTime.Now;
}
// 在HttpWebRequest.GetResponse之後執行的方法,比如記錄日誌
public void AfterRequest(HttpWebRequest request, HttpWebResponse response, Exception exp)
{
var costTime = (DateTime.Now - _beginTime).TotalMilliseconds.ToString("N0");
Console.WriteLine($"{request.RequestUri} 耗時:{costTime}毫秒");
if (response != null)
Console.WriteLine(((int)response.StatusCode).ToString() + ":" + response.Headers);
else if (exp != null)
Console.WriteLine($"出錯了:{exp.Message}");
}
}
3、HTTP API接口聲明如下:
[FeignClient("", Url = "https://47.107.125.247", Configuration = typeof(FeignConfigDeom))]
public interface FeignTestConfig
{
// 發起正常請求
[GetMapping("test/api.aspx")]
FeignDtoDemo GetDtoObj();
// 發起404請求
[GetMapping("xxx")]
FeignDtoDemo GetErr();
}
4、發起Http調用的代碼:
static void TestConfig()
{
FeignTestConfig feign = ProxyLoader.GetProxy<FeignTestConfig>();
// 可以看到調用前後會輸出日誌,和請求耗時
FeignDtoDemo dto = feign.GetDtoObj();
WriteMsg(JsonConvert.SerializeObject(dto));
try
{
feign.GetErr();// 可以看到調用後會輸出錯誤信息
}
catch { }
}
- 常見問題或建議:
-
FeignClient標記的接口,必須聲明爲 public。
-
Feign方法參數,不添加特性聲明時,默認爲[RequestBody],即該參數將作爲POST的數據內容,不限參數類型;
-
Feign方法參數,只允許一個參數聲明爲[RequestBody],超過1個,將會拋出異常。
注1:不要忘記第1點,參數無特性聲明,默認爲[RequestBody];
注2:如果超過1個參數,那其它參數必須標記爲[RequestNone]、[RequestParam]或[RequestHeader] -
Feign方法參數,如果有參數爲 [RequestBody],且方法聲明爲GetMapping,則強制轉爲PostMapping。
-
Feign方法參數,如果聲明爲[RequestParam],則會把它以key=value形式,追加到url後面。
-
如果不希望拋出異常,要在自定義配置類的 ErrorHandle 方法裏,返回null即可。
-
如果需要修改FeignClient裏的某個方法的url,不使用默認的類級Url,請給方法添加一個System.Uri類型的參數,請參考上面示例:【使用System.Uri類型參數,修改方法發起請求的url】
-
如果需要在運行時才能確定方法返回值類型,請給方法添加一個System.Type類型參數,並在調用時傳遞即可,請參考上面示例:【使用Type類型參數,修改方法返回數據類型】
-
目前FeignClient僅支持讀取App.Config或Web.Config配置,如果需要讀取自定義配置,請在自定義配置類的OnCreate方法裏處理,請參考上面示例:【自定義配置:攔截請求,自定義序列化和自定義異常處理等】
-
可以把該庫結合Autofac等Ioc容器,進行統一管理,如:
var builder = new ContainerBuilder();
builder.Register(c => ProxyLoader.GetProxy<IFeigntest>()).As<IFeigntest>();
var container = builder.Build();
var feign = container.Resolve<IFeigntest>();