Spring Cloud Feign 總結


Spring Cloud中, 服務又該如何調用 ?

各個服務以HTTP接口形式暴露 , 各個服務底層以HTTP Client的方式進行互相訪問。

SpringCloud開發中,Feign是最方便,最為優雅的服務調用實現方式。

Feign 是一個聲明式,模板化的HTTP客戶端,可以做到用HTTP請求訪問遠程服務就像調用本地方法一樣。簡單搭建步驟如下 :

1. 首先加入pom.xml

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2. 主類上面添加注解@EnableFeignClients,該注解表示當程序啟動時,會進行包掃描,默認掃描所有帶@FeignClient注解的類進行處理

package name.ealen;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;

/**
 * Created by EalenXie on 2018/10/12 18:24.
 */
@SpringBootApplication
@EnableFeignClients
@EnableDiscoveryClient
public class FeignOpenClientApplication {

    public static void main(String[] args) {
        SpringApplication.run(FeignOpenClientApplication.class,args);
    }
}

3. 簡單配置appliation.yml 注冊到Eureka Server。

server:
  port: 8090
spring:
  application:
    name: spring-cloud-feign-openClient
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/

4. 使用@FeignClient為本應用聲明一個簡單的能調用的客戶端。為了方便,找個現成的開放接口,比如Github開放的api,GET /search/repositories。

GitHub接口文檔 : https://developer.github.com/v3/search/#search-repositories

package name.ealen.client;

import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

/**
 * Created by EalenXie on 2019/1/9 11:28.
 */
@FeignClient(name = "github-client", url = "https://api.github.com")
public interface GitHubApiClient {

    @RequestMapping(value = "/search/repositories", method = RequestMethod.GET)
    String searchRepositories(@RequestParam("q") String queryStr);
}

其中,@FeignClient 即是指定客戶端信息注解,務必聲明在接口上面,url手動指定了客戶端的接口地址。

5. 為其寫一個簡單Controller進行一波測試 :

package name.ealen.web;

import name.ealen.client.GitHubApiClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * Created by EalenXie on 2018/10/12 18:32.
 */
@RestController
public class FeignOpenClientController {

    @Resource
    private GitHubApiClient gitHubApiClient;

    @RequestMapping("/search/github/repository")
    public String searchGithubRepositoryByName(@RequestParam("name") String repositoryName) {
        return gitHubApiClient.searchRepositories(repositoryName);
    }
}

6. 依次啟動Eureka Server,和該應用。然后訪問 : http://localhost:8090/search/github/repository?name=spring-cloud-dubbo

注 : 有時候在測試的時候,很容易報500 null的異常,可能是因為GitHub連接拒絕的原因,這里只是為了測試,所以可以忽略,多嘗試幾次即可。

關於Feign Client配置細節

1. 重點配置 @FeignClient 注解,我這里專門對源碼屬性做了說明 :

在上例中,我們只是簡單的指定了name和url屬性,如果需要專門針對該客戶端進行屬性按需調整,可以調整以下參數 屬性值 :

package org.springframework.cloud.netflix.feign;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.core.annotation.AliasFor;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignClient {

	@AliasFor("name")
	String value() default "";  
	
	@Deprecated
	String serviceId() default "";
	
	@AliasFor("value")
	String name() default "";

	String qualifier() default "";

	String url() default "";

	boolean decode404() default false;

	Class<?>[] configuration() default {};
    
	Class<?> fallback() default void.class;
	
	Class<?> fallbackFactory() default void.class;

	String path() default "";
    
	boolean primary() default true;
}
name:               指定Feign Client的名稱,如果項目使用了 Ribbon,name屬性會作為微服務的名稱,用於服務發現。
serviceId:          用serviceId做服務發現已經被廢棄,所以不推薦使用該配置。
value:              指定Feign Client的serviceId,如果項目使用了 Ribbon,將使用serviceId用於服務發現,但上面可以看到serviceId做服務發現已經被廢棄,所以也不推薦使用該配置。
qualifier:          為Feign Client 新增注解@Qualifier
url:                請求地址的絕對URL,或者解析的主機名
decode404:          調用該feign client發生了常見的404錯誤時,是否調用decoder進行解碼異常信息返回,否則拋出FeignException。
fallback:           定義容錯的處理類,當調用遠程接口失敗或超時時,會調用對應接口的容錯邏輯,fallback 指定的類必須實現@FeignClient標記的接口。實現的法方法即對應接口的容錯處理邏輯。
fallbackFactory:    工廠類,用於生成fallback 類示例,通過這個屬性我們可以實現每個接口通用的容錯邏輯,減少重復的代碼。
path:               定義當前FeignClient的所有方法映射加統一前綴。
primary:            是否將此Feign代理標記為一個Primary Bean,默認為ture

例如我們要為其添加一個fallback的容錯,和覆蓋掉默認的configuration。

1. 首先為其添加一個fallback容錯的處理類.
package name.ealen.client;

/**
 * Created by EalenXie on 2018/11/11 19:19.
 */
public class GitHubApiClientFallBack implements GitHubApiClient {

    @Override
    public String searchRepositories(String queryStr) {
        return "call github api fail";
    }
}
2. 然后為其添加一個默認配置類,為了方便了解,我這里只是寫了一下默認的配置。
package name.ealen.config;

import feign.Contract;
import feign.Logger;
import feign.Retryer;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GitHubFeignConfiguration {
    /**
     * Feign 客戶端的日志記錄,默認級別為NONE
     * Logger.Level 的具體級別如下:
     * NONE:不記錄任何信息
     * BASIC:僅記錄請求方法、URL以及響應狀態碼和執行時間
     * HEADERS:除了記錄 BASIC級別的信息外,還會記錄請求和響應的頭信息
     * FULL:記錄所有請求與響應的明細,包括頭信息、請求體、元數據
     */
    @Bean
    Logger.Level gitHubFeignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

注意 : 編碼器,解碼器,重試器,調用解析器請謹慎配置,一般來說默認就行。筆者對這些配置研究得很淺,所以沒有寫自定義的配置。

3. 此時我們修改我們的GitHubApiClient。指定上面兩個類即可
package name.ealen.client;
import name.ealen.config.GitHubFeignConfiguration;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
/**
 * Created by EalenXie on 2019/1/9 11:28.
 */
@FeignClient(name = "github-client", url = "https://api.github.com", path = "", serviceId = "", qualifier = "", fallback = GitHubApiClientFallBack.class, decode404 = false, configuration = GitHubFeignConfiguration.class)
public interface GitHubApiClient {
    @RequestMapping(value = "/search/repositories", method = RequestMethod.GET)
    String searchRepositories(@RequestParam("q") String queryStr);
}
4. Feign也支持屬性文件對上面屬性的配置,比如下面的配置和GitHubFeignConfiguration的配置是等價的 :
feign:
  client:
    config:
      ##對名字為 github-client 的feign client做配置
      github-client:                                # 對應GitHubApiClient類的@FeignClient的name屬性值
        decoder404: false                           # 是否解碼404
        loggerLevel: full                           # 日志記錄級別

2. 重點配置 @EnableFeignClients 注解,我這里專門對源碼屬性做了說明 :

package org.springframework.cloud.netflix.feign;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.context.annotation.Import;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {

	String[] value() default {};

	String[] basePackages() default {};

	Class<?>[] basePackageClasses() default {};

	Class<?>[] defaultConfiguration() default {};

	Class<?>[] clients() default {};
}
value:                  等價於basePackages屬性,更簡潔的方式
basePackages:           指定多個包名進行掃描
basePackageClasses:     指定多個類或接口的class,掃描時會在這些指定的類和接口所屬的包進行掃描。
defaultConfiguration:   為所有的Feign Client設置默認配置類
clients:                指定用@FeignClient注釋的類列表。如果該項配置不為空,則不會進行類路徑掃描。

同樣的,為所有Feign Client 也支持文件屬性的配置,如下 :

feign:
  client:
    config:                                         
    # 默認為所有的feign client做配置(注意和上例github-client是同級的)
      default:                                      
        connectTimeout: 5000                        # 連接超時時間
        readTimeout: 5000                           # 讀超時時間設置  

注 : 如果通過Java代碼進行了配置,又通過配置文件進行了配置,則配置文件的中的Feign配置會覆蓋Java代碼的配置。

但也可以設置feign.client.defalult-to-properties=false,禁用掉feign配置文件的方式讓Java配置生效。

3. Feign 請求和響應開啟GZIP壓縮,提高通訊效率

1. 配置如下:
feign:
  compression:
    request:
      enable: true  #配置請求支持GZIP壓縮,默認為false
      mime-types: text/xml, application/xml, application/json  #配置壓縮支持的Mime Type
      min-request-size: 2048 #配置壓縮數據大小的上下限
    reponse:
      enable: true #配置響應支持GZIP壓縮,默認為false

對應配置源碼可以看看 :

2. 由於開啟GZIP壓縮之后,Feign之間的調用通過二進制協議進行傳輸,返回的值需要修改為ResponseEntity<byte[]>才可以正常顯示,否則會導致服務之間的調用結果亂碼。
3. 例如此時修改GitHubApiClient類的Feign client的配置 :
import name.ealen.config.GitHubFeignConfiguration;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
/**
 * Created by EalenXie on 2019/1/9 11:28.
 */
@FeignClient(name = "github-client", url = "https://api.github.com", path = "", serviceId = "", qualifier = "", decode404 = false, configuration = GitHubFeignConfiguration.class)
public interface GitHubApiClient {

//    @RequestMapping(value = "/search/repositories", method = RequestMethod.GET)
//    Object searchRepositories(@RequestParam("q") String queryStr);

    @RequestMapping(value = "/search/repositories", method = RequestMethod.GET)
    ResponseEntity<byte[]> searchRepositories(@RequestParam("q") String queryStr);
}

4. Feign超時配置

feign的調用分為兩層。ribbon和hystrix(默認集成),默認情況下,hystrix是關閉的,所以當ribbon發生超時異常時,可以如下配置調整ribbon超時時間 :

#ribbon的超時時間
ribbon:
  ReadTimeout: 60000           # 請求處理的超時時間
  ConnectTimeout: 30000        # 請求連接的超時時間

至於為什么這個配置會生效,我們可以大概看一下源碼里面相關 鍵值對 的描述 :

DefaultClientConfigImpl中 有許多的屬性鍵配置 :

CommonClientConfigKey :

AbstractRibbonCommand 中的 ribbon 和 hystrix都用到的 getRibbonTimeout()方法 :

默認情況下,feign中的hystrix是關閉的。

如果開啟了hystrix。此時的ribbon的超時時間和Hystrix的超時時間的結合就是Feign的超時時間,當hystrix發生了超時異常時,可以如下配置調整hystrix的超時時間 :

feign:
  hystrix:
    enable: true
hystrix:
  shareSecurityContext: true    # 設置這個值會自動配置一個Hystrix並發策略會把securityContext從主線程傳輸到你使用的Hystrix command
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMillisecond: 10000   # hystrix超時時間調整 默認為1s
      circuitBreaker:
        sleepWindowInMilliseconds: 10000     # 短路多久以后開始嘗試是否恢復,默認5s
        forceClosed: false # 是否允許熔斷器忽略錯誤,默認false, 不開啟

關於HystrixCommandProperties類的以上配置說明,詳細可以參閱 : https://www.jianshu.com/p/b9af028efebb

注 : 當開啟了Ribbon之后,可能會出現首次調用失敗的情況。

原因 : 因為hystrix的默認超時時間是1s,而feign首次的請求都會比較慢,如果feign的響應時間(ribbon響應時間)大於了1s,就會出現調用失敗的問題。

解決方法 :

1. 將Hystrix的超時時間盡量修改得長一點。(有時候feign進行文件上傳的時候,如果時間太短,可能文件還沒有上傳完就超時異常了,這個配置很有必要)
2. 禁用Hystirx的超時時間 : hystrix.command.default.execution.timeout.enabled=false
3. Feign直接禁用Hystrix(不推薦) : feign.hystrix.enabled=false

Feign 的HTTP請求相關

1. Feign 默認的請求 Client 替換

feign在默認情況下使用JDK原生的URLConnection 發送HTTP請求。(沒有連接池,保持長連接)

1. 使用HTTP Client替換默認的Feign Client

引入pom.xml :

<!--Apache HttpClient 替換Feign原生的httpclient-->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
</dependency>

配置application.yml支持httpclient :

feign: 
  httpclient:
    enable: true
2. 使用okhttp替換Feign默認的Client

引入pom.xml :

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
</dependency>

配置application.yml支持okhttp :

feign: 
  httpclient:
    enable: false
  okhttp:
    enable: true

配置okhttp :

import feign.Feign;
import okhttp3.ConnectionPool;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.cloud.netflix.feign.FeignAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

@Configuration
@ConditionalOnClass(Feign.class)
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class FeignOkHttpConfig {
    @Bean
    public okhttp3.OkHttpClient okHttpClient() {
        return new okhttp3.OkHttpClient.Builder()
                .connectTimeout(60, TimeUnit.SECONDS)   //設置連接超時
                .readTimeout(60, TimeUnit.SECONDS)      //設置讀超時
                .writeTimeout(60, TimeUnit.SECONDS)     //設置寫超時
                .retryOnConnectionFailure(true)                 //是否自動重連
                .connectionPool(new ConnectionPool())           //構建OkHttpClient對象
                .build();
    }
}

2. Feign的Get多參數傳遞

Feign 默認不支持GET方法直接綁定POJO的,目前解決方式如下 :

1. 把POJO拆散成一個個單獨的屬性放在方法參數里面;
2. 把方法的參數變成Map傳遞;
3. GET傳遞@RequestBody。(此方式違反了Restful規范,而且我們一般不會這樣寫)

《重新定義Spring Cloud實戰》一書中介紹了一種最佳實踐方式,通過Feign的攔截器的方式進行處理。實現原理是通過Feign的RequestInterceptor中的apply方法,統一攔截轉換處理Feign中的GET方法多參數。處理如下 :

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.util.*;

@Component
public class FeignRequestInterceptor implements RequestInterceptor {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void apply(RequestTemplate template) {
        //feign 不支持GET方法傳POJO,json body 轉query
        if (template.method().equals("GET") && template.body() != null) {
            try {
                JsonNode jsonNode = objectMapper.readTree(template.body());
                template.body(null);
                Map<String, Collection<String>> queries = new HashMap<>();
                buildQuery(jsonNode, "", queries);
                template.queries(queries);
            } catch (IOException e) {
                //提示:根據實踐項目情況處理此處異常,這里不做擴展。
                e.printStackTrace();
            }
        }
    }

    private void buildQuery(JsonNode jsonNode, String path, Map<String, Collection<String>> queries) {
        if (!jsonNode.isContainerNode()) {   // 葉子節點
            if (jsonNode.isNull()) return;
            Collection<String> values = queries.computeIfAbsent(path, k -> new ArrayList<>());
            values.add(jsonNode.asText());
            return;
        }
        if (jsonNode.isArray()) {   // 數組節點
            Iterator<JsonNode> it = jsonNode.elements();
            while (it.hasNext()) {
                buildQuery(it.next(), path, queries);
            }
        } else {
            Iterator<Map.Entry<String, JsonNode>> it = jsonNode.fields();
            while (it.hasNext()) {
                Map.Entry<String, JsonNode> entry = it.next();
                if (StringUtils.hasText(path))
                    buildQuery(entry.getValue(), path + "." + entry.getKey(), queries);
                else   // 根節點
                    buildQuery(entry.getValue(), entry.getKey(), queries);
            }
        }
    }
}

3. feign的文件上傳

1. 首先我們編寫一個簡單文件上傳服務的應用,並將其注冊到Eureka Server上面

簡單配置一下,application的name為feign-file-upload-application。為其寫一個上傳的接口 :

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;

@RestController
public class FeignUploadController {
    private static final Logger log = LoggerFactory.getLogger(FeignUploadController.class);
    @PostMapping(value = "/server/uploadFile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public String fileUploadServer(MultipartFile file) throws Exception {
        log.info("upload file name : {}", file.getName());
        //上傳文件放到 /usr/temp/uploadFile/ 目錄下
        file.transferTo(new File("/usr/temp/uploadFile/" + file.getName()));
        return file.getOriginalFilename();
    }
}
2. 編寫一個要使用上傳功能的feign 客戶端 :

feign客戶端應用還需要加入依賴,pom.xml :

<!-- Feign文件上傳依賴-->
<dependency>
    <groupId>io.github.openfeign.form</groupId>
    <artifactId>feign-form</artifactId>
    <version>3.0.3</version>
</dependency>
<dependency>
    <groupId>io.github.openfeign.form</groupId>
    <artifactId>feign-form-spring</artifactId>
    <version>3.0.3</version>
</dependency>

客戶端指定接口信息 :

import name.ealen.config.FeignMultipartSupportConfiguration;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;

@FeignClient(value = "feign-file-upload-application", configuration = FeignMultipartSupportConfiguration.class)
public interface FileUploadFeignService {
    /***
     * 1.produces,consumes必填
     * 2.注意區分@RequestPart和RequestParam,不要將
     * : @RequestPart(value = "file") 寫成@RequestParam(value = "file")
     */
    @RequestMapping(method = RequestMethod.POST, value = "/uploadFile/server", produces = {MediaType.APPLICATION_JSON_UTF8_VALUE}, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public String fileUpload(@RequestPart(value = "file") MultipartFile file);
}
import feign.form.spring.SpringFormEncoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Scope;
import feign.codec.Encoder;
/**
 * Feign文件上傳Configuration
 */
@Configuration
public class FeignMultipartSupportConfiguration {
    @Bean
    @Primary
    @Scope("prototype")
    public Encoder multipartFormEncoder() {
        return new SpringFormEncoder();
    }
}

注意 : 文件上傳功能的feign client 與其他的feign client 配置要分開,因為用的是不同的Encoder和處理機制,以免互相干擾,導致請求拋Encoder不支持的異常。

4. feign的調用傳遞headers里面的信息內容

默認情況下,當通過Feign調用其他的服務時,Feign是不會帶上當前請求的headers信息的。

如果我們需要調用其他服務進行鑒權的時候,可能會需要從headers中獲取鑒權信息。則可以通過實現Feign的攔截RequestInterceptor接口,進行獲取headers。然后手動配置到feign請求的headers中去。

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;

@Component
public class FeignHeadersInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        Enumeration<String> headerNames = request.getHeaderNames();
        if (headerNames != null) {
            while (headerNames.hasMoreElements()) {
                String keys = headerNames.nextElement();
                String values = request.getHeader(keys);
                template.header(keys, values);
            }
        }
    }
}


免責聲明!

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



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