自己動手寫一個服務閘道器
作者:孤獨煙原文:https://www。cnblogs。com/rjzheng/p/9220437。html
引言
什麼是閘道器?為什麼需要使用閘道器?
如圖所示,在不使用閘道器的情況下,我們的服務是直接暴露給服務呼叫方。當呼叫方增多,勢必需要新增定製化訪問許可權、校驗等邏輯。當新增API閘道器後,再第三方呼叫端和服務提供方之間就建立了一面牆,這面牆直接與呼叫方通訊進行許可權控制。
本文所實現的閘道器原始碼抄襲了——-Oh,不對,是借鑑。借鑑了Zuul閘道器的原始碼,提煉出其核心思路,實現了一套簡單的閘道器原始碼,博主將其改名為Eatuul。
題外話
本文是業內能搜到的第一篇自己動手實現閘道器的文章。博主寫的手把手系列的文章,目的是在以最簡單的方式,揭露出中介軟體的核心原理,讓讀者能夠迅速瞭解實現的核心。需要說明的是,這不是原始碼分析系列的文章,因此寫出來的程式碼,省去了一些複雜的內容,畢竟大家能理解到該中介軟體的核心原理即可。如果想看原始碼分析系列的,請關注博主,後期會將spring、spring boot、dubbo、mybatis等開源框架一一揭示。
正文
設計思路
先大致說一下,就是定義一個Servlet接收請求。然後經過preFilter(封裝請求引數),routeFilter(轉發請求),postFilter(輸出內容)。三個過濾器之間,共享request、response以及其他的一些全域性變數。如下圖所示
和真正的Zuul的區別?
主要區別有如下幾點
(1)Zuul中在異常處理模組,有一個ErrorFilter來處理,博主在實現的時候偷懶了,略去。
(2)Zuul中PreFilters,RoutingFilters,PostFilters預設都實現了一組,具體如下表所示
圖
博主總不可能每一個都給你們實現一遍吧。所以偷懶了,每種只實現一個。但是呼叫順序還是不變,按照PreFilters->RoutingFilters->PostFilters的順序呼叫
(3)在routeFilters確實有轉發請求的Filter,然而博主偷天換日了,改用RestTemplate實現。
程式碼結構
大家去spring官網上搭建一套springboot的專案,博主就不展示pom的程式碼了。直接將專案結構展示一下,如下圖所示
EatuulServlet.java
這個是閘道器的入口,邏輯也十分簡單,分為三步
(1)將request,response放入threadlocal中
(2)執行三組過濾器
(3)清除threadlocal中的的環境變數
原始碼如下
EatuulRunner.java
這個是具體的執行器。需要說明一下,在Zuul中,ZuulRunner在獲取具體有哪些過濾器的時候,有一個FileLoader可以動態讀取配置載入。博主在實現我們自己的EatuulRunner時候,略去動態讀取的過程,直接靜態寫死。
原始碼如下
EatuulFilter.java
接下來就是一系列Filter的程式碼了,先上父類EatuulFilter的原始碼
package com。rjzheng。eatuul。filter;public abstract class EatuulFilter { abstract public String filterType(); abstract public int filterOrder(); abstract public void run();}
RequestWrapperFilter.java
這個是PreFilter,前置執行過濾器,負責封裝請求。步驟如下所示
(1)封裝請求頭
(2)封裝請求體
(3)構造出RestTemplate能識別的RequestEntity
(4)將RequestEntity放入全域性threadlocal之中
程式碼如下所示
RoutingFilter.java
這個是routeFilter,這裡我偷懶了,直接做轉發請求,並且將返回值ResponseEntity放入全域性threadlocal中
SendResponseFilter.java
這個是postFilters,將ResponseEntity輸出即可
package com。rjzheng。eatuul。filter。post;import java。util。List;import java。util。Map;import javax。servlet。ServletOutputStream;import javax。servlet。http。HttpServletResponse;import org。springframework。http。HttpHeaders;import org。springframework。http。ResponseEntity;import com。rjzheng。eatuul。filter。EatuulFilter;import com。rjzheng。eatuul。http。RequestContext;public class SendResponseFilter extends EatuulFilter{ @Override public String filterType() { return “post”; } @Override public int filterOrder() { return 1000; } @Override public void run() { try { addResponseHeaders(); writeResponse(); } catch (Exception e) { e。printStackTrace(); } } private void addResponseHeaders() { RequestContext ctx = RequestContext。getCurrentContext(); HttpServletResponse servletResponse = ctx。getResponse(); ResponseEntity responseEntity = ctx。getResponseEntity(); HttpHeaders httpHeaders = responseEntity。getHeaders(); for(Map。Entry
RequestContext.java
最後是一直在說的全域性threadlocal變數
package com。rjzheng。eatuul。http;import java。util。HashMap;import java。util。Map;import java。util。concurrent。ConcurrentHashMap;import javax。servlet。http。HttpServletRequest;import javax。servlet。http。HttpServletResponse;import org。springframework。http。RequestEntity;import org。springframework。http。ResponseEntity;public class RequestContext extends ConcurrentHashMap
如何測試?
自己另外起一個server埠為9090如下所示
package com。rjzheng。eatservice;import org。springframework。boot。autoconfigure。SpringBootApplication;import org。springframework。boot。builder。SpringApplicationBuilder;import org。springframework。boot。web。servlet。ServletComponentScan;import com。rjzheng。eatservice。controller。IndexController;@SpringBootApplication@ServletComponentScan(basePackageClasses = IndexController。class)public class Application { public static void main(String[] args) { new SpringApplicationBuilder(Application。class)。properties(“server。port=9090”)。run(args); }}
再來一個controller
package com。rjzheng。eatservice。controller;import org。springframework。web。bind。annotation。RequestMapping;import org。springframework。web。bind。annotation。RestController;@RestControllerpublic class IndexController { @RequestMapping(“/index”) public String index() { return “hello!world”; }}
然後,你就發現可以從localhost:8080/index進行跳轉訪問了
結論
本文模擬了一下zuul閘道器的原始碼,借鑑了一下其精髓的部分。希望大家能有所收穫