自己动手写一个服务网关
什么是網(wǎng)關(guān)?為什么需要使用網(wǎng)關(guān)?
如圖所示,在不使用網(wǎng)關(guān)的情況下,我們的服務(wù)是直接暴露給服務(wù)調(diào)用方。當調(diào)用方增多,勢必需要添加定制化訪問權(quán)限、校驗等邏輯。當添加API網(wǎng)關(guān)后,在第三方調(diào)用端和服務(wù)提供方之間就創(chuàng)建了一面墻,這面墻直接與調(diào)用方通信進行權(quán)限控制。
本文所實現(xiàn)的網(wǎng)關(guān)源碼抄襲了---Oh,不對,是借鑒。借鑒了Zuul網(wǎng)關(guān)的源碼,提煉出其核心思路,實現(xiàn)了一套簡單的網(wǎng)關(guān)源碼,博主將其改名為Eatuul。
題外話
本文是業(yè)內(nèi)能搜到的第一篇自己動手實現(xiàn)網(wǎng)關(guān)的文章。博主寫的手把手系列的文章,目的是在以最簡單的方式,揭露出中間件的核心原理,讓讀者能夠迅速了解實現(xiàn)的核心。需要說明的是,這不是源碼分析系列的文章,因此寫出來的代碼,省去了一些復(fù)雜的內(nèi)容,畢竟大家能理解到該中間件的核心原理即可。如果想看源碼分析系列的,請關(guān)注博主,后期會將spring、spring boot、dubbo、mybatis等開源框架一一揭示。
正文設(shè)計思路
先大致說一下,就是定義一個Servlet接收請求。然后經(jīng)過preFilter(封裝請求參數(shù)),routeFilter(轉(zhuǎn)發(fā)請求),postFilter(輸出內(nèi)容)。三個過濾器之間,共享request、response以及其他的一些全局變量。如下圖所示
和真正的Zuul的區(qū)別?主要區(qū)別有如下幾點
博主總不可能每一個都給你們實現(xiàn)一遍吧。所以偷懶了,每種只實現(xiàn)一個。但是調(diào)用順序還是不變,按照PreFilters->RoutingFilters->PostFilters的順序調(diào)用
在routeFilters確實有轉(zhuǎn)發(fā)請求的Filter,然而博主偷天換日了,改用RestTemplate實現(xiàn).
代碼結(jié)構(gòu)
大家去spring官網(wǎng)上搭建一套springboot的項目,博主就不展示pom的代碼了。直接將項目結(jié)構(gòu)展示一下,如下圖所示
EatuulServlet.java
這個是網(wǎng)關(guān)的入口,邏輯也十分簡單,分為三步
(1)將request,response放入threadlocal中
(2)執(zhí)行三組過濾器
(3)清除threadlocal中的的環(huán)境變量
源碼如下
EatuulRunner.java
這個是具體的執(zhí)行器。需要說明一下,在Zuul中,ZuulRunner在獲取具體有哪些過濾器的時候,有一個FileLoader可以動態(tài)讀取配置加載。博主在實現(xiàn)我們自己的EatuulRunner時候,略去動態(tài)讀取的過程,直接靜態(tài)寫死。
源碼如下
package?com.rjzheng.eatuul.http;import?java.util.ArrayList; import?java.util.List; import?java.util.concurrent.ConcurrentHashMap;import?javax.servlet.http.HttpServletRequest; import?javax.servlet.http.HttpServletResponse;import?com.rjzheng.eatuul.filter.EatuulFilter; import?com.rjzheng.eatuul.filter.post.SendResponseFilter; import?com.rjzheng.eatuul.filter.pre.RequestWrapperFilter; import?com.rjzheng.eatuul.filter.route.RoutingFilter;public?class?EatRunner?{//靜態(tài)寫死過濾器private?ConcurrentHashMap<String, List<EatuulFilter>> hashFiltersByType =?new?ConcurrentHashMap<String, List<EatuulFilter>>(){{ ?put("pre",new?ArrayList<EatuulFilter>(){{add(new?RequestWrapperFilter());}});put("route",new?ArrayList<EatuulFilter>(){{add(new?RoutingFilter());}});put("post",new?ArrayList<EatuulFilter>(){{add(new?SendResponseFilter());}});}};public?void?init(HttpServletRequest req, HttpServletResponse resp)?{RequestContext ctx = RequestContext.getCurrentContext();ctx.setRequest(req);ctx.setResponse(resp);}public?void?preRoute()?throws?Throwable?{runFilters("pre"); ?}public?void?route()?throws?Throwable{runFilters("route"); ? ?}public?void?postRoute()?throws?Throwable{runFilters("post");}public?void?runFilters(String sType)?throws?Throwable?{List<EatuulFilter> list =?this.hashFiltersByType.get(sType);if?(list !=?null) {for?(int?i =?0; i < list.size(); i++) {EatuulFilter zuulFilter = list.get(i);zuulFilter.run();}}} }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,前置執(zhí)行過濾器,負責封裝請求。步驟如下所示
(1)封裝請求頭
(2)封裝請求體
(3)構(gòu)造出RestTemplate能識別的RequestEntity
(4)將RequestEntity放入全局threadlocal之中
代碼如下所示
package?com.rjzheng.eatuul.filter.pre;import?java.io.IOException; import?java.io.InputStream; import?java.net.URI; import?java.net.URISyntaxException; import?java.util.Collections; import?java.util.List;import?javax.servlet.http.HttpServletRequest;import?org.springframework.http.HttpHeaders; import?org.springframework.http.HttpMethod; import?org.springframework.http.RequestEntity; import?org.springframework.util.MultiValueMap; import?org.springframework.util.StreamUtils;import?com.rjzheng.eatuul.filter.EatuulFilter; import?com.rjzheng.eatuul.http.RequestContext;public?class?RequestWrapperFilter?extends?EatuulFilter{@Overridepublic?String?filterType()?{// TODO Auto-generated method stubreturn?"pre";}@Overridepublic?int?filterOrder()?{// TODO Auto-generated method stubreturn?-1;}@Overridepublic?void?run()?{String rootURL =?"http://localhost:9090";RequestContext ctx =RequestContext.getCurrentContext();HttpServletRequest servletRequest = ctx.getRequest();String targetURL = rootURL + servletRequest.getRequestURI();RequestEntity<byte[]> requestEntity =?null;try?{requestEntity = createRequestEntity(servletRequest, targetURL);}?catch?(Exception e) {e.printStackTrace();}//4、將requestEntity放入全局threadlocal之中ctx.setRequestEntity(requestEntity);}private?RequestEntity?createRequestEntity(HttpServletRequest request,String url)?throws?URISyntaxException, IOException?{String method = request.getMethod();HttpMethod httpMethod = HttpMethod.resolve(method);//1、封裝請求頭MultiValueMap<String, String> headers =createRequestHeaders(request);//2、封裝請求體byte[] body = createRequestBody(request);//3、構(gòu)造出RestTemplate能識別的RequestEntityRequestEntity requestEntity =?new?RequestEntity<byte[]>(body,headers,httpMethod,?new?URI(url));return?requestEntity;}private?byte[] createRequestBody(HttpServletRequest request)?throws?IOException {InputStream inputStream = request.getInputStream();return?StreamUtils.copyToByteArray(inputStream);}private?MultiValueMap<String, String>?createRequestHeaders(HttpServletRequest request)?{HttpHeaders headers =?new?HttpHeaders();List<String> headerNames = Collections.list(request.getHeaderNames());for(String headerName:headerNames) {List<String> headerValues = Collections.list(request.getHeaders(headerName));for(String headerValue:headerValues) {headers.add(headerName, headerValue);}}return?headers;} }RoutingFilter.java
這個是routeFilter,這里我偷懶了,直接做轉(zhuǎn)發(fā)請求,并且將返回值ResponseEntity放入全局threadlocal中
package?com.rjzheng.eatuul.filter.route;import?org.springframework.http.RequestEntity; import?org.springframework.http.ResponseEntity; import?org.springframework.web.client.RestTemplate;import?com.rjzheng.eatuul.filter.EatuulFilter; import?com.rjzheng.eatuul.http.RequestContext;public?class?RoutingFilter?extends?EatuulFilter{@Overridepublic?String?filterType()?{// TODO Auto-generated method stubreturn?"route";}@Overridepublic?int?filterOrder()?{// TODO Auto-generated method stubreturn?0;}@Overridepublic?void?run(){RequestContext ctx = RequestContext.getCurrentContext();RequestEntity requestEntity = ctx.getRequestEntity();RestTemplate restTemplate =?new?RestTemplate();ResponseEntity responseEntity = restTemplate.exchange(requestEntity,byte[].class);ctx.setResponseEntity(responseEntity);}}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{@Overridepublic?String?filterType()?{return?"post";}@Overridepublic?int?filterOrder()?{return?1000;}@Overridepublic?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<String, List<String>> entry:httpHeaders.entrySet()) {String headerName = entry.getKey();List<String> 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 setservletResponse.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<String,?Object>?{protected?static?Class<? extends RequestContext> contextClass = RequestContext.class;protected?static?final?ThreadLocal<? extends RequestContext> threadLocal =?new?ThreadLocal<RequestContext>() {@Overrideprotected?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);elseremove(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;@RestController public class IndexController {@RequestMapping("/index")public String index() {return?"hello!world";} }然后,你就發(fā)現(xiàn)可以從localhost:8080/index進行跳轉(zhuǎn)訪問了
結(jié)論
本文模擬了一下zuul網(wǎng)關(guān)的源碼,借鑒了一下其精髓的部分。希望大家能有所收獲
?
出處:?http://rjzheng.cnblogs.com/
作者:孤獨煙,作者公號,可以關(guān)注一波
《新程序員》:云原生和全面數(shù)字化實踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的自己动手写一个服务网关的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 中台是个什么鬼 | 白话中台战略
- 下一篇: 微服务 | 我为啥不看好 Service