springcloud(五)遠程調用Feign(含源碼跟蹤)

feign

feign是一個聲明式的Web服務客戶端。這使得Web服務客戶端的寫入更加方便 要使用Feign創建一個界面並對其進行註釋。它具有可插入註釋支持,包括Feign註釋和JAX-RS註釋。Feign還支持可插拔編碼器和解碼器。Spring Cloud增加了對Spring MVC註釋的支持,並使用Spring Web中默認使用的HttpMessageConverters。Spring Cloud集成Ribbon和Eureka以在使用Feign時提供負載均衡的http客戶端。

在分佈式橫行的時代,服務與服務之間的通信也越來越頻繁,而spring cloud作爲分佈式的佼佼者,服務通信這一塊當然也不再話下,oprnFeign就是cloud提供服務通信的組建。

demo展示

構建feign組件

需求

在訂單模塊通過用戶id查詢用戶信息

1.新建eureka
請參考springcloud(一)註冊中心eureka

2.新建config配置中心
請參考:springcloud(二)配置中心config

3.打開cloud-order,引入openFeign依賴

<!--遠程調用-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

4.在cloud-user中提供一個根據用戶id查詢用戶信息的接口
 

package com.ymy.coulduser.controller;

import com.ymy.coulduser.vo.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.*;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@RestController
@RefreshScope
@Slf4j
public class UserController {


    private static Map<Integer,User> map = new ConcurrentHashMap<Integer, User>();

    static {
        User user1 = new User();
        user1.setId(1);
        user1.setUserName("張三");
        user1.setAge(18);
        map.put(1,user1);
        User user2 = new User();
        user2.setId(2);
        user2.setUserName("李四");
        user2.setAge(20);
        map.put(2,user2);
    }

    @Value("${test.name}")
    private String name;

    @RequestMapping(value = "/test",method = RequestMethod.GET)
    public String getUserName(){

        return "hello:"+name+"     人生總是起起落落落落落落落落落落落落落落落落落落落落落落落落落落落落落落落落!";
    }


    @GetMapping(value = "userInfo")
    public User getUserInfo(@RequestParam("userId") Integer userId){
        if(null == userId){
            log.info("用戶id爲空!");
            return null;
        }
        User user = map.get(userId);
        return user;
    }
}

這裏我是模擬的,數據都是在Map中造的,這裏的重點不是數據,而是服務調用的這個過程。

5.cloud-order中新建一個與cloud-user通信的接口類

內容爲:
 

package com.ymy.feign;

import com.ymy.entity.vo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(value = "cloud-user")
public interface UserClient {

    @GetMapping(value = "userInfo")
    User getUserInfo(@RequestParam("userId") final Integer userId);
}
@FeignClient:表示該類是一個遠程調用類,交給jdk動態代理處理。
value:需要調用的服務名。
TestController:
package com.ymy.controller;

import com.ymy.entity.vo.Order;
import com.ymy.entity.vo.User;
import com.ymy.feign.UserClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;

@RestController
@Slf4j
@RefreshScope
public class TestController {


    @Value("${test.name}")
    private String name;

    private UserClient userClient;

    TestController(UserClient userClient){
        this.userClient = userClient;
    }

    @GetMapping(value = "/test")
    public String test(){

        return "hello:"+name+"     人生總是起起落落落落落落落落落落落落落落落落落落落落落落落落落落落落落落落落!";
    }


    @GetMapping(value = "orderInfo")
    public Order getOrderInfo(){
        Order order = new Order();
        order.setId(1);
        order.setUserId(2);
        order.setOorderPrice(BigDecimal.valueOf(100l));
        order.setNum(2);

        User userInfo = userClient.getUserInfo(order.getUserId());
        if(null == userInfo){
            log.info("沒有查詢到用戶信息");
        }

        order.setUser(userInfo);

        return order;
    }
}


6.在啓動類中開啓feign遠程調用
package com.ymy;

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

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class CloudOrderApplication {

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

}
@SpringBootApplication:springboot啓動註解
@EnableDiscoveryClient:將服務註冊到註冊中心
@EnableFeignClients:開啓feign,如果不加此註解,服務調用將不會成功。

7.啓動cloud-eureka、cloud-config、cloud-user、cloud-order,測試

 其中user則是cloud-order通過調用cloud-user服務拿到的,這是理想狀態,如果出現意外情況呢?比如cloud-user服務宕機了會發生什麼呢?,現在我停掉cloud-user服務,再次請求:


這是你會發現,服務器出現了報錯,這種錯誤是不能出現在用戶眼中的,有沒有方法解決呢?

fallback 熔斷機制

feign自帶熔斷,所以我們不需要在引入其他的依賴,feign的熔斷有兩種實現方式,如下:

第一種:實現類
什麼是實現類呢?請注意看

UserClient是一個接口,這時候我們需要用一個類來實現這個接口
UserClientHystrix.java

package com.ymy.feign.hystrix;

import com.ymy.entity.vo.User;
import com.ymy.feign.UserClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class UserClientHystrix implements UserClient {

    @Override
    public User getUserInfo(Integer userId) {
        //這裏就是當遠程調用失敗時,feign會自動進入接口的實現類,就是之前所說的熔斷,
        // 在這裏,你可以做對應的處理,比如返回一個空的User對象,或者啓用備用方案,調用聯外一臺服務等等。
        //這裏我們直接返回空User對象
        log.info("請求cloud-user服務發生錯誤");
        return null;
    }
}

還需要一步那就是在接口指明實現類:
 

@FeignClient(value = "cloud-user",fallback = UserClientHystrix.class)

重啓cloud-order,cloud-user保持宕機狀態:

發現報錯了,而且還是和第一次的錯誤不一致,這是什麼問題導致的呢?這是因爲我們沒有啓動feign的熔斷機制,默認時關閉狀態,找到bootstrap.yml文件:
 

feign:
  hystrix:
    enabled: true   #開啓feign熔斷

重啓服務再次測試:

控制檯:

第二種:指定回調
指定實現類這種方式需要對每個接口做實現,如果所有的請求接口都只需要返回空的話,這樣就會重複寫很多一樣的實現方法,所以這裏引入了回調。

1.註釋掉實現類,引入hystrix依賴:

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

2.引入註解:@HystrixCommand

@HystrixCommand:這裏指明服務調用失敗時需要做什麼。
fallbackMethod:指明失敗之後進入哪個回調方法。

/**
     * 失敗之後走這裏
     * @return
     */
    public Order defaultBack(){
        log.info("請求cloud-user服務發生錯誤");
        Order order = new Order();
        order.setId(110);
        order.setUserId(120);
        order.setOorderPrice(BigDecimal.valueOf(1000l));
        order.setNum(20);
        return order;
    }

3.在啓動類中加入:@EnableCircuitBreaker

重啓測試:

以上兩種方式都可以做熔斷處理,具體選擇哪種,還要看業務的需求。

源碼跟蹤

@EnableFeignClients做了什麼?

進入FeignClientsRegistrar類

一看,矇蔽了,這麼多方法,這要怎麼看,其實我們需要注意的並不多,我們重點來看一下registerFeignClients方法,打上斷點,啓動服務:


basePackages:指的是掃描的包,下一步對掃描出來的包做遍歷

請看candidateComponents的返回結果:

 這裏是一個集合,找到com.ymy下面有多少個被FeignClient註解標識的類。

 

在UserClient類中只指定了value屬性,所以這裏只展示value和name,隨後就會將這些被@FeignClient註解標識的類交由jdk動態代理。

這是啓動部分,我們再來看看調用過程

feign調用跟蹤

spring-cloud-openfeign源碼:https://github.com/spring-cloud/spring-cloud-openfeign
Feign源碼https://github.com/OpenFeign/feign

spring-cloud-openfeign是我們項目中使用的依賴,而spring-cloud-openfeign又引入了feign依賴,這裏我們只看調用過程,真正的調用過程在Feign源碼中,請看:SynchronousMethodHandler.java

調用時,會走這裏的代理,斷點走起

Object[] argv :參數,這裏我只傳入了一個userId   值爲2。
我們再來看看buildTemplateFromArgs.create(argv):

 mutable:

varBuilder:

參數列表,Map集合,回到invoke()方法,

Options options = findOptions(argv) :拿到連接超時等信息

 

這是默認配置,如果我們在配置文件配置了超時時間,那麼這裏獲取到的就是我們配置的。

進入 executeAndDecode()
 

Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
    Request request = targetRequest(template);

    if (logLevel != Logger.Level.NONE) {
      logger.logRequest(metadata.configKey(), logLevel, request);
    }

    Response response;
    long start = System.nanoTime();
    try {
      response = client.execute(request, options);
    } catch (IOException e) {
      if (logLevel != Logger.Level.NONE) {
        logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
      }
      throw errorExecuting(request, e);
    }
    long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);

    boolean shouldClose = true;
    try {
      if (logLevel != Logger.Level.NONE) {
        response =
            logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime);
      }
      if (Response.class == metadata.returnType()) {
        if (response.body() == null) {
          return response;
        }
        if (response.body().length() == null ||
            response.body().length() > MAX_RESPONSE_BUFFER_SIZE) {
          shouldClose = false;
          return response;
        }
        // Ensure the response body is disconnected
        byte[] bodyData = Util.toByteArray(response.body().asInputStream());
        return response.toBuilder().body(bodyData).build();
      }
      if (response.status() >= 200 && response.status() < 300) {
        if (void.class == metadata.returnType()) {
          return null;
        } else {
          Object result = decode(response);
          shouldClose = closeAfterDecode;
          return result;
        }
      } else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {
        Object result = decode(response);
        shouldClose = closeAfterDecode;
        return result;
      } else {
        throw errorDecoder.decode(metadata.configKey(), response);
      }
    } catch (IOException e) {
      if (logLevel != Logger.Level.NONE) {
        logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime);
      }
      throw errorReading(request, response, e);
    } finally {
      if (shouldClose) {
        ensureClosed(response.body());
      }
    }
  }
Request request = targetRequest(template); 拿到一個完整的request域:

進入 client.execute(request, options)方法:

看到這裏是不是看到了熟悉的味道,沒錯,這裏就是通過http協議發送請求,所以cloud的feign與dubbo是由區別的,dubbo採用rpc,feign則採用http,rpc基於netty,各有各的優點,選型的時候選擇適合公司的纔是最好的,其實到這裏基本上可以不用往下說了,如果感興趣的朋友可以跟着源碼走一下。
 

FeignClientsRegistrar.java:項目啓動是調用,處理被FeignClient標識的接口

SynchronousMethodHandler:feign的動態代理,遠程調用的調用。

最後再給一張client的圖:

總結

feign可以讓程序以非常少的代碼就能實現服務與服務之間的通信,還能再調用出錯時給出熔斷機制,是一個很強大的組件。

關於使用dubbo還是使用spring cloud,我覺得還是具體系統具體分析,比如開發人員熟悉dubbo,那還是用dubbo比較好,學習新的框架需要時間成本,還要根據項目的大小,開發的難易程度等等做出對應的選擇,不是最新的就是最好的,選擇適合自己的纔是最好的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章