springboot中請求地址轉(zhuǎn)發(fā)的兩種方案
一、背景需求
現(xiàn)有一個平臺,如果在上面發(fā)布軟件,需要在平臺注冊所有的接口,注冊好后平臺會給每一個接口都提供一個不同的新地址(所有的請求在平臺注冊后都是類似"http://localhost:8080/{appkey}/{token}"的格式,每個接口都擁有一個不同的appkey作為標(biāo)識,token可通過另一個請求獲?。?,在前端調(diào)用請求的時候,必須請求平臺提供的地址,然后平臺會替前端轉(zhuǎn)發(fā)到真實的地址去請求后端。
為了減少注冊和審核的工作量,我們可以只注冊少量接口,然后在這些接口內(nèi)我們自行轉(zhuǎn)發(fā)。
二、方案一
zuul轉(zhuǎn)發(fā):
在平臺注冊增刪改查等若干個虛擬的接口地址,然后在前端將所有接口封裝成這些虛擬接口,并在請求參數(shù)內(nèi)傳遞真實的接口地址,通過平臺轉(zhuǎn)發(fā)到后端之后我們通過zuul過濾器再轉(zhuǎn)發(fā)到自己真實的接口地址上。(注:登錄接口比較特殊,登錄在后端是寫在主服務(wù)內(nèi)的,zuul網(wǎng)關(guān)不會進行攔截,這里單獨注冊;其余接口統(tǒng)一寫在同一個服務(wù)內(nèi),便于統(tǒng)一轉(zhuǎn)發(fā)配置)
前端代碼演示:
這里是在vue中寫的一個axios的請求攔截器,統(tǒng)一對真實接口進行封裝
舉個例:
我們在平臺上注冊一個虛擬地址:
http://localhost:8080/comSelect/getData
=>
注冊后請求地址變?yōu)椋?br />http://xxx.xxx.xxx:xxxx/appKeySelect123/{token}
axios.interceptors.request.use(
config => {
if (!config.url.startsWith("http")) {
//模擬一個token,真實token可通過平臺提供的另一請求獲取
let token = "token";
//將接口地址放在covertUrl參數(shù)內(nèi)傳遞給后端
if (config.method == "post") {
//post請求的兩種content-type格式
if (typeof config.data == "string") {
//請求參數(shù)表單格式
//qs可用于格式化參數(shù)
let conData = qs.parse(config.data);
conData.covertUrl = config.url;
config.data = qs.stringify(conData);
} else {
//請求體格式
config.data.covertUrl = config.url;
}
} else if (config.method == "get"){
//axios中g(shù)et請求可用params指定url傳值
config.params.covertUrl = config.url;
}
//封裝成平臺要求的請求地址,真實的url存于參數(shù)covertUrl中
config.url = urlPack(token, config.url);
}
return config;
},
error => {
return Promise.reject(error);
}
);
//接口地址封裝,將所有接口統(tǒng)一分為增刪改查四個接口
function urlPack(token, url) {
let appKey;
//登陸
let appKeyLogin = "/appKeyLogin123/";
//增
let appKeyAdd = "/appKeyAdd123/";
//刪
let appKeyDelete = "/appKeyDelete123/";
//改
let appKeyUpdate = "/appKeyUpdate123/";
//查
let appKeySelect = "/appKeySelect123/"; //http://localhost:8080/comSelect/getData
//隨便拿幾個接口舉例
switch (url) {
case "/sysUser/app_login":
appKey = appKeyLogin;
break;
case "/appcommon/appVersion/getVersion":
appKey = appKeySelect;
break;
case "/appcommon/appMenu/getMenu":
appKey = appKeySelect;
break;
}
return "http://localhost:8080" + appKey + token;
}
后端代碼演示:
#這里需要注意,必須在zuul的路由配置里添加平臺轉(zhuǎn)發(fā)之后傳遞過來的虛擬路由,不然zuul會報出找不到路由的錯
zuul:
#路由添加
routes:
#虛擬服務(wù)地址
comSelect:
path: /comSelect/**
#真實的路由服務(wù),這里的地址是真實注冊到了eureka的服務(wù)地址,也可以動態(tài)獲取
appcommon:
path: /appcommon/**
serviceId: appcommon
/**
* 轉(zhuǎn)換成真正的url地址,路由轉(zhuǎn)發(fā)
*/
@Slf4j
@Component
public class ZuulAppRouteFilter extends ZuulFilter {
/**
* filterType:返回一個字符串代表過濾器的類型,在zuul中定義了四種不同生命周期的過濾器類型,具體如下:
* pre:路由之前
* routing:路由之時
* post: 路由之后
* error:發(fā)送錯誤調(diào)用
*/
@Override
public String filterType() {
return FilterConstants.ROUTE_TYPE;
}
/**
* 過濾器優(yōu)先級,同一filterType下的過濾器,數(shù)值越大優(yōu)先級越低
*/
@Override
public int filterOrder() {
return 1;
}
/**
* 是否啟用過濾器,這里可以做一些邏輯判斷
*/
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
//真正的接口地址
String path = "";
try {
//請求參數(shù)(url傳值或表單傳值)
Map<String, String[]> parameterMap = request.getParameterMap();
//請求參數(shù)(請求體)
String requestBody = null;
try {
requestBody = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
} catch (IOException e) {
e.printStackTrace();
}
if (parameterMap.size() == 0) {
if (requestBody != null && !"".equals(requestBody)) {
try {
JSONObject jsonObj = JSONObject.parseObject(requestBody);
if (jsonObj.get("covertUrl") != null) {
Object covertUrl = jsonObj.get("covertUrl");
path = String.valueOf(covertUrl);
}
} catch (Exception e) {
log.error("path[" + path + "]返回的不是json格式數(shù)據(jù),返回信息:" + requestBody);
}
}
} else {
if (parameterMap.get("covertUrl") != null) {
String[] description = parameterMap.get("covertUrl");
path = URLDecoder.decode(description[0]);
}
}
//所有的服務(wù)全部指向serviceId為appcommon這個路由
//如果需要轉(zhuǎn)發(fā)到其他服務(wù)則通過判斷path來寫判斷
String serviceId = "";
if (path.contains("appcommon")) {
serviceId = "appcommon";
} else if (path.contains("sync")) {
serviceId = "sync";
}
//請求地址轉(zhuǎn)發(fā)到真實的接口上
ctx.put(FilterConstants.REQUEST_URI_KEY, path);
} catch (Exception ignored) {
log.error(request.getRequestURL().toString() + "解析失敗");
}
return null;
}
}
三、方案二:
java反射:
在平臺注冊增刪改查等若干個接口地址,并在后端編寫這些接口作為統(tǒng)一分發(fā)接口,然后在前端將所有接口封裝成這些接口,并在請求參數(shù)內(nèi)傳遞接口的類名和對應(yīng)的方法名,通過平臺轉(zhuǎn)發(fā)傳遞到后端之后,后端利用Java的反射機制調(diào)用真實的接口地址,轉(zhuǎn)發(fā)到對應(yīng)的接口上。
前端代碼演示:
舉個例:
我們在平臺上注冊的地址:
http://localhost:8080/appcommon/common/query
=>
注冊后請求地址變?yōu)椋?br />http://localhost:8080/appKeySelect123/{token}
axios.interceptors.request.use(
config => {
if (!config.url.startsWith("http")) {
//模擬一個token,真實token可通過平臺提供的另一請求獲取
let token = "token";
let req;
if (config.method == "post") {
if (typeof config.data == "string") {
//請求參數(shù)表單格式
let conData = qs.parse(config.data);
config.data = qs.stringify(conData);
req = reqPack(token, config.url, config.data);
config.url = req.url;
config.data = req.reqData;
} else {
//請求體格式
req = reqPack(token, config.url, config.data);
config.url = req.url;
config.data = req.reqData;
}
} else {
req = reqPack(token, config.url, config.params);
config.url = req.url;
config.params = req.reqData;
}
//封裝成平臺要求的請求地址,真實的url存于參數(shù)covertUrl中
config.url = urlPack(token, config.url);
}
return config;
},
error => {
return Promise.reject(error);
}
);
//接口地址封裝,將所有接口統(tǒng)一分為增刪改查四個接口
function urlPack(token, url, data) {
//總線所需的key
let appKey;
//登陸
let appKeyLogin = "/appKeyLogin123/";
//增
let appKeyAdd = "/appKeyAdd123/";
//刪
let appKeyDelete = "/appKeyDelete123/";
//改
let appKeyUpdate = "/appKeyUpdate123/";
//查
let appKeySelect = "/appKeySelect123/"; //http://localhost:8080/appcommon/common/query
//請求參數(shù)
let reqData = {
//類名
className: "",
//方法名
methodName: "",
//接口所需參數(shù)
params: data
}
//指定不同接口的類名和方法名,用于分發(fā)調(diào)用
switch (url) {
//登錄請求比較特殊,單獨注冊,參數(shù)不封裝
case "/sysUser/app_login":
appKey = appLogin;
return {
url: GLOBAL.$RequestBaseUrl1 + appKey,
reqData: data
}
break;
case "/appcommon/appVersion/getVersion":
appKey = appKeySelect;
reqData.className = "AppVersionController";
reqData.methodName = "getAppVersion";
break;
case "/sync/risk/road/getAllRoad":
appKey = appKeySelect;
break;
case "/appcommon/appMenu/getMenu":
appKey = appKeySelect;
reqData.className = "AppMenuController";
reqData.methodName = "getMenu";
break;
}
return {
url: GLOBAL.$RequestBaseUrl1 + appKey + token,
reqData: reqData
};
}
后端代碼演示:
/**
* 公共接口實例
*/
@Data
public class CommonObj {
/**
* 類名
*/
private String className;
/**
* 方法名
*/
private String methodName;
/**
* 實際參數(shù)
*/
private Map<String,Object> params;
}
/**
* Spring定義的類實現(xiàn)ApplicationContextAware接口會自動的將應(yīng)用程序上下文加入
*/
@Slf4j
@Component
public class MySpringUtil implements ApplicationContextAware {
//上下文對象實例
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (MySpringUtil.applicationContext == null) {
MySpringUtil.applicationContext = applicationContext;
}
}
//獲取applicationContext
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
//通過name獲取 Bean.
public static Object getBean(String name) {
return getApplicationContext().getBean(name);
}
//通過class獲取Bean.
public static <T> T getBean(Class<T> clazz) {
return getApplicationContext().getBean(clazz);
}
//通過name,以及Clazz返回指定的Bean
public static <T> T getBean(String name, Class<T> clazz) {
return getApplicationContext().getBean(name, clazz);
}
}
/**
* app公共接口調(diào)用,通過反射分發(fā)調(diào)用接口
*
* @author xht
*/
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
/**
* 利用反射調(diào)用接口
*/
public Response reflectControl(CommonObj commonObj){
String className = commonObj.getClassName();
String methodName = commonObj.getMethodName();
Map<String, Object> params = commonObj.getParams();
Response response;
try {
//1、獲取spring容器中的Bean
//類名首字母小寫
className = StringUtils.uncapitalize(className);
Object proxyObject = MySpringUtil.getBean(className);
//2、利用bean獲取class對象,進而獲取本類以及父類或者父接口中所有的公共方法(public修飾符修飾的)
Method[] methods = proxyObject.getClass().getMethods();
//3、獲取指定的方法
Method myMethod = null;
for (Method method : methods) {
if (method.getName().equalsIgnoreCase(methodName)) {
myMethod = method;
break;
}
}
//4、封裝方法需要的參數(shù)
if (myMethod != null) {
Object resObj;
resObj = myMethod.invoke(proxyObject, params);
response = (Response) resObj;
} else {
response = Response.error("未找到對應(yīng)方法");
}
} catch (Exception e) {
e.printStackTrace();
response = Response.error(e.getMessage());
}
return response;
}
/**
* 公共新增接口
*/
@PostMapping("/add")
public Response commonAdd(@RequestBody CommonObj commonObj) {
return reflectControl(commonObj);
}
/**
* 公共刪除接口
*/
@PostMapping("/delete")
public Response commonDelete(@RequestBody CommonObj commonObj) {
return reflectControl(commonObj);
}
/**
* 公共修改接口
*/
@PostMapping("/edit")
public Response commonEdity(@RequestBody CommonObj commonObj) {
return reflectControl(commonObj);
}
/**
* 公共查詢接口
*/
@PostMapping("/query")
public Response commonQuery(@RequestBody CommonObj commonObj) {
return reflectControl(commonObj);
}
}到此這篇關(guān)于springboot中請求地址轉(zhuǎn)發(fā)的兩種方案的文章就介紹到這了,更多相關(guān)springboot 請求地址轉(zhuǎn)發(fā)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java中instanceOf關(guān)鍵字的用法及特性詳解
當(dāng)我們在進行向下轉(zhuǎn)型時,如果兩個對象之間沒有直接或間接的繼承關(guān)系,在轉(zhuǎn)換時有可能會產(chǎn)生強制類型轉(zhuǎn)換異常,我們可以使用java中自帶的instanceOf關(guān)鍵字來解決這個問題,所以本篇文章,會帶大家學(xué)習(xí)instanceOf的用法及特性,需要的朋友可以參考下2023-05-05
java學(xué)習(xí)DongTai被動型IAST工具部署過程
被動型IAST被認為是DevSecOps測試階段實現(xiàn)自動化安全測試的最佳工具,而就在前幾天,洞態(tài)IAST正式開源了,這對于甲方構(gòu)建安全工具鏈來說,絕對是一個大利好2021-10-10
淺析Java中SimpleDateFormat為什么是線程不安全的
SimpleDateFormat是Java中用于日期時間格式化的一個類,它提供了對日期的解析和格式化能力,本文主要來和大家一起探討一下SimpleDateFormat為什么是線程不安全的,感興趣的可以了解下2024-02-02

