美文网首页Spring Cloud 程序员spring boot
深入理解feign(01)-使用入门

深入理解feign(01)-使用入门

作者: juconcurrent | 来源:发表于2018-12-06 13:15 被阅读7次

    前言

    feign的使用文档中,开篇第一句就是Feign makes writing java http clients easier,中文译为Feign使得写java http客户端更容易。言下之意,Feign其实是一个java http客户端的类库。

    本文我们将对Feign做一个大概的了解,并对其基本用法进行掌握,后续章节我们将深入Feign的各种应用场景及源码,让我们不仅知其然,还要知其所以然。

    为什么选择Feign

    如果读者有用过JerseySpringCXF来实现web服务端,那么一定会对他们通过注解方法来定义接口的方式大开眼界。那么,我们为什么选择Feign,它又有哪些优势呢?

    1. Feign允许我们通过注解的方式实现http客户端的功能,给了我们除了Apache HttpComponents之外的另一种选择
    2. Feign能用最小的性能开销,让我们调用web服务器上基于文本的接口。同时允许我们自定义编码器解码器错误处理器等等

    Feign如何工作的呢

    Feign通过注解和模板的方式来定义其工作方式,参数(包括url、method、request和response等)非常直观地融入到了模板中。尽管Feign设计成了只支持基于文本的接口,但正是它的这种局限降低了实现的复杂性。而我们写http客户端代码的时候,超过90%的场景是基于文本的接口调用。另一个方面,使用Feign还可以简化我们的单元测试。

    基本用法

    典型的用法如下所示。

    public interface UserService {
        @RequestLine("GET /user/get?id={id}")
        User get(@Param("id") Long id);
    }
    
    public class User {
        Long id;
        String name;
    }
    
    public class Main {
        public static void main(String[] args) {
            UserService userService = Feign.builder()
                .options(new Request.Options(1000, 3500))
                .retryer(new Retryer.Default(5000, 5000, 3))
                .target(UserService.class, "http://api.server.com");
            System.out.println("user: " + userService.get(1L));
        }
    }
    

    Feign下面接口的注解

    Feign通过注解和模板的方式来定义契约,那么又有哪些注解,分别是做什么用的呢?下面的表格参考了Feign官网的Annotation,给出了基本用法。

    Annotation Interface Target Usage
    @RequestLine Method 用于定义method和uri模板,其值由@Param传入。
    @Param Parameter 模板变量,它的值将被用于替换表达式。
    @Headers Method, Type 用于定义header模板,其值由@Param传入。该注解可声明在Type上,也可声明在Method上。当声明在Type上时,相当于其下面的所有Method都声明了。当声明在Method上时,仅对当前Method有效。
    @QueryMap Parameter 可定义成一个key-value的Map,也可以定义成POJO,用以扩展进查询字符串。
    @HeaderMap Parameter 可定义成一个key-value的Map,用于扩展请求头。
    @Body Method 用于定义body模板,其值由@Param传入。

    模板和表达式

    模板和表达式模式,是基于URI Template - RFC 6570来实现的。表达式通过在方法上@Param修饰的参数来填充。

    表达式必须以{}来包装变量名。也可使用正则表达式来验证,变量名+:+正则表达式的方式。表达式定义如下所示:

    1. {name}
    2. {name:[a-zA-Z]*}

    可以运用表达式的地方有下面几处。

    1. @RequestLine
    2. @QueryMap
    3. @Headers
    4. @HeaderMap
    5. @Body

    他们将遵循URI Template - RFC 6570规约。

    1. 未正确匹配的表达式将被忽略(忽略的意思就是,该变量在表达式中将被设置为null)
    2. 表达式值设置之前不会通过Encoder进行编码
    3. @Body使用的时候必须在Header里通过Content-Type指明内容类型

    @Paramexpander属性,该属性为Class类型,可以通过编码的方式更灵活地进行转换。如果返回的结果为null或空字符串,表达式将被忽略。

    public interface Expander {
        String expand(Object value);
    }
    

    另外,@Param可同时运用到多处,如下所示:

    public interface ContentService {
      @RequestLine("GET /api/documents/{contentType}")
      @Headers("Accept {contentType}")
      String getDocumentByType(@Param("contentType") String type);
    }
    

    Feign的自定义设置

    可以通过Feign.builder()来自定义设置一些拦截器,用于增强其语义。比如我们可以增加超时拦截器、编码拦截器、解码拦截器、重试拦截器等等。如下所示:

    interface Bank {
      @RequestLine("POST /account/{id}")
      Account getAccountInfo(@Param("id") String id);
    }
    
    public class BankService {
      public static void main(String[] args) {
        Bank bank = Feign.builder().decoder(
            new AccountDecoder())
            .target(Bank.class, "https://api.examplebank.com");
      }
    }
    

    Feign集成第三方组件

    可以和很容易地和第三方组件结合使用,扩展了其功能,也增加了其灵活性。我们可以查阅官网文档,链接地址:integrations

    1. Gson

    通过encoder和decoder来使用

    public class Example {
      public static void main(String[] args) {
        GsonCodec codec = new GsonCodec();
        GitHub github = Feign.builder()
                             .encoder(new GsonEncoder())
                             .decoder(new GsonDecoder())
                             .target(GitHub.class, "https://api.github.com");
      }
    }
    

    2. Jackson

    Gson一样,也是通过encoder和decoder来使用

    public class Example {
      public static void main(String[] args) {
          GitHub github = Feign.builder()
                         .encoder(new JacksonEncoder())
                         .decoder(new JacksonDecoder())
                         .target(GitHub.class, "https://api.github.com");
      }
    }
    

    3. JAXB

    Gson一样,也是通过encoder和decoder来使用

    public class Example {
      public static void main(String[] args) {
        Api api = Feign.builder()
                 .encoder(new JAXBEncoder())
                 .decoder(new JAXBDecoder())
                 .target(Api.class, "https://apihost");
      }
    }
    

    4. JAX-RS

    JAX-RS定义了自己的一套注解,我们可以通过和JAX-RS注解的集成来定义我们自己的注解皮肤。该功能需要结合contract使用。

    interface GitHub {
      @GET @Path("/repos/{owner}/{repo}/contributors")
      List<Contributor> contributors(@PathParam("owner") String owner, @PathParam("repo") String repo);
    }
    
    public class Example {
      public static void main(String[] args) {
        GitHub github = Feign.builder()
                           .contract(new JAXRSContract())
                           .target(GitHub.class, "https://api.github.com");
      }
    }
    

    5. OkHttp

    OkHttp是一个http客户端类库,我们也可以将其包装成Feign的形式。该功能需要结合client来使用。

    public class Example {
      public static void main(String[] args) {
        GitHub github = Feign.builder()
                         .client(new OkHttpClient())
                         .target(GitHub.class, "https://api.github.com");
      }
    }
    

    6. Ribbon

    Ribbon提供了客户端负载均衡功能。我们也可以和其一起集成使用。

    public class Example {
      public static void main(String[] args) {
        MyService api = Feign.builder()
              .client(RibbonClient.create())
              .target(MyService.class, "https://myAppProd");
      }
    }
    

    7. Hystrix

    Hystrix是一个断路器组件,为了保证分布式系统的健壮性,在某一些服务不可用的情况下,可避免出现雪崩效应。也可以和Feign结合使用

    public class Example {
      public static void main(String[] args) {
        MyService api = HystrixFeign.builder().target(MyService.class, "https://myAppProd");
      }
    }
    

    8. SOAP

    SOAP是基于http之上的一种协议,其通信方式使用的是xml格式。

    public class Example {
      public static void main(String[] args) {
        Api api = Feign.builder()
             .encoder(new SOAPEncoder(jaxbFactory))
             .decoder(new SOAPDecoder(jaxbFactory))
             .errorDecoder(new SOAPErrorDecoder())
             .target(MyApi.class, "http://api");
      }
    }
    

    9. SLF4J

    slf4j是一个日志门面,给各种日志框架提供了统一的入口。

    public class Example {
      public static void main(String[] args) {
        GitHub github = Feign.builder()
                         .logger(new Slf4jLogger())
                         .target(GitHub.class, "https://api.github.com");
      }
    }
    

    Decoders

    当我们的接口返回类型不为feign.ResponseStringbyte[]void时,我们必须定义一个非默认的解码器。以Gson为例

    public class Example {
      public static void main(String[] args) {
        GitHub github = Feign.builder()
                         .decoder(new GsonDecoder())
                         .target(GitHub.class, "https://api.github.com");
      }
    }
    

    当我们想在feign.Response进行解码之前做一些事情,我们可以通过mapAndDecode来自定义。

    public class Example {
      public static void main(String[] args) {
        JsonpApi jsonpApi = Feign.builder()
                             .mapAndDecode((response, type) -> jsopUnwrap(response, type), new GsonDecoder())
                             .target(JsonpApi.class, "https://some-jsonp-api.com");
      }
    }
    

    Encoders

    当我们定义的接口method为POST,且传入的类型不为String或者byte[],我们需要自定义编码器。同时需要在header上指明Content-Type

    static class Credentials {
      final String user_name;
      final String password;
    
      Credentials(String user_name, String password) {
        this.user_name = user_name;
        this.password = password;
      }
    }
    
    interface LoginClient {
      @RequestLine("POST /")
      void login(Credentials creds);
    }
    
    public class Example {
      public static void main(String[] args) {
        LoginClient client = Feign.builder()
                                  .encoder(new GsonEncoder())
                                  .target(LoginClient.class, "https://foo.com");
    
        client.login(new Credentials("denominator", "secret"));
      }
    }
    

    扩展功能

    1. 基本使用

    接口的定义可以是单一的接口,也可以是带继承层级的接口列表。

    interface BaseAPI {
      @RequestLine("GET /health")
      String health();
    
      @RequestLine("GET /all")
      List<Entity> all();
    }
    interface CustomAPI extends BaseAPI {
      @RequestLine("GET /custom")
      String custom();
    }
    

    我们也可以定义泛型类型

    @Headers("Accept: application/json")
    interface BaseApi<V> {
    
      @RequestLine("GET /api/{key}")
      V get(@Param("key") String key);
    
      @RequestLine("GET /api")
      List<V> list();
    
      @Headers("Content-Type: application/json")
      @RequestLine("PUT /api/{key}")
      void put(@Param("key") String key, V value);
    }
    
    interface FooApi extends BaseApi<Foo> { }
    
    interface BarApi extends BaseApi<Bar> { }
    

    2. 日志级别

    Feign会根据不同的日志级别,来输出不同的日志,在Feign里面定义了4种日志级别。

    /**
     * Controls the level of logging.
     */
    public enum Level {
      /**
       * No logging.不记录日志
       */
      NONE,
      /**
       * Log only the request method and URL and the response status code and execution time.
       * 仅仅记录请求方法、url、返回状态码及执行时间
       */
      BASIC,
      /**
       * Log the basic information along with request and response headers.
       * 在记录基本信息上,额外记录请求和返回的头信息
       */
      HEADERS,
      /**
       * Log the headers, body, and metadata for both requests and responses.
       * 记录全量的信息,包括:头信息、body信息、请求和返回的元数据等
       */
      FULL
    }
    

    使用方式如下:

    public class Example {
      public static void main(String[] args) {
        GitHub github = Feign.builder()
                         .decoder(new GsonDecoder())
                         .logger(new Logger.JavaLogger().appendToFile("logs/http.log"))
                         .logLevel(Logger.Level.FULL)
                         .target(GitHub.class, "https://api.github.com");
      }
    }
    

    3. 请求拦截器

    我们可以通过定义一个请求拦截器RequestInterceptor来对请求数据进行修改,比如添加一个请求头或者校验授权信息等等。

    static class ForwardedForInterceptor implements RequestInterceptor {
      @Override public void apply(RequestTemplate template) {
        template.header("X-Forwarded-For", "origin.host.com");
      }
    }
    
    public class Example {
      public static void main(String[] args) {
        Bank bank = Feign.builder()
                     .decoder(accountDecoder)
                     .requestInterceptor(new ForwardedForInterceptor())
                     .target(Bank.class, "https://api.examplebank.com");
      }
    }
    

    4. 动态查询参数@QueryMap

    一般情况下,我们使用@QueryMap时,传入的参数为Map<String, Object>类型,如下所示:

    public interface Api {
      @RequestLine("GET /find")
      V find(@QueryMap Map<String, Object> queryMap);
    }
    

    但有时候,为了让我们的参数定义得更清晰易懂,我们也可以使用POJO方式,如下所示。这种方式是通过反射直接获取字段名称和值的方式来实现的。如果POJO里面的某个字段为null或者空串,将会从查询参数中移除掉(也就是不生效)。

    public interface Api {
     @RequestLine("GET /find")
     V find(@QueryMap CustomPojo customPojo);
    }
    

    如果我们更喜欢使用gettersetter的方式来读取和设置值,那么我们可以自定义查询参数编码器。

    public class Example {
      public static void main(String[] args) {
        MyApi myApi = Feign.builder()
                     .queryMapEncoder(new BeanQueryMapEncoder())
                     .target(MyApi.class, "https://api.hostname.com");
      }
    }
    

    5. 自定义错误处理器

    Feign有默认的错误处理器,当我们想自行处理错误,也是可以的。可以通过自定义ErrorDecoder来实现。

    public class Example {
      public static void main(String[] args) {
        MyApi myApi = Feign.builder()
                     .errorDecoder(new MyErrorDecoder())
                     .target(MyApi.class, "https://api.hostname.com");
      }
    }
    

    它会捕获http返回状态码为非2xx的错误,并调用ErrorDecoder. decode()方法。我们可以抛出自定义异常,或者做额外的处理逻辑。如果我们想重复多次调用,需要抛出RetryableException,并定义且注册额外的Retryer

    6. 自定义Retry

    我们可以通过实现Retryer接口的方式来自定义重试策略。Retry会对IOExceptionErrorDecoder组件抛出的RetryableException进行重试。如果达到了最大重试次数仍不成功,我们可以抛出RetryException

    自定义Retryer的使用如下所示:

    public class Example {
      public static void main(String[] args) {
        MyApi myApi = Feign.builder()
                     .retryer(new MyRetryer())
                     .target(MyApi.class, "https://api.hostname.com");
      }
    }
    

    7. 接口的静态方法和默认方法

    在java8及以上版本,我们可以在接口里面定义静态方法和默认方法。Feign也支持这种写法,但是有特殊的作用。

    1. 静态方法可以写自定义的Feign定义
    2. 默认方法可以在参数中传入默认值
    interface GitHub {
      @RequestLine("GET /repos/{owner}/{repo}/contributors")
      List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
    
      @RequestLine("GET /users/{username}/repos?sort={sort}")
      List<Repo> repos(@Param("username") String owner, @Param("sort") String sort);
    
      default List<Repo> repos(String owner) {
        return repos(owner, "full_name");
      }
    
      /**
       * Lists all contributors for all repos owned by a user.
       */
      default List<Contributor> contributors(String user) {
        MergingContributorList contributors = new MergingContributorList();
        for(Repo repo : this.repos(owner)) {
          contributors.addAll(this.contributors(user, repo.getName()));
        }
        return contributors.mergeResult();
      }
    
      static GitHub connect() {
        return Feign.builder()
                    .decoder(new GsonDecoder())
                    .target(GitHub.class, "https://api.github.com");
      }
    }
    

    总结

    本文先简单介绍了Feign,然后给出了一个入门级的例子,最后对每个功能、组件和扩展进行了补充说明。楼主相信通过这些文字,足够让我们进入Feign的大门了。

    后面我们将更加深入地了解Feign,尤其是Feign的源码。

    参考链接

    相关文章

      网友评论

        本文标题:深入理解feign(01)-使用入门

        本文链接:https://www.haomeiwen.com/subject/wbjgcqtx.html