1. 概述
我目前在公司负责开放平台项目,使用spring-cloud-gateway作为开放平台网关,我们网关处理两类数据,都是通过post提交,一类普通数据接口,是通过application/json提交到网关的, 一类是文件上传接口,通过multipart/form-data的方式提交到网关,今天分享下使用multipart/form-data遇到的问题及解决方式。
2. 问题
因为我们是开放平台,开发者接入开放平台,调用开放平台接口的时候会顺带提交开发者信息,方便开放平台后续业务处理,在文件上传接口中,开发者信息也是一并通过multipart/form-data的方式提交的,此时,我需要从multipart/form-data中获取解析出开发者信息,问题出现在获取开发者信息这里。
通过api的方式获取MultipartData,是类似这样的代码:
return exchange.getMultipartData().flatMap(data ->{
Map<String, Part> map = data.toSingleValueMap();
Part msgIdPart = map.get("msgId");
String msgId = parseFormFieldPartValue(msgIdPart);
record.setMsgId(msgId);
Part orgIdPart = map.get("orgId");
String orgId = parseFormFieldPartValue(orgIdPart);
exchange.getAttributes().put(OpenApiConstants.ORGID, orgId);
Part appIdPart = map.get("appId");
String appId = parseFormFieldPartValue(appIdPart);
exchange.getAttributes().put(OpenApiConstants.APPID, appId);
获取MultiparData非常完美,但问题是WebFlux的body数据只能打开一次,当我获取完MultipartData之后,想通过gateway把body数据传输到后台服务,此时body数据为空,后台服务获取不到数据。
3. 解决方案
3.1 解决方案一
既然body数据只能打开一次,那打开之后能不能在设置body呢,application/json类型的接口我就是这么做的,区别是application/json数据可以把Flux中的数据转换成字符串类型,比较好处理。multipart/form-data数据需要借助api,处理成Part类型的数据,怎样把Part类型的数据在转化成body呢,我尝试了很多种方式,最终以失败告终。因为工期等原因,我没有太深入的研究这部分内容,如果各位有好的方式,还望多多指教,如果后期我研究出来了,我会来更新。
3.2 解决方案二
第二种解决方案,是阻断gateway的filter链,此时不在通过gateway本身的机制把请求转发到后台服务了,而是直接发起对后台的调用。这种方式比较复杂,需要考虑分布式的一些问题,比如如何获取后端服务,如何对后端服务进行负载均衡调用等。工期也不允许我这么做,所以我没有采用第二种方式。
3.3 解决方案三
第三种方案,就是把multipart/form-data数组转换成string(在这之前我借用了网上一种方式,缓存body,使body可以多次获取,但是缓存的方式与通过api获取MultipartData数据互斥),然后解析string,数据转换成string之后,是类似如下的格式:
--joHDquiO2YE1x7arAlPHp9qOVpgrL-1q_
Content-Disposition: form-data; name="file"; filename="测试文件上传.txt"
Content-Type: application/octet-stream
测试文件上传
--joHDquiO2YE1x7arAlPHp9qOVpgrL-1q_
Content-Disposition: form-data; name="uploader"
test
--joHDquiO2YE1x7arAlPHp9qOVpgrL-1q_
Content-Disposition: form-data; name="appId"
testapp
--joHDquiO2YE1x7arAlPHp9qOVpgrL-1q_
Content-Disposition: form-data; name="isTemp"
0
--joHDquiO2YE1x7arAlPHp9qOVpgrL-1q_
Content-Disposition: form-data; name="tenantId"
000000
--joHDquiO2YE1x7arAlPHp9qOVpgrL-1q_
Content-Disposition: form-data; name="msgId"
c0dc333c-373a-4326-87ec-ddd64e2bf081
--joHDquiO2YE1x7arAlPHp9qOVpgrL-1q_
Content-Disposition: form-data; name="orgId"
orgId
--joHDquiO2YE1x7arAlPHp9qOVpgrL-1q_
Content-Disposition: form-data; name="timestamp"
1661162528991
--joHDquiO2YE1x7arAlPHp9qOVpgrL-1q_--
当上传的是文本文件的时候,这个格式还勉强可以接受,如果上传文件是类似图片的二进制数据,格式就非常难以阅读了,不过不影响解决问题,
我写了一段儿代码,来解析这种格式,在windows和linux上亲测可用,虽然非常难看,但确实可以解决问题,代码如下所示:
public static Map<String, String> readMultiFormData(String s, String splitStr, List<String> fileParam) {
try {
log.info("MultiFormDataUtil.readMultiFormData lineSeparator:{} , s : {}, splitStr : {} ", System.lineSeparator(), s, splitStr);
String [] strArray = s.split(splitStr);
String sysSeparter = System.lineSeparator();
Map<String, String> paramMap = new HashMap<>();
Arrays.stream(strArray).map(item ->{
try {
return item.replaceAll(sysSeparter, ";");
} catch (Exception e) {
return "";
}
}).filter(item ->{
//去除空的
item = item.trim();
return StringUtils.isNotEmpty(item);
}).map(item->{
item = item.trim();
while (';' == item.charAt(0)) {
item = item.substring(1);
}
while (';' == item.charAt(item.length()-1)) {
item = item.substring(0, item.length()-1);
}
return item;
}).filter(item ->{
return item.contains("Content-Disposition");
}).forEach(item ->{
parseData(paramMap, item, ";", fileParam);
});
return paramMap;
} catch (Exception e) {
log.error("MultiFormDataUtil.readMultiFormData error " + e.getMessage(), e);
return Collections.EMPTY_MAP;
}
}
private static Map<String, String> parseData(Map<String, String> paramMap, String str, String sep, List<String> fileParam) {
log.info("MultiFormDataUtil.parseData str:{}, sep:{}", str, sep);
String key = "";
String[] array = str.split(sep);
for(int i=0; i<array.length; i++) {
String ss = array[i].trim();
if(ss.startsWith("name=")) {
key = ss.substring(ss.indexOf('=') + 2, ss.length()-1);
key = key.trim();
String value = "";
if(fileParam.contains(key)) {
//是file时寻找file名称
value = findFileName(array);
value = value.trim();
} else {
//不是file时计算key的值
value = array[array.length-2];
value = value.trim();
}
if(paramMap.get(key) == null) {
paramMap.put(key, value);
} else {
String oldValue = paramMap.get(key);
paramMap.put(key, oldValue + ";" + value);
}
break;
}
}
return paramMap;
}
/**
* 解析file名称
* @return
*/
private static String findFileName(String [] arrays) {
try {
String fileName = Arrays.stream(arrays).filter(item ->{
return item.contains("filename") ? true : false;
}).map(item ->{
item = item.substring(item.indexOf('=') + 2, item.length()-1);
return item;
}).findFirst().orElse("");
return fileName;
} catch (Exception e) {
return "";
}
}
入口方法readMultiFormData需要三个参数
第一个参数是待解析的文本内容。
第二个参数是分隔符,可以通过contentType.getParameter(“boundary”)的方式获取
第三个参数是上传文件的参数名,我代码里是file
解析出来的结果如下图所示:
有用的到的朋友可以尝试下,如果大家有更好的解决这个问题的方式,不要忘了告诉我,感谢!
4. 总结
本文介绍了在spring-cloud-gateway中通过multipart/form-data上传文件时,获取参数的三种思路并给出了第三种思路的实现方式。