文章目录
- 简介
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>();