書接上回:
SpringCloud專題之一:Eureka
Spring Cloud專題之二:OpenFeign
Spring Cloud專題之三:Hystrix
經過前面三章對Spring Cloud的基本組件的介紹,我們可以構建一個簡單的微服務架構系統了,比如,通過使用Spring Cloud Eureka實作高可用的服務注冊中心以及實作微服務的注冊與發現;通過Spring Cloud OpenFeign 實作服務間負載均衡的介面呼叫;同時,為了使分布式系統更為健壯,對于依賴的服務呼叫使用SpringCloud Hystrix來進行包裝,實作執行緒隔離并加入熔斷機制,以避免在微服務架構中因個別服務出現例外而引起級聯故障蔓延,
上面的架構實作系統功能是完全沒有問題,但是還可以進一步思考,這樣的架構還有不足的地方會使運維人員或開發人員感到很痛苦,
? 首先,我們從運維人員的角度來看看,他們平時都需要做一些什么作業來支持這樣的架構,當客戶端應用單擊某個功能的時候往往會發出一些對微服務獲取資源的請求到后端,這些請求通過F5、Nginx等設施的路由和負載均衡分配后,被轉發到各個不同的服務實體上,而為了讓這些設施能夠正確路由與分發請求,運維人員需要手工維護這些路由規則與服務實體串列,當有實體增級訓是地址變動等情況發生的時候,也需要手工地去同步修改這些資訊以保持實體資訊與中間件配置內容的一致性,在系統規模不大的時候,維護這些資訊的作業還不會太過復雜,但是如果當系統規模不斷增大,那么這些看似簡單的維護任務會變得越來越難,并且出現配置錯誤的概率也會逐漸增加,很顯然,這樣的做法并不可取,所以我們需要一套機制來有效降低維護路由規則與服務實體串列的難度,
? 其次,我們再從開發人員的角度來看看,在這樣的架構下,會產生一些怎樣的問題呢?大多數情況下,為了保證對外服務的安全性,我們在服務端實作的微服務介面,往往都會有一定的權限校驗機制,比如對用戶登錄狀態的校驗等;同時為了防止客戶端在發起請求時被篡改等安全方面的考慮,還會有一些簽名校驗的機制存在,這時候,由于使用了微服務架構的理念,我們將原本處于一個應用中的多個模塊拆成了多個應用,但是這些應用提供的介面都需要這些校驗邏輯,我們不得不在這些應用中都實作這樣一套校驗邏輯,隨著微服務規模的擴大,這些校驗邏輯的冗余變得越來越多,突然有一天我們發現這套校驗邏輯有個BUG需要修復,或者需要對其做一些擴展和優化,此時我們就不得不去每個應用里修改這些邏輯,而這樣的修改不僅會引起開發人員的抱怨,更會加重測驗人員的負擔,所以,我們也需要一套機制能夠很好地解決微服務架構中,對于微服務介面訪問時各前置校驗的冗余問題,
為了解決上面的架構問題,API網關應運而生,而Spring Cloud Zuul就是Spring Colud 提供的這樣的一個API網關,Zuul提供了動態路由、監控、彈性負載和安全功能,Zuul底層利用各種filter實作如下功能:
- 認證和安全:識別每個需要認證的資源,拒絕不符合要求的請求,
- 性能監測:在服務邊界追蹤并統計資料,提供精確的生產視圖,
- 動態路由:根據需要將請求動態路由到后端集群,
- 壓力測驗:逐漸增加對集群的流量以了解其性能,
- 負載卸載:預先為每種型別的請求分配容量,當請求超過容量時自動丟棄,
- 靜態資源處理:直接在邊界回傳某些回應,
代碼實踐
本次的代碼實踐還是在前幾篇文章的代碼的基礎上所作的,
1.創建zuul-gateway的工程并引入依賴
<!--zuul的依賴-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!--eureka-client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2.創建應用主類,使用@EnableZuulProxy注解開啟Zuul的API網關服務功能
@SpringBootApplication
@EnableZuulProxy
public class ZuulGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulGatewayApplication.class, args);
}
}
3.在組態檔中配置Zuul應用的基礎資訊,這里不像之前的服務使用properties作為組態檔,而是菜用yaml作為配置(后面會講)
server:
port: 9010
spring:
application:
name: zuul-gateway
# 指定Eureka server的注冊中心的位置,出來將Zuul的注冊成服務之外,也讓Zuul能夠獲取注冊中心的實體清單
eureka:
client:
service-url:
defaultZone: http://eureka-server1:9001/eureka/
傳統的路由方式
使用Zuul實作路由的功能非常簡單,之需要對api-gateway服務增加關于路由規則的配置即可,
#Zuul實作的傳統的路由配置
zuul:
routes:
hello-server-url:
path: /hello-server/**
url: http://localhost:9003
該配置會將所有發往API網關服務的請求中符合/hello-server/**規則的訪問都路由轉發到 http://localhost:9003 這個地址上,也就是:我們在訪問 http://localhost:9010/hello-server/sayHello的時候,API服務網關會將該請求路由到http://localhost:9003/sayHello上,
注意上面一組path和url映射的路由名要相同,

這種方式直觀容易理解,API網關直接根據請求的URL路徑找到最匹配的path運算式,直接轉發給該運算式對應的url以實作外部請求的路由,
面向服務的路由
在properties組態檔中配置路由
# Zuul面向服務的配置服務
zuul:
routes:
api-hello-server:
path: /hello-server/**
service-id: hello-server
api-customer-server:
path: /customer-server/**
service-id: customer-server
在這里分別使用了api-hello-server和pi-customer-server來映射服務提供者(hello-server)和服務消費者(customer-server)的路由,通過上面的配置方式,我們不足要再為每個路由維護微服務的具體實體的位置,而是通過path和service-id的映射,使得維護作業變得非常簡單,

這種方式,整合了Eureka來實作,將API網關看作Eureka的一個應用服務,除了將自己注冊到Eureka服務注冊中心上之外,也會從注冊中心獲取所有的服務以及他們的實體清單,在Eureka的幫助下,API網關服務就已經維護了所有serviceId與實體地址的映射關系,那么只需要通過Ribbon的負載均衡策略,直接在這些清單種選擇一個具體的實體進行轉發就能完成路由作業了,
為啥選擇yaml作為組態檔
隨著版本的迭代,可能會對服務做一個功能的拆分,將原本屬于hello-service的某些共鞥你拆分到了另一個全新的hello-service-ext服務中,而這些拆分的外部呼叫URL路徑希望能夠符合規則/hello-service/ext/**,所以需要做如下配置:
zuul.routes.hello-service.path=/hello-service/**
zuul.routes.hello-service.serviceId=hello-service
zuul.routes.hello-service-ext.path=/hello-service/ext/**
zuul.routes.hello-service-ext.serviceId=hello-service-ext
此時,呼叫hello-service-ext服務的 URL路徑實際上會同時被/hello-service/** 和/hello-service/ext/** 兩個運算式所匹配,在邏輯上,API網關服務需要優先選擇/hello-service/ext/** 路由,然后再匹配/hello-service/** 路由才能實作上述需求,但是如果使用上面的配置方式,實際上是無法保證這樣的路由優先順序的,
由于properties的配置內容無法保證有序,所以為了保證路由的優先順序,需要使用yaml檔案來配置,這也是為啥配置zuul的時候要選擇使用yaml作為組態檔,
請求過濾
在實作了請求路由功能之后,我們的微服務應用提供的介面就可以通過統一的API網關入口被客戶端訪問到了,但是每個客戶端用戶請求微服務應用提供的介面時,他們的訪問權限往往都有一定的限制,系統并不會將所有的微服務介面都對他們開放,
為了實作對客戶端請求的安全校驗和權限控制,最簡單的方法就是為每個微服務應用都實作一套用于檢驗簽名和鑒別權限的過濾器或者攔截器,但是,因為同一個系統中的各種檢驗邏輯很多情況下都是相同或者類似的,這樣做的話會出現代碼冗余,后期維護例外麻煩,所以比較好的做法時將這些校驗邏輯剝離出去,構建出一個獨立的鑒權服務,
Zuul允許開發者在API網關上通過定義過濾器來實作對請求的攔截與過濾,實作的方法非常簡單,只需要繼承ZuulFilter抽象類并實作他定義的4個抽象函式就可以完成對請求的過濾和攔截了,
在這里我們實作一個簡單的請求過濾功能:登錄系統檢驗token,如果token不為空,則不可以訪問,
/**
* @className: LoginFilter
* @description: 實作登錄過濾校驗
* @author: charon
* @create: 2021-07-04 22:46
*/
public class LoginFilter extends ZuulFilter {
private static Logger log = LoggerFactory.getLogger(LoginFilter.class);
/**
* 過濾器的型別,它決定了過濾器在請求的那個生命周期執行,
* 主要有四種型別:
* pre: 可以在請求被路由之前呼叫
* routing: 在路由請求時被呼叫
* post: 在routing和error過濾器之后被呼叫
* error: 處理請求時發生錯誤時被呼叫
* @return
*/
@Override
public String filterType() {
return "pre";
}
/**
* 過濾器的執行順序,當請求在一個階段中存在多個過濾器時,需要根據該方法回傳的值來過濾依次執行,數值越小優先級越高
* @return
*/
@Override
public int filterOrder() {
return 0;
}
/**
* 判斷該過濾器市夠需要被執行
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* 過濾器的具體邏輯這里通過context.setSendZuulResponse(false);令zuul過濾該請求,不對其進行路由,
*
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
Object token = request.getHeader("token");
if (Objects.isNull(token)) {
log.error("token為空,不允許訪問");
context.setSendZuulResponse(false);
// 防止回傳給前端時出現中文亂碼
context.getResponse().setContentType("text/html;charset=utf-8");
context.setResponseStatusCode(401);
context.setResponseBody("當前狀態未登錄,請重新登錄,");
return null;
}
log.error("token不為空,允許正常訪問");
return null;
}
}
為自定義的過濾器創建具體的bean才能啟動該過濾器,
@Bean
public LoginFilter loginFilter(){
return new LoginFilter();
}
在完成了上面的改造之后,重啟服務,并使用下面兩種請求對其進行驗證:
-
http://localhost:9010/customer-server/sayHello1?name=chatron1: 如果請求中不帶token的引數,則會報"token為空,不允許訪問"的錯誤,回傳錯誤頁面

-
http://localhost:9010/customer-server/sayHello1?name=chatron1&token=1111: 如果請求中帶有token引數,則可以正常訪問

原始碼分析
在使用zuul的時候,最主要的就是在啟動類上添加@EnableZuulProxy的注解,所以我們先從注解開始看,
@EnableCircuitBreaker
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ZuulProxyMarkerConfiguration.class)
public @interface EnableZuulProxy {
}
可以看到,這個注解類引入了ZuulProxyMarkerConfiguration這個類,跟進這個類:
@Configuration(proxyBeanMethods = false)
public class ZuulProxyMarkerConfiguration {
@Bean
public Marker zuulProxyMarkerBean() {
return new Marker();
}
class Marker {
}
}
發現這個類與Eureka的EurekaServerMarkerConfiguration類一樣(作者是同一人),主要就是把Marker類變成了Spring的Bean,作為自動配置Zuul的開關,又了MEurekaServerMarkerConfiguration.Marker這個bean之后,Zuul代理的自動配置類(ZuulProxyAutoConfiguration)就能加載了,
在ZuulProxyAutoConfiguration這個類里注入了一些Filters,
@Bean
@ConditionalOnMissingBean(PreDecorationFilter.class)
public PreDecorationFilter preDecorationFilter(RouteLocator routeLocator,
ProxyRequestHelper proxyRequestHelper) {
return new PreDecorationFilter(routeLocator,
this.server.getServlet().getContextPath(), this.zuulProperties,
proxyRequestHelper);
}
// route filters
@Bean
@ConditionalOnMissingBean(RibbonRoutingFilter.class)
public RibbonRoutingFilter ribbonRoutingFilter(ProxyRequestHelper helper,
RibbonCommandFactory<?> ribbonCommandFactory) {
RibbonRoutingFilter filter = new RibbonRoutingFilter(helper, ribbonCommandFactory,
this.requestCustomizers);
return filter;
}
@Bean
@ConditionalOnMissingBean({ SimpleHostRoutingFilter.class,
CloseableHttpClient.class })
public SimpleHostRoutingFilter simpleHostRoutingFilter(ProxyRequestHelper helper,
ZuulProperties zuulProperties,
ApacheHttpClientConnectionManagerFactory connectionManagerFactory,
ApacheHttpClientFactory httpClientFactory) {
return new SimpleHostRoutingFilter(helper, zuulProperties,
connectionManagerFactory, httpClientFactory);
}
@Bean
@ConditionalOnMissingBean({ SimpleHostRoutingFilter.class })
public SimpleHostRoutingFilter simpleHostRoutingFilter2(ProxyRequestHelper helper,
ZuulProperties zuulProperties, CloseableHttpClient httpClient) {
return new SimpleHostRoutingFilter(helper, zuulProperties, httpClient);
}
而ZuulProxyAutoConfiguration的繼承了ZuulServerAutoConfiguration類,參考了一些相關的配置,在缺失ZuulServletBean的情況下注入ZuulServlet,而這個類是Zuul的核心類:
@Bean
@ConditionalOnMissingBean(name = "zuulServlet")
@ConditionalOnProperty(name = "zuul.use-filter", havingValue = "https://www.cnblogs.com/pluto-charon/archive/2021/07/06/false",
matchIfMissing = true)
public ServletRegistrationBean zuulServlet() {
ServletRegistrationBean<ZuulServlet> servlet = new ServletRegistrationBean<>(
new ZuulServlet(), this.zuulProperties.getServletPattern());
servlet.addInitParameter("buffer-requests", "false");
return servlet;
}
同時在這個類中,還注入了其他的過濾器,比如:
- pre型別的過濾器:ServletDetectionFilter、DebugFilter、Servlet30WrapperFilter
- post型別的過濾器:SendResponseFilter
- error型別的過濾器:SendErrorFilter
- route型別的過濾器:SendForwardFilter
跟進ZuulServlet類,可以看到ZuulServlet直接繼承了HttpServlet類,所以ZuulServlet依然是走的http通信協議,跟進ZuulServlet.service方法,這里面清晰的描繪了Zuul的路由程序,
- pre、route、post都不拋出例外,順序是:pre->route->post,error不執行,
- pre拋出例外,順序是:pre->error->post,
- route拋出例外,順序是:pre->route->error->post,
- post拋出例外,順序是:pre->route->post->error,
@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
try {
// 為每個請求生成request和response,存入ConcurrentHashMap中
init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
// 初始化背景關系
RequestContext context = RequestContext.getCurrentContext();
context.setZuulEngineRan();
// 處理pre型別的過濾器
try {
preRoute();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
// 處理route型別的過濾器
try {
route();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
// 處理post型別的過濾器
try {
postRoute();
} catch (ZuulException e) {
error(e);
return;
}
} catch (Throwable e) {
error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}
跟進每種過濾器型別的執行方法,可以發現找到Zuul過濾器的核心處理器:FilterProcessor,在這個類中,主要有兩個方法:
- runFilters (String sType):該方法會根據傳入的 filterType來呼叫getFiltersByType (String filterType)獲取排序后的過濾器串列,然后輪詢這些過濾器,并呼叫processZuulFilter (ZuulFilter filter)來依次執行它們,
- processZuulFilter(ZuulFilter filter):該方法定義了用來執行 filter的具體邏輯,包括對請求背景關系的設定,判斷是否應該執行,執行時一些例外的處理等,
在processZuulFilter()這個方法中最后都是呼叫的繼承了ZuulFilter抽象類的過濾器的各自實作的run(),
Zuul作為網關,主要的實作都包含在了ZuulFilter的實作當中,以一個ConcurrentHashMap實作的RequestContext來傳遞節點資料,如果想做一些自定義的處理可以通過繼承ZuulFilter并重寫4個方法即可,
參考文章:
翟永超老師的《Spring Cloud微服務實戰》
https://blog.csdn.net/weixin_38106322/article/details/103457742
https://zhuanlan.zhihu.com/p/28376627
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/288954.html
標籤:其他
