关于SpringCloud gateway修改 “application/json” 类型的请求体的示例代码,网上的教程很全。但是对于修改 "multipart/form-data" 类型——既包含文件流信息又包含参数信息的请求体的示例代码并不是很多。本篇主要是根据本人工作碰到的问题来简单说明一下对于post请求发送的 "multipart/form-data" 类型请求体如何修改。因为本人技术有限,所以代码可能不优雅,如果有更好的修改办法,欢迎探讨。
首先我们需要认识到一点,就是无论是application/json类型的数据,还是multipart/form-data类型数据,还是以后可能存在的application/x-www-form-urlencoded类型的数据,如果需要进行修改,其实都是对request中的body信息进行修改。所以基于这个思想,我们的修改步骤分为:1)读取request请求中的body信息;2)对body信息进行修改;3)重新组装request请求
在此之前,我们需要先知道一些相关点:
- ServerHttpRequest实例是用于承载请求相关的属性和请求体,所以我们修改请求的body内容,其实是对它进行修改。
- ServerHttpRequest对象可以从ServerWebExchange中进行获取,但是ServerWebExchange实例可以理解为不可变实例,如果我们想要修改它,需要通过 mutate() 方法生成一个新的实例。
- ServerWebExchange实例中持有的ServerHttpRequest实例的具体实现是ReactorServerHttpRequest;ReactorServerHttpRequest的父类AbstractServerHttpRequest中初始化内部属性headers的时候把请求的HTTP头部封装为只读的实例;所以, 不能直接从ServerHttpRequest实例中直接获取请求头HttpHeaders实例并且进行修改。
- 不同content-type类型的body有着自己固定的格式要去,我们需要的就是在完成我们想要的修改后把body恢复到规定的格式,此外就是请求头中的Content-Length字段要刷新成我们修改后body的长度
- 对于 post请求发送的"multipart/form-data" 类型的数据:请求头中的 Content-Type 是 multipart/form-data; 并且会随机生成 一个 boundary, 用于区分请求 body 中的各个数据; 每个数据以 --boundary 开始, 紧接着换行,下面是内容描述信息, 接着换2行, 接着是数据; 然后以 --boundary-- 结尾, 最后换行;示例报文格式如下:
POST HTTP/1.1
Host: www.demo.com
Cache-Control: no-cache
Postman-Token: 679d816d-8757-14fd-57f2-fbc2518dddd9
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="key"
value
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="testKey"
testValue
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="imgFile"; filename="no-file"
Content-Type: application/octet-stream
<data in here>
------WebKitFormBoundary7MA4YWxkTrZu0gW--
有了以上了解后,我们现在开始进行步骤的实施:
1. 读取request中的body信息
我们在读取body内容的时候需要先用到ServerWebExchange来进行ServerHttpRequest对象的获取。ServerWebExchange是一个HTTP请求-响应交互的契约。提供对HTTP请求和响应的访问,并公开额外的服务器端处理相关属性和特性,如请求属性。
ServerHttpRequest request = erverWebExchange.getRequest();
之后再使用ServerHttpRequest对象进行body信息读取,在这里我们是把读取到的字节流信息转为了String
exchange.getRequest().getBody().flatMap(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
String bodyString = null;
try {
bodyString = new String(bytes, "utf-8");
}catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
log.info(bodyString);//打印请求参数
return Mono.just(bodyString);});
2. 对body信息进行修改
上一步我们已经可以获取到request中的body内容了,既然可以获取到内容,就说明我们可以对body进行修改后替换。
首先需要对获取到的String字符串进行解析,本人是根据content-type中的boundary字符传进行的分割解析:
String boundary = contentType.substring(contentType.lastIndexOf("boundary=") + 9);
String[] split = bodyString.split(boundary);
之后就可以根据解析到的信息进行自己想要的修改替换。修改替换完成后就可以按照报文格式进行body信息的拼接。示例拼接的工具类代码如下(此代码参考了hutool的代码信息),如果想要进一步优化,可以去看下相关的成熟的工具类信息,自己进行拼接。
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.resource.MultiResource;
import cn.hutool.core.io.resource.Resource;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.HttpUtil;
import com.google.common.collect.Maps;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
@Slf4j
public class RequestAddParaUtils {
private static final String CONTENT_DISPOSITION_TEMPLATE = "Content-Disposition: form-data; name=\"{}\"\r\n\r\n";
private static final String CONTENT_DISPOSITION_FILE_TEMPLATE = "Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\n";
private static final String CONTENT_TYPE_MULTIPART_PREFIX = ContentType.MULTIPART.getValue() + "; boundary=";
private static final String CONTENT_TYPE_FILE_TEMPLATE = "Content-Type: {}\r\n\r\n";
/**
*
* @param contentType 请求类型
* @param bodyString 请求body信息
*/
@SneakyThrows
public static String addPara(String contentType, String bodyString) {
StringBuffer stringBuffer = new StringBuffer();
String boundary = contentType.substring(contentType.lastIndexOf("boundary=") + 9);//获取随机字符传信息
String boundary_end = StrUtil.format("--{}--\r\n", boundary);
Map<String, Object> formMap = Maps.newHashMap();
/**
*
* 根据自己需求进行对bodyString信息修改,例如下面,根据boundary对传入的bodyString进行了分割
* String[] split = bodyString.split(boundary);
* 然后将修改完后的信息封装到formMap中,需要注意的是,file文件需要以new FileResource(file, fileName)的形式当作value放到formMap中
*/
for (Map.Entry<String, Object> entry : formMap.entrySet()) {
stringBuffer.append(appendPart(boundary, entry.getKey(), entry.getValue()));
}
stringBuffer.append(boundary_end);//拼接结束信息
log.info(stringBuffer.toString());
return stringBuffer.toString();
}
/**
* 添加Multipart表单的数据项
*
* @param boundary 随机串信息
* @param formFieldName 表单名
* @param value 值,可以是普通值、资源(如文件等)
* @throws IORuntimeException IO异常
*/
private static String appendPart(String boundary, String formFieldName, Object value) throws IORuntimeException {
StringBuffer stringBuffer = new StringBuffer();
// 多资源
if (value instanceof MultiResource) {
for (Resource subResource : (MultiResource) value) {
appendPart(boundary, formFieldName, subResource);
}
return stringBuffer.toString();
}
stringBuffer.append("--").append(boundary).append(StrUtil.CRLF);
if (value instanceof Resource) {
// 文件资源(二进制资源)
final Resource resource = (Resource) value;
final String fileName = resource.getName();
stringBuffer.append(StrUtil.format(CONTENT_DISPOSITION_FILE_TEMPLATE, formFieldName, ObjectUtil.defaultIfNull(fileName, formFieldName)));
// 根据name的扩展名指定互联网媒体类型,默认二进制流数据
stringBuffer.append(StrUtil.format(CONTENT_TYPE_FILE_TEMPLATE, HttpUtil.getMimeType(fileName, "application/octet-stream")));
} else {
// 普通数据
stringBuffer.append(StrUtil.format(CONTENT_DISPOSITION_TEMPLATE, formFieldName)).append(value);
}
stringBuffer.append(StrUtil.CRLF);
return stringBuffer.toString();
}
然后将组装好的finalStringData信息封装到Flux中。
Flux cachedFlux = Flux.defer(() -> Mono.just(exchange.getResponse().bufferFactory()
.wrap(finalStringData.getBytes())));
3.重新组装request请求
我们修改好body的内容后,就可以进行request的重新组装了,这一步主要是更新Content-Length的信息和我们重新组装的body信息。
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(
exchange.getRequest()) {
@Override
public HttpHeaders getHeaders() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
httpHeaders.remove(HttpHeaders.CONTENT_TYPE);
httpHeaders.set(HttpHeaders.CONTENT_LENGTH, finalStringData.getBytes().length + "");
return httpHeaders;
}
@Override
public Flux getBody() {
return cachedFlux;
}
};
4.代码归纳与说明
以上就是对于post请求发送的"multipart/form-data" 类型的数据进行修改的所有步骤,然后加上其他的细枝末节,总体代码信息如下
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.resource.MultiResource;
import cn.hutool.core.io.resource.Resource;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.google.common.collect.Maps;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.*;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Map;
@Component
@Slf4j
public class WrapperRequestGlobalFilter implements GlobalFilter, Ordered {//, Ordered
private static final String CONTENT_DISPOSITION_TEMPLATE = "Content-Disposition: form-data; name=\"{}\"\r\n\r\n";
private static final String CONTENT_DISPOSITION_FILE_TEMPLATE = "Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\n";
private static final String CONTENT_TYPE_FILE_TEMPLATE = "Content-Type: {}\r\n\r\n";
@SneakyThrows
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String contentType = request.getHeaders().getContentType().toString();
if (contentType.contains("multipart/form-data")) {
return DataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
String newBody = "";
dataBuffer.read(bytes);
try {
String bodyString = new String(bytes, "utf-8");
log.info(bodyString);//打印请求参数
newBody = addPara(contentType, bodyString);//进行信息修改
} catch (Exception e) {
e.printStackTrace();
}
DataBufferUtils.release(dataBuffer);
String finalStringData = newBody;
Flux cachedFlux = Flux.defer(() -> Mono.just(exchange.getResponse().bufferFactory()
.wrap(finalStringData.getBytes())));
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(
exchange.getRequest()) {
@Override
public HttpHeaders getHeaders() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
httpHeaders.remove(HttpHeaders.CONTENT_TYPE);
httpHeaders.set(HttpHeaders.CONTENT_LENGTH, finalStringData.getBytes().length + "");
return httpHeaders;
}
@Override
public Flux getBody() { return cachedFlux; }
};
return chain.filter(exchange.mutate().request(mutatedRequest).build());
}).doOnError(e -> {
//此为异常处理,可加可不加
e.printStackTrace();
throw new MonoException("未获取到请求的数据....");
});
} else {
//其他类型同理,可以举一反三
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
public class MonoException extends RuntimeException {
public MonoException(String message) {
super(message);
}
}
/**
* @param contentType 请求类型
* @param bodyString 请求body信息
*/
@SneakyThrows
public static String addPara(String contentType, String bodyString) {
StringBuffer stringBuffer = new StringBuffer();
String boundary = contentType.substring(contentType.lastIndexOf("boundary=") + 9);//获取随机字符传信息
String boundary_end = StrUtil.format("--{}--\r\n", boundary);
Map<String, Object> formMap = Maps.newHashMap();
/**
*
* 根据自己需求进行对bodyString信息修改,例如下面,根据boundary对传入的bodyString进行了分割
* String[] split = bodyString.split(boundary);
* 然后将修改完后的信息封装到formMap中,需要注意的是,file文件需要以new FileResource(file, fileName)的形式当作value放到formMap中
*/
for (Map.Entry<String, Object> entry : formMap.entrySet()) {
stringBuffer.append(appendPart(boundary, entry.getKey(), entry.getValue()));
}
stringBuffer.append(boundary_end);//拼接结束信息
log.info(stringBuffer.toString());
return stringBuffer.toString();
}
/**
* 添加Multipart表单的数据项
*
* @param boundary 随机串信息
* @param formFieldName 表单名
* @param value 值,可以是普通值、资源(如文件等)
* @throws IORuntimeException IO异常
*/
private static String appendPart(String boundary, String formFieldName, Object value) throws IORuntimeException {
StringBuffer stringBuffer = new StringBuffer();
// 多资源
if (value instanceof MultiResource) {
for (Resource subResource : (MultiResource) value) {
appendPart(boundary, formFieldName, subResource);
}
return stringBuffer.toString();
}
stringBuffer.append("--").append(boundary).append(StrUtil.CRLF);
if (value instanceof Resource) {
// 文件资源(二进制资源)
final Resource resource = (Resource) value;
final String fileName = resource.getName();
stringBuffer.append(StrUtil.format(CONTENT_DISPOSITION_FILE_TEMPLATE, formFieldName, ObjectUtil.defaultIfNull(fileName, formFieldName)));
// 根据name的扩展名指定互联网媒体类型,默认二进制流数据
stringBuffer.append(StrUtil.format(CONTENT_TYPE_FILE_TEMPLATE, HttpUtil.getMimeType(fileName, "application/octet-stream")));
} else {
// 普通数据
stringBuffer.append(StrUtil.format(CONTENT_DISPOSITION_TEMPLATE, formFieldName)).append(value);
}
stringBuffer.append(StrUtil.CRLF);
return stringBuffer.toString();
}
}