Spring微服務實戰--註解版(第六章)

第6章 使用Zuul進行服務路由

在像微服務架構這樣的分佈式架構中,有時候需要做一些統一的動作,例如日誌記錄和追蹤、記錄接口調用的時間等,爲了解決這個問題,需要將一些橫切關注點抽象成一個獨立的服務,這個服務會作爲所有微服務的過濾器和路由器,這種橫切關注點被成爲服務網關。服務客戶端不再直接調用微服務,取而代之的是,所有調用都通過服務網關進行路由,然後被路由到最終目的地。Zuul是開源的服務網關實現。
我們通過一個簡單的圖看下什麼是服務網關。
在這裏插入圖片描述
服務網關充當的是客戶端與服務端之間的中介,網關從客戶端調用中分離出路徑,此路徑用於確定客戶端真正想調用的服務,可以看到,其實網關還是所有微服務調用的入口,相當於守門人的角色,這樣橫切關注點就可以在網關實現,而不用每個服務都加上切面。服務網關中可以做的事情如下:

  • 路由—網關將所有服務調用放置在URL和API路由的後面,這樣客戶端調用只需要知道一個IP和Port即可。服務網關可以根據傳入的url判斷客戶端真正請求的服務,執行智能路由
  • 驗證和授權—由於所有服務調用都要經過網關進行路由,所以網關是檢查服務調用是否已經進行了驗證並被授權調用該服務
  • 度量數據收集和日誌記錄—可以做些基本的統計工作,如服務調用次數,服務響應時間,日誌關聯Id的記錄。
    接下來介紹SpringCloud集成Zuul。Zuul提供了較多功能,我們具體介紹:
    1、將所有服務的路由映射到一個服務中,也就是提供服務的統一入口
    2、構建過濾器。
    下面介紹怎樣建立一個Zuul項目。和以前一樣,修改構建腳本、修改引導類、修改配置文件。
    首先構建腳本添加對於zuul依賴項:
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-zuul</artifactId>
    </dependency>

這個依賴告訴SpringCloud框架,該服務將運行Zuul,並適當地初始化Zuul。
接下來修改引導類,爲了使服務成爲一個Zuul服務,需要添加一個@EnableZuulProxy的註解

@SpringBootApplication
//使服務成爲一個Zuul服務
@EnableZuulProxy
public class ZuulServerApplication {

    @LoadBalanced
    @Bean
    public RestTemplate getRestTemplate(){
        RestTemplate template = new RestTemplate();
        List interceptors = template.getInterceptors();
        if (interceptors == null) {
            template.setInterceptors(Collections.singletonList(new UserContextInterceptor()));
        } else {
            interceptors.add(new UserContextInterceptor());
            template.setInterceptors(interceptors);
        }

        return template;
    }

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

還需要注意的一點是,Zuul也需要註冊到Eureka上。其將使用Eureka來通過服務ID查找服務,然後使用Ribbon對來自Zuul的請求進行客戶端負載均衡。
我們應該清楚,Zuul的核心是一個反向代理,反向代理負責捕獲客戶端的請求,然後代表客戶端調用遠程資源,Zuul必須知道如何將進來的請求映射到不同的服務中,主要有以下幾種機制
通過服務發現自動映射路由
理論上將Zuul的所有映射都是在其application.yml中定義路由來完成的。但是Zuul可以根據其服務ID自動路由請求,例如如下的URL。
http://localhost:5555/organizationservice/v1/organizations/442adb6e-fa58-47f3-9ca2-ed1fecdfe86c。其中Zuul服務器通過http://localhost:5555進行訪問。路徑的第一部分organizationservice嘗試調用在Eureka上註冊的邏輯名稱爲organizationservice的服務。
使用服務發現手動映射路由
Zuul允許開發人員更細粒度地明確定義路由映射,而不是單純地依賴服務的Eureka服務ID創建的自動路由,可以通過在Zuul服務上修改application.yml配置文件來手動定義路由映射。如果想要排除掉Eureka服務ID路由的自動映射,只提供自定義的組織服務路由,可以使用ignored-services參數,如果屏蔽掉所有的自動映射,屬性值需設爲‘*’。另外服務網關的一種常見模式是通過使用/api之類的前綴來爲所有服務添加前綴,從而區別API路由和內容路由。如下:

zuul:
  prefix:  /api
  ignored-services: '*'
  routes:
      producer:
        strip-prefix: false
      organizationservice: /organization/**
      licensingservice: /licensing/**

雖然Zuul作爲網關代理確實很靈巧,,但是Zuul的真正威力在於執行一組一致的應用程序,例如:安全性、日誌記錄、服務跟蹤等。因爲開發人員想將這些邏輯應用於所有微服務中,而不是在每個微服務中都要使用切面或者註解。那麼此時Zuul的過濾器就大殺四方了。通過Zuul的過濾器,我們可以自定義自己的邏輯,Zuul的過濾器有三種:

  • 前置過濾器,在請求發送到實際目的地之前被調用,例如常用的判斷Http的請求首部
  • 後置過濾器,將響應返回給客戶端時被調用
  • 路由過濾器,簡單來講,就是通過路由過濾器的請求可以一部分被調用到A服務,一部分調用到B服務,服務升級時,可以使用,一小部分客戶使用升級後的服務作爲測試,大部分客戶使用原服務。
    下面通過一個圖,簡單描述下:
    在這裏插入圖片描述
    下面,我們構建第一個過濾器
    前置過濾器
    功能需求:檢查所有到網關的請求,並判斷是否存在名爲tmx-correlation-id的HTTP首部。這有點類似於日誌追蹤中的GUID,用於跨多個服務跟蹤用戶請求。前置過濾器的代碼如下:
@Component
//所有Zuul過濾器必須擴展ZuulFilter,並覆蓋如下四個方法
public class TrackingFilter extends ZuulFilter{
    private static final int      FILTER_ORDER =  1;
    private static final boolean  SHOULD_FILTER=true;
    private static final Logger logger = LoggerFactory.getLogger(TrackingFilter.class);
    
    //封裝的過濾器類
    @Autowired
    FilterUtils filterUtils;

    /**
     * 告訴Zuul,這是前置過濾器
     * @return
     */
    @Override
    public String filterType() {
        return FilterUtils.PRE_FILTER_TYPE;
    }

    /**
     * 不同類型過濾器的執行順序
     * @return
     */
    @Override
    public int filterOrder() {
        return FILTER_ORDER;
    }

    /**
     * 是否執行該過濾器
     * @return
     */
    public boolean shouldFilter() {
        return SHOULD_FILTER;
    }

    private boolean isCorrelationIdPresent(){
      if (filterUtils.getCorrelationId() !=null){
          return true;
      }

      return false;
    }

    private String generateCorrelationId(){
        return java.util.UUID.randomUUID().toString();
    }

    /**
     * 每次服務調用通過過濾器時,都會調用。檢查首部是否存在,如果不存在,則設置一個
     * @return
     */
    public Object run() {

        if (isCorrelationIdPresent()) {
           logger.debug("tmx-correlation-id found in tracking filter: {}. ", filterUtils.getCorrelationId());
        }
        else{
            filterUtils.setCorrelationId(generateCorrelationId());
            logger.debug("tmx-correlation-id generated in tracking filter: {}.", filterUtils.getCorrelationId());
        }

        RequestContext ctx = RequestContext.getCurrentContext();
        logger.debug("Processing incoming request for {}.",  ctx.getRequest().getRequestURI());
        return null;
    }
}

FilterUtils類用於封裝所有過濾器使用的常用功能,如下:

@Component
public class FilterUtils {

    public static final String CORRELATION_ID = "tmx-correlation-id";
    public static final String AUTH_TOKEN     = "tmx-auth-token";
    public static final String USER_ID        = "tmx-user-id";
    public static final String ORG_ID         = "tmx-org-id";
    public static final String PRE_FILTER_TYPE = "pre";
    public static final String POST_FILTER_TYPE = "post";
    public static final String ROUTE_FILTER_TYPE = "route";

    public String getCorrelationId(){
        RequestContext ctx = RequestContext.getCurrentContext();

        if (ctx.getRequest().getHeader(CORRELATION_ID) !=null) {
            return ctx.getRequest().getHeader(CORRELATION_ID);
        }
        else{
            return  ctx.getZuulRequestHeaders().get(CORRELATION_ID);
        }
    }

    public void setCorrelationId(String correlationId){
        RequestContext ctx = RequestContext.getCurrentContext();
        ctx.addZuulRequestHeader(CORRELATION_ID, correlationId);
    }

    public  final String getOrgId(){
        RequestContext ctx = RequestContext.getCurrentContext();
        if (ctx.getRequest().getHeader(ORG_ID) !=null) {
            return ctx.getRequest().getHeader(ORG_ID);
        }
        else{
            return  ctx.getZuulRequestHeaders().get(ORG_ID);
        }
    }

    public void setOrgId(String orgId){
        RequestContext ctx = RequestContext.getCurrentContext();
        ctx.addZuulRequestHeader(ORG_ID,  orgId);
    }

    public final String getUserId(){
        RequestContext ctx = RequestContext.getCurrentContext();
        if (ctx.getRequest().getHeader(USER_ID) !=null) {
            return ctx.getRequest().getHeader(USER_ID);
        }
        else{
            return  ctx.getZuulRequestHeaders().get(USER_ID);
        }
    }

    public void setUserId(String userId){
        RequestContext ctx = RequestContext.getCurrentContext();
        ctx.addZuulRequestHeader(USER_ID,  userId);
    }

    public final String getAuthToken(){
        RequestContext ctx = RequestContext.getCurrentContext();
        return ctx.getRequest().getHeader(AUTH_TOKEN);
    }

    public String getServiceId(){
        RequestContext ctx = RequestContext.getCurrentContext();

        //We might not have a service id if we are using a static, non-eureka route.
        if (ctx.get("serviceId")==null) return "";
        return ctx.get("serviceId").toString();
    }
}

現在我們啓動網關服務和組織服務,調用下:
在這裏插入圖片描述

後置過濾器
後置過濾器是收集指標並完成與用戶相關聯的日誌記錄的理想場所,下面實現將傳遞給微服務的關聯ID注入回用戶。使用後置過濾器將關聯ID注入HTTP響應首部中。後置過濾器的代碼如下:

@Component
public class ResponseFilter extends ZuulFilter{
    private static final int  FILTER_ORDER=1;
    private static final boolean  SHOULD_FILTER=true;
    private static final Logger logger = LoggerFactory.getLogger(ResponseFilter.class);
    
    @Autowired
    FilterUtils filterUtils;

    @Override
    public String filterType() {
        return FilterUtils.POST_FILTER_TYPE;
    }

    @Override
    public int filterOrder() {
        return FILTER_ORDER;
    }

    @Override
    public boolean shouldFilter() {
        return SHOULD_FILTER;
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();

        logger.debug("Adding the correlation id to the outbound headers. {}", filterUtils.getCorrelationId());
        ctx.getResponse().addHeader(FilterUtils.CORRELATION_ID, filterUtils.getCorrelationId());

        logger.debug("Completing outgoing request for {}.", ctx.getRequest().getRequestURI());

        return null;
    }
}

我們調用下,看下響應中是否存在對應的首部。
在這裏插入圖片描述

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