我們做分布式系統,為了不暴露具體的服務,以及實作各種統一處理,常常使用網關來管理接口。SpringCloud分布式系統中常用zuul來實作網關功能。zuul最基本的功能,就是把所有的接口都收到自己這裡,按照規則和負載均衡的配置分發。
zuul實作路由最常用的方法是在屬性檔案properties或者 yml中進行配置。
我們首先建立幾個必要的服務:
1. eureka注冊中心
按照正常方式建立,無需多餘内容。application.yml配置:
server:
port: 8101
eureka:
instance:
hostname: localhost
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka
spring:
application:
name: eureka
2. 兩個微服務user和 student
依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
application.yml
server:
port: 8107
spring:
application:
name: user
eureka:
instance:
hostname: studio.chris.com
client:
service-url:
defaultZone: http://${eureka.instance.hostname}:8101/eureka
和
server:
port: 8108
spring:
application:
name: student
eureka:
instance:
hostname: studio.chris.com
client:
service-url:
defaultZone: http://${eureka.instance.hostname}:8101/eureka
并沒有什麼差别,隻是服務名不同而已。這裡的服務名設計的比較簡單,主要為友善進行服務發現路由的時候使用簡潔。要注意避免歧義,也就是說解析的時候不要出現歧義,就是一個微服務名稱不能是另一個微服務名稱頭部的子字元串,否則可能在某些情況下會導緻路由錯誤。比如stu和student就不合适。
在每個微服務下面寫個接口,如
@RestController
@RequestMapping("/api")
public class StudentController {
@Value("${server.port}")
String port;
@GetMapping("/test")
public String test() {
return "Called test api from student module.port: " + port;
}
}
我們用port做标記來差別不同的執行個體,以驗證zuul給我們做了負載均衡。
3. zuul
pom依賴
<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-netflix-zuul</artifactId>
</dependency>
application.yml
server:
port: 8080
spring:
application:
name: zuul
eureka:
instance:
hostname: studio.chris.com
client:
service-url:
defaultZone: http://${eureka.instance.hostname}:8101/eureka
可以看出yml的配置基本上都是一樣的,無非差別了個端口和服務名稱。而那個端口,在我們進行多執行個體部署的時候又是需要更改的。
啟動類注解
@EnableEurekaClient
@EnableDiscoveryClient
@EnableZuulProxy
好了,所有服務我們已經建構好,現在需要配置zuul
zuul路由的最常見方式實在application.yml檔案中配置
添加:
zuul:
routes:
user-manage:
path: /user/**
serviceId: user
student-manage:
path: /student/**
serviceId: student
user-manage和student-manage是兩個路由,這個名稱隻要差別開就好了。serviceId和path是配對的,比如user-manage的配置,凡是/user開頭的接口調用都會被分發到在注冊中心注冊為user的服務中去均衡調用。
如:請求localhost:8080/user/api/test,結果會出現類似以下這樣的結果
Called test api from user module.port: 8107
這是的zuul也算是配置完成了,我們把各個服務啟動起來測試。注冊中心和zuul啟動起來,其他兩個微服務要啟動多個執行個體,我們在Idea中打開多個終端,運作以下指令:
mvn spring-boot:run -e -Dmaven.test.skip=true -Dserver.port=8211
最後的端口号每次運作的時候都要修改。
我們在浏覽器中打開注冊中心的監控頁面:
http://localhost:8101/
可以看到我們啟動起來的微服務,其中user和student是多執行個體的。

我們通過網關8080調用接口,會發現傳回的字元串端口号在變化,這就是因為負載均衡分發到了不同的執行個體。
其實這種路由還可以這樣配置
zuul:
user: /user/**
student: /student/**
這種鍵值對的方式更簡潔,key為注冊服務名稱,value為比對的路徑表達式。
注意:這種方法最常見,但是也很笨拙,如果我們增加了一個微服務,都要在yml檔案中配置,也就要重新開機網關。但是一般我們不會這樣做,網關不要經常重新開機,配置檔案也不需要經常修改。我們把yml中關于zuul的配置全部删除幹淨。
接着,我們在pom中依賴一個包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
<version>1.3.5.RELEASE</version>
</dependency>
好了,這就完了,既不用配置,也不用寫代碼,直接啟動就好了。就算新添加一個微服務,也就是照常從網關調用就好了。zuul會根據注冊中心中的發現的服務來自動進行路由。我們的zuul就再也不用關了。
最後需要補充的一個點,網關就是個關,大鬼小鬼都要走這裡。它的作用除了路由、負載均衡之外,還有統一處理。由于網關在接口方法調用之前,是以可以做一些路由前的預處理。比如,我們分布式架構中最重視權限,一般的權限驗證都是在微服務中根據自己的需求去做驗證邏輯,但是架構層面統一要求的邏輯可以在網關處理,以節省資源。比如,我們要求所有的網絡請求都要攜帶身份驗證資訊,沒有就不允許進行任何接口調用,我們就可以在zuul過濾器中來實作。
實作方式就是寫一個過濾器,來繼承ZuulFilter
package com.chris.zuul;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Created by Chris Chan
* 2020/3/14 12:52
* Use for:
* Explain:
*/
@Component
@WebFilter(urlPatterns = "/*")
public class ChrisZuulFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
HttpServletResponse response = context.getResponse();
//授權過濾 不帶身份證不讓進場 立即退出
String token = request.getHeader("Authorization");
if (StringUtils.isEmpty(token)) {
try {
response.sendError(401, "Authorization is empty");
} catch (Exception e) {
}
return null;
}
return null;
}
}
這樣我們請求的時候就一定要帶一個Basic或者Bearer類似的Authorization頭資訊,不帶的不再進行路由分發。至于這些頭資訊對不對,可以在這裡進行檢測,也可以放行到微服務中自己進行處理。