Feign是java的轻量级http客户端,通过注解和接口就能快速实现http客户端,方便快捷;
官方git地址:OpenFeign/feign: Feign makes writing java http clients easier (github.com)
Feign集成的功能挺多的,这是官网的展示图:
简单使用
maven导入feign基本库:
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
<version>13.3</version>
</dependency>
先准备一个测试用的http服务端:
package test;
import java.io.IOException;
import java.net.InetSocketAddress;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
public class MyHttpServer {
public static void main(String[] args) {
try {
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
server.createContext("/testget", new MyHttpHandler());
server.createContext("/testpost", new MyHttpHandler2());
System.out.println("http server start");
server.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
class MyHttpHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
System.out.println("GETuri:" + exchange.getRequestURI());
exchange.getRequestHeaders().entrySet().forEach(x -> {System.out.println("Header:" + x);});
exchange.sendResponseHeaders(200, 0);
exchange.getResponseBody().write("hello world".getBytes());
exchange.close();
System.out.println();
}
}
class MyHttpHandler2 implements HttpHandler{
@Override
public void handle(HttpExchange exchange) throws IOException {
System.out.println("POSTuri:" + exchange.getRequestURI());
exchange.getRequestHeaders().entrySet().forEach(x -> {System.out.println("Header:" + x);});
byte[] body = exchange.getRequestBody().readAllBytes();
System.out.println("body:" + new String(body));
exchange.sendResponseHeaders(200, 0);
exchange.getResponseBody().write("{\"name\":\"Jack\", \"age\":13}".getBytes());
exchange.close();
System.out.println();
}
}
简单测试demo:
使用注解和接口定义http请求:
package test;
import feign.Body;
import feign.Param;
import feign.RequestLine;
public interface MyHttpClient {
@RequestLine("GET /testget?key={key}")
String test1(@Param("key")String k);
@RequestLine("POST /testpost?str={x}")
@Body("hello {x}")
String test2(@Param("x")String str);
}
调用代码,使用Feign.builder()创建客户端,target()指定接口和base url:
package test;
import feign.Feign;
public class Test {
public static void main(String[] args) {
System.out.println("start");
MyHttpClient client = Feign.builder()
.target(MyHttpClient.class, "http://127.0.0.1:8080");
String res = client.test1("test");
System.out.println(res);
System.out.println(client.test2("Tom"));
}
}
启动http服务端,再启动测试feign客户端:
服务端打印:
客户端打印:
日志模块
还可以通过Feign的builder设置连接超时读取超时和日志模块:
MyHttpClient client = Feign.builder()
.options(new Request.Options(5, TimeUnit.SECONDS, 10, TimeUnit.SECONDS, true))
.logger(new Logger.JavaLogger("MyLog").appendToFile("logs/my.log"))
.logLevel(Logger.Level.FULL)
.target(MyHttpClient.class, "http://127.0.0.1:8080");
这个用的是自带的JavaLogger日志模块,需要注意设置日志文件的路径文件夹需要先创建好才可以,也可以使用例如slf4j日志模块,不过就需要maven导入对应模块了,feign有很多模块可以导入,基础模块就是feign-core,还有其他例如:feign-jackson、feign-okhttp、feign-slf4j、feign-httpclient、feign-gson等等,使用对应的功能就需要导入对应的模块,为了最好的兼容性,所有模块版本最好保持一致,嫌麻烦官方也给了扩展模块集合包,可以直接导入对应版本的扩展集合包(不包括基础feign-core):
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-bom</artifactId>
<version>13.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
生成的日志:
处理json
收发json数据测试:(特定格式需要maven导入并设置对应的编解码,像json、xml等等)
例如使用gson模块,maven导入:
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-gson</artifactId>
<version>13.3</version>
</dependency>
准备两个类,接收的数据和发送的数据:
package test;
public class FeignPostResult {
public String name;
public int age;
@Override
public String toString() {
return "FeignPostResult [name=" + name + ", age=" + age + "]";
}
}
package test;
public class Book {
public int id;
public String bookName;
public Book(int id, String bookName) {
super();
this.id = id;
this.bookName = bookName;
}
}
接口里添加一个test3方法测试:
@RequestLine("POST /testpost")
@Headers("Content-Type: application/json")
FeignPostResult test3(Book book);
调用类,需要使用Feign.builder()设置编解码为gson:
测试结果:
Feign标签
关于Feign的一些标签:
- @RequestLine,用于接口方法上
定义请求方法是GET还是POST,定义请求uri的模版,使用{}将变量括起来,对应@Param变量参数使用;(uri模板可参考RFC 6570 - URI Template (ietf.org),部分支持)
(当前版本Feign 13.3,RequestLine和QueryMap遵循URI模板- RFC 6570规范的Level 1,对于Map和List都会默认展开,只支持单变量,随着更新版本会支持更多)
- @Param,用于接口方法的参数前
用于对应其他标签里模版的变量替换,null值和未定义参数将被忽略;
(默认是使用toString()替换,@Param可以使用可选属性expander指定一个实现Expander接口的实现类,该类实现String expand(Object value)方法即可)
例如:对于同一个调用 client.test6(new Book(1111, "Java虚拟机"));
服务端收到:
如果给Book类加上toString()方法:
服务端收到:
实现Expander类的expand方法,并使用@Param的expander指定:
package test;
import feign.Param.Expander;
public class MyParamExp implements Expander{
@Override
public String expand(Object value) {
Book obj = (Book) value;
return obj.id + ":" + obj.bookName;
}
}
服务端收到:
- @Headers,方法上和接口名上都可用
用于给请求添加Header模板,用在方法上只对该方法有效,用在接口名上对所有方法都有效,也可搭配{}和@Param使用;
- @QueryMap,用于接口方法的参数前
在请求uri后面生成使用&分割的key=val查询条件;(会自动补加?和&)
已经有?就不会再加了:
(@RequestLine("GET /testget?")写法是错误的,会异常)
(参数可以是Map,或者直接是POJO类,会将类属性和值映射过去,也可以在属性上使用@Param()修改映射的属性名称)
- @HeaderMap,用于接口方法的参数前
从参数里批量添加自定义Header;
- @Body,用于接口方法上
给POST请求添加请求体模板;
多服务地址
如果一个方法需要请求不同的服务器地址,可以在方法里添加一个URI参数指定目标,参数位置随便,如果添加多个URI参数,只有最后一个生效;
自定义Target
我们也可以实现Target<T>接口,实现自己的统一逻辑,Target源码,一共需要实现4个方法:
那个HardCodedTarget<T>就是默认实现,可以参考下,Feign.builder的target方法有两个,可以直接传入自定义的Target:
测试demo:
package test;
import java.util.concurrent.TimeUnit;
import feign.Feign;
import feign.Logger;
import feign.Request;
import feign.RequestTemplate;
import feign.Target;
public class Test {
public static void main(String[] args) {
System.out.println("start");
MyHttpClient client = Feign.builder()
.options(new Request.Options(5, TimeUnit.SECONDS, 10, TimeUnit.SECONDS, true))
.logger(new Logger.JavaLogger("MyLog").appendToFile("logs/my.log"))
.logLevel(Logger.Level.FULL)
// .target(MyHttpClient.class, "http://127.0.0.1:8080");
.target(new MyTarget("http://127.0.0.1:8080", "OPEN", "helloKEY"));
client.test6(new Book(1111, "Java虚拟机"));
client.test2("testtest");
}
}
class MyTarget implements Target<MyHttpClient>{
private final String myUrl;
private final String myType;
private final String myKey;
public MyTarget(String myurl, String myType, String myKey) {
this.myUrl = myurl;
this.myType = myType;
this.myKey = myKey;
}
@Override
public Class<MyHttpClient> type() {
return MyHttpClient.class;
}
@Override
public String name() {
return "mytarget";
}
@Override
public String url() {
return this.myUrl;
}
@Override
public Request apply(RequestTemplate input) {
if (input.url().indexOf("http") != 0) {
input.target(url());
}
// 为每个请求都添加头信息
input.header("TYPE-KEY", myType, myKey);
// 把全局@Headers("MyHeader: All")过滤掉
input.removeHeader("Myheader");
return input.request();
}
}
结果:
断路器
另外还可以使用HystrixFeign版本的断路器功能,需要maven导入:
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-hystrix</artifactId>
<version>13.3</version>
</dependency>
测试代码:
public static void main(String[] args) {
MyHttpClient client = HystrixFeign.builder()
.options(new Request.Options(5, TimeUnit.SECONDS, 10, TimeUnit.SECONDS, true))
.logger(new Logger.JavaLogger("MyLog").appendToFile("logs/my.log"))
.logLevel(Logger.Level.FULL)
.target(MyHttpClient.class, "http://127.0.0.1:8080");
client.test2("test HystrixFeign");
}
结果:
接口继承
定义的http接口也支持接口继承,例如再定义一个继承接口:
package test;
import feign.RequestLine;
public interface MyHttpClient2 extends MyHttpClient {
@RequestLine("GET /testget?client2")
void client2test();
}
测试类:
public static void main(String[] args) {
MyHttpClient2 client2 = Feign.builder()
.target(MyHttpClient2.class, "http://127.0.0.1:8080");
client2.client2test();
client2.test2("from client2");
}
结果,可以看到接口自己的方法和继承的方法都可以正常使用:
拦截器
也可以实现RequestInterceptor接口的apply方法,使用自己的拦截器,apply方法类似Target接口里那个,不过无返回值;
package test;
import feign.RequestInterceptor;
import feign.RequestTemplate;
public class MyRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
template.header("MyInterceptorHeader", "HELLO WORLD");
template.removeHeader("MyHeader");
}
}
在测试类的Feign.builder()的requestInterceptor()方法里设置下自定义的拦截器就行:
结果:
错误处理
关于错误处理,可以使用builder的errorDecoder方法,传入自定义的实现了ErrorDecoder接口的异常处理类,所有返回状态码非2xx的都会触发这个异常方法,ErrorDecoder有默认的实现类Default,可以参考这个类;
自定义Error实现类:
class MyErrorDecoder implements ErrorDecoder{
@Override
public Exception decode(String methodKey, Response response) {
System.out.println("My Error Decoder Run");
return new Exception("My Exception");
}
}
测试方法:
public static void main(String[] args) {
MyHttpClient2 client2 = Feign.builder()
.requestInterceptor(new MyRequestInterceptor())
.errorDecoder(new MyErrorDecoder())
.target(MyHttpClient2.class, "http://127.0.0.1:8080");
// client2.client2test();
client2.test2("from client2");
}
这个测试需要修改服务端代码,将返回的200改成非2xx的状态码,例如把post请求的200改为返回300:
运行结果:
重试机制
关于重试,Feign会自动重试IO异常的请求,如果自定义ErrorDecoder抛出RetryableException异常也会触发重试机制;
例如重新修改MyErrorDecoder,直接跑出RetryableException异常触发重试:
class MyErrorDecoder implements ErrorDecoder{
@Override
public Exception decode(String methodKey, Response response) {
System.out.println("My Error Decoder Run");
return new RetryableException(response.status(), "do retry", response.request().httpMethod(), 3L, response.request());
}
}
自定义重试类,实现continueOrPropagate()方法,自己设定个重试3次,间隔3s一次:
package test;
import feign.RetryableException;
import feign.Retryer;
import feign.Retryer.Default;
public class MyRetryer implements Retryer {
private int retryTimes = 0;
private int maxTetryTimes = 3;
@Override
public void continueOrPropagate(RetryableException e) {
System.out.println("Retry Run");
if(++retryTimes <= maxTetryTimes) {
e.request().requestTemplate().removeHeader("Retry");
e.request().requestTemplate().header("Retry", retryTimes + "");
try {
Thread.sleep(3000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
System.out.println("start retry," + retryTimes);
} else {
System.out.println("stop retry");
throw e;
}
}
@Override
public Retryer clone() {
return this;
}
}
在Feign.biuder里除了添加errorDecoder外再添加retryer:
public static void main(String[] args) {
MyHttpClient2 client2 = Feign.builder()
.requestInterceptor(new MyRequestInterceptor())
.errorDecoder(new MyErrorDecoder())
.retryer(new MyRetryer())
.target(MyHttpClient2.class, "http://127.0.0.1:8080");
// client2.client2test();
client2.test2("from client2");
}
测试结果,除了第一次主动调用http请求外,还有3次重试调用,最后第4次的时候抛出异常不再重试:
如果用的jdk版本>=jdk8,还可以使用接口的静态方法和默认方法,比如把builder构建放到接口里:
调用可以看起来更简洁些,是一样的效果:
异步请求
Feign10.8版本开始,还支持异步请求,使用AsyncFeign.biuder构建,接口方法返回CompletableFuture而不再是具体数据:
接口里可以定义:
@RequestLine("GET /testget")
CompletableFuture<String> test7();
调用使用Future处理:
public static void main(String[] args) {
MyHttpClient client = AsyncFeign.builder()
.target(MyHttpClient.class, "http://127.0.0.1:8080");
CompletableFuture<String> future = client.test7();
try {
String str = future.get(5, TimeUnit.SECONDS);
System.out.println("future result:" + str);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
e.printStackTrace();
}
}
结果是一样的,从同步变成了异步: