spring cloud Zuul + 路由熔斷【服務降級】 --- 心得


1.前言

    剛入門 時,使用 ribbon + hystrix + restTemplate  ,實現了簡單的 接口訪問 + 客戶端負載均衡 + 服務熔斷保護 ;

然后學習了 feign ,整合了  ribbon + hystrix + restTemplate  的功能優點 並實現了上面功能 ;

上面的是實現服務與服務之間的服務熔斷保護。

  如今 ,引入了 zuul ,API網關 ,外部用戶統一訪問zuul服務器,網關攔截請求 並作驗證等操作通過后 路由到 指定的 內部 服務器集群 【zuul默認開啟客戶端負載均衡,類似ribbon】,

那么 ,如果 恰好 路由到的指定服務器時剛好宕機或者線程崩潰了怎么辦?或者說當我們的后端服務出現異常的時候,我們不希望將異常拋出給最外層,期望服務可以自動進行降級處理。

這是網關與服務之間的服務熔斷保護

  Zuul給我們提供了這樣的支持。當某個服務出現異常時,直接返回我們預設的信息。【類似於 hystrix 的熔斷處理】

2.准備一個服務提供者,端口 8001

 目錄結構

 

 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>cen.cloud</groupId>
        <artifactId>cen-mycloud</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>provider-8001</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>provider-8001</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!--        spring boot web 組件-->
        <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>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!--eureka 注冊中心依賴包 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>

        <!-- 修改后立即生效,熱部署 -->
        <!-- 熱修改后端-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>springloaded</artifactId>
            <version>1.2.4.RELEASE</version>
        </dependency>
        <!-- 熱修改前端-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <!--            <optional>true</optional>-->
        </dependency>
    </dependencies>

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

</project>
View Code

 

application.properties

# 服務名稱
spring.application.name=provider-8001
# 端口
server.port=8001


#設置與Eureka Server交互的地址查詢服務和注冊服務都需要依賴這個地址,
# [#找不到其他服務注冊中心地址會報錯]
eureka.client.serviceUrl.defaultZone=http://localhost:7001/eureka/
#  ,http://localhost:7002/eureka/
View Code

 

controller層

package com.example.provider8001.controller;


import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class PRController {

    @RequestMapping(value = "/getname",method = RequestMethod.GET)
    public String getname(String name){
        System.out.println("接收名字="+name);
        return "你大爺叫:"+name;
    }

}
View Code

啟動類

package com.example.provider8001;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
//開啟發現服務
@EnableEurekaClient
public class Provider8001Application {

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

}
View Code

 

 3.准備一個服務消費者,端口 9001

目錄結構

 

  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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>cen.cloud</groupId>
        <artifactId>cen-mycloud</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>consumer-9001</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>consumer-9001</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!--        spring boot web 組件-->
        <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>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!--eureka 注冊中心依賴包 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>

        <!-- 修改后立即生效,熱部署 -->
        <!-- 熱修改后端-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>springloaded</artifactId>
            <version>1.2.4.RELEASE</version>
        </dependency>
        <!-- 熱修改前端-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <!--            <optional>true</optional>-->
        </dependency>


        <!--feign依賴包-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!--配置中心-客戶端依賴包-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>


        <!--健康檢測管理中心 ,可刷新配置文件-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!--spring cloud bus,消息總線-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</artifactId>
        </dependency>




    </dependencies>

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

</project>
View Code

 

application.properties 為空

bootstrap.proterties  【配置內容有 消息中間件 rabbitmq 和bus 的配置,如果沒有安裝rabbitmq則不配置】

spring.application.name=consumer-9001
server.port=9001
#
# 當前微服務注冊到eureka中(消費端),可不寫 ,默認為true
#eureka.client.register-with-eureka=true
eureka.client.service-url.defaultZone=http://localhost:7001/eureka/
#
#配置中心客戶端配置
#獲取指定配置文件名稱 ,多個則以英文符號 , 隔開,不可有空格
spring.cloud.config.name=gittest
#獲取配置的策略 , 讀取文件:dev開發環境、test測試、pro生產
spring.cloud.config.profile=dev
#獲取配置文件的分支,默認是master。如果是是本地獲取的話,則無用,
spring.cloud.config.label=master
#開啟配置信息發現
spring.cloud.config.discovery.enabled=true
#指定配置中心服務端的service-id,便於擴展為高可用配置集群,不區分大小寫
spring.cloud.config.discovery.serviceId=config-server-6001
#
#健康檢測管理中心配置
#springboot 1.5.X 以上默認開通了安全認證,這里可加可不加,不影響
#management.security.enabled=false
#springboot 2.x 默認只開啟了info、health的訪問接口,*代表開啟所有訪問接口
management.endpoints.web.exposure.include=*

#
#
## spring cloud bus 刷新配置
##rabbitmq 服務所在ip
#使用 localhost 會出錯 ,使用 127.0.0.1 則沒問題
spring.rabbitmq.host=127.0.0.1
#默認端口 5672
spring.rabbitmq.port=5672
#默認賬戶
spring.rabbitmq.password=guest
#默認密碼
spring.rabbitmq.username=guest
##
##
## 開啟消息跟蹤
spring.cloud.bus.trace.enabled=true
#
#
#feign開啟熔斷器必須加這句話,不然無法使用,直接報500狀態碼
feign.hystrix.enabled=true
#設置連接超時時間
#feign.client.config.default.connect-timeout=10000
#feign.client.config.default.read-timeout=10000
View Code

 

controller層

package com.example.consumer9001.controller;


import com.example.consumer9001.feignInter.FeignService1;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

@RefreshScope
@RestController
public class NameController {
    @Autowired
    private FeignService1 feignService1;

    @RequestMapping(value = "/doname", method = RequestMethod.GET)
    public String doname(String name) {
        System.out.println("接收名字=" + name + "==" + new Date());
        return "我是消費者端口9001,微服務處理結果是:" + feignService1.getname(name);
    }


    @Value("${yourname}")
    private String namestr;

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

        String str = "我是消費者端口9001,獲取遠程配置文件信息:" + namestr + "===" + new Date();
        System.out.println(str);
        return str;
    }

//    http://localhost:9001/getname

}
View Code

 

feign 服務接口

package com.example.consumer9001.feignInter;

import com.example.consumer9001.feignInter.myFallbackFactory.FeignServuce1FallbackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

//注入遠程服務的應用名【不區分大小寫】 ,以及熔斷回調類
@FeignClient(name = "provider-8001", fallbackFactory = FeignServuce1FallbackFactory.class )
public interface FeignService1 {


//    對應遠程服務具體接口的名稱和參數
    @RequestMapping(value = "/getname", method = RequestMethod.GET)
//    需要添加 @RequestParam ,用於糾正參數映射 ,不然會報錯 405 ,
//    feign.FeignException$MethodNotAllowed: status 405 reading
    public String getname(@RequestParam("name") String name);


}
View Code

 

feign 服務降級回路操作

package com.example.consumer9001.feignInter.myFallbackFactory;

import com.example.consumer9001.feignInter.FeignService1;
import feign.hystrix.FallbackFactory;
import org.springframework.stereotype.Component;

import java.util.Date;


/**
 * feign使用斷路器【熔斷器】 ,當熔斷發生后,運行這里的方法。類似於異常拋出
 * 這里主要是處理異常出錯的情況(降級/熔斷時服務不可用,fallback就會找到這里來)
 */
@Component  // 不要忘記添加,不要忘記添加,不加則無法使用熔斷器
public class FeignServuce1FallbackFactory implements FallbackFactory<FeignService1> {
    @Override
    public FeignService1 create(Throwable throwable) {
        return new FeignService1() {
            @Override
            public String getname(String name) {
                //這里寫熔斷后的具體操作邏輯

                return "輸入參數是" + name + ",feign使用了斷路器【熔斷器】,限制服務處於熔斷狀態,運行了類似於拋出異常的方法,時間=" + new Date();
            }
        };
    }
}
View Code

 

feign里的 Ribbon客戶端負載均衡策略設置

package com.example.consumer9001.myconfig;

import com.netflix.loadbalancer.BestAvailableRule;
import com.netflix.loadbalancer.IRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

///注解@Configuration千萬千萬不要忘記添加 ,不然無法使用這個靜態配置類
@Configuration
public class ConfigBean {

    //設置負載均衡策略
    @Bean
    public IRule myRule() {
        //其他看看 https://www.cnblogs.com/htyj/p/10705472.html
        //

        //輪詢策略,其實里面就是一個計數器
//        return new RoundRobinRule();

        //該策略通過遍歷負載均衡器中維護的所有實例,會過濾調故障的實例,並找出並發請求數最小的一個,所以該策略的特征是選擇出最空閑的實例
        //如果集群有個服務器掛了,就可以過略的他,防止訪問了故障服務器
        return new BestAvailableRule();

    }

}
View Code

 

啟動類

package com.example.consumer9001;

import com.example.consumer9001.myconfig.ConfigBean;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
//服務客戶端【發現服務】
@EnableEurekaClient
//@EnableDiscoveryClient ,也可以使用這個
//指定feign接口掃描范圍 ,也可以不寫
@EnableFeignClients(basePackages = {"com.example.consumer9001.feignInter"})
//開啟客戶端負載均衡自定義策略,參數name是該服務器的應用名字 ,configuration設置 策略配置類
@RibbonClient(name = "consumer-9001" ,configuration = ConfigBean.class)
public class Consumer9001Application {

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

}
View Code

 

4.准備一個Zuul 網關 ,端口 5001

目錄結構

 

 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>cen.cloud</groupId>
        <artifactId>cen-mycloud</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>zuul-server-5001</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>zuul-server-5001</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--==========================================================================-->
        <!--eureka 注冊中心依賴包 -->
        <!--        這是服務中心端的依賴包-->
        <!--        <dependency>-->
        <!--            <groupId>org.springframework.cloud</groupId>-->
        <!--            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>-->
        <!--        </dependency>-->
        <!--        可是服務客戶端的依賴包,兩個包都有一樣的功能-發現服務,但是下面這個包無 服務端的注解-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!--==========================================================================-->
        <!--zuul 網關-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>


    </dependencies>

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

</project>
View Code

 

application.properties 

spring.application.name=zuul-server-5001
server.port=5001
#
eureka.client.service-url.defaultZone=http://localhost:7001/eureka/
#全局添加前綴,如 localhost:114/myzuul/test/bb ,用於識別是否需要轉發路由操作
#不可使用 /zuul ,猜測這是保留字
zuul.prefix=/mzuul
#//默認是false,這里是全局配置
#zuul.strip-prefix: //是否將這個代理前綴去掉
#
#忽略所有的,表示禁用默認路由,只認我們自己配置的路由.
#zuul.ignored-services="*"
#
#自定義路由設置
#攔截路徑
zuul.routes.bd.path=/bd/**
#攔截后訪問的指定地址
zuul.routes.bd.url=https://www.baidu.com/
#
##攔截路徑
#zuul.routes.CONSUMER-9001.path=/CONSUMER-9001/**
##攔截后訪問的指定地址
#zuul.routes.CONSUMER-9001.service-id=CONSUMER-9001
#
#攔截后訪問的指定服務,使用服務名,根據注冊中心獲取的服務列表映射具體服務ip地址
#zuul.routes.api-b.service-id=520LOVE


#http://localhost:5001/mzuul/CONSUMER-9001/gettname

# 心得 :如果使用 服務列表默認映射的 攔截路徑 ,則寫在 httpurl 的服務名必須小寫 ,即便 遠程服務名 有大小寫字符 ,
# 但在請求路徑也必須全部改成小寫 ,否則報錯 404
View Code

 

 

自定義攔截文件夾 myFilter 的兩個過濾文件 ,隨筆 有詳細講解   https://www.cnblogs.com/c2g5201314/p/12996687.html

package com.example.zuulserver5001.myFilter;


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.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.UnsupportedEncodingException;

//注冊bean ,不使用@Component注解則需要去啟動類創建一個方法new一個LoginFilter類后
// 使用 @bean注解注冊bean,一樣的作用
@Component
public class LoginFilter extends ZuulFilter {


    /**
     * 選擇過濾器類型
     */
    @Override
    public String filterType() {
        //一共有下面4種過濾器
//        public static final String ERROR_TYPE = "error";
//        public static final String POST_TYPE = "post";
//        public static final String PRE_TYPE = "pre";
//        public static final String ROUTE_TYPE = "route";
        return FilterConstants.PRE_TYPE;
    }

    /**
     * 通過返回的int值來定義過濾器的執行順序,數字越小優先級越高
     */
    @Override
    public int filterOrder() {
        return 0;
    }

    /**
     * 返回一個`Boolean`值,判斷該過濾器是否需要執行。返回true執行,返回false不執行。
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 過濾器的具體業務邏輯
     */
    @Override
    public Object run() throws ZuulException {
        System.out.println("進入zuul攔截-login攔截");
        //獲取上下文
        RequestContext ctx = RequestContext.getCurrentContext();
        //獲取Request
        HttpServletRequest request = ctx.getRequest();
        //獲取請求參數
        String token = request.getParameter("token");
        System.out.println("參數token=" + token);
//
//
        if (StringUtils.isBlank(token)) {
            //參數內容為空
            //攔截,拒絕路由
            ctx.setSendZuulResponse(false);
            //返回狀態碼
            ctx.setResponseStatusCode(401);
            //返回的響應體信息
            try {
                //不可以直接寫中文,前端會顯示中文亂碼,加上這就解決中文亂碼問題
                //以文本格式顯示,字體比較大
                ctx.getResponse().setContentType("text/html;charset=UTF-8");
                //以json格式顯示,字體比較小
//                ctx.getResponse().setContentType("application/json;charset=UTF-8");
//               上一句等同於  ctx.getResponse().setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                //
                ctx.getResponse().getWriter().write("token is 空的-------401");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return null;
    }
}
View Code

 

 

package com.example.zuulserver5001.myFilter;

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.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.logging.Filter;

@Component
public class LoginCheckFilter extends ZuulFilter {
    //上下文,不可以設為 private
    RequestContext ctx = null;
    //Request
    private HttpServletRequest request = null;

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

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

    @Override
    public boolean shouldFilter() {
        System.out.println("進入zuul攔截-loginCheck攔截,判斷是否開啟該攔截");
        //獲取上下文
        ctx = RequestContext.getCurrentContext();
//        判斷上一層攔截是否通過
        if(!ctx.sendZuulResponse()){
            //上一層攔截不通過
            System.out.println(" 上一層攔截不通過,不開啟loginCheck攔截");
            //該攔截不需要開啟
            return false;
        }
        //上層攔截通過
        //獲取Request
        request = ctx.getRequest();
        //獲取請求路徑
        String urlStr = request.getRequestURI().toString();
        //當訪問路徑含有/mzuul/bd/則開啟該攔截
        return urlStr.contains("/mzuul/bd");
    }

    @Override
    public Object run() throws ZuulException {
        System.out.println("運行loginCheck攔截邏輯");
        //獲取請求參數
        String token = request.getParameter("token");
        System.out.println("---參數token=" + token);
        if (StringUtils.isBlank(token) | (token != null && !token.equals("kk"))) {
//            token 是空的 或者 不是 kk
            //攔截
            System.out.println("攔截,拒絕路由請求, token 是空的 或者 不是 kk");
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(7781);
            try {
                ctx.getResponse().setContentType("text/html;charset=UTF-8");
                ctx.getResponse().getWriter().write("請求參數="+token+",當參數是kk才可以通過");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return null;
    }
}
View Code

 

 

路由熔斷后的服務降級回路操作

 

package com.example.zuulserver5001.myProducerFallback;

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;

//注冊bean
@Component
public class Consumer9001Fallback implements FallbackProvider {

    //測試請求1
//    http://localhost:5001/mzuul/consumer-9001?token=kk
    //測試請求2
//    http://localhost:5001/mzuul/consumer-9001/doname?token=kk&name=lili

    //指定要處理的遠程服務
    @Override
    public String getRoute() {
        //必須是小寫,即使 遠程服務名的字符有大寫,這里也必須換成小寫,否則報錯,無法執行服務降級操作,【與http網址一樣,必須小寫】
        return "consumer-9001";
    }

    /**
     * 具體退路操作邏輯
     */
    private ClientHttpResponse fallbackResponse(){
        return new ClientHttpResponse() {
            //返回http狀態
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return HttpStatus.OK;
            }
            //返回狀態碼
            @Override
            public int getRawStatusCode() throws IOException {
                //200是正常
                return 200;
            }
            //返回狀態內容
            @Override
            public String getStatusText() throws IOException {
                return "OK";
            }

            //目前這里不清楚是干什么的
            @Override
            public void close() {

            }
            //返回響應體
            @Override
            public InputStream getBody() throws IOException {
                //以字節流形式返回
                return new ByteArrayInputStream("beng--i can do nothing,崩潰了,11223344".getBytes());
            }

            //返回響應頭
            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders httpHeaders = new HttpHeaders();
                //設置響應數據的編碼類型
                httpHeaders.setContentType(MediaType.APPLICATION_JSON_UTF8);
                return httpHeaders;
            }
        };
    }



    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        if (cause != null && cause.getCause() != null) {
            String reason = cause.getCause().getMessage();
            System.out.println("異常原因:\n"+reason);
//            logger.info("Excption {}",reason);
        }

        return this.fallbackResponse();
    }
}


/*
總結:
(1)zuul使用服務列表的默認映射,那么在網址訪問的時候,在路徑寫遠程服務名時必須字符全小寫【不論遠程服務名字符是否有大寫】,否則找不到服務;
(2)當遠程服務異常,zuul調用該服務的熔斷/降級操作,在回路操作設置時指定的遠程服務名必須字符全小寫【不論遠程服務名字符是否有大寫】,
     否則報錯,無法執行該服務降級操作;
(3)使用 zuul --> 服務消費者 --> 服務提供者  的分布式微服務架構 。
    當提供者出現異常時,消費者對其服務降級,執行回路操作 ;
    但如果是zuul 路由到消費者去調用提供者服務,當提供者出現異常時,則將會執行zuul對消費者的服務降級,執行指定消費者的回路操作,
    【打印原因 java.net.SocketTimeoutException: Read timed out】
    但是過了一段時間后再次訪問zuul 路由到消費者去調用該提供者服務,將會返回消費者對提供者執行的回路操作結果。【很是奇怪,原因還不清楚】
    當消費者出現異常時,那么zuul將會執行對消費者的服務降級,執行該消費者的回路操作;

使用 zuul --> 服務消費者 --> 服務提供者  的分布式微服務架構 。
如果是zuul 路由到消費者去調用提供者服務,當提供者出現異常時,則將會執行zuul對消費者的服務降級,執行指定消費者的回路操作,
但是過了一段時間后再次訪問zuul 路由到消費者去調用該提供者服務,將會返回消費者對提供者執行的回路操作結果。
【很是奇怪,原因還不清楚】
 */
View Code

 

 

 

啟動類

 

package com.example.zuulserver5001;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.netflix.zuul.EnableZuulServer;

@SpringBootApplication
//開啟發現服務
@EnableEurekaClient
//開啟zuul網關代理
@EnableZuulProxy
public class ZuulServer5001Application {

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

}
View Code

 

 

 

5.其他准備

具體配置源碼 這里 省略 ,可參考去我的其他隨筆

提前准備一個 端口 7001 的 服務注冊中心 ,參考隨筆地址   https://www.cnblogs.com/c2g5201314/p/12877948.html

提前准備一個 端口 6001 的 服務配置中心 ,參考隨筆地址  https://www.cnblogs.com/c2g5201314/p/12897753.html

 

 

 

 

6.測試

 (1)依次啟動 的工程端口號 7001 【注冊中心】,6001【配置中心】 ,8001【提供者】,9001【消費者】,5001【Zuul網關】

 

 

 

 

 

 

 

 

 (2)測試目的:檢測服務消費者能否調用服務提供者的服務

訪問端口  9001 , http://localhost:9001/doname?name=tom

 

 

 

 調用成功

 

(3)測試目的:檢測外部訪問Zuul 能否路由到服務消費者 后調用 服務消費者 的服務

訪問端口 5001  , http://localhost:5001/mzuul/consumer-9001/getname?token=gh

 

 

 

調用成功

 

(4)測試目的:檢測外部訪問Zuul 能否路由到服務消費者 后調用 服務提供者 的服務

訪問端口 5001  ,http://localhost:5001/mzuul/consumer-9001/doname?token=kg&name=tom

 

 

 調用成功

 

 (5)測試目的:檢測服務提供者崩潰后,消費者能否服務熔斷后做服務降級回路操作

關閉端口8001 的工程 ,

訪問端口  9001 , http://localhost:9001/doname?name=tom

 

 

 

 回路操作成功

 

 (6)測試目的:檢測服務消費崩潰后,Zuul網關能否路由熔斷后做服務降級回路操作

 重新開啟端口8001的工程  ,然后再關閉端口9001的工程 

訪問端口 5001  ,http://localhost:5001/mzuul/consumer-9001/doname?token=kg&name=tom

 

 

 回路操作成功

 

再次訪問訪問端口 5001  ,http://localhost:5001/mzuul/provider-8001/getname?token=kg&name=tom   ,直接路由的 服務提供者服務 ,正常使用 ,因此回路操作僅局限於指定服務

 

 

(7)測試目的:檢測服務提供者崩潰后,Zuul路由到 服務消費者后調用服務提供者到底會返回 服務消費者的服務降級結果 還是執行了 Zuul 網關對服務消費者路由熔斷后的服務降級回路操作

 重新開啟端口9001的工程  ,然后再關閉端口8001的工程 

訪問端口 5001  ,http://localhost:5001/mzuul/consumer-9001/doname?token=kg&name=tom

 

 顯然 ,服務提供者崩潰后,執行了 Zuul 網關對服務消費者路由熔斷后的服務降級回路操作

查看控制台

 

 顯示異常 java.net.SocketTimeoutException: Read timed out

 

可是,萬萬沒想到 ,過了一段時間后 ,

再次訪問端口 5001  ,http://localhost:5001/mzuul/consumer-9001/doname?token=kg&name=tom

 

 

 竟然 返回 了 服務消費者對服務提供者的服務降級結果

查看控制台,並沒有打印原因

 

 

奇怪,奇怪,真奇怪 !!!!!!目前還沒有找到原因,因為沒有老師,這就是自學的弊端!

 

 

查閱了大量資料,出現這個現象的原因是 Zuul 路器熔斷的超時時間小於遠程服務消費者的feign熔斷超時時間 導致的,

那么在 Zuul 工程 的 application.properties文件加入配置

 

 完整的源碼

spring.application.name=zuul-server-5001
server.port=5001
#
eureka.client.service-url.defaultZone=http://localhost:7001/eureka/
#全局添加前綴,如 localhost:114/myzuul/test/bb ,用於識別是否需要轉發路由操作
#不可使用 /zuul ,猜測這是保留字
zuul.prefix=/mzuul
#//默認是false,這里是全局配置
#zuul.strip-prefix: //是否將這個代理前綴去掉
#
#忽略所有的,表示禁用默認路由,只認我們自己配置的路由.
#zuul.ignored-services="*"
#
#自定義路由設置
#攔截路徑
zuul.routes.bd.path=/bd/**
#攔截后訪問的指定地址
zuul.routes.bd.url=https://www.baidu.com/
#
##攔截路徑
#zuul.routes.CONSUMER-9001.path=/CONSUMER-9001/**
##攔截后訪問的指定地址
#zuul.routes.CONSUMER-9001.service-id=CONSUMER-9001
#
#攔截后訪問的指定服務,使用服務名,根據注冊中心獲取的服務列表映射具體服務ip地址
#zuul.routes.api-b.service-id=520LOVE


#http://localhost:5001/mzuul/CONSUMER-9001/gettname

# 心得 :如果使用 服務列表默認映射的 攔截路徑 ,則寫在 httpurl 的服務名必須小寫 ,即便 遠程服務名 有大小寫字符 ,
# 但在請求路徑也必須全部改成小寫 ,否則報錯 404

#熔斷超時時間設置
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=6000
spring.cloud.loadbalancer.retry.enabled=true
ribbon.ReadTimeout=60000
ribbon.ConnectTimeout=60000
View Code

 

將所有工程重新運行后,再次執行一次本次測試步驟 ,

 

服務提供者崩潰后,直接返回 服務消費者對服務提供者的服務降級結果

 

 

 

 

7.總結

(1)zuul使用服務列表的默認映射,那么在網址訪問的時候,在路徑寫遠程服務名時必須字符全小寫【不論遠程服務名字符是否有大寫】,否則找不到服務;

(2)當遠程服務異常,zuul調用該服務的熔斷/降級操作,在回路操作設置時指定的遠程服務名必須字符全小寫【不論遠程服務名字符是否有大寫】,
否則報錯,無法執行該服務降級操作;

(3)使用 zuul --> 服務消費者 --> 服務提供者 的分布式微服務架構 。
當提供者出現異常時,消費者對其服務降級,執行回路操作 ;
但如果是zuul 路由到消費者去調用提供者服務,當提供者出現異常時,則將會執行zuul對消費者的服務降級,執行指定消費者的回路操作,
【打印原因 java.net.SocketTimeoutException: Read timed out】
但是過了一段時間后再次訪問zuul 路由到消費者去調用該提供者服務,將會返回消費者對提供者執行的回路操作結果。
  出現這個現象的原因是 Zuul 路器熔斷的超時時間小於遠程服務消費者的feign熔斷超時時間 導致的,修改Zuul的熔斷超時時間即可。
當消費者出現異常時,那么zuul將會執行對消費者的服務降級,執行該消費者的回路操作。

 

 

完整的項目我放在了GitHub倉庫 ,在分支 branch5-31

https://github.com/cen-xi/test/tree/branch5-31

 


免責聲明!

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



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