JavaDemo——Feign轻量级接口式http客户端

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();
		}
	}

结果是一样的,从同步变成了异步:

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值