聲明式服務(wù)調(diào)用: Spring Cloud Feign
Spring Cloud Feign 是什么侥涵?
之前有spring cloud ribbon
和spring cloud hystrix
砌函,這二個(gè)框架的使用幾乎都是同時(shí)出現(xiàn)的冀墨。是否有更高層次的封裝來整合這二個(gè)基礎(chǔ)工具以簡(jiǎn)化開發(fā)呢第焰?spring cloud feign
就是一個(gè)這樣的工具峻堰。它基于Netfix Feign
實(shí)現(xiàn)咽袜,整合了spring cloud Ribbon
和spring cloud Hystrix
丸卷,除了提供這二者的強(qiáng)大功能之外,它還提供了一種生命式的web服務(wù)客戶端定義方式询刹。
spring cloud feign
在此基礎(chǔ)上做了進(jìn)一步封裝谜嫉,由他來幫助我們定義和實(shí)現(xiàn)依賴服務(wù)接口的定義。在spring cloud feign
的實(shí)現(xiàn)下凹联,我們只需創(chuàng)建一個(gè)接口并調(diào)用注解的方式來配置它沐兰,即可完成對(duì)服務(wù)提供方的接口綁定,簡(jiǎn)化了使用spring cloud ribbon時(shí)自動(dòng)封裝服務(wù)調(diào)用客戶端的開發(fā)量蔽挠。spring cloud feign具備可插拔的注解支持住闯,包括feign注解和JAX-RS注解。同時(shí)澳淑,為了適應(yīng)Spring的廣大用戶比原,它在Netfix Feign的基礎(chǔ)上擴(kuò)展了spring mvc的注解支持,這對(duì)于習(xí)慣spring mvc的開發(fā)者來說杠巡,無疑是個(gè)好消息量窘,因?yàn)檫@樣可以大大減少學(xué)習(xí)使用它的成本。另外忽孽,對(duì)于feign自身的一些主要組件绑改,比如說編碼器和解碼器等,它也以可插拔的方式提高兄一,在有需要的時(shí)候我們可以方便地?cái)U(kuò)展和替換它們。
Feign是一種聲明式识腿、模板化的HTTP客戶端出革。在Spring Cloud中使用Feign, 我們可以做到使用HTTP請(qǐng)求遠(yuǎn)程服務(wù)時(shí)能與調(diào)用本地方法一樣的編碼體驗(yàn),開發(fā)者完全感知不到這是遠(yuǎn)程方法渡讼,更感知不到這是個(gè)HTTP請(qǐng)求骂束。
快速入門
創(chuàng)建pay-service服務(wù),加入依賴成箫,與之前的模塊不一樣的就是加入了spring-cloud-starter-feign依賴展箱,
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
</dependencies>
創(chuàng)建主體應(yīng)用類,并在主體應(yīng)用類上加上注解@EnableFeignClients:
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class PayApplication {
public static void main(String[] args) {
SpringApplication.run(PayApplication.class,args);
}
}
定義UserService接口蹬昌,通過@FeignClient("user-service")
注解指定服務(wù)名來綁定服務(wù)混驰,然后在使用Spring mvc的注解綁定具體的user-service服務(wù)中提供的rest接口。
@FeignClient("user-service")
public interface UserService {
@RequestMapping(value = "/user/index",method = RequestMethod.GET)
String index();
@RequestMapping(value = "/user/hello",method = RequestMethod.GET)
String hello();
接著,創(chuàng)建一個(gè)PayController來實(shí)現(xiàn)對(duì)feign客戶端的調(diào)用栖榨,使用@Autowired注解
直接注入上面定義的UserService實(shí)例昆汹,并在相應(yīng)的方法中調(diào)用這個(gè)綁定了user-service服務(wù)接口的客戶端來向改服務(wù)發(fā)起接口的定義。
@RestController
@RequestMapping("/pay")
public class PayController {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
UserService userService;
@RequestMapping("/index")
public String index(){
return userService.index();
}
@RequestMapping("/hello")
public String hello(){
return userService.hello();
}
}
最后婴栽,同ribbon實(shí)現(xiàn)的服務(wù)消費(fèi)一樣满粗,需要在application.yml
中指定服務(wù)注冊(cè)中心,
spring:
application:
name: pay-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
instance-id: ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}}
server:
port: 7070
測(cè)試驗(yàn)證Ribbon客戶端負(fù)載均衡愚争,同時(shí)啟動(dòng)服務(wù)注冊(cè)和user-servcei映皆,user-service啟動(dòng)了二個(gè)實(shí)例,然后啟動(dòng)pay-service轰枝,
發(fā)送多個(gè)請(qǐng)求http://192.168.5.3:7070/pay/index捅彻,發(fā)現(xiàn)二個(gè)user-service都能在控制臺(tái)打印日志,我們看到了feign實(shí)現(xiàn)的消費(fèi)者狸膏,依然是利用了Ribbon維護(hù)了user-service的服務(wù)列表信息沟饥,并且通過輪詢實(shí)現(xiàn)了客戶端的負(fù)載均衡。而與Ribbon不同的是湾戳,通過feign我們只需要定義服務(wù)綁定接口贤旷,以聲明式的方法,優(yōu)雅而簡(jiǎn)單的實(shí)現(xiàn)了服務(wù)調(diào)用砾脑。
Spring Cloud的Feign支持的一個(gè)中心概念就是命名客戶端幼驶。 每個(gè)Feign客戶端都是組合的組件的一部分,它們一起工作以按需調(diào)用遠(yuǎn)程服務(wù)器韧衣,并且該集合具有您將其作為使用@FeignClient注釋的參數(shù)名稱盅藻。 Spring Cloud使用FeignClientsConfiguration
創(chuàng)建一個(gè)新的集合作為每個(gè)命名客戶端的ApplicationContext(應(yīng)用上下文)。 這包含(除其他外)feign.Decoder畅铭,feign.Encoder和feign.Contract氏淑。
你可以自定義FeignClientsConfiguration以完全控制這一系列的配置。比如我們下面的demo:
定義一個(gè)order服務(wù)硕噩,并加入依賴:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
</dependencies>
定義主體啟動(dòng)類:
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class,args);
}
}
定義Controller:
@RestController
@RequestMapping("/order")
public class OrderController {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
UserService userService;
@RequestMapping("/index")
public String index(){
logger.info("index方法");
return userService.index();
}
}
定義Feign客戶端接口:
@FeignClient(value = "user-service",configuration = FooConfiguration.class)
public interface UserService {
@RequestLine("GET /user/index")
String index();
}
使用了配置@Configuration參數(shù)假残,自己定義了FooConfiguration類來自定義FeignClientsConfiguration
,并且FeignClientsConfiguration類的類路徑不在啟動(dòng)類OrderApplication的掃描路徑下炉擅,是因?yàn)槿绻趻呙枘夸浵聲?huì)覆蓋該項(xiàng)目所有的Feign接口的默認(rèn)配置辉懒。
FooConfiguration定義:
import feign.Contract;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FooConfiguration {
//使用Feign自己的注解,使用springmvc的注解就會(huì)報(bào)錯(cuò)
@Bean
public Contract feignContract() {
return new feign.Contract.Default();
}
}
配置文件:
spring:
application:
name: order-service
eureka:
client:
service-url:
defaultZone: http://cfy.lessismore:123456@localhost:8761/eureka
instance:
instance-id: ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}}
server:
port: 9090
參數(shù)綁定
快速入門中谍失,我們使用了spring cloud feign
實(shí)現(xiàn)的是一個(gè)不帶參數(shù)的REST服務(wù)綁定】袅現(xiàn)實(shí)中的各種業(yè)務(wù)接口要比它復(fù)雜的多,我們會(huì)在http的各個(gè)位置傳入各種不同類型的參數(shù)快鱼,并且在返回請(qǐng)求響應(yīng)的時(shí)候也可能是一個(gè)復(fù)雜的對(duì)象結(jié)構(gòu)颠印。
擴(kuò)展一下user-servcice服務(wù)纲岭,增加一些接口定義,其中包含Request參數(shù)的請(qǐng)求嗽仪,帶有Header信息的請(qǐng)求荒勇,帶有RequestBody的請(qǐng)求以及請(qǐng)求響應(yīng)體中是一個(gè)對(duì)象的請(qǐng)求,擴(kuò)展了三個(gè)接口分別是hello,hello2闻坚,hello3
@RestController
@RequestMapping("/user")
public class UserController {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private DiscoveryClient client;
@RequestMapping(value="/index",method = RequestMethod.GET)
public String index(){
ServiceInstance instance = client.getLocalServiceInstance();
logger.info("/user,host:"+instance.getHost()+",service id:"+instance.getServiceId()+",port:"+instance.getPort());
return "user index, local time="+ LocalDateTime.now();
}
@GetMapping("/hello")
public String userHello() throws Exception{
ServiceInstance serviceInstance = client.getLocalServiceInstance();
//線程阻塞
int sleeptime = new Random().nextInt(3000);
logger.info("sleeptime:"+sleeptime);
Thread.sleep(sleeptime);
logger.info("/user,host:"+serviceInstance.getHost()+",service id:"+serviceInstance.getServiceId()+",port:"+serviceInstance.getPort());
return "user hello";
}
@RequestMapping(value = "/hello1",method = RequestMethod.GET)
public String hello(@RequestParam String username){
return "hello "+username;
}
@RequestMapping(value = "hello2",method = RequestMethod.GET)
public User hello2(@RequestHeader String username,@RequestHeader Integer age){
return new User(username,age);
}
@RequestMapping(value = "hello3",method = RequestMethod.POST)
public String hello3(@RequestBody User user){
return "hello "+user.getUsername() +", "+user.getAge()+", "+user.getId();
}
}
訪問:
localhost:8080/user/hello1?username=zhihao.miao
User對(duì)象的定義如下沽翔,需要注意的是要有User的默認(rèn)的構(gòu)造函數(shù),不然窿凤,spring cloud feign根據(jù)json字符串轉(zhuǎn)換User對(duì)象的時(shí)候會(huì)拋出異常仅偎。
public class User {
private String username;
private int age;
private int id;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public User(String username, int age) {
this.username = username;
this.age = age;
}
public User() {
}
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", age=" + age +
", id=" + id +
'}';
}
}
優(yōu)點(diǎn)與缺點(diǎn)
使用spring cloud feign
的繼承特性的優(yōu)點(diǎn)很明顯,可以將接口的定義從Controller 中剝離雳殊,同時(shí)配合 maven 倉庫就可以輕易實(shí)現(xiàn)接口定義的共享橘沥,實(shí)現(xiàn)在構(gòu)建期的接口綁定,從而有效的減少服務(wù)客戶端的綁定配置夯秃。這么做雖然可以很方便的實(shí)現(xiàn)接口定義和依賴的共享座咆,不用在復(fù)制粘貼接口進(jìn)行綁定,但是這樣的做法使用不當(dāng)?shù)脑挄?huì)帶來副作用仓洼。由于接口在構(gòu)建期間就建立起了依賴介陶,那么接口變化就會(huì)對(duì)項(xiàng)目構(gòu)建造成了影響,可能服務(wù)提供方修改一個(gè)接口定義色建,那么會(huì)直接導(dǎo)致客戶端工程的構(gòu)建失敗哺呜。所以,如果開發(fā)團(tuán)隊(duì)通過此方法來實(shí)現(xiàn)接口共享的話箕戳,建議在開發(fā)評(píng)審期間嚴(yán)格遵守面向?qū)ο蟮拈_閉原則某残,盡可能低做好前后版本兼容,防止因?yàn)榘姹驹蛟斐山涌诙x的不一致陵吸。
Feign日志的配置
為每個(gè)創(chuàng)建的Feign客戶端創(chuàng)建一個(gè)記錄器玻墅。默認(rèn)情況下,記錄器的名稱是用于創(chuàng)建Feign客戶端的接口的完整類名壮虫。Feign日志記錄僅響應(yīng)DEBUG級(jí)別椭豫。logging.level.project.user.UserClient: DEBUG
在配置文件application.yml 中加入:
logging:
level:
com.jalja.org.spring.simple.dao.FeignUserClient: DEBUG
在自定義的Configuration的類中添加日志級(jí)別
@Configuration
public class FooConfiguration {
/* @Bean
public Contract feignContract() {
//這將SpringMvc Contract 替換為feign.Contract.Default
return new feign.Contract.Default();
}*/
@Bean
Logger.Level feignLoggerLevel() {
//設(shè)置日志
return Logger.Level.FULL;
}
}
PS:Feign請(qǐng)求超時(shí)問題
Hystrix默認(rèn)的超時(shí)時(shí)間是1秒茄靠,如果超過這個(gè)時(shí)間尚未響應(yīng)蛉幸,將會(huì)進(jìn)入fallback代碼桥帆。而首次請(qǐng)求往往會(huì)比較慢(因?yàn)镾pring的懶加載機(jī)制,要實(shí)例化一些類)谆构,這個(gè)響應(yīng)時(shí)間可能就大于1秒了
解決方案有三種,以feign為例框都。
方法一
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 5000
該配置是讓Hystrix的超時(shí)時(shí)間改為5秒
方法二
hystrix.command.default.execution.timeout.enabled: false
該配置搬素,用于禁用Hystrix的超時(shí)時(shí)間
方法三
feign.hystrix.enabled: false
該配置,用于索性禁用feign的hystrix。該做法除非一些特殊場(chǎng)景熬尺,不推薦使用摸屠。
Less is more.