SpringBoot實(shí)現(xiàn)反向代理的示例代碼
最近收到一個(gè)新的需求,需要根據(jù)自定義的負(fù)載均衡策略從動(dòng)態(tài)主機(jī)池選主之后,再通過(guò)反向代理到選中的主機(jī)上,這里面就涉及到服務(wù)注冊(cè)、負(fù)載均衡策略、反向代理。本篇文章只涉及到如何實(shí)現(xiàn)反向代理功能。
功能實(shí)現(xiàn)
如果只是需要反向代理功能,那么有很多中間件可以選擇,比如:Nginx、Kong、Spring Cloud Gateway,Zuul等都可以實(shí)現(xiàn),但是還有一些客制化的需求,所以只能自己擼代碼實(shí)現(xiàn)了,附上源碼。
請(qǐng)求攔截
實(shí)現(xiàn)請(qǐng)求攔截有兩種方式,過(guò)濾器和攔截器,我們采用過(guò)濾器的方式去實(shí)現(xiàn)請(qǐng)求攔截。
在Spring 體系中最常用到的過(guò)濾器應(yīng)該就是OncePerRequestFilter,這是一個(gè)抽象類(lèi)。我們創(chuàng)建一個(gè)類(lèi)叫ForwardRoutingFilter去繼承這個(gè)類(lèi),同時(shí)實(shí)現(xiàn)Ordered,用于設(shè)置過(guò)濾器的優(yōu)先級(jí)
@Slf4j @Component public class ForwardRoutingFilter extends OncePerRequestFilter implements Ordered { ? @Override ? public int getOrder() { ? ? return 0; // 值越小,優(yōu)先級(jí)別越高 ? } ? @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); ? } }
啟動(dòng)服務(wù)之后,瀏覽器中輸入http://127.0.0.1:8080/aa,查看console 中的日志,可以看到過(guò)濾器以及開(kāi)始工作了。
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
接下來(lái),我們的實(shí)現(xiàn)就圍繞著這個(gè)過(guò)濾器去做了。
配置規(guī)則定義
通常情況下,我們會(huì)在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ù)說(shuō)明:
- txt復(fù)制代碼uri: 最終請(qǐng)求的服務(wù)地址,如果是lb:// 開(kāi)頭的,說(shuō)明需要進(jìn)行負(fù)責(zé)均衡
- path: 用于匹配代理的路徑,命中的會(huì)被進(jìn)行代理轉(zhuǎn)發(fā)
- rewrite: 是否重寫(xiě)path,如果true, 訪問(wèn) http://127.0.0.1:8080/uomg/api/rand.img1 請(qǐng)求path中/uomg會(huì)被刪除,最終訪問(wèn)的是 https://api.uomg.com/api/rand.img1
在pom.xml dependencies 中添加新的依賴(lài),用于自動(dòng)裝填配置
<!--讀取文件配置--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> </dependency>
創(chuàng)建實(shí)體類(lèi)RouteInstance和配置類(lèi)MyRoutes,這樣服務(wù)啟動(dòng)之后就會(huì)自動(dòng)讀取裝填my.routes下所有配置的實(shí)例到配置類(lèi)了
@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; }
代理實(shí)現(xiàn)
在pom.xml dependencies 中添加需要用到的依賴(lài)
<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>
接下來(lái)就是改造我們之前的ForwardRoutingFilter 過(guò)濾器類(lèi)了
@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為空,不進(jìn)行代理轉(zhuǎn)發(fā),交由過(guò)濾器鏈后續(xù)過(guò)濾器處理 ? ? ? filterChain.doFilter(request, response); ? ? } else { ? ? ? // 創(chuàng)建一個(gè)service 去處理代理轉(zhuǎn)發(fā)邏輯 ? ? ? routingDelegate.doForward(instance, request, response); ? ? ? return; ? ? } ? } }
代理轉(zhuǎn)發(fā)會(huì)使用到RestTemplate,默認(rèn)使用的是java.net.URLConnection去進(jìn)行http請(qǐng)求,我們這邊替換成httpclient,具體配置就不貼出來(lái)了。
編寫(xiě)兩個(gè)工具欄,分別用于轉(zhuǎn)換 HttpServletRequest 為 RequestEntity 和 HttpServletResponse 為 ResponseEntity,并把結(jié)果寫(xiě)回客戶(hù)端
@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()); ? ? //處理中文被自動(dòng)編碼問(wèn)題 ? ? String query = request.getQueryString() == null ? EMPTY : URLDecoder.decode(request.getQueryString(), "utf-8"); ? ? // 需要重寫(xiě)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é)果寫(xiě)回客戶(hù)端 ? ?* ? ?* @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()); ? ? } ? } }
以下為實(shí)際處理邏輯RoutingDelegateService的代碼
@Slf4j @Service public class RoutingDelegateService { ? private HttpResponseMapper responseMapper; ? private HttpRequestMapper requestMapper; ? @Resource ? private RestTemplate restTemplate; ? /** ? ?* 根據(jù)相應(yīng)策略轉(zhuǎn)發(fā)請(qǐng)求到對(duì)應(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) { ? ? ? // 需要負(fù)載均衡,獲取appName ? ? ? String appName = StringUtils.substringAfter(instance.getUri(), MyConstants.LB_PREFIX); ? ? ? //從請(qǐng)求頭中獲取是否必須按user去路由到同一節(jié)點(diǎn) ? ? ? // 可用節(jié)點(diǎn) ? ? ? ServerInstance chooseInstance = chooseLBInstance(appName); ? ? ? if (chooseInstance == null) { ? ? ? ? // 無(wú)可用節(jié)點(diǎn),返回異常, ? ? ? ? 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 為負(fù)載均衡之后的URI地址 ? ? ? ? String uri = MyConstants.HTTP_PREFIX + chooseInstance.getHost() + ":" + chooseInstance.getPort(); ? ? ? ? instance.setUri(uri); ? ? ? } ? ? } ? ? // 轉(zhuǎn)發(fā)請(qǐng)求 ? ? try { ? ? ? goForward(request, response, instance); ? ? } catch (Exception e) { ? ? ? // 連接超時(shí)、返回異常 ? ? ? 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ā)送請(qǐng)求到對(duì)應(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é)果,因?yàn)榉祷亟Y(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 選擇對(duì)應(yīng)的host ? ? ServerInstance instance = new ServerInstance(); ? ? instance.setHost("127.0.0.1"); ? ? instance.setPort(10000); ? ? return instance; ? } ? /** ? ?* 寫(xiě)回字符串結(jié)果到客戶(hù)端 ? ?* ? ?* @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(); ? ? } ? } }
啟動(dòng)server,瀏覽器中輸入http://127.0.0.1:8080/oioweb/api/common/rubbish?name=香蕉,就可以把請(qǐng)求代理到https://api.oioweb.cn/api/common/rubbish?name=香蕉了
{ "code": 200, "result": [ { "name": "香蕉", "type": 2, "aipre": 0, "explain": "廚余垃圾是指居民日常生活及食品加工、飲食服務(wù)、單位供餐等活動(dòng)中產(chǎn)生的垃圾。", "contain": "常見(jiàn)包括菜葉、剩菜、剩飯、果皮、蛋殼、茶渣、骨頭等", "tip": "純流質(zhì)的食物垃圾、如牛奶等,應(yīng)直接倒進(jìn)下水口。有包裝物的濕垃圾應(yīng)將包裝物去除后分類(lèi)投放、包裝物請(qǐng)投放到對(duì)應(yīng)的可回收物或干垃圾容器" }, { "name": "香蕉干", "type": 2, "aipre": 0, "explain": "廚余垃圾是指居民日常生活及食品加工、飲食服務(wù)、單位供餐等活動(dòng)中產(chǎn)生的垃圾。", "contain": "常見(jiàn)包括菜葉、剩菜、剩飯、果皮、蛋殼、茶渣、骨頭等", "tip": "純流質(zhì)的食物垃圾、如牛奶等,應(yīng)直接倒進(jìn)下水口。有包裝物的濕垃圾應(yīng)將包裝物去除后分類(lèi)投放、包裝物請(qǐng)投放到對(duì)應(yīng)的可回收物或干垃圾容器" }, { "name": "香蕉皮", "type": 2, "aipre": 0, "explain": "廚余垃圾是指居民日常生活及食品加工、飲食服務(wù)、單位供餐等活動(dòng)中產(chǎn)生的垃圾。", "contain": "常見(jiàn)包括菜葉、剩菜、剩飯、果皮、蛋殼、茶渣、骨頭等", "tip": "純流質(zhì)的食物垃圾、如牛奶等,應(yīng)直接倒進(jìn)下水口。有包裝物的濕垃圾應(yīng)將包裝物去除后分類(lèi)投放、包裝物請(qǐng)投放到對(duì)應(yīng)的可回收物或干垃圾容器" } ], "msg": "success" }
到此這篇關(guān)于SpringBoot實(shí)現(xiàn)反向代理的示例代碼的文章就介紹到這了,更多相關(guān)SpringBoot 反向代理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaEE開(kāi)發(fā)基于Eclipse的環(huán)境搭建以及Maven Web App的創(chuàng)建
本文主要介紹了如何在Eclipse中創(chuàng)建的Maven Project,本文是JavaEE開(kāi)發(fā)的開(kāi)篇,也是基礎(chǔ)。下面內(nèi)容主要包括了JDK1.8的安裝、JavaEE版本的Eclipse的安裝、Maven的安裝、Tomcat 9.0的配置、Eclipse上的M2Eclipse插件以及STS插件的安裝。2017-03-03解決JPA?save()方法null值覆蓋掉mysql預(yù)設(shè)的默認(rèn)值問(wèn)題
這篇文章主要介紹了解決JPA?save()方法null值覆蓋掉mysql預(yù)設(shè)的默認(rèn)值問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11Spring+Quartz實(shí)現(xiàn)動(dòng)態(tài)任務(wù)調(diào)度詳解
這篇文章主要介紹了Spring+Quartz實(shí)現(xiàn)動(dòng)態(tài)任務(wù)調(diào)度詳解,最近經(jīng)?;趕pring?boot寫(xiě)定時(shí)任務(wù),并且是使用注解的方式進(jìn)行實(shí)現(xiàn),分成的方便將自己的類(lèi)注入spring容器,需要的朋友可以參考下2024-01-01SpringBoot實(shí)現(xiàn)異步的八種方法
Spring Boot 的異步處理主要是通過(guò)非阻塞I/O和回調(diào)機(jī)制來(lái)實(shí)現(xiàn)的,目的是提高應(yīng)用的并發(fā)性能,它支持多種方式來(lái)創(chuàng)建異步任務(wù),本文給大家介紹了SpringBoot實(shí)現(xiàn)異步的八種方法,需要的朋友可以參考下2024-07-07MyBatis一級(jí)緩存與二級(jí)緩存原理與作用分析
mybatis-plus是一個(gè)Mybatis的增強(qiáng)工具,在Mybatis的基礎(chǔ)上只做增強(qiáng)不做改變,為簡(jiǎn)化開(kāi)發(fā)、提高效率而生,這篇文章帶你了解Mybatis的一級(jí)和二級(jí)緩存2022-12-12java遞歸實(shí)現(xiàn)拼裝多個(gè)api的結(jié)果操作方法
本文給大家分享java遞歸實(shí)現(xiàn)拼裝多個(gè)api的結(jié)果的方法,說(shuō)白了就是好幾個(gè)API結(jié)果拼裝成的,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2021-09-09