springboot中请求地址转发的两种方案

 更新时间:2023年11月24日 15:29:02   作者:Mr.var  
在开发过程中,我们经常需要将请求从一个服务转发到另一个服务,以实现不同服务之间的协作,本文主要介绍了springboot中请求地址转发的两种方案,感兴趣的可以了解一下

一、背景需求

现有一个平台,如果在上面发布软件,需要在平台注册所有的接口,注册好后平台会给每一个接口都提供一个不同的新地址(所有的请求在平台注册后都是类似"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 请求地址转发内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家! 

相关文章

  • Java多线程之线程同步

    Java多线程之线程同步

    这篇文章主要介绍了Java多线程之线程同步,文中有非常详细的代码示例,对正在学习java的小伙伴们有非常好的帮助,需要的朋友可以参考下
    2021-05-05
  • java时间 java.util.Calendar深入分析

    java时间 java.util.Calendar深入分析

    这篇文章主要介绍了java时间 java.util.Calendar深入分析的相关资料,需要的朋友可以参考下
    2017-02-02
  • springcloud下hibernate本地化方言配置方式

    springcloud下hibernate本地化方言配置方式

    这篇文章主要介绍了springcloud下hibernate本地化方言配置方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-09-09
  • Spring MessageSource获取消息不符合预期的问题解决方案

    Spring MessageSource获取消息不符合预期的问题解决方案

    最近我参与的产品要做国际化支持,选择了用Spring MessageSource来实现,这个Spring 框架提供的工具使用很简单,网上有各种教程文章,这里不做赘述,只说一个实际遇到的问题及解决方案,需要的朋友可以参考下
    2024-01-01
  • Java8默认方法Default Methods原理及实例详解

    Java8默认方法Default Methods原理及实例详解

    这篇文章主要介绍了Java8默认方法Default Methods原理及实例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-01-01
  • Java正则判断日期格式是否正确的方法示例

    Java正则判断日期格式是否正确的方法示例

    这篇文章主要介绍了Java正则判断日期格式是否正确的方法,结合实例形式分析了Java针对日期字符串正则判断的相关操作技巧,需要的朋友可以参考下
    2017-03-03
  • Java实现PDF转Word的示例代码(无水印无页数限制)

    Java实现PDF转Word的示例代码(无水印无页数限制)

    这篇文章主要为大家详细介绍了如何利用Java语言实现PDF转Word文件的效果,并可以无水印、无页数限制。文中的示例代码讲解详细,需要的可以参考一下
    2022-05-05
  • Springboot自带线程池的实现

    Springboot自带线程池的实现

    本文主要介绍了Springboot自带线程池的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-05-05
  • Java中Servlet的生命周期

    Java中Servlet的生命周期

    这篇文章主要介绍了Java中Servlet的生命周期,Servlet 初始化后调用 init () 方法、Servlet 调用 service() 方法来处理客户端的请求、Servlet 销毁前调用 destroy() 方法,下面来看看具体的解析吧,需要的小伙伴可以参考一下
    2022-01-01
  • Java项目工程代码深度刨析总结

    Java项目工程代码深度刨析总结

    一个项目工程里的代码是怎样的呢?对于初学者或者没有参与过项目的零经验同学这都是未知且让人好奇的,本篇文章带你一探究竟,踏入真实项目的大门
    2022-08-08

最新评论