玩轉Spring Cloud之API網關(zuul)

最近由於工做緣由,一直沒有空寫文章,因此都是邊忙項目,邊利用空閒時間,週末時間學習總結,最終在下班回家後加班加點寫完本篇文章,如有不足之處,還請諒解,謝謝!html

本文內容導航:java

1、網關的做用

2、網關與ESB的區別

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

  2.1.建立zuul api gateway server空項目git

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

  2.3.集成加入到 Eureka 註冊中心,實現集羣高可用web

  2.4.配置經過serviceid進行路由spring

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

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

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

  2.8.進階用法:經過從新註冊ZuulProperties並指明從config server(配置中心)來得到路由配置信息後端

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

1、網關的做用

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

  網關是把原來多個服務之間多對多的調用關係變爲多對一的調用關係,一般用於向客戶端或者合做夥伴應用提供統一的服務接入方式;

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

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

2、網關與ESB的區別

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

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

3、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 多模塊項目,網上大把教程,好比:http://www.javashuo.com/article/p-nectskpe-gg.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 實現限流:http://www.javashuo.com/article/p-majevaqa-gw.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

相關文章
相關標籤/搜索