玩轉Spring Cloud之API網關(zuul)


最近因為工作原因,一直沒有空寫文章,所以都是邊忙項目,邊利用空閑時間,周末時間學習總結,最終在下班回家后加班加點寫完本篇文章,若有不足之處,還請諒解,謝謝!

本文內容導航:

一、網關的作用

二、網關與ESB的區別

三、zuul網關組件應用示例說明

  2.1.創建zuul api gateway server空項目

  2.2.配置通過url進行路由,演示最簡單模式

  2.3.集成加入到 Eureka 注冊中心,實現集群高可用

  2.4.配置通過serviceid進行路由

  2.5.自定義繼承自ZuulFilter的AuthFilter過濾器,進行鑒權

  2.6.自定義實現FallbackProvider接口的RemoteServiceFallbackProvider熔斷降級提供者類,以便當下游API不可用時可以進行熔斷降級處理

  2.7.進階用法:通過自定義實現RefreshableRouteLocator的CustomRouteLocator動態路由定位器類,以實現可靈活動態管理路由(路由存儲在DB中)

  2.8.進階用法:通過重新注冊ZuulProperties並指明從config server(配置中心)來獲得路由配置信息

  2.9.服務之間通過zuul網關調用

一、網關的作用

  網關就好比古代城門,所有的出入口都從指定的大門進出,大門有士兵把守,禁止非法進入城內,確保進出安全;在設計模式中有點類似門面模式;

  網關是把原來多個服務之間多對多的調用關系變為多對一的調用關系,通常用於向客戶端或者合作伙伴應用提供統一的服務接入方式;

  網關提供統一的身份校驗、動態路由、負載均衡、安全管理、統計、監控、流量管理、灰度發布、壓力測試等功能

  更多作用和說明可參見:http://www.ityouknow.com/springcloud/2017/06/01/gateway-service-zuul.html

二、網關與ESB的區別

  ESB(企業服務總線):可以提供比傳統中間件產品更為廉價的解決方案,同時它還可以消除不同應用之間的技術差異,讓不同的應用服務器協調運作,實現了不同服務之間的通信與整合。從功能上看,ESB提供了事件驅動和文檔導向的處理模式,以及分布式的運行管理機制,它支持基於內容的路由和過濾,具備了復雜數據的傳輸能力,並可以提供一系列的標准接口。(摘要百度百科)

  ESB簡單講就是可以進行:系統集成,協議轉換,路由轉發,過濾,消費服務等,相關服務可能會依賴耦合ESB,而API網關相比ESB比較輕量簡單,可能大部份功能API網關也具備,但API網關通常使用REST風格來實現,故服務提供方、消費方可能不知道有API網關的存在。具體可參考:在微服務架構中,我們還需要ESB嗎?

三、zuul網關組件應用示例說明

  2.1.創建zuul api gateway server空項目

   首先通過IDEA spring initializer【也有顯示為:Spring Assistant】(或直接通過https://start.spring.io/)創建一個spring boot項目(demo項目命名:zuulapigateway),創建過程中選擇:zuul依賴,生成項目后的POM XML文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cn.zuowenjun.cloud</groupId>
    <artifactId>zuulapigateway</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>zuulapigateway</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.RELEASE</spring-cloud.version>
    </properties>

    <dependencies>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
    </repositories>

</project>

  然后添加bootstrap.yml(或application.yml)配置文件,這里使用bootstrap.yml,是因為示例代碼后面要使用coud config client實現動態獲取路由配置,這種模式下為了能夠初始化配置數據,必需放在bootstrap.yml文件中。配置如下內容:

server:
  port: 1008

spring:
  application:
    name: zuulapigateway

  最后在spring boot啟動類上(ZuulapigatewayApplication)添加@EnableZuulProxy注解即可,比較簡單就不貼出代碼了。這樣就可以啟動運行了,一個最簡單最基本的zuul網關實例就跑起來了。由於現在沒有配置任何route轉發路由,故無法直接驗證結果,下面就分幾種情況進行演示說明。

  2.2.配置通過url進行路由,演示最簡單模式

  在bootstrap.yml配置文件中添加zuul routes配置,這里我們直接配置最簡單的方式(path->url:即當訪問zuul 指定路徑則直接轉發到對應的URL上),配置如下:

#配置zuul網關靜態路由信息
zuul:
  routes:
    zwj: #直接path到URL路由(注意:URL模式不會觸發網關的Fallback,參考:https://blog.csdn.net/qq_41293765/article/details/80911414)
      path: /**
      url: http://www.zuowenjun.cn/

  配置后,啟動運行網關項目,在瀏覽器中訪問:http://localhost:1008/,會發現顯示的內容是http://www.zuowenjun.cn/的首頁內容。這說明網關路由轉發功能已生效。

  2.3.集成加入到 Eureka 注冊中心,實現集群高可用

  雖然2.2中我們實現了簡單的path->url的路由轉發,但實際生產中,我們不可能只有一個zuul網關實例,因為網關是所有服務消費者的統一入口,如果網關掛掉了,那們就無法請求后端的服務提供者,故必需是集群高可用的,而實現集群高可用,最簡單的方式就是部署多個zuul網關實例,並注冊到注冊中心,這樣當某一個zuul網關實例出問題,還會有其它zuul網關實例進行服務,不影響系統正常運行。集成加入到 Eureka 注冊中心很簡單,如果不清楚可以查看我之前的文章《玩轉Spring Cloud之服務注冊發現(eureka)及負載均衡消費(ribbon、feign)》,這里還是簡要說明一下:

  首先在POM XML中添加eureka-client依賴,maven依賴如下:

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

  然后在bootstrap.yml配置文件中添加eureka client相關配置,如下:

#配置連接到注冊中心,目的:1.網關本身的集群高可用;2.可以獲得所有已注冊服務信息,可以通過path->serviceId進行路由
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8800/eureka/

  最后在spring boot啟動類上(ZuulapigatewayApplication)添加@EnableDiscoveryClient注解即可。我們可以把這個zuul網關項目的端口號改成不同的依次啟動多個,這樣我們就可以在eureka server上看到多個zuul網關實例注冊信息,集群也就搭建好了,這里就不貼圖了。【注意:啟動zuul網關項目前,請務必請正常開啟eureka server項目(eurekaserver,這里直接使用之前文章中示例的eureka server項目)

  2.4.配置通過serviceid進行路由

   在2.2中通過直接配置path->url實現路由轉發,無需注冊中心,雖然簡單但因為寫死了URL也就失去的靈活性,故在微服務場景中我們更多的是通過服務ID進行識別與路由,這樣會相比URL靈活很多,至少不用管service URL,由zuul通過注冊中心動態獲取serviceId對應的url,這樣后續如果url更改了都不用改動zuul網關,是不是比較爽。

  只要我們集成了注冊中心后,就配置了默認的路由規則:/{serviceid}/**,這樣我們若需訪問某個微服務(前提是訪問的微服務項目必需也加入到eureka 注冊中心),直接按照這個path格式來即可,比如訪問一個服務:http://localhost:1086/demo/message/zuowenjun。

  為了便於演示本文服務提供者、服務消費者、服務網關,故我重新編寫了一個基於IDEA多模塊(多項目)的父項目(demo-microservice-parent),項目結構如下圖示:

  

  項目簡要說明:

  demo-microservice-parent是父POM項目,僅提供POM依賴管理與繼承,packaging類型為POM,目的是:所有子項目只需按需添加maven依賴即可,且無需指定version,統一由父POM管理與配置。

  testservice-api是controller接口定義項目,之所以單獨定義,是因為考慮到服務提供者需要實現API接口以提供服務,而服務消費者也需要繼承及實現該API接口從而可以最終實現FeignClient代理接口及熔斷降級回調實現類,避免重復定義接口。

  testservice-provider是服務提供者,實現testservice-api接口

  testservice-consumer是服務消費者,繼承及實現testservice-api接口,以便可以遠程調用testservice-provider的API

   至於如何創建IDEA 多模塊項目,網上大把教程,比如:https://www.cnblogs.com/tibit/p/6185704.html,故我不再復述了。

   這里我們先通過zuul網關請求訪問testservice-provider,默認路由(/{serviceid}/**)如:http://localhost:1008/testservice/demo/message/zuowenjun ,就出現testservice-provider的接口響應的內容,與下面指定path->serviceId相同(因為最終都是請求到同一個服務接口,只是zuul網關的入口地址不同而矣),配置path->serviceId如下:

zuul:
  routes:
    testservice: #通過path到指定服務ID路由(服務發現)
      path: /test/**
      serviceId: testservice

當再次通過zuul網關請求訪問testservice-provider,路由(/test/**),如:http://localhost:1008/test/demo/message/zuowenjun,最終響應結果如下:

 

  2.5.自定義繼承自ZuulFilter的AuthFilter過濾器,進行鑒權

   網關的作用之一就是可以實現統一的身份校驗(簡稱:鑒權),這里采取過濾器來實現當請求網關時,從請求頭上獲得token並進行驗證,驗證通過才能正常路由轉發,否則報401錯誤;代碼實現很簡單如下:

package cn.zuowenjun.cloud;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

/**
 * 自定義token驗證過濾器,實現請求驗證
 */
@Component
public class AuthFilter extends ZuulFilter {

    private static final Logger logger= LoggerFactory.getLogger(AuthFilter.class);

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;//路由執行前
    }

    @Override
    public int filterOrder() {
        return 0;//過濾器優先順序,數字越小越先執行
    }

    @Override
    public boolean shouldFilter() {
        if(RequestContext.getCurrentContext().getRequest().getRequestURL().toString().contains("/testgit/")){
            return false;
        }
        return true;//是否需要過濾
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

        Object token = request.getHeader("token");
        //校驗token
        Boolean isValid=false;

        if (StringUtils.equals(String.valueOf(token),"zuowenjun.cn.zuul.token.888888")){ //模擬TOKEN驗證,驗證通過
            isValid=true;
        }

        if (!isValid) {
            logger.error("token驗證不通過,禁止訪問!");
            ctx.setSendZuulResponse(false);//false表示不發送路由響應給消費端,即不會去路由請求后端服務
            ctx.getResponse().setContentType("text/html;charset=UTF-8");
            ctx.setResponseBody("token驗證不通過,禁止訪問!");
            ctx.setResponseStatusCode(401);
            return null;
        }

        logger.info(String.format("token is %s", token));

        return null;
    }
}

因為AuthFilter類上添加了@Component注解,這樣在Spring boot啟動時,會自動注冊到Spring IOC容器中並被zuul框架所使用,關於zuul過濾器的知識,可參見:https://www.jianshu.com/p/ff863d532767

 當通過zuul網關訪問接口時,如:http://localhost:1008/test/demo/numbers/1/15,因為有AuthFilter過濾器,而且請求時並沒有傳入正確的token,結果被攔截並報401錯誤,如下圖示:

當在請求頭上加入正確的token后,再次重試訪問zuul網關接口,就能正常的返回結果了,如下圖示:

  2.6.自定義實現FallbackProvider接口的RemoteServiceFallbackProvider熔斷降級提供者類,以便當下游API不可用時可以進行熔斷降級處理

   當zuul網關路由轉發請求下游服務時,如果下游服務不可用(報錯)或不可達(請求或響應超時等),那么就會出現服務無法被正常消費,這在分布式系統中是常見的,因為網絡是不可靠的,無法保證100%高可用,那么當網關路由轉發請求下游服務失敗時,應該采取必要的降級措施,以盡可能的提供替代方案保證服務可用。這里采用自定義實現FallbackProvider接口的RemoteServiceFallbackProvider熔斷降級提供者類,這個與微服務中使用的Hystrix熔斷降級是同樣的原理,zuul網關內部也默認集成了Hystrix、Ribbon,實現代碼如下:

package cn.zuowenjun.cloud;


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * 遠程服務熔斷降級回調提供者類
 */
@Component
public class RemoteServiceFallbackProvider implements FallbackProvider {

    private Logger logger = LoggerFactory.getLogger(RemoteServiceFallbackProvider.class);

    @Override
    public String getRoute() {
        return "*";//指定熔斷降級回調適用的服務名稱,*表示所有都適用,否則請指定適用的serviceId
    }

    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {

        logger.warn(String.format("route:%s,exceptionType:%s,stackTrace:%s", route, cause.getClass().getName(), cause.getStackTrace()));
        return new ClientHttpResponse() {
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return HttpStatus.OK;
            }

            @Override
            public int getRawStatusCode() throws IOException {
                return HttpStatus.OK.value();
            }

            @Override
            public String getStatusText() throws IOException {
                return HttpStatus.OK.getReasonPhrase();
            }

            @Override
            public void close() {

            }

            @Override
            public InputStream getBody() throws IOException {
                return new ByteArrayInputStream(("服務不可用,原因:" + cause.getMessage()).getBytes());
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_JSON);
                return headers;
            }
        };
    }
}

與zuul過濾器原理類似,在RemoteServiceFallbackProvider上添加了@Component注解,這樣在Spring boot啟動時,會自動注冊到Spring IOC容器中並被zuul框架所使用,當出現路由轉發請求下游服務失敗時就會返回降級處理的內容,如下圖所示:

  2.7.進階用法:通過自定義實現RefreshableRouteLocator的CustomRouteLocator動態路由定位器類,以實現可靈活動態管理路由(路由存儲在DB中)

   前面介紹了在zuul網關項目的配置文件bootstrap.yml中配置路由轉發規則,比如:path->url,path->serviceId,顯然path->serviceId會靈活一些,而且只有這樣才會用上負載均衡及熔斷降級,但如果隨着微服務項目越來越多,每次都得改zuul網關的配置文件而且還得重啟項目,這樣簡值是要命的,故這里分享采取自定義實現RefreshableRouteLocator的CustomRouteLocator動態路由定位器類,以實現可靈活動態管理路由(路由存儲在DB中),並配合RefreshRouteService類(發布刷新事件通知,當DB中的配置改變后,應該調用RefreshRouteService.refreshRoute方法即可完成自動刷新路由配置信息,無需重啟項目,實現原理可參考:https://github.com/lexburner/zuul-gateway-demo

package cn.zuowenjun.cloud;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cloud.netflix.zuul.filters.RefreshableRouteLocator;
import org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * 自定義動態路由定位器
 * Refer https://github.com/lexburner/zuul-gateway-demo
 */
@Component
public class CustomRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {

    public final static Logger logger = LoggerFactory.getLogger(CustomRouteLocator.class);

    private JdbcTemplate jdbcTemplate;
    private ZuulProperties properties;

    @Autowired
    public CustomRouteLocator(ServerProperties server, ZuulProperties properties, JdbcTemplate jdbcTemplate) {
        super(server.getServlet().getContextPath(), properties);
        this.properties = properties;
        this.jdbcTemplate = jdbcTemplate;

        logger.info("servletPath:{}",server.getServlet().getContextPath());
    }


    @Override
    public void refresh() {
        super.doRefresh();
    }

    @Override
    protected Map<String, ZuulProperties.ZuulRoute> locateRoutes() {
        LinkedHashMap<String, ZuulProperties.ZuulRoute> routesMap = new LinkedHashMap<>();

        //先后順序很重要,這里優先采用DB中配置的路由映射信息,然后才使用本地文件路由配置
        routesMap.putAll(locateRoutesFromDB());
        routesMap.putAll(super.locateRoutes());

        LinkedHashMap<String, ZuulProperties.ZuulRoute> values = new LinkedHashMap<>();
        for (Map.Entry<String, ZuulProperties.ZuulRoute> entry : routesMap.entrySet()) {
            String path = entry.getKey();
            if (!path.startsWith("/")) {
                path = "/" + path;
            }
            if (StringUtils.isNotBlank(this.properties.getPrefix())) {
                path = this.properties.getPrefix() + path;
                if (!path.startsWith("/")) {
                    path = "/" + path;
                }
            }
            values.put(path, entry.getValue());
        }

        return values;
    }

    @Cacheable(value = "locateRoutes",key = "RoutesFromDB",condition ="true")
    public Map<String, ZuulProperties.ZuulRoute> locateRoutesFromDB(){
        Map<String, ZuulProperties.ZuulRoute> routes = new LinkedHashMap<>();
        List<CustomZuulRoute> results = jdbcTemplate.query("select * from zuul_gateway_routes where enabled =1 ",new BeanPropertyRowMapper<>(CustomZuulRoute.class));

        for (CustomZuulRoute result : results) {
            if(StringUtils.isBlank(result.getPath())
                    || (StringUtils.isBlank(result.serviceId) && StringUtils.isBlank(result.getUrl()))){
                continue;
            }

            ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute();
            try {
                BeanUtils.copyProperties(result,zuulRoute);
            } catch (Exception e) {
                logger.error("load zuul route info from db has error",e);
            }
            routes.put(zuulRoute.getPath(),zuulRoute);
        }

        return routes;
    }


    public static class CustomZuulRoute {
        private String id;
        private String path;
        private String serviceId;
        private String url;
        private boolean stripPrefix = true;
        private Boolean retryable;

        public String getId() {
            return id;
        }

        public void setId(String id) {
            this.id = id;
        }

        public String getPath() {
            return path;
        }

        public void setPath(String path) {
            this.path = path;
        }

        public String getServiceId() {
            return serviceId;
        }

        public void setServiceId(String serviceId) {
            this.serviceId = serviceId;
        }

        public String getUrl() {
            return url;
        }

        public void setUrl(String url) {
            this.url = url;
        }

        public boolean isStripPrefix() {
            return stripPrefix;
        }

        public void setStripPrefix(boolean stripPrefix) {
            this.stripPrefix = stripPrefix;
        }

        public Boolean getRetryable() {
            return retryable;
        }

        public void setRetryable(Boolean retryable) {
            this.retryable = retryable;
        }
    }
}

  上述代碼重點關注:locateRoutesFromDB方法,這個方法主要就是完成從zuul_gateway_routes表中查詢配置信息,並加入到routesMap中,這樣本地路由配置與DB路由配置結合在一起,相互補。表結構(表字段與ZuulProperties.ZuulRoute屬性名保持相同)如下圖示:

另外代碼中有使用到spring cache注解,以免每次都查詢DB,需要在POM XML中添加spring-boot-starter-cache maven依賴(並在spring boot啟動類添加@EnableCaching),同時既然用到了DB查詢路由配置,肯定也需要添加jdbc+mssql相關maven依賴項,具體配置如下:

        <!--添加CACHE依賴,以便可以實現注解CACHE-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>

        <!--添加cloud config client依賴,實現從config server取zuul配置-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>

        <dependency>
        <groupId>com.microsoft.sqlserver</groupId>
        <artifactId>mssql-jdbc</artifactId>
        </dependency>

RefreshRouteService類代碼如下:

package cn.zuowenjun.cloud;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.RoutesRefreshedEvent;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.context.ApplicationEventPublisher;

/**
 * 刷新路由服務(當DB路由有變更時,應調用refreshRoute方法)
 */
public class RefreshRouteService {

    @Autowired
    ApplicationEventPublisher publisher;

    @Autowired
    RouteLocator routeLocator;

    public void refreshRoute() {
        RoutesRefreshedEvent routesRefreshedEvent = new RoutesRefreshedEvent(routeLocator);
        publisher.publishEvent(routesRefreshedEvent);
    }

}

  我們通過zuul網關請求訪問服務testservice-provider,采用DB中的路由配置(如:/testsrv/msg/*),如:http://localhost:1008/testsrv/msg/zuowenjun,響應結果如下圖示:

采用DB中的另一個路由配置(如:/testx/**)訪問另一個服務接口,如:http://localhost:1008/testx/demo/numbers/1/10,響應結果如下圖示:

  2.8.進階用法:通過重新注冊ZuulProperties並指明從config server(配置中心)來獲得路由配置信息

   除了2.7中使用DB作為存儲路由配置的介質,我們其實還可以采用config server(配置中心)來實現,這里使用spring cloud config(使用git作為配置存儲介質,當然使用其它介質也可以,如:SVN,server端本地配置文件等形式,具體可參見該系列上一篇文章),我們先在github指定目錄創建創建一個配置文件(目錄位置:https://github.com/zuowj/learning-demos/master/config/zuulapigateway-dev.yml),路由配置內容如下:

zuul:
  routes:
    test-fromgit:
      path: /testgit/**
      serviceId: testservice

然后啟動該系列上一篇文章中所用到的spring cloud config server示例項目(demo-configserver),保證config server正常啟動;

最后在zuul網關項目 POM XML添加spring confg client依賴項:(其實看上篇文章就可以,這里算再次說明以便鞏固吧)

 <!--添加cloud config client依賴,實現從config server取zuul配置-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>

  在spring boot啟動類添加重新注冊ZuulProperties的方法zuulProperties(單獨使用config文件也是可以的,這里只是圖簡單),注意由於ZuulProperties默認就被注冊了,故這里必需顯式加上:@Primary,以表示優先使用該方法注冊bean,代碼如下:

package cn.zuowenjun.cloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.cloud.netflix.zuul.filters.discovery.PatternServiceRouteMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;

@EnableZuulProxy
@EnableDiscoveryClient
@EnableCaching
@SpringBootApplication
public class ZuulapigatewayApplication {

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

    /**
     * 實現從config server獲取配置數據並映射到ZuulProperties中
     * @return
     */
    @Bean
    @Primary
    @RefreshScope
    @ConfigurationProperties("zuul")
    public ZuulProperties zuulProperties(){
        return new ZuulProperties();
    }

    /**
     * 實現自定義serviceId到route的映射(如正則映射轉換成route)
     * @return
     */
    @Bean
    public PatternServiceRouteMapper serviceRouteMapper() {
        //實現當serviceId符合:服務名-v版本號,則轉換成:版本號/服務名,如:testservice-v1-->1/testservice
        return new PatternServiceRouteMapper("(?<name>^.+)-(?<version>v.+$)", "${version}/${name}");
    }

}

如上代碼zuulProperties方法上還添加了@RefreshScope注解,表示可以實現配置變更后自動刷新,當然這里只是僅僅添加了這個注解而矣,並沒有實現自動刷新,實現自動刷新配置相對較復雜,大家可以查看我上一篇講spring cloud config文章,里面有介紹方法及參考文章,當然如果想要更好的配置中心中間件,個人認為攜程的Apollo config還是不錯的,大家可以自行上網查詢相關資料。另外還有個serviceRouteMapper方法,這個是可以實現自定義serviceId到route的映射(如正則映射轉換成route)

我們通過zuul網關請求訪問服務testservice-provider,采用config server中的路由配置(如:/testgit/**),如:http://localhost:1008/testgit/demo/numerbs/10/20,響應結果如下圖示:

 

特別說明:三種路由配置均可同時並存,相互補充。

 

  2.9.服務之間通過zuul網關調用

   上面都是演示直接通過zuul網關對應的路由請求服務接口,而實際情況下,可能是兩個微服務項目之間調用,雖說也可以直接使用httpClient來直接請求zuul網關消費服務,但在spring cloud中一般都是使用FeignClient作為遠程服務代理接口來實現的,以前是FeignClient注解上指定servierName即可,那么如果要連接zuul網關該如何處理呢?目前有二種方法:

  第一種:FeignClient的name仍然指向要消費者的服務名,然后url指定zuul網關路由url,類似如下:(優點是:原來的接口定義都不用變,只需增加url,缺點是:寫死了網關的url,生產中不建議使用)

@FeignClient(name = "testservice",url="http://localhost:1008/testservice",fallback =DemoRemoteService.DemoRemoteServiceFallback.class )

  第二種:FeignClient的name指向網關的名字(即把網關當成統一的服務入口),無需再指定url,然后接口中的RequestMapping的value應加上遠程調用服務的名字,再正常加后面的url(優點是:直接依賴zuul網關,沒有寫死Url,缺點是:破環了api接口 url的請求地址,不利於框架整合,就目前demo-microservice-parent項目中api為獨立接口項目,這種情況則不太適合,只適合單獨定義遠程服務調用接口 )

@FeignClient(name = "zuulapigateway",fallback =DemoRemoteService.DemoRemoteServiceFallback.class )
public interface DemoRemoteService extends DemoService {

@RequestMapping(value = "/testservice/demo/message/{name}")
    String getMessage(@PathVariable("name") String name);

}

可能還有其它方式,但由於時間精力有限,可能暫時無法研究那么深,如果大家有發現其它方式可以下方評論交流,謝謝!這樣改造后,其它代碼都不用變就實現了服務之間通過zuul網關路由轉發請求服務API。

 最后補充說明關於zuul重試實現方法:

1.在zuul網關項目添加spring-retry依賴項,如下:

 <!--添加重試依賴,使zuul支持重試-->
        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
        </dependency>

2.在zuul網關項目bootstrap.yml配置添加重試相關的參數:

zuul:
   retryable: true

   ribbon:
    #  ribbon重試超時時間
    ConnectTimeout: 250
    #  建立連接后的超時時間
    ReadTimeout: 1000
    #  對所有操作請求都進行重試
    OkToRetryOnAllOperations: true
    #  切換實例的重試次數
    MaxAutoRetriesNextServer: 2
    #  對當前實例的重試次數
    MaxAutoRetries: 1

如上配置后,當我們通過zuul網關請求某個服務時,若請求服務失敗時,則會觸發重試請求,直到達到配置重試參數的上限后,才會觸發熔斷降級處理的結果。本示例中我把testservice-provider的getMessage方法額外增加sleep,確保請求超時,以模擬服務異常,同時在請求時打印請求信息,通過調試可以看到,當zuul網關路由轉發請求該服務API時,由於響應超時,導致重試兩次,最終返回熔斷降級處理的結果。API重試兩次記錄如下圖示:

 

zuul其它相關知識要點還未涉及到的,可以參考如下相關文章:

Zuul 實現限流:https://www.cnblogs.com/tiancai/p/9623063.html

Zuul 超時、重試、並發參數設置:https://blog.csdn.net/xx326664162/article/details/83625104

 


 

本文示例相關項目代碼已上傳到GITHUB,具體如下:

demo-zuulapigateway(zuul網關項目):  https://github.com/zuowj/learning-demos/tree/master/java/demo-zuulapigateway   

demo-microservice-parent(多模塊(服務提供者、服務消費者)項目): https://github.com/zuowj/learning-demos/tree/master/java/demo-microservice-parent

demo-eurekaserver(eurea config server):  https://github.com/zuowj/learning-demos/tree/master/java/demo-eurekaserver

demo-configserver(cloud config server): https://github.com/zuowj/learning-demos/tree/master/java/demo-configserver


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM