在一個實際業務當中通常都會調用多個服務接口,而每個服務接口的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服務示例