Spring Cloud(三) :微服務網關(Zuul)

在一個實際業務當中通常都會調用多個服務接口,而每個服務接口的ip/端口or域名都不一樣,這樣在實際調用中會變得十分繁瑣,而且當服務接口ip/端口or域名修改後,業務系統也需要進行相應的修改,大大增加了開發維護成本,所以一般的做法都是在多個服務接口上游再添加一層,我們通常稱之爲網關。網關能夠實現多種功能,比如反向代理,負載均衡,攔截器。在攔截器中我們還可以實現身份驗證,反網絡爬蟲等等功能。
在Spring Cloud中,可以使用Zuul來實現網關層。
在這裏插入圖片描述
服務調用者向Zuul服務發送調用請求,Zuul服務通過各種filter進行身份驗證,反爬蟲等等操作後,根據配置信息從Eureka服務註冊中心獲取到調用的服務的實際ip/端口等信息,然後將請求發向服務提供者。

PS:本片內容都基於Spring Boot 2.X

這裏繼續在上篇中的項目基礎上進行擴展。
總體爲1個服務註冊中心,1個配置中心,3個服務(serviceI,serviceII,serviceIII),1個網關。其中I,II兩個服務爲不同的服務,剩下的III服務與I服務完全一樣,註冊用的service id一致,只有端口和提供的服務輸出不同(來驗證負載均衡)。
整體代碼下載Spring Cloud Zuul服務示例

一.服務註冊中心

SpringCloudServiceCenter項目繼續維持不變,啓動。(端口8761)


二.配置中心

SpringCloudConfig項目也繼續維持不變,啓動。(端口8091)
同時新建myServiceII-dev.properties和myServiceII-prod.properties(內容和myServiceI對應的相同即可),並向遠程git倉庫推送。


三.服務I

SpringCloudServiceI項目維持不變
service id 爲myServiceI,並添加了路徑/myServiceI,端口爲8762


四.服務II

新建SpringCloudServiceII項目,配置部分與SpringCloudServiceI大致一樣。
service id 爲myServiceII,並添加了路徑/myServiceII,端口爲8763
(1)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>

	<groupId>com.my.serviceII</groupId>
	<artifactId>SpringCloundServiceII</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>SpringCloundServiceII</name>
	<description>com.my.serviceII</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.1.0.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
		<spring-cloud.version>Greenwich.M1</spring-cloud.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-config</artifactId>
		</dependency>
		<!--添加  重試機制 的依賴
   		 因網絡的抖動等原因導致config-client在啓動時候訪問config-server沒有訪問成功從而報錯,
    	希望config-client能重試幾次,故重試機制
    	-->
    	<dependency>
        	<groupId>org.springframework.retry</groupId>
       		<artifactId>spring-retry</artifactId>
    	</dependency>
    	<dependency>
        	<groupId>org.springframework.boot</groupId>
        	<artifactId>spring-boot-starter-aop</artifactId>
    	</dependency>
    	<!-- spring cloud actuator 配置信息刷新 -->
		<dependency>
    		<groupId>org.springframework.boot</groupId>
    		<artifactId>spring-boot-starter-actuator</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>
			<snapshots>
				<enabled>false</enabled>
			</snapshots>
		</repository>
	</repositories>


</project>

(2)application.properties配置

server.servlet.context-path=/myServiceII
server.port=8763
#spring.application.name=myServiceII
spring.application.name=myServiceII
eureka.client.service-url.defautZone=http://serviceCenter:8761/eureka/

#retry
#和重試機制相關的配置有如下四個:
# 配置重試次數,默認爲6
spring.cloud.config.retry.max-attempts=6
# 間隔乘數,默認1.1
spring.cloud.config.retry.multiplier=1.1
# 初始重試間隔時間,默認1000ms
spring.cloud.config.retry.initial-interval=1000
# 最大間隔時間,默認2000ms
spring.cloud.config.retry.max-interval=2000

#spring 2.X actuator  
#http://ip:port/actuator/refresh
management.endpoints.web.exposure.include=refresh,health,info

(3)bootstrap.properties配置

#config
#開啓配置服務發現
spring.cloud.config.discovery.enabled=true
#配置服務實例名稱
spring.cloud.config.discovery.service-id=myConfigServer
#配置文件所在分支
spring.cloud.config.label=master
spring.cloud.config.profile=dev
#配置服務中心
spring.cloud.config.uri=http://localhost:8091/

#啓動失敗時能夠快速響應
spring.cloud.config.fail-fast=true

(4)添加ServiceApiController.java,其實和serviceI的一樣,這裏就是用來模擬另一個服務的接口。

package com.my.serviceII.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping(value="/Api")
public class ServiceApiController {
	@Value("${name}")
	private String name;
	
	@ResponseBody
	@RequestMapping(value="/getInfo")
	public String getInfo() {
		return "serviceII+"+name;
	}
}

(5)啓動項SpringCloundServiceIiApplication.java

package com.my.serviceII;

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

@SpringBootApplication
@EnableEurekaClient
public class SpringCloundServiceIiApplication {

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

五.服務III

實際作爲服務I的副本,當然直接用服務I改個端口號啓動也可以。我這裏是又新建了一個服務III(SpringCloudServiceIII)
內容和服務器基本一致,不同的地方在配置中將端口號修改爲8764
(1)修改application.properties

server.port=8764

(2)修改獲取的配置,改爲dev。
修改bootstrap.properties

spring.cloud.config.profile=dev

(3)修改接口內容
ServiceApiController

package com.my.serviceIII.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RefreshScope
@RequestMapping(value="/Api")
public class ServiceApiController {
	@Value("${name}")
	private String name;
	
	@ResponseBody
	@RequestMapping(value="/getInfo")
	public String getInfo() {
		return "serviceIII+"+name;
	}
}


六.路由網關(Zuul)

新建SpringCloudZuul項目。
(1)pom.xml

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

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

(2)application.properties配置(重點)

spring.application.name=api-gateway
server.port=5555
 
#忽略所有請求,不包括zuul.routes指定的路徑
#zuul.ignored-services=* 
# routes to serviceId 這裏邊是通過serviceid來綁定地址,當在路徑後添加/api-a/   則是訪問service-A對應的服務。
# ** 表示多層級,*表示單層級
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=myServiceI
 
zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.serviceId=myServiceII
 
# routes to url  這裏是綁定具體的ip地址
zuul.routes.api-a-url.path=/api-a-url/**
zuul.routes.api-a-url.url=http://localhost:8762/
 
eureka.client.service-url.defautZone=http://serviceCenter:8761/eureka/

這裏配置當訪問/api-a/**路徑時將會把請求發送到service id爲myServiceI的服務,而上面的服務I和服務III的service id都是myServiceI,所以當訪問該路徑時將會被負載均衡。同時也可以採用zuul.routes.api-a-url.url來配置實際url地址,這裏訪問/api-a-url/**時將會轉發到服務I的接口。

(3)啓動項SpringCloundZuulApplication.java

package com.my.zuul;

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

@SpringBootApplication
@EnableZuulProxy
@EnableEurekaClient
public class SpringCloundZuulApplication {

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

七.驗證

(1)現在啓動3個服務和Zuul網關。
能在註冊界面http://localhost:8761/看到如下情形,可以看到service id 爲myServiceI的服務有2個,分別爲8762(服務I)和8764(服務III)
在這裏插入圖片描述

(2)分別測試下3個服務接口是否能調通。正常情況爲如下輸出
服務I
在這裏插入圖片描述
服務II
在這裏插入圖片描述
服務III
在這裏插入圖片描述


下面開始使用路由網關訪問服務接口,路由網關端口爲5555
(3)負載均衡
多次訪問http://localhost:5555/api-a/myServiceI/Api/getInfo能看到如下兩種輸出
在這裏插入圖片描述
在這裏插入圖片描述

證明負載均衡正常運行。

(4)訪問服務II
在這裏插入圖片描述

(5)上面是通過service id 映射,這裏試試通過url映射的方式訪問
在這裏插入圖片描述
OK,能訪問到服務I。


八.熔斷處理

當路由網關後的微服務宕機或者無響應時,服務調用者卻還在不停的調用服務,每個調用的請求都會超時,久而久之Zuul路由網關就會累積大量的請求,這些又會消耗大量的系統資源,最後導致Zuul路由網關掛掉。所以Zuul提供了一套回退機制,能夠使得出現這類大量請求堆積時,讓系統進行熔斷處理,快速返回給調用者一些信息,從而減輕Zuul路由網關負擔。
這裏有一個坑,大部分介紹Zuul熔斷處理的文章都會提到使用的是 Zuulfallbackprovider接口實現的回退,但是由於版本更替,該接口已經過時,現在所以用的是FallbackProvider接口,二者主要區別如下:

http://www.itmuch.com/spring-cloud/edgware-new-zuul-fallback/
Dalston及更低版本通過實現ZuulFallbackProvider 接口,從而實現回退;
Edgware及更高版本通過實現FallbackProvider 接口,從而實現回退。 在Edgware中:
FallbackProvider是ZuulFallbackProvider的子接口。
ZuulFallbackProvider已經被標註Deprecated ,很可能在未來的版本中被刪除。
FallbackProvider接口比ZuulFallbackProvider多了一個ClientHttpResponse
fallbackResponse(Throwable cause); 方法,使用該方法,可獲得造成回退的原因。

這裏在SpringCloudZuul基礎上進行擴展
(1)添加ServiceFallback.java
在getRoute()方法中填寫需要進行回退處理的服務的service id,例如我寫的是服務I的service id :myServiceI。如果想要讓所有服務都進行回退處理的話就 return "*"

package com.my.zuul.fallback;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;

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 com.netflix.hystrix.exception.HystrixTimeoutException;

/**
 * 
 * zuulfallbackprovider 已過時
 *
 */
@Component
public class ServiceFallback implements FallbackProvider{

	@Override
	public String getRoute() {
		// TODO Auto-generated method stub
		return "myServiceI";//service id ,如果想要支持所有的就return "*" or return null;
	}

	@Override
	public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
		if (cause instanceof HystrixTimeoutException) {
			return response(HttpStatus.GATEWAY_TIMEOUT);
	    } else {
	      return this.fallbackResponse();
	    }
	}

	public ClientHttpResponse fallbackResponse() {
		return this.response(HttpStatus.INTERNAL_SERVER_ERROR);
	}

	private ClientHttpResponse response(final HttpStatus status) {
		return new ClientHttpResponse() {
			
			@Override
			public HttpStatus getStatusCode() throws IOException {
				return status;
			}

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

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

			@Override
			public void close() {
			}

			@Override
			public InputStream getBody() throws IOException {
				String result = "服務不可用,請稍後再試。"+getStatusCode();
				return new ByteArrayInputStream(result.getBytes());
			}

			@Override
			public HttpHeaders getHeaders() {
				// headers設定
				HttpHeaders headers = new HttpHeaders();
				MediaType mt = new MediaType("application", "json", Charset.forName("UTF-8"));
				headers.setContentType(mt);
				return headers;
			}
	    };
	}
}

然後啓動註冊中心,配置中心,服務II,網關。
通過網關訪問服務I和III http://localhost:5555/api-a/myServiceI/Api/getInfo
然後也可以通過調用getStatusCode()這些方法來返回具體出錯的原因。而在ZuulFallbackProvider接口中是不提供具體錯誤信息返回的,這也是ZuulFallbackProvider過時的原因。然後訪問服務II,應該是可以訪問的。
在這裏插入圖片描述


九.ZuulFilter過濾器

通常可以使用過濾器來進行身份驗證,反爬蟲等操作。
身份驗證一般來說在服務調用方都會發送一個token過來,然後就可以使用攔截器來效驗該token了,比如jwt驗證框架
ZuulFilter使用方式
新建IdentityVerificationFilter.java

package com.my.zuul.filter;

import javax.servlet.http.HttpServletRequest;

import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;

@Component
public class IdentityVerificationFilter extends ZuulFilter{

	@Override
	public boolean shouldFilter() {
		// TODO Auto-generated method stub
		return true;
	}

	@Override
	public Object run() throws ZuulException {
		// TODO Auto-generated method stub
		System.out.println("my filter");
		
		RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
 
        Object token = request.getParameter("token");
 
        //校驗token
        if (token == null) {
            //"token爲空,禁止訪問!"
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            return null;
        } else {
            //TODO 根據token獲取相應的登錄信息,進行校驗(略)
        }
 
        return null;
	}

	@Override
	public String filterType() {
		// TODO Auto-generated method stub
		return FilterConstants.PRE_TYPE;
	}

	@Override
	public int filterOrder() {
		// TODO Auto-generated method stub
		return 0;
	}

}

然後啓動註冊中心,配置中心,服務I,網關。
訪問http://localhost:5555/api-a/myServiceI/Api/getInfo
從控制檯可以看到輸出
在這裏插入圖片描述
網頁上訪問爲401
在這裏插入圖片描述

然後我們使用http://localhost:5555/api-a/myServiceI/Api/getInfo?token=123訪問
在這裏插入圖片描述
就能訪問了。當然具體的token效驗規則還要看你的選型。

還有一種就是後面的微服務使用了spring security中的basic Auth(即:不允許匿名訪問,必須提供用戶名、密碼),也可以在Filter中處理。
可以這樣使用,修改run() 方法

	public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
         ......
        //添加Basic Auth認證信息
        ctx.addZuulRequestHeader("Authorization", "Basic " + getBase64Credentials("app01", "*****"));
 
        return null;
    }

整體代碼下載Spring Cloud Zuul服務示例

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