SpringCloud微服務服務間呼叫之OpenFeign介紹
開發微服務,免不了需要服務間呼叫。Spring Cloud框架提供了RestTemplate和FeignClient兩個方式完成服務間呼叫,本文簡要介紹如何使用OpenFeign完成服務間呼叫。
OpenFeign思維導圖
在此奉上我整理的OpenFeign相關的知識點思維導圖。
基礎配置使用例子
(1)服務端:
@RestController @RequestMapping("hello") public class HelloController implements HelloApi { @Override public String hello(String name) { return "Hello, "+name+"!"; } }
API宣告:
public interface HelloApi { @GetMapping("/hello/{name}") String hello(@PathVariable("name") String name); @GetMapping("/bye/{name}") ResponseValue<String> bye(@PathVariable("name") String name); @GetMapping(value = "/download") byte[] download(HttpServletResponse response); }
(2)客戶端:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
開啟配置 @EnableFeignClients
,呼叫服務的程式碼:
@FeignClient(name="hello1", url = "127.0.0.1:8080", path = "hello") public interface HelloApiExp extends HelloApi { @GetMapping("/download") Response download(); }
呼叫時的程式碼:
@RestController
@RequestMapping("client")
public class HelloClient {
@Autowired
private HelloApiExp helloApi;
@GetMapping("/hello/{name}")
public String hello(@PathVariable("name") String name){
return helloApi.hello(name);
}
}
瀏覽器訪問URL:http://127.0.0.1:8080/client/hello/Mark,頁面返回: Hello, Mark!
@FeignClient的簡單用法
屬性名稱 | 屬性說明 | 預設值 |
---|---|---|
name/value | 作為serviceId,bean name | |
contextId | 作為bean name,代替name/value的值 | |
qualifier | 限定詞 | |
url | http的URL字首(不包括協議名):主機名和埠號 | |
decode404 | 請求遇到404則丟擲FeignExceptions | false |
path | 服務字首,等同於ContextPath | |
primary | whether to mark the feign proxy as a primary bean | true |
高階配置——使用configuration配置類
通過自定義配置類統一配置Feign的各種功能屬性,FeignClientsConfiguration為預設配置:
@FeignClient(name="hello1", url = "127.0.0.1:8080", configuration = FeignClientsConfiguration.class)
public interface HelloApi {
@GetMapping("/{name}")
String hello(@PathVariable("name") String name);
}
Decoder feignDecoder
Decoder類,將http返回的Entity字元解碼(反序列化)為我們需要的例項,如自定義的POJO物件。一般使用FeignClientsConfiguration預設的feignDecoder就能滿足返回String、POJO等絕大多數場景。
@Bean
@ConditionalOnMissingBean
public Decoder feignDecoder() {
return new OptionalDecoder(
new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
}
Encoder feignEncoder
Encode類對請求引數做編碼(序列化)後,傳送給http服務端。使用spring cloud預設的feignEncoder可以滿足我們絕大多數情況。
使用Feign實現檔案上傳下載時需要特殊處理,使用feign-form
能夠方便的實現。這裡我們對feign-form
在spring cloud中的使用舉一個簡單的例子。
HelloApi介面宣告:
public interface HelloApi {
@GetMapping(value = "/download")
byte[] download(HttpServletResponse response);
@PostMapping(value = "upload",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
ResponseValue<String> upload(@RequestBody MultipartFile file);
}
服務端程式碼:
@RestController
@RequestMapping("hello")
public class HelloController implements HelloApi {
@Override
public byte[] download(HttpServletResponse response) {
FileInputStream fis = null;
try{
File file = new File("E:\\圖片\\6f7cc39284868762caaed525.jpg");
fis = new FileInputStream(file);
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition",
"attachment;filename=class.jpg");
return IOUtils.toByteArray(fis, file.length());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
@Override
public ResponseValue<String> upload(@RequestBody MultipartFile file) {
File destFile = new File("d:\\1.jpg");
ResponseValue<String> response = new ResponseValue<>();
try {
file.transferTo(destFile);
return response.ok("上傳成功!", null);
} catch (IOException e) {
e.printStackTrace();
return response.fail("上傳失敗,錯誤原因:"+e.getMessage());
}
}
}
客戶端程式碼:
pom.xml引入依賴:
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form</artifactId>
<version>3.8.0</version>
</dependency>
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form-spring</artifactId>
<version>3.8.0</version>
</dependency>
增加FeignClient配置類:
@Configuration
public class FeignMultipartSupportConfig extends FeignClientsConfiguration {
@Bean
public Encoder feignFormEncoder() {
return new SpringFormEncoder();
}
}
FeignClient介面宣告:
import feign.Response;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(name="hello1", url = "127.0.0.1:8080", path = "hello",
configuration = FeignMultipartSupportConfig.class)
public interface HelloApiExp extends HelloApi {
@GetMapping("/download")
Response download();
}
呼叫端程式碼:
@RestController
@RequestMapping("client")
public class HelloClient {
@GetMapping(value = "/download")
public byte[] download(HttpServletResponse response){
response.setHeader("Content-Disposition",
"attachment;filename=class.jpg");
//response.setHeader("Content-Type","application/octet-stream");
Response resp = helloApi.download();
Response.Body body = resp.body();
try(InputStream is = body.asInputStream()) {
return IOUtils.toByteArray(is, resp.body().length());
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
@PostMapping(value = "upload",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseValue<String> upload(@RequestBody MultipartFile file){
return helloApi.upload(file);
}
}
Retryer feignRetryer
請求重試策略類,預設不重試,可配置成feign.Retryer.Default,啟用重試,預設間隔100毫秒重試一次,最大間隔時間限制為1秒,最大重試次數5次。
@Configuration
public class FeignRetryConfig extends FeignClientsConfiguration {
@Bean
@Override
public Retryer feignRetryer() {
return new Retryer.Default();
}
}
Feign.Builder feignBuilder
FeignClient的Builder,我們可以通過他使用程式碼的方式設定相關屬性,代替@FeignClient的註解過的介面,如下面的程式碼:
@GetMapping("/hello/{name}")
public String hello(@PathVariable("name") String name){
String response = feignBuilder
.client(new OkHttpClient())
.encoder(new SpringFormEncoder())
.requestInterceptor(new ForwardedForInterceptor())
.logger(new Slf4jLogger())
.logLevel(Logger.Level.FULL)
.target(String.class, "http://127.0.0.1:8080");
return response;
//return helloApi.hello(name);
}
其實@FeignClient生成的代理類也是通過它構建的。程式碼中的feignBuilder.client()可以使用RibbonClient,就集成了Ribben。
FeignLoggerFactory feignLoggerFactory
設定LoggerFactory類,預設為Slf4j。
Feign.Builder feignHystrixBuilder
配置Hystrix,從下面的配置類可以看出,@ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
,如果引用了Hystrix的相關依賴,並且屬性feign.hystrix.enabled
為true,則構建@FeignClient代理類時使用的FeignBuilder會使用feignHystrixBuilder。Feign通過這種方式集成了Hystrix。
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
protected static class HystrixFeignConfiguration {
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "feign.hystrix.enabled")
public Feign.Builder feignHystrixBuilder() {
return HystrixFeign.builder();
}
}
OpenFeign+consul使用示例
背景介紹
本示例使用consul作為服務註冊中心,基於SpringCloud框架開發兩個微服務,一個user-service(服務提供方),一個feignusercommodity-service(服務呼叫方),具體版本資訊如下
軟體/框架 | 版本 |
---|---|
consul | v1.2.0 |
Spring Boot | 2.0.1.RELEASE |
Spring Cloud | Finchley.RELEASE |
openFeign使用預設版本的,也就是spring-cloud-starter-openfeign 2.0.0版本。
主要程式碼
核心程式碼主要包括兩點,
1, 對應介面新增@FeignClient,並完成對應服務提供者的requestMapping對映。
2,在啟動類加上@EnableFeignClients(basePackages = {"com.yq.client"}), 我的serviceClieng位於com.yq.client包。
提供方的主要介面如下:
ServiceClient類的主要實現如下.
注意:User 類在兩個服務中是一樣,實際專案中我們可以把它放到公共依賴包中。
@FeignClient(value = "user-service", fallbackFactory = UserServiceFallbackFactory.class)
public interface UserServiceClient {
@RequestMapping(value="/v1/users/{userId}", method= RequestMethod.GET, produces = "application/json;charset=UTF-8")
public User getUser(@PathVariable(value = "userId") String userId);
@RequestMapping(value="/v1/users/queryById", method= RequestMethod.GET, produces = "application/json;charset=UTF-8")
public User getUserByQueryParam(@RequestParam("userId") String userId);
@RequestMapping(value="/v1/users", method= RequestMethod.POST, produces = "application/json;charset=UTF-8")
public String createUser();
}
完整程式碼看 user-servcie, feignusercommodity-service,裡面的pom檔案,serviceClient都是完整的可以執行的。 歡迎加星,fork。
效果截圖
第一張截圖,兩個服務都正常在consul上註冊,完成服務間呼叫
第二張截圖,兩個服務都正常在consul上註冊,完成服務間呼叫, 這是consul down了,服務間呼叫可以繼續,因為feignusercommodity-service服務快取了user-service服務的服務提供地址資訊
第三張截圖,feignusercommodity-service服務正常在consul上註冊,但是user-service沒有註冊,系統給出了“com.netflix.client.ClientException: Load balancer does not have available server for client: user-service”
第四張截圖,user-service提供方的對應方法報異常,服務呼叫能正常獲取到該異常並顯示。
故障轉移
使用Feign可以完成服務間呼叫,但是總存在一種情況:服務提供方沒有註冊到註冊中心、服務提供方還沒開發完成(因為也就無法呼叫)等等。此時如果我們需要完成服務間呼叫該如何做呢?
Feign
提供了fallback
機制,也就是當對方服務還沒ready(一般情況是服務提供方在註冊中心上沒有可用的例項),可以返回資訊供服務進行下,也就是服務降級。
故障轉移機制,如果@FeignClient
指定了fallback
或fallbackFactory
屬性,http請求呼叫失敗時會路由到fallback處理類的相同方法中。
fallback
@FeignClient宣告:
@FeignClient(name="hello1", url = "127.0.0.1:8080", path = "hello",
configuration = FeignMultipartSupportConfig.class,
fallback = HelloApiFallback.class)
public interface HelloApiExp extends HelloApi {
@GetMapping("/download")
Response download();
}
HelloApiFallback程式碼需要實現HelloApiExp介面(包括父介面)的所有方法:
@Slf4j
public class HelloApiFallback implements HelloApiExp {
@Override
public Response download() {
log.error("下載檔案出錯。");
return null;
}
@Override
public String hello(String name) {
log.error("呼叫hello接口出錯。");
return "呼叫hello接口出錯,請聯絡管理員。";
}
@Override
public ResponseValue<String> bye(String name) {
log.error("呼叫bye接口出錯。");
ResponseValue<String> response = new ResponseValue<>();
return response.fail("呼叫hello接口出錯,請聯絡管理員。");
}
@Override
public byte[] download(HttpServletResponse response) {
log.error("呼叫bye接口出錯。");
return new byte[0];
}
@Override
public ResponseValue<String> upload(MultipartFile file) {
log.error("呼叫上傳檔案接口出錯。");
ResponseValue<String> response = new ResponseValue<>();
return response.fail("上傳檔案出錯,請聯絡管理員。");
}
}
fallbackFactory
為@FeignClient介面所有方法指定統一的故障處理方法。
@FeignClient(name="hello1", url = "127.0.0.1:8080", path = "hello",
configuration = FeignMultipartSupportConfig.class,
fallbackFactory = FallbackFactory.Default.class)
public interface HelloApiExp extends HelloApi {
@GetMapping("/download")
Response download();
}
FallbackFactory.Default實現如下,請求失敗後,統一路由到create(Throwable cause)方法。
/** Returns a constant fallback after logging the cause to FINE level. */
final class Default<T> implements FallbackFactory<T> {
// jul to not add a dependency
final Logger logger;
final T constant;
public Default(T constant) {
this(constant, Logger.getLogger(Default.class.getName()));
}
Default(T constant, Logger logger) {
this.constant = checkNotNull(constant, "fallback");
this.logger = checkNotNull(logger, "logger");
}
@Override
public T create(Throwable cause) {
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "fallback due to: " + cause.getMessage(), cause);
}
return constant;
}
@Override
public String toString() {
return constant.toString();
}
}
Feign結合Hystrix可以實現服務降級
主要使用consul 1.2.0, Spring Boot 1.5.12, Spring Cloud Edgware.RELEASE。
需要引入Hystrix依賴並在啟動類和配置檔案中啟用Hystrix
pom檔案增加如下依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
啟動類加上@EnableHystrix
,@EnableHystrixDashboard
,@EnableFeignClients
@SpringBootApplication
@EnableHystrix
@EnableHystrixDashboard
@EnableDiscoveryClient
@EnableCircuitBreaker
//@EnableTurbine
@EnableFeignClients(basePackages = {"com.yq.client"})
public class HystrixDemoApplication {
private static final Logger logger = LoggerFactory.getLogger(HystrixDemoApplication.class);
public static void main(String[] args) {
SpringApplication.run(HystrixDemoApplication.class, args);
logger.info("HystrixDemoApplication Start done.");
}
}
配置檔案中feign啟用hystrix
feign.hystrix.enabled=true
實現自己的fallback服務
feignClient類
@FeignClient(value = "user-service", fallback = UserServiceClientFallbackFactory.class)
@Component
public interface UserServiceClient {
@RequestMapping(value = "/v1/users/{userId}", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
String getUserDetail(@PathVariable("userId") String userId);
}
自定義的fallback類
@Component
@Slf4j
public class UserServiceClientFallbackFactory implements UserServiceClient{
@Override
public String getUserDetail(String userId) {
log.error("Fallback2, userId={}", userId);
return "user-service not available2 when query '" + userId + "'";
}
}
效果截圖
第一張截圖
雖然我們建立了fallback類,也引入了Hystrix,但是沒有啟用feign.hystrix.enabled=true,所以無法實現服務降級,服務間呼叫還是直接報異常。
第二張截圖
我們建立了fallback類,也引入了Hystrix,同時啟用feign.hystrix.enabled=true,所以當user-service不可用時,順利實現服務降級。
第三張, user-service服務正常, fallback不影響原有服務間呼叫正常進行。
參考文件
官方文件在這裡: http://cloud.spring.io/spring-cloud-openfeign/single/spring-cloud-openfeign.html
fallback官方文件:http://cloud.spring.io/spring-cloud-openfeign/single/spring-cloud-openfeign.html#spring-cloud-feign-hystrix-fallback