openfeign是一个java的http客户端,用来简化http调用,先看一个最简单的demo:
这是服务端接口:
@RestController
public class DemoController {
@GetMapping("/hello")
public String hello(){
return "hello";
}
}
openfeign是如何来调用这个接口的呢?
public interface Demo {
@RequestLine("GET /hello")
String hello();
}
public static void main(String[] args) {
Demo demo = Feign.builder()
.target(Demo.class, "http://localhost:8080/");
String result = demo.hello();
System.out.println(result);
}
先定义了一个Demo接口,在接口中的方法上添加对应的注解,然后直接调用接口方法就行了,是不是跟retrofit非常相似,猜一下它里面肯定是用动态代理来实现的,就跟mybatis里面用接口去访问数据库的道理是一样一样的,这里是用接口去访问网络。
OpenFeign的注解
使用还是非常简单的,我们看下它都有哪些注解。
- @RequestLine:作用在方法上,定义了uri模板,参数来自@Param
- @Param:作用与参数,定义uri模板变量
- @Headers:作用于类和方法,定义header模板
- @QueryMap:作用与参数,Map参数,放在url中
- @HeaderMap:作用与参数,Map参数,放在header中
- @Body:作用于方法,定义模板
@Param做URI模板传参
public interface Demo {
@RequestLine("GET /uriTemplate/{id}")
String id(@Param("id")String id);
}
GET /uriTemplate/{id}就是个模板,其中{id}里面的id就是个模板变量,跟参数中的@Param("id")相对应。
Expander参数扩展
//服务端方法
@GetMapping("/expander/{username}")
public String expander(@PathVariable("username") String username){
return username;
}
//接口定义
public interface Demo {
@RequestLine("GET /expander/{username}")
String expander(@Param(value = "username", expander = UserExpander.class)User user);
public class UserExpander implements Param.Expander{
@Override
public String expand(Object value) {
return ((User)value).getUsername();
}
}
}
//客户端调用
User u = new User(20, "Joshua", "123456");
//Joshua
System.out.println(demo.expander(u));
@QueryMap做URL传参
@GetMapping("/paramMap")
public String paramMap(String username, String password, Integer age){
return username+":"+password+":"+age;
}
//这是接口
public interface Demo {
@RequestLine("GET /paramMap")
String paramMap(@QueryMap Map<String, String> map);
}
//客户端调用
Map<String, String> map = new HashMap<>();
map.put("username", "Joshua");
map.put("password", "");
map.put("age", null);//或者不传,效果一样
//Joshua::null
System.out.println(demo.paramMap(map));
@QueryMap后面可以是Map,也可以是POJO对象,如果参数传递空字符串服务端收到的也是空串,参数不传或者传null服务端收到也是null。
还可以使用QueryMapEncoder做完全的自定义,比如:
//服务端接口
@GetMapping("/queryMapEncoder")
public String queryMapEncoder(String username, String password){
return "queryMapEncoder:" + username+":"+password;
}
//feign 接口
public interface Demo {
@RequestLine("GET /queryMapEncoder")
String queryMapEncoder(@QueryMap User user);
}
//客户端调用
Demo demo = Feign.builder()
.queryMapEncoder(new UserQueryMapEncoder())
.target(Demo.class, "http://localhost:8080/");
System.out.println(demo.queryMapEncoder(new User(20, "Joshua", "123456")));
//输出结果:queryMapEncoder:Joshua:123456
public static class UserQueryMapEncoder implements feign.QueryMapEncoder {
@Override
public Map<String, Object> encode(Object object) {
User user = (User)object;
Map<String, Object> map = new HashMap<>();
map.put("username", user.getUsername());
map.put("password", user.getPassword());
return map;
}
}
@Body做POST请求
就是把@Body里面的字符串放到http请求体里面,比如:
//服务端接口
@PostMapping("/bodyJson")
public String bodyJson(@RequestBody User user){
return "bodyJson:" + user.getUsername()+":"+user.getPassword();
}
//feign 接口
public interface Demo {
@RequestLine("POST /bodyJson")
@Body("%7B\"username\":\"{username}\",\"password\":\"{password}\"%7D")
@Headers("Content-Type: application/json")
String bodyJson(@Param("username")String username, @Param("password")String password);
}
//客户端调用
System.out.println(demo.bodyJson("Joshua", "123456"));
//输出结果:bodyJson:Joshua:123456
不过正常人应该不会想去这么调用吧。
Encoder与Decoder做POST请求与响应
一般接口数据都是json格式的,此时需要定义编码器和解码器,Encoder用来把请求参数编码成在网络上传输的字符串,Decoder用于把服务端的响应字符串解码成应用使用的对象,比如:
//这是服务端接口
@RestController
@RequestMapping("/encoder")
public class EncoderController {
private static AtomicInteger AGE = new AtomicInteger(20);
@PostMapping("/add")
public User add(@RequestBody User user){
user.setAge(AGE.incrementAndGet());
return user;
}
}
//这个Feign客户端接口
public interface Encoder {
@RequestLine("POST /encoder/add")
//注意一定要添加上这个header
@Headers("Content-Type: application/json")
User add(User user);
}
//客户端使用
public static void main(String[] args) {
Encoder demo = Feign.builder()
.encoder(new GsonEncoder())
.decoder(new GsonDecoder())
.target(Encoder.class, "http://localhost:8080/");
User u = demo.add(new User(null, "Johusa", "xjs"));
System.out.println(u);
}
//这里需要添加下依赖:
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-gson</artifactId>
<version>11.0</version>
</dependency>
请求拦截
可给对请求做统一的拦截处理,比如可以添加请求头:
//服务端方法
@RequestMapping("/itcpt")
@RestController
public class ItcptController {
@GetMapping("/getByUsername/{username}")
public String getByUsername(@PathVariable("username") String username,
@RequestHeader("token")String token){
return "getByUsername:"+username+",token:"+token;
}
}
//feign 接口
public interface Itcpt {
@RequestLine("GET /itcpt/getByUsername/{username}")
public String getByUsername(@Param("username") String username);
}
//客户端调用
public static void main(String[] args) {
Itcpt itcpt = Feign.builder()
// 添加了拦截器
.requestInterceptor(new TokenInterceptor())
.target(Itcpt.class, "http://localhost:8080/");
System.out.println(itcpt.getByUsername("Joshua"));
//因此这里会输出:getByUsername:Joshua,token:TK-1234567890
}
public class TokenInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
template.header("token", "TK-1234567890");
}
}
异常处理
可以使用ErrorDecoder来捕获所有的异常,比如:
//服务端接口
@GetMapping("/hello")
public String hello(){
throw new RuntimeException("服务端异常");
}
//feign 接口
@RequestLine("GET /error/hello")
String hello();
//客户端调用
public static void main(String[] args) {
Demo demo = Feign.builder()
.errorDecoder(new MyErrorDecoder())
.target(Demo.class, "http://localhost:8080/");
String result = demo.hello();
System.out.println(result);
}
public static class MyErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
int status = response.status();
if(status == 500){
try{
String res = Util.toString(response.body().asReader(Util.UTF_8));
JsonElement je = new JsonParser().parse(res);
JsonObject jo = je.getAsJsonObject();
String message = jo.get("message").getAsString();
return new BizException(message);
}catch(Exception e){
e.printStackTrace();
return e ;
}
}else{
return new BizException("服务端异常");
}
}
}
把服务端异常统一包装成BizException。
异步请求
可以使用AsyncFeign来做异步请求,比如:
//feign接口
public interface Demo {
//注意这里的返回值
@RequestLine("GET /async/hello")
CompletableFuture<String> hello();
}
//客户端调用
public static void main(String[] args)throws Exception {
Demo demo = AsyncFeign.asyncBuilder()
.target(Demo.class, "http://localhost:8080/");
CompletableFuture<String> result = demo.hello();
System.out.println(result.get());
}
集成hystrix
首先添加feign-hystrix依赖:
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-hystrix</artifactId>
<version>11.0</version>
</dependency>
然后使用HystrixFeign来调用接口,比如:
//服务端接口
@RestController
@RequestMapping("/hystrix")
public class HystrixController {
@GetMapping("/hello/{id}/{username}")
public String hello(@PathVariable("id") int id, @PathVariable("username")String username){
return id+","+username;
}
}
//feign接口
public interface Demo {
//同步调用
@RequestLine("GET /hystrix/hello/{id}/{username}")
String sync(@Param("id") int id, @Param("username")String username);
//异步调用
@RequestLine("GET /hystrix/hello/{id}/{username}")
HystrixCommand<String> async1(@Param("id") int id, @Param("username")String username);
//异步调用
@RequestLine("GET /hystrix/hello/{id}/{username}")
CompletableFuture<String> async2(@Param("id") int id, @Param("username")String username);
}
//客户端调用
public static void main(String[] args)throws Exception {
Demo demo = HystrixFeign.builder()
.target(Demo.class,"http://localhost:8080/");
String result = demo.sync(100, "Joshua");
System.out.println(result);
HystrixCommand<String> command = demo.async1(100, "Joshua");
System.out.println(command.execute());
CompletableFuture<String> future = demo.async2(100, "Joshua");
System.out.println(future.get());
}
Group和Command key
默认的groupKey就是target的名字,而target的名字默认是baseurl。
默认的Command key跟logging key是一样的。
以demo.sync()为例子,它的groupKey是http://localhost:8080/,而Command key是Demo#sync(int,String)。但是可以通过HystrixFeign.Builder#setterFactory(SetterFactory)来修改:
public static class MySetterFactory implements SetterFactory{
@Override
public HystrixCommand.Setter create(Target<?> target, Method method) {
String groupKey = target.name();
String commandKey = method.getName()+"_"+method.getAnnotation(RequestLine.class).value();
System.out.println(groupKey+","+commandKey);
return HystrixCommand.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey))
.andCommandKey(HystrixCommandKey.Factory.asKey(commandKey));
}
}
public static void main(String[] args)throws Exception {
Demo demo = HystrixFeign.builder()
.setterFactory(new MySetterFactory())
.target(Demo.class,
"http://localhost:8080/");
String result = demo.sync(100, "Joshua");
System.out.println(result);
}
Fallback支持
只需要在HystrixFeign.Builder.target()的最后一个参数传递fallback或者fallback factory即可,比如:
@RestController
@RequestMapping("/hystrix")
public class HystrixController {
@GetMapping("/getById/{id}")
public String fallback(@PathVariable("id") Integer id){
throw new RuntimeException();
}
}
//feign接口
public interface Demo {
@RequestLine("GET /hystrix/getById/{id}")
String getById(@Param("id") Integer id);
}
//客户端调用
public static class MyFallbackFactory implements FallbackFactory<Demo>{
@Override
public Demo create(Throwable throwable) {
System.out.println(throwable.getClass().getName());
return new Demo() {
@Override
public String sync(int id, String username) {
return null;
}
@Override
public HystrixCommand<String> async1(int id, String username) {
return null;
}
@Override
public CompletableFuture<String> async2(int id, String username) {
return null;
}
@Override
public String getById(Integer id) {
return "fallback";
}
};
}
}
public static void main(String[] args)throws Exception {
Demo demo = HystrixFeign.builder()
.target(Demo.class,
"http://localhost:8080/",
new MyFallbackFactory());
String result = demo.getById(100);
System.out.println(result);
}
输出fallback,同时可以看到异常被feign给包装了一下,类型是feign.FeignException$InternalServerError,feign抛出的异常都是继承于FeignException。
如果想直接把异常抛出而不走fallback该如何处理呢?只需要让feign抛出HystrixBadRequestException就行了:
//服务端接口
@GetMapping("/getByName/{username}")
public String getByName(@PathVariable(value="username") String username,
@RequestHeader(value="token", required = false)String token){
if(StringUtils.isEmpty(token)){
throw new BizException(1, "参数token不能为空");
}
return "getByName:"+username;
}
public class BizException extends RuntimeException{
public BizException(int errorType, String msg){
//把异常类型添加到异常信息里面去
super(errorType+":"+msg);
}
}
//feign接口
@RequestLine("GET /hystrix/getByName/{username}")
String getByName(@Param("username") String username);
//客户端调用:
public static void main(String[] args)throws Exception {
Demo demo = HystrixFeign.builder()
.errorDecoder(new MyErrorDecoder())
.target(Demo.class,
"http://localhost:8080/",
new MyFallbackFactory());
String name = demo.getByName("Joshua");
//这里会收到feign包装以后的HystrixBadRequestException
System.out.println(name);
}
//主要就是MyErrorDecoder
public static class MyErrorDecoder implements ErrorDecoder{
@Override
public Exception decode(String methodKey, Response response) {
try{
String json = Util.toString(response.body().asReader(Util.UTF_8));
JsonElement je = new JsonParser().parse(json);
JsonObject jo = je.getAsJsonObject();
String message = jo.get("message").getAsString();
//从异常信息中解析出来异常的类型
String type = message.substring(0,message.indexOf(":"));
String msg = message.substring(message.indexOf(":")+1);
//如果是特定类型,则包装成HystrixBadRequestException
//那么hystrix将不再走fallback和熔断逻辑
if("1".equals(type)){
return new HystrixBadRequestException(msg);
}
return FeignException.errorStatus(methodKey, response);
}catch (Exception e){
e.printStackTrace();
return e;
}
}
}
测试代码下载:https://github.com/xjs1919/enumdemo下面的openfeign。
欢迎扫码查看更多文章:
qrcode.jpg
网友评论