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);
}
}
}
}