Chapter Six《SpringCloud微服務(wù)實(shí)戰(zhàn)》

聲明式服務(wù)調(diào)用: Spring Cloud Feign

Spring Cloud Feign 是什么侥涵?

之前有spring cloud ribbonspring cloud hystrix砌函,這二個(gè)框架的使用幾乎都是同時(shí)出現(xiàn)的冀墨。是否有更高層次的封裝來整合這二個(gè)基礎(chǔ)工具以簡(jiǎn)化開發(fā)呢第焰?spring cloud feign就是一個(gè)這樣的工具峻堰。它基于Netfix Feign實(shí)現(xiàn)咽袜,整合了spring cloud Ribbonspring 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轰枝,


image.png

發(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

image.png
image.png
image.png

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.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市粱哼,隨后出現(xiàn)的幾起案子季二,更是在濱河造成了極大的恐慌,老刑警劉巖揭措,帶你破解...
    沈念sama閱讀 221,198評(píng)論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件胯舷,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡绊含,警方通過查閱死者的電腦和手機(jī)桑嘶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來躬充,“玉大人逃顶,你說我怎么就攤上這事〕渖酰” “怎么了以政?”我有些...
    開封第一講書人閱讀 167,643評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)津坑。 經(jīng)常有香客問我妙蔗,道長(zhǎng),這世上最難降的妖魔是什么疆瑰? 我笑而不...
    開封第一講書人閱讀 59,495評(píng)論 1 296
  • 正文 為了忘掉前任眉反,我火速辦了婚禮,結(jié)果婚禮上穆役,老公的妹妹穿的比我還像新娘寸五。我一直安慰自己,他們只是感情好耿币,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評(píng)論 6 397
  • 文/花漫 我一把揭開白布梳杏。 她就那樣靜靜地躺著,像睡著了一般淹接。 火紅的嫁衣襯著肌膚如雪十性。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,156評(píng)論 1 308
  • 那天塑悼,我揣著相機(jī)與錄音劲适,去河邊找鬼。 笑死厢蒜,一個(gè)胖子當(dāng)著我的面吹牛霞势,可吹牛的內(nèi)容都是我干的烹植。 我是一名探鬼主播,決...
    沈念sama閱讀 40,743評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼愕贡,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼草雕!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起固以,我...
    開封第一講書人閱讀 39,659評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤墩虹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后嘴纺,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體败晴,經(jīng)...
    沈念sama閱讀 46,200評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評(píng)論 3 340
  • 正文 我和宋清朗相戀三年栽渴,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了尖坤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,424評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡闲擦,死狀恐怖慢味,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情墅冷,我是刑警寧澤纯路,帶...
    沈念sama閱讀 36,107評(píng)論 5 349
  • 正文 年R本政府宣布,位于F島的核電站寞忿,受9級(jí)特大地震影響驰唬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜腔彰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評(píng)論 3 333
  • 文/蒙蒙 一叫编、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧霹抛,春花似錦搓逾、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至端逼,卻和暖如春朗兵,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背顶滩。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評(píng)論 1 271
  • 我被黑心中介騙來泰國(guó)打工矛市, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人诲祸。 一個(gè)月前我還...
    沈念sama閱讀 48,798評(píng)論 3 376
  • 正文 我出身青樓浊吏,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親救氯。 傳聞我的和親對(duì)象是個(gè)殘疾皇子找田,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容