springboot中请求地址转发方案
作者:mmseoamin日期:2023-12-18

一、背景需求

现有一个平台,如果在上面发布软件,需要在平台注册所有的接口,注册好后平台会给每一个接口都提供一个不同的新地址(所有的请求在平台注册后都是类似"http://localhost:8080/{appkey}/{token}"的格式,每个接口都拥有一个不同的appkey作为标识,token可通过另一个请求获取),在前端调用请求的时候,必须请求平台提供的地址,然后平台会替前端转发到真实的地址去请求后端。

为了减少注册和审核的工作量,我们可以只注册少量接口,然后在这些接口内我们自行转发。

二、方案一

zuul转发:

在平台注册增删改查等若干个虚拟的接口地址,然后在前端将所有接口封装成这些虚拟接口,并在请求参数内传递真实的接口地址,通过平台转发到后端之后我们通过zuul过滤器再转发到自己真实的接口地址上。(注:登录接口比较特殊,登录在后端是写在主服务内的,zuul网关不会进行拦截,这里单独注册;其余接口统一写在同一个服务内,便于统一转发配置)

前端代码演示:

这里是在vue中写的一个axios的请求拦截器,统一对真实接口进行封装

举个例:

我们在平台上注册一个虚拟地址:

http://localhost:8080/comSelect/getData

=>

注册后请求地址变为:

http://xxx.xxx.xxx:xxxx/appKeySelect123/{token}

axios.interceptors.request.use(
	config => {
		if (!config.url.startsWith("http")) {
			    //模拟一个token,真实token可通过平台提供的另一请求获取
				let token = "token";
				
				//将接口地址放在covertUrl参数内传递给后端
				if (config.method == "post") {
				    //post请求的两种content-type格式
					if (typeof config.data == "string") {
						//请求参数表单格式
						//qs可用于格式化参数
						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中get请求可用params指定url传值
					config.params.covertUrl = config.url;
				}
                //封装成平台要求的请求地址,真实的url存于参数covertUrl中
				config.url = urlPack(token, config.url);
		}
		return config;
	},
	error => {
		return Promise.reject(error);
	}
);
//接口地址封装,将所有接口统一分为增删改查四个接口
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的路由配置里添加平台转发之后传递过来的虚拟路由,不然zuul会报出找不到路由的错
zuul:
  #路由添加
  routes:
    #虚拟服务地址
    comSelect:
      path: /comSelect/**
    #真实的路由服务,这里的地址是真实注册到了eureka的服务地址,也可以动态获取
    appcommon:
      path: /appcommon/**
      serviceId: appcommon
/**
 * 转换成真正的url地址,路由转发
 */
@Slf4j
@Component
public class ZuulAppRouteFilter extends ZuulFilter {
    /**
     * filterType:返回一个字符串代表过滤器的类型,在zuul中定义了四种不同生命周期的过滤器类型,具体如下:
     *     pre:路由之前
     *     routing:路由之时
     *     post: 路由之后
     *     error:发送错误调用
     */
    @Override
    public String filterType() {
        return FilterConstants.ROUTE_TYPE;
    }
    /**
     * 过滤器优先级,同一filterType下的过滤器,数值越大优先级越低
     */
    @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 {
            //请求参数(url传值或表单传值)
            Map parameterMap = request.getParameterMap();
            //请求参数(请求体)
            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格式数据,返回信息:" + requestBody);
                    }
                }
            } else {
                if (parameterMap.get("covertUrl") != null) {
                    String[] description = parameterMap.get("covertUrl");
                    path = URLDecoder.decode(description[0]);
                }
            }
            
            //所有的服务全部指向serviceId为appcommon这个路由
            //如果需要转发到其他服务则通过判断path来写判断
            String serviceId = "";
            if (path.contains("appcommon")) {
                    serviceId = "appcommon";
             } else if (path.contains("sync")) {
                    serviceId = "sync";
            }
            //请求地址转发到真实的接口上
            ctx.put(FilterConstants.REQUEST_URI_KEY, path);
        } catch (Exception ignored) {
            log.error(request.getRequestURL().toString() + "解析失败");
        }
        return null;
    }
}

三、方案二:

java反射:

在平台注册增删改查等若干个接口地址,并在后端编写这些接口作为统一分发接口,然后在前端将所有接口封装成这些接口,并在请求参数内传递接口的类名和对应的方法名,通过平台转发传递到后端之后,后端利用Java的反射机制调用真实的接口地址,转发到对应的接口上。

前端代码演示:

举个例:

我们在平台上注册的地址:

http://localhost:8080/appcommon/common/query

=>

注册后请求地址变为:

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") {
						//请求参数表单格式
						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存于参数covertUrl中
				config.url = urlPack(token, config.url);
		}
		return config;
	},
	error => {
		return Promise.reject(error);
	}
);
//接口地址封装,将所有接口统一分为增删改查四个接口
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
	//请求参数
	let reqData = {
		//类名
		className: "",
		//方法名
		methodName: "",
		//接口所需参数
		params: data
	}
	
	//指定不同接口的类名和方法名,用于分发调用
	switch (url) {
	    //登录请求比较特殊,单独注册,参数不封装
		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;
    /**
     * 实际参数
     */
    private Map params;
}
/**
 * Spring定义的类实现ApplicationContextAware接口会自动的将应用程序上下文加入
 */
@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 getBean(Class clazz) {
        return getApplicationContext().getBean(clazz);
    }
    //通过name,以及Clazz返回指定的Bean
    public static  T getBean(String name, Class clazz) {
        return getApplicationContext().getBean(name, clazz);
    }
}
/**
 * app公共接口调用,通过反射分发调用接口
 *
 * @author xht
 */
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
    /**
     * 利用反射调用接口
     */
    public Response reflectControl(CommonObj commonObj){
        String className = commonObj.getClassName();
        String methodName = commonObj.getMethodName();
        Map 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、封装方法需要的参数
            if (myMethod != null) {
                Object resObj;
                resObj = myMethod.invoke(proxyObject, params);
                response = (Response) resObj;
            } else {
                response = Response.error("未找到对应方法");
            }
        } 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);
    }
}