Feign在實際項目中的應用實踐總結


Feign在實際項目中的應用實踐總結

Feign是什么?

是一個聲明式的HTTP請求處理庫,可以將命令式的http請求的編程,更改為聲明式的http請求編程。

下面是傳統的命令式編程模式和Feign所代表的聲明式編程模式的對比,可以清晰的看到聲明式的代碼邏輯比命令式更加的簡潔,就像本地調用一樣。

命令式:

public class MyApp {
  public static void main(String... args) {
    CloseableHttpResponse httpResponse = 
      httpClientTool.doGet("https://api.github.com//repos/" + owner +"/"+ repo +"/contributors");
    String responseBody = EntityUtils.toString(httpResponse.getEntity());
    List<Contributor> contributors = JsonTool.parseArray(responseBody, Contributor.class);
    for (Contributor contributor : contributors) {
      System.out.println(contributor.login + " (" + contributor.contributions + ")");
    }
  }
}

聲明式:

interface GitHub {
  @RequestLine("GET /repos/{owner}/{repo}/contributors")
  List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);

  @RequestLine("POST /repos/{owner}/{repo}/issues")
  void createIssue(Issue issue, @Param("owner") String owner, @Param("repo") String repo);

}


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

    // Fetch and print a list of the contributors to this library.
    List<Contributor> contributors = github.contributors("OpenFeign", "feign");
    for (Contributor contributor : contributors) {
      System.out.println(contributor.login + " (" + contributor.contributions + ")");
    }
  }
}

從上面的對比中可以看到。通過命令式的http請求編程,過程復雜需要編碼請求以及請求請求參數的定義,以及響應結果的字符串解析為Java對象的全過程。但聲明式的http請求編程,只要事先定義好一個請求的模板(interface)剩下來的就直接交給Feign通過動態代理,將請求的細節托管不需要編碼者處理具體的請求邏輯。

Feign可以幫我解決什么問題,有什么優點?

Feign可以讓我們書寫http請求代碼時更加容易,這也是他官方給Feign這個庫的定義

Feign makes writing java http clients easier

聲明式編程和命令式編程相比的優點

  1. 通過接口進行http請求的定義,可以做到集中管理請求,而且不用編寫請求的細節。
  2. 通過http請求的控制反轉將http請求的細節交給Feign來完成,提高的請求編碼的穩定性和可靠性。
  3. 通過使用動態代理和基於注解的模板模式的設計,使請求編碼的代碼量大幅縮減,減少重復代碼,提高開發效率。

什么情況下不適合使用Feign?

Feign也不是所有場景下都適用的,比如在請求地址不確定,參數不確定的編程的業務場景中,Feign這種需要預先定義好請求路徑和目標主機地址的聲明式模板反而會讓代碼靈活性的降低。這種場景下不適合Feign的使用,請選擇命令式的http編程模式。

Feign是如何實現聲明式的HTTP請求定義的?

  1. 通過動態代理技術根據用戶定義的接口自動生成代理類來執行具體的請求操作
  2. Feign只提供了HTTP客戶端的接口抽象而並沒有實現HTTP客戶端的具體實現,所以說Feign並不是一個HTTP請求客戶端,而需要和常用的HTTP請求客戶端進行組合使用,其目的是簡化HTTP客戶端的使用
  3. 提供序列化和反序列化的接口抽象,使用者可以按照自己的需求靈活的指定具體的序列化和反序列化實現類庫
  4. 支持指標監控和常用熔斷器的集成
  5. 支持多種Restful接口的定義規范,可以和已有微服務架構進行良好的集成

Spring項目如何整合進Feign?

常用的基礎依賴

<!-- https://mvnrepository.com/artifact/io.github.openfeign/feign-core -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-core</artifactId>
    <version>11.0</version>
</dependency>

<!-- https://mvnrepository.com/artifact/io.github.openfeign/feign-jackson -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-jackson</artifactId>
    <version>11.0</version>
</dependency>

<!-- https://mvnrepository.com/artifact/io.github.openfeign/feign-httpclient -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
    <version>11.0</version>
</dependency>

首先需要feign-cor引入核心代碼,feign-jackson基於JSON的Restful接口的的序列化和反序列化工具庫,feign-httpclientFeign集成Apach-Http-Client作為請求的具體客戶端實現。

這里需要注意,如果項目不是基於Spring-Cloud的項目的微服務,不建議直接使用 spring-cloud-starter-openfeign,雖然他用起來很方便,可以實現自動配置,但不利於Feign和HTTP請求客戶端的細節把控,而且會引入需要沒必要的依賴。簡單的項目直接使用核心jar包就可以了。

定義聲明式接口

// 接口定義
interface GitHub {
  @RequestLine("GET /repos/{owner}/{repo}/contributors")
  List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);

  @RequestLine("POST /repos/{owner}/{repo}/issues")
  void createIssue(Issue issue, @Param("owner") String owner, @Param("repo") String repo);

}

// 數據類型定義
public static class Contributor {
  String login;
  int contributions;
}

// 數據類型定義
public static class Issue {
  String title;
  String body;
  List<String> assignees;
  int milestone;
  List<String> labels;
}

配置並生成一個代理類對象

@Configuration
public class FeignClientConfig {
  // 系統中有默認jackson對象直接拿來用
  @Resource
  private ObjectMapper jacksonMapper;
  
  @Bean
  public ApacheHttpClient apacheHttpClient() {
    // 這里可以定義httpclient的具體配置
    CloseableHttpClient httpClinet = HttpClients.custom().build();
    return new ApacheHttpClient(httpClinet);
  }

  @Bean
  public JacksonDecoder jacksonDecoder() {
    // 這里可以直接使用SpringBoot自帶的jackson作為反序列化器
    return new JacksonDecoder(jacksonMapper);
  }
  
  @Bean
  public JacksonEncoder jacksonEncoder() {
    // 這里可以直接使用SpringBoot自帶的jackson作為序列化器
    return new JacksonEncoder(jacksonMapper);
  }
  
  
  // 生成一個實現GitHub這個接口的了代理對象並將其注入為一個Spring的Bean
  @Bean
  public GitHub gitHubFeignClient(
		ApacheHttpClient apacheHttpClient,
    JacksonEncoder jacksonEncoder,
    JacksonDecoder jacksonDecoder
  ) {
      return Feign.builder()
        // 自定義請求客戶端
        .client(apacheHttpClient)
        // 自定義請求體序列化器
        .encoder(jacksonEncoder)
        // 自定義響應體反序列化器
        .decoder(jacksonDecoder)
        // 定義目標主機
        .target(GitHub.class, "https://api.github.com");
    }
}

發起請求

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

    // Fetch and print a list of the contributors to this library.
    List<Contributor> contributors = github.contributors("OpenFeign", "feign");
    for (Contributor contributor : contributors) {
      System.out.println(contributor.login + " (" + contributor.contributions + ")");
    }
  }
}

如何自定義底層HTTP客戶端的實現?

要自定義HTTP客戶端的實現,可以通過Feign.builder().client()的方法來指定

public Feign.Builder client(Client client) {
  this.client = client;
  return this;
}

可以看到自定義的Client實例需要實現Client接口

所以我們需要使用依賴jar包

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
    <version>11.0</version>
</dependency>

查看類的內部,ApacheHttpClient實現了Client接口,並且有一個使用HttpClient類作為參數的構造函數

public final class ApacheHttpClient implements Client {
  public ApacheHttpClient(HttpClient client) {
        this.client = client;
    }
}

所以我們只需要通過ApacheHttpClient的構造函數傳入HttpClient的實例對象,就可以完成Feign和HttpClient的適配工作

Feign.builder().client(new ApacheHttpClient(httpClientInstance));

如何實現全局HTTP請求的統一 tracer 日志?

在企業應用場景中,統一的HTTP的Tracer是非常有必要的。為什么?往小了說可以幫你定位問題,往大了說就能保住你飯碗。通常的一個Web系統,都會接入其他三方的資源和服務來擴充自己的服務能力,而常用的接入方案就是Rest接口。而三方的服務你是沒辦法保證的,只能保證自己的服務是沒有問題的。在線上如果出現服務調用失敗,統一的Tracer日志可進行錯誤的定位,是三方的服務接口出問題還是自己的代碼寫的有問題。在很多情況下,自己做為服務提供者,往往在出現問題后客戶會要求查看接口請求日志。如果沒有做HTTP的統一Tracer日志,只能空口評說了,搞不好飯碗都丟了。總之一句話,系統和系統交互的邊界上一定要打日志,一定要打日志,一定要打日志(重要的事情說三遍!)

那如何給Feign接入統一的日志呢?

根據官方文檔,Feign是支持Logger配置的,使用者可以選擇適合自己的Logger進行請求日志的記錄。但是Feign的日志打印有一個很嚴重的問題,他是分步打印的。也就是說一個HTTP請求,請求發起打印一行,請求結束打印一行。想想這會發生什么事情?

在系統打規模並發的情況下,一個請求的發起階段和響應階段的日志並不是在一起的,中間可能穿插着其他請求的日志記錄。當我們需要去追溯日志的時候,發現根本沒有辦法定位到哪一個請求出了問題,只能找到請求的某一個階段的日志。

現在有幾個需求需要實現。

  1. 將請求的請求階段的數和響應階段的數據放在同一行日志中
  2. 每個請求都會帶有唯一的請求ID,服務調用過程中都會攜帶這個請求ID

實現方式就是使用HttpClient的請求攔截器和響應攔截器進行對請求的攔截。HttpClient在請求過程中有一個請求級別的對象,RequestContext。這個對象在整個請求的生命周期中都可以訪問和讀寫。那我們可以使用這個請求上下文對象用來記錄請求過程中的參數

請求日志記錄的代碼實現

先定義我們的請求日志需要記錄請求生命周期的哪些字段?

// http請求日志字段枚舉類
public enum HttpLogEnum {
        protocolVersion,
        method,
        url,
        requestHeaders,
        requestBody,
        responseHeaders,
        responseCode,
        responseBody
}

定義請求攔截器,將請求階段需要記錄的參數綁定到請求上下文對象上

// 實現HttpRequestInterceptor請求攔截器接口
public static class RequestInterceptor implements HttpRequestInterceptor {
  // 實現process方法
  @Override
  public void process(HttpRequest httpRequest, HttpContext httpContext) throws HttpException, IOException {
    // 先將請求對象 httpRequest 進行包裝操作方便獲取請求階段數據
    final HttpRequestWrapper httpRequestWrapper = HttpRequestWrapper.wrap(httpRequest);
    // 獲取請求的協議版本
    httpContext.setAttribute(HttpLogEnum.protocolVersion.name(), httpRequestWrapper.getProtocolVersion().toString());
    // 獲取請求的Method(GET、POST、PUT...)
    httpContext.setAttribute(HttpLogEnum.method.name(), httpRequestWrapper.getMethod());
    // 請求的url
    httpContext.setAttribute(HttpLogEnum.url.name(), httpRequestWrapper.getURI().toString());
    // 請求的請求頭
    httpContext.setAttribute(HttpLogEnum.requestHeaders.name(), getHeaderMap(httpRequestWrapper.getAllHeaders()));

    // 請求的請求體
    if (httpRequestWrapper instanceof HttpEntityEnclosingRequest) {
      final HttpEntity entity = ((HttpEntityEnclosingRequest) httpRequestWrapper).getEntity();
      String content = EntityUtils.toString(entity, ENCODING);
      httpContext.setAttribute(HttpLogEnum.requestBody.name(), content);
      // 由於請求體前面已被讀取,讀取后需要重新寫入請求體
      ((HttpEntityEnclosingRequest) httpRequest).setEntity(new StringEntity(content, ContentType.get(entity)));
    }else {
      // 沒有攜帶請求體的請求
      httpContext.setAttribute(HttpLogEnum.requestBody.name(), null);
    }
  }
}

定義響應攔截器,獲取響應的結果

// 實現HttpResponseInterceptor響應攔截接口
public static class ResponseInterceptor implements HttpResponseInterceptor {
  @Override
  public void process(HttpResponse httpResponse, HttpContext httpContext) throws HttpException, IOException {
    // 獲取響應頭
    httpContext.setAttribute(HttpLogEnum.responseHeaders.name(), getHeaderMap(httpResponse.getAllHeaders()));
    // 獲取HTTP響應碼
    httpContext.setAttribute(HttpLogEnum.responseCode.name(), httpResponse.getStatusLine().getStatusCode());
    final HttpEntity entity = httpResponse.getEntity();
    String responseBody = EntityUtils.toString(entity, ENCODING);
    // 獲取請求的響應體
    httpContext.setAttribute(HttpLogEnum.responseBody.name(), responseBody);
    // 重新寫入響應體
    httpResponse.setEntity(new StringEntity(responseBody, ContentType.get(entity)));
    // 將httpContext對象保存的字段進行獲取包裝成一個Map對象
    Map<String, Object> log = new HashMap<>(HttpLogEnum.values().length);
    // 進行字段的讀取
    for (HttpLogEnum item : HttpLogEnum.values()) {
      log.put(item.name(), httpContext.getAttribute(item.name()));
    }
    // 通過json工具對Map序列化成json文本,並通過標准輸出進行打印
    System.out.println(JsonUtil.toJson(log));
  }
}

將請求/響應攔截器綁定到HttpClient對象上,並注冊到Spring的容器中去

@Configuration
public class HttpClinetConfig {
    @Bean
    public CloseableHttpClient httpClient() {
        return HttpClients.custom()
                  // 請求日志攔截器
                .addInterceptorFirst(new RequestInterceptor())
                  // 響應日志攔截器
                .addInterceptorFirst(new ResponseInterceptor())
                .build();
    }
}

將HttpClient指定為Feign的客戶端實現

@Configuration
public class FeignClientConfig {
	/*
	
	.....其他代碼
	*/
  
  // 注入HttpClient的Bean
  @Resource
  private CloseableHttpClient httpClinet;
  
  @Bean
  public ApacheHttpClient apacheHttpClient() {
    // 將HttpClient包裝成Feign的ApacheHttpClient
    return new ApacheHttpClient(httpClinet);
  }
  
  // 生成一個實現GitHub這個接口的了代理對象並將其注入為一個Spring的Bean
  @Bean
  public GitHub gitHubFeignClient(
		ApacheHttpClient apacheHttpClient,
    JacksonEncoder jacksonEncoder,
    JacksonDecoder jacksonDecoder
  ) {
      return Feign.builder()
        // 設置上面定義的請求客戶端實現
        .client(apacheHttpClient)
        // 自定義請求體序列化器
        .encoder(jacksonEncoder)
        // 自定義響應體反序列化器
        .decoder(jacksonDecoder)
        // 定義目標主機
        .target(GitHub.class, "https://api.github.com");
    }
}

如何集成指標監控?以及數據的收集工作?

增加依賴

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
  <groupId>io.github.openfeign</groupId>
  <artifactId>feign-micrometer</artifactId>
  <version>11.0</version>
</dependency>

配置指標收集器

@Configuration
public class FeignClientBuilder {
  /* 其他代碼  */
  
  // 注入actuator的指標注冊器
    @Resource
    private MeterRegistry meterRegistry;

  // 將meterRegistry包裝成feign的MicrometerCapability類
    @Bean
    public MicrometerCapability micrometerCapability() {
        return new MicrometerCapability(meterRegistry);
    }

    @Bean
    public BananaFeignClient bananaFeignClient(
            ApacheHttpClient apacheHttpClient,
            JacksonEncoder jacksonEncoder,
            JacksonDecoder jacksonDecoder,
      			MicrometerCapability micrometerCapability
    ) {
        return Feign.builder()
          // 將micrometerCapability綁定到Feign上
                .addCapability(micrometerCapability)
                .client(apacheHttpClient)
                .encoder(jacksonEncoder)
                .decoder(jacksonDecoder)
                .target(BananaFeignClient.class, bananaHost);
    }
}

增加actuator配置暴露metrics端點,不配置的話默認使用 health和info端點暴露

management:
  endpoints:
    web:
      exposure:
        include:
          - httptrace
          - info
          - health
          - metrics

重啟服務后,查看http://localhost:8080/actuator/metrics

{
    "names": [
        "jvm.buffer.count",
        "jvm.buffer.memory.used",
        "jvm.buffer.total.capacity",
        "jvm.classes.loaded",
        "jvm.classes.unloaded",
        "jvm.gc.live.data.size",
        "jvm.gc.max.data.size",
        "jvm.gc.memory.allocated",
        "jvm.gc.memory.promoted",
        "jvm.gc.pause",
        "jvm.memory.committed",
        "jvm.memory.max",
        "jvm.memory.used",
        "jvm.threads.daemon",
        "jvm.threads.live",
        "jvm.threads.peak",
        "jvm.threads.states",
        "logback.events",
        "process.cpu.usage",
        "process.files.max",
        "process.files.open",
        "process.start.time",
        "process.uptime",
        "system.cpu.count",
        "system.cpu.usage",
        "system.load.average.1m",
        "tomcat.sessions.active.current",
        "tomcat.sessions.active.max",
        "tomcat.sessions.alive.max",
        "tomcat.sessions.created",
        "tomcat.sessions.expired",
        "tomcat.sessions.rejected"
    ]
}

發現並沒有Feign相關的指標,因為沒有發起過請求,所以沒有數據。我們先發起一下請求后看到

{
    "names": [
        "feign.Client",
        "feign.Feign",
        "feign.codec.Decoder",
        "feign.codec.Decoder.response_size",
        ......
    ]
}

多了Feign相關的指標

查看指標細節 http://localhost:8080/actuator/metrics/feign.Client

{
    "name": "feign.Client",
    "description": null,
    "baseUnit": "seconds",
    "measurements": [
        {
            "statistic": "COUNT",
            "value": 1
        },
        {
            "statistic": "TOTAL_TIME",
            "value": 0.169881274
        },
        {
            "statistic": "MAX",
            "value": 0.043015818
        }
    ],
    "availableTags": [
        {
            "tag": "method",
            "values": [
                "validateUserToken"
            ]
        },
        {
            "tag": "host",
            "values": [
                "blog.mufeng.tech"
            ]
        },
        {
            "tag": "client",
            "values": [
                "tech.mufeng.feign.defined.ExampleFeignClient"
            ]
        }
    ]
}

接入prometheus監控后就能集成監控和報警的功能

參考資料

Feign開源項目倉庫地址


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM