SpringCloud(3) :微服務閘道器(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;
}