自己動手寫一個服務閘道器

作者:孤獨煙原文: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> entry:httpHeaders。entrySet()) { String headerName = entry。getKey(); List headerValues = entry。getValue(); for(String headerValue:headerValues) { servletResponse。addHeader(headerName, headerValue); } } } private void writeResponse()throws Exception { RequestContext ctx = RequestContext。getCurrentContext(); HttpServletResponse servletResponse = ctx。getResponse(); if (servletResponse。getCharacterEncoding() == null) { // only set if not set servletResponse。setCharacterEncoding(“UTF-8”); } ResponseEntity responseEntity = ctx。getResponseEntity(); if(responseEntity。hasBody()) { byte[] body = (byte[]) responseEntity。getBody(); ServletOutputStream outputStream = servletResponse。getOutputStream(); outputStream。write(body); outputStream。flush(); } }}

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 { protected static Class<? extends RequestContext> contextClass = RequestContext。class; protected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal() { @Override protected RequestContext initialValue() { try { return contextClass。newInstance(); } catch (Throwable e) { throw new RuntimeException(e); } } }; public static RequestContext getCurrentContext() { RequestContext context = threadLocal。get(); return context; } public HttpServletRequest getRequest() { return (HttpServletRequest) get(“request”); } public void setRequest(HttpServletRequest request) { put(“request”, request); } public HttpServletResponse getResponse() { return (HttpServletResponse) get(“response”); } public void setResponse(HttpServletResponse response) { set(“response”, response); } public void setRequestEntity(RequestEntity requestEntity){ set(“requestEntity”,requestEntity); } public RequestEntity getRequestEntity() { return (RequestEntity) get(“requestEntity”); } public void setResponseEntity(ResponseEntity responseEntity){ set(“responseEntity”,responseEntity); } public ResponseEntity getResponseEntity() { return (ResponseEntity) get(“responseEntity”); } public void set(String key, Object value) { if (value != null) put(key, value); else remove(key); } public void unset() { threadLocal。remove(); }}

如何測試?

自己另外起一個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閘道器的原始碼,借鑑了一下其精髓的部分。希望大家能有所收穫