OpenFeign使用详解

  • Post author:
  • Post category:其他


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。

欢迎扫码查看更多文章:

在这里插入图片描述



版权声明:本文为goldenfish1919原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。