springboot中请求地址转发的两种方案
作者:Mr.var
一、背景需求
现有一个平台,如果在上面发布软件,需要在平台注册所有的接口,注册好后平台会给每一个接口都提供一个不同的新地址(所有的请求在平台注册后都是类似"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<String, String[]> 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<String,Object> 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> 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公共接口调用,通过反射分发调用接口 * * @author xht */ @RestController @RequestMapping("/common") @Slf4j public class CommonController { /** * 利用反射调用接口 */ 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、封装方法需要的参数 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); } }
到此这篇关于springboot中请求地址转发的两种方案的文章就介绍到这了,更多相关springboot 请求地址转发内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!