一、背景需求
现有一个平台,如果在上面发布软件,需要在平台注册所有的接口,注册好后平台会给每一个接口都提供一个不同的新地址(所有的请求在平台注册后都是类似"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);
}
}