SpringBoot實現(xiàn)反向代理的示例代碼
最近收到一個新的需求,需要根據(jù)自定義的負載均衡策略從動態(tài)主機池選主之后,再通過反向代理到選中的主機上,這里面就涉及到服務(wù)注冊、負載均衡策略、反向代理。本篇文章只涉及到如何實現(xiàn)反向代理功能。
功能實現(xiàn)
如果只是需要反向代理功能,那么有很多中間件可以選擇,比如:Nginx、Kong、Spring Cloud Gateway,Zuul等都可以實現(xiàn),但是還有一些客制化的需求,所以只能自己擼代碼實現(xiàn)了,附上源碼。
請求攔截
實現(xiàn)請求攔截有兩種方式,過濾器和攔截器,我們采用過濾器的方式去實現(xiàn)請求攔截。
在Spring 體系中最常用到的過濾器應(yīng)該就是OncePerRequestFilter,這是一個抽象類。我們創(chuàng)建一個類叫ForwardRoutingFilter去繼承這個類,同時實現(xiàn)Ordered,用于設(shè)置過濾器的優(yōu)先級
@Slf4j @Component public class ForwardRoutingFilter extends OncePerRequestFilter implements Ordered { ? @Override ? public int getOrder() { ? ? return 0; // 值越小,優(yōu)先級別越高 ? } ? @Override ? protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { ? ? log.info("ForwardRoutingFilter doFilterInternal,request uri: {}", request.getRequestURI()); ? ? filterChain.doFilter(request, response); ? } }
啟動服務(wù)之后,瀏覽器中輸入http://127.0.0.1:8080/aa,查看console 中的日志,可以看到過濾器以及開始工作了。
2023-06-12T14:25:09.059+08:00 INFO 17472 --- [nio-8080-exec-2] c.r.b.filter.ForwardRoutingFilter : ForwardRoutingFilter doFilterInternal,request uri: /aa
2023-06-12T14:25:09.735+08:00 INFO 17472 --- [nio-8080-exec-1] c.r.b.filter.ForwardRoutingFilter : ForwardRoutingFilter doFilterInternal,request uri: /favicon.ico
接下來,我們的實現(xiàn)就圍繞著這個過濾器去做了。
配置規(guī)則定義
通常情況下,我們會在application.yml去配置哪些path需要被轉(zhuǎn)發(fā)到具體的服務(wù)上去,例如
my: routes: - uri: lb://ai-server path: /ai/** rewrite: false - uri: https://api.oioweb.cn path: /oioweb/** rewrite: true
參數(shù)說明:
- txt復(fù)制代碼uri: 最終請求的服務(wù)地址,如果是lb:// 開頭的,說明需要進行負責(zé)均衡
- path: 用于匹配代理的路徑,命中的會被進行代理轉(zhuǎn)發(fā)
- rewrite: 是否重寫path,如果true, 訪問 http://127.0.0.1:8080/uomg/api/rand.img1 請求path中/uomg會被刪除,最終訪問的是 https://api.uomg.com/api/rand.img1
在pom.xml dependencies 中添加新的依賴,用于自動裝填配置
<!--讀取文件配置--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> </dependency>
創(chuàng)建實體類RouteInstance和配置類MyRoutes,這樣服務(wù)啟動之后就會自動讀取裝填my.routes下所有配置的實例到配置類了
@Data public class RouteInstance { private String uri; private String path; private boolean rewrite; }
@Configuration @ConfigurationProperties(prefix = "my", ignoreInvalidFields = true) @Data public class MyRoutes { private List<RouteInstance> routes; }
代理實現(xiàn)
在pom.xml dependencies 中添加需要用到的依賴
<dependency> ? <groupId>org.apache.commons</groupId> ? <artifactId>commons-lang3</artifactId> </dependency> <dependency> ? <groupId>commons-io</groupId> ? <artifactId>commons-io</artifactId> ? <version>2.11.0</version> </dependency> <dependency> ? <groupId>commons-beanutils</groupId> ? <artifactId>commons-beanutils</artifactId> ? <version>1.9.4</version> </dependency> <dependency> ? <groupId>org.apache.httpcomponents.client5</groupId> ? <artifactId>httpclient5</artifactId> ? <version>5.2.1</version> </dependency> <dependency> ? <groupId>com.alibaba.fastjson2</groupId> ? <artifactId>fastjson2</artifactId> ? <version>2.0.32</version> </dependency>
接下來就是改造我們之前的ForwardRoutingFilter 過濾器類了
@Slf4j @Component public class ForwardRoutingFilter extends OncePerRequestFilter implements Ordered { ? @Resource ? private MyRoutes routes; ? @Resource ? private RoutingDelegateService routingDelegate; ? @Override ? public int getOrder() { ? ? return 0; ? } ? @Override ? protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { ? ? log.info("ForwardRoutingFilter doFilterInternal,request uri: {}", request.getRequestURI()); ? ? String currentURL = StringUtils.isEmpty(request.getContextPath()) ? request.getRequestURI() : ? ? ? ? StringUtils.substringAfter(request.getRequestURI(), request.getContextPath()); ? ? AntPathMatcher matcher = new AntPathMatcher(); ? ? RouteInstance instance = routes.getRoutes().stream().filter(i -> matcher.match(i.getPath(), currentURL)).findFirst().orElse(new RouteInstance()); ? ? if (instance.getUri() == null) { ? ? ? //轉(zhuǎn)發(fā)的uri為空,不進行代理轉(zhuǎn)發(fā),交由過濾器鏈后續(xù)過濾器處理 ? ? ? filterChain.doFilter(request, response); ? ? } else { ? ? ? // 創(chuàng)建一個service 去處理代理轉(zhuǎn)發(fā)邏輯 ? ? ? routingDelegate.doForward(instance, request, response); ? ? ? return; ? ? } ? } }
代理轉(zhuǎn)發(fā)會使用到RestTemplate,默認使用的是java.net.URLConnection去進行http請求,我們這邊替換成httpclient,具體配置就不貼出來了。
編寫兩個工具欄,分別用于轉(zhuǎn)換 HttpServletRequest 為 RequestEntity 和 HttpServletResponse 為 ResponseEntity,并把結(jié)果寫回客戶端
@Slf4j public class HttpRequestMapper { ? public RequestEntity<byte[]> map(HttpServletRequest request, RouteInstance instance) throws IOException { ? ? byte[] body = extractBody(request); ? ? HttpHeaders headers = extractHeaders(request); ? ? HttpMethod method = extractMethod(request); ? ? URI uri = extractUri(request, instance); ? ? return new RequestEntity<>(body, headers, method, uri); ? } ? private URI extractUri(HttpServletRequest request, RouteInstance instance) throws UnsupportedEncodingException { ? ? //如果content path 不為空,移除content path ? ? String requestURI = StringUtils.isEmpty(request.getContextPath()) ? request.getRequestURI() : ? ? ? ? StringUtils.substringAfter(request.getRequestURI(), request.getContextPath()); ? ? //處理中文被自動編碼問題 ? ? String query = request.getQueryString() == null ? EMPTY : URLDecoder.decode(request.getQueryString(), "utf-8"); ? ? // 需要重寫path ? ? if (instance.isRewrite()) { ? ? ? String prefix = StringUtils.substringBefore(instance.getPath(), "/**"); ? ? ? requestURI = StringUtils.substringAfter(requestURI, prefix); ? ? } ? ? URI redirectURL = UriComponentsBuilder.fromUriString(instance.getUri() + requestURI).query(query).build().encode().toUri(); ? ? log.info("real request url: {}", redirectURL.toString()); ? ? return redirectURL; ? } ? private HttpMethod extractMethod(HttpServletRequest request) { ? ? return valueOf(request.getMethod()); ? } ? private HttpHeaders extractHeaders(HttpServletRequest request) { ? ? HttpHeaders headers = new HttpHeaders(); ? ? Enumeration<String> headerNames = request.getHeaderNames(); ? ? while (headerNames.hasMoreElements()) { ? ? ? String name = headerNames.nextElement(); ? ? ? List<String> value = list(request.getHeaders(name)); ? ? ? headers.put(name, value); ? ? } ? ? return headers; ? } ? private byte[] extractBody(HttpServletRequest request) throws IOException { ? ? return toByteArray(request.getInputStream()); ? } } java復(fù)制代碼public class HttpResponseMapper { ? public void map(ResponseEntity<byte[]> responseEntity, HttpServletResponse response) throws IOException { ? ? setStatus(responseEntity, response); ? ? setHeaders(responseEntity, response); ? ? setBody(responseEntity, response); ? } ? private void setStatus(ResponseEntity<byte[]> responseEntity, HttpServletResponse response) { ? ? response.setStatus(responseEntity.getStatusCode().value()); ? } ? private void setHeaders(ResponseEntity<byte[]> responseEntity, HttpServletResponse response) { ? ? responseEntity.getHeaders().forEach((name, values) -> values.forEach(value -> response.addHeader(name, value))); ? } ? /** ? ?* 把結(jié)果寫回客戶端 ? ?* ? ?* @param responseEntity ? ?* @param response ? ?* @throws IOException ? ?*/ ? private void setBody(ResponseEntity<byte[]> responseEntity, HttpServletResponse response) throws IOException { ? ? if (responseEntity.getBody() != null) { ? ? ? response.getOutputStream().write(responseEntity.getBody()); ? ? } ? } }
以下為實際處理邏輯RoutingDelegateService的代碼
@Slf4j @Service public class RoutingDelegateService { ? private HttpResponseMapper responseMapper; ? private HttpRequestMapper requestMapper; ? @Resource ? private RestTemplate restTemplate; ? /** ? ?* 根據(jù)相應(yīng)策略轉(zhuǎn)發(fā)請求到對應(yīng)后端服務(wù) ? ?* ? ?* @param instance RouteInstance ? ?* @param request ?HttpServletRequest ? ?* @param response HttpServletResponse ? ?*/ ? public void doForward(RouteInstance instance, HttpServletRequest request, HttpServletResponse response) { ? ? boolean shouldLB = StringUtils.startsWith(instance.getUri(), MyConstants.LB_PREFIX); ? ? if (shouldLB) { ? ? ? // 需要負載均衡,獲取appName ? ? ? String appName = StringUtils.substringAfter(instance.getUri(), MyConstants.LB_PREFIX); ? ? ? //從請求頭中獲取是否必須按user去路由到同一節(jié)點 ? ? ? // 可用節(jié)點 ? ? ? ServerInstance chooseInstance = chooseLBInstance(appName); ? ? ? if (chooseInstance == null) { ? ? ? ? // 無可用節(jié)點,返回異常, ? ? ? ? JSONObject result = new JSONObject(); ? ? ? ? result.put("status", MyConstants.NO_AVAILABLE_NODE_STATUS); ? ? ? ? result.put("msg", MyConstants.NO_AVAILABLE_NODE_MSG); ? ? ? ? renderString(response, result.toJSONString()); ? ? ? ? return; ? ? ? } else { ? ? ? ? //設(shè)置route instance uri 為負載均衡之后的URI地址 ? ? ? ? String uri = MyConstants.HTTP_PREFIX + chooseInstance.getHost() + ":" + chooseInstance.getPort(); ? ? ? ? instance.setUri(uri); ? ? ? } ? ? } ? ? // 轉(zhuǎn)發(fā)請求 ? ? try { ? ? ? goForward(request, response, instance); ? ? } catch (Exception e) { ? ? ? // 連接超時、返回異常 ? ? ? e.printStackTrace(); ? ? ? log.error("request error {}", e.getMessage()); ? ? ? JSONObject result = new JSONObject(); ? ? ? result.put("status", MyConstants.UNKNOWN_EXCEPTION_STATUS); ? ? ? result.put("msg", e.getMessage()); ? ? ? renderString(response, result.toJSONString()); ? ? } ? } ? /** ? ?* 發(fā)送請求到對應(yīng)后端服務(wù) ? ?* ? ?* @param request ?HttpServletRequest ? ?* @param response HttpServletResponse ? ?* @param instance RouteInstance ? ?* @throws IOException ? ?*/ ? private void goForward(HttpServletRequest request, HttpServletResponse response, RouteInstance instance) throws IOException { ? ? requestMapper = new HttpRequestMapper(); ? ? RequestEntity<byte[]> requestEntity = requestMapper.map(request, instance); ? ? //用byte數(shù)組處理返回結(jié)果,因為返回結(jié)果可能是字符串也可能是數(shù)據(jù)流 ? ? ResponseEntity<byte[]> responseEntity = restTemplate.exchange(requestEntity, byte[].class); ? ? responseMapper = new HttpResponseMapper(); ? ? responseMapper.map(responseEntity, response); ? } ? private ServerInstance chooseLBInstance(String appName) { ? ? //TODO 根據(jù)appName 選擇對應(yīng)的host ? ? ServerInstance instance = new ServerInstance(); ? ? instance.setHost("127.0.0.1"); ? ? instance.setPort(10000); ? ? return instance; ? } ? /** ? ?* 寫回字符串結(jié)果到客戶端 ? ?* ? ?* @param response ? ?* @param string ? ?*/ ? public void renderString(HttpServletResponse response, String string) { ? ? try { ? ? ? response.setStatus(200); ? ? ? response.setContentType("application/json"); ? ? ? response.setCharacterEncoding("utf-8"); ? ? ? response.getWriter().print(string); ? ? } catch (IOException e) { ? ? ? e.printStackTrace(); ? ? } ? } }
啟動server,瀏覽器中輸入http://127.0.0.1:8080/oioweb/api/common/rubbish?name=香蕉,就可以把請求代理到https://api.oioweb.cn/api/common/rubbish?name=香蕉了
{ "code": 200, "result": [ { "name": "香蕉", "type": 2, "aipre": 0, "explain": "廚余垃圾是指居民日常生活及食品加工、飲食服務(wù)、單位供餐等活動中產(chǎn)生的垃圾。", "contain": "常見包括菜葉、剩菜、剩飯、果皮、蛋殼、茶渣、骨頭等", "tip": "純流質(zhì)的食物垃圾、如牛奶等,應(yīng)直接倒進下水口。有包裝物的濕垃圾應(yīng)將包裝物去除后分類投放、包裝物請投放到對應(yīng)的可回收物或干垃圾容器" }, { "name": "香蕉干", "type": 2, "aipre": 0, "explain": "廚余垃圾是指居民日常生活及食品加工、飲食服務(wù)、單位供餐等活動中產(chǎn)生的垃圾。", "contain": "常見包括菜葉、剩菜、剩飯、果皮、蛋殼、茶渣、骨頭等", "tip": "純流質(zhì)的食物垃圾、如牛奶等,應(yīng)直接倒進下水口。有包裝物的濕垃圾應(yīng)將包裝物去除后分類投放、包裝物請投放到對應(yīng)的可回收物或干垃圾容器" }, { "name": "香蕉皮", "type": 2, "aipre": 0, "explain": "廚余垃圾是指居民日常生活及食品加工、飲食服務(wù)、單位供餐等活動中產(chǎn)生的垃圾。", "contain": "常見包括菜葉、剩菜、剩飯、果皮、蛋殼、茶渣、骨頭等", "tip": "純流質(zhì)的食物垃圾、如牛奶等,應(yīng)直接倒進下水口。有包裝物的濕垃圾應(yīng)將包裝物去除后分類投放、包裝物請投放到對應(yīng)的可回收物或干垃圾容器" } ], "msg": "success" }
到此這篇關(guān)于SpringBoot實現(xiàn)反向代理的示例代碼的文章就介紹到這了,更多相關(guān)SpringBoot 反向代理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaEE開發(fā)基于Eclipse的環(huán)境搭建以及Maven Web App的創(chuàng)建
本文主要介紹了如何在Eclipse中創(chuàng)建的Maven Project,本文是JavaEE開發(fā)的開篇,也是基礎(chǔ)。下面內(nèi)容主要包括了JDK1.8的安裝、JavaEE版本的Eclipse的安裝、Maven的安裝、Tomcat 9.0的配置、Eclipse上的M2Eclipse插件以及STS插件的安裝。2017-03-03解決JPA?save()方法null值覆蓋掉mysql預(yù)設(shè)的默認值問題
這篇文章主要介紹了解決JPA?save()方法null值覆蓋掉mysql預(yù)設(shè)的默認值問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11Spring+Quartz實現(xiàn)動態(tài)任務(wù)調(diào)度詳解
這篇文章主要介紹了Spring+Quartz實現(xiàn)動態(tài)任務(wù)調(diào)度詳解,最近經(jīng)?;趕pring?boot寫定時任務(wù),并且是使用注解的方式進行實現(xiàn),分成的方便將自己的類注入spring容器,需要的朋友可以參考下2024-01-01java遞歸實現(xiàn)拼裝多個api的結(jié)果操作方法
本文給大家分享java遞歸實現(xiàn)拼裝多個api的結(jié)果的方法,說白了就是好幾個API結(jié)果拼裝成的,本文通過實例代碼給大家介紹的非常詳細,需要的朋友參考下吧2021-09-09