仿Java實現的,DotNet版本的Feign類庫

- 簡介

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 { }
}

- 常見問題或建議:

  1. FeignClient標記的接口,必須聲明爲 public。

  2. Feign方法參數,不添加特性聲明時,默認爲[RequestBody],即該參數將作爲POST的數據內容,不限參數類型;

  3. Feign方法參數,只允許一個參數聲明爲[RequestBody],超過1個,將會拋出異常。
    注1:不要忘記第1點,參數無特性聲明,默認爲[RequestBody];
    注2:如果超過1個參數,那其它參數必須標記爲[RequestNone]、[RequestParam]或[RequestHeader]

  4. Feign方法參數,如果有參數爲 [RequestBody],且方法聲明爲GetMapping,則強制轉爲PostMapping。

  5. Feign方法參數,如果聲明爲[RequestParam],則會把它以key=value形式,追加到url後面。

  6. 如果不希望拋出異常,要在自定義配置類的 ErrorHandle 方法裏,返回null即可。

  7. 如果需要修改FeignClient裏的某個方法的url,不使用默認的類級Url,請給方法添加一個System.Uri類型的參數,請參考上面示例:【使用System.Uri類型參數,修改方法發起請求的url】

  8. 如果需要在運行時才能確定方法返回值類型,請給方法添加一個System.Type類型參數,並在調用時傳遞即可,請參考上面示例:【使用Type類型參數,修改方法返回數據類型】

  9. 目前FeignClient僅支持讀取App.Config或Web.Config配置,如果需要讀取自定義配置,請在自定義配置類的OnCreate方法裏處理,請參考上面示例:【自定義配置:攔截請求,自定義序列化和自定義異常處理等】

  10. 可以把該庫結合Autofac等Ioc容器,進行統一管理,如:

var builder = new ContainerBuilder();
builder.Register(c => ProxyLoader.GetProxy<IFeigntest>()).As<IFeigntest>();
var container = builder.Build();
var feign = container.Resolve<IFeigntest>();
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章