### 1.背景
> 最近有个文件上传的需求,客户端上传二进制流,服务端接收该二进制文件流进行文件处理。因为项目中有一层参数过滤器,读取过一次request中的参数。接口中需再次从request读取流转为byte数组,最后转为MultipartFile进行文件验证上传操作。request中参数多次读取已在之前的文章中实现[多次读取request里面的参数值](https://www.maplefix.top/archives/read-params-inrequest-multiple-times)。本文在原来的的基础上稍加改造,在处理byte数组转MultipartFile的需求。
### 2.AuthRequestWrapper改造
在原来重写的AuthRequestWrapper基础上进行改造,主要是将原始请求内容由String改为byte[],便于处理。
~~~
@Slf4j
public class AuthRequestWrapper extends HttpServletRequestWrapper {
/**
* 原始请求中的请求体(如果是上传请求会包含二进制文件流)
*/
private final byte[] cachedBytes;
/**
* 构造方法设置请求体
* @param request 原始请求
*/
AuthRequestWrapper(HttpServletRequest request){
super(request);
this.cachedBytes = HttpRequestUtils.getCachedBytes();
}
/**
* 重写HttpServletRequestWrapper的getInputStream和getReader方法
* 将原始request中的数据重新设置进去
* @return ServletInputStream
*/
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream bais = new ByteArrayInputStream(cachedBytes);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read(){
return bais.read();
}
};
}
@Override
public BufferedReader getReader(){
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
}
~~~
### 3. HttpRequestUtils改造
在原的HttpRequestUtils基础上保存body只保存参数值,对于文件上传请求的流和参数使用byte[]保存,便于处理。
~~~
@Slf4j
public class HttpRequestUtils {
/**
* 原始请求中的请求参数
*/
private static String body;
/**
* 原始的请求body(如果是上传请求则包含二进制文件流)
*/
private static byte[] cachedBytes;
/**
* 通用请求格式转换
* @param httpServletRequest 原始请求
* @return 将请求中的参数转为map形式返回
*/
public static Map commonHttpRequestParamConvert(HttpServletRequest httpServletRequest) {
Map params = new HashMap<>();
try {
//用来保存请求数据流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Map requestParams = httpServletRequest.getParameterMap();
if (requestParams != null && !requestParams.isEmpty()) {
requestParams.forEach((key, value) -> params.put(key, value[0]));
String param = JSON.toJSONString(params);
//将原始请求中的数据包括参数和二进制文件流重新写入到cachedBytes中
IOUtils.copy(httpServletRequest.getInputStream(), baos);
setCachedBytes(baos.toByteArray());
setBody(param);
} else {
StringBuilder paramSb = new StringBuilder();
try {
String str;
BufferedReader br = httpServletRequest.getReader();
while((str = br.readLine()) != null){
paramSb.append(str);
}
} catch (Exception e) {
log.error("httpServletRequest 获取requestBody发生异常:" + e);
}
//从流冲读取的参数不为空才进行参数处理
if (paramSb.length() > 0) {
JSONObject paramJsonObject = JSON.parseObject(paramSb.toString());
if (paramJsonObject != null && !paramJsonObject.isEmpty()) {
paramJsonObject.forEach((key, value) -> params.put(key, value));
}
String param = paramSb.toString();
JSONObject object = JSONObject.parseObject(param);
String reqUserid = object.getString("reqUserid");
String uri = httpServletRequest.getRequestURI();
String methodName = uri.substring(uri.lastIndexOf("/")+ 1);
//查询类的方法名为get的跑批请求不需登录,所以调用将reqUserid置为固定值,对应oms里面reqUserid的值
if(methodName.equals(Constants.GET) && reqUserid.equals(Constants.PLATFORM) ){
object.put("reqUserid", Constants.PLATFORM_USERID);
}
//将原始请求中的数据cachedBytes中
setCachedBytes(paramSb.toString().getBytes());
setBody(paramSb.toString());
}
}
} catch (Exception e) {
log.error("commonHttpRequestParamConvert转换发生异常:" + e);
}
return params;
}
/**
* 获取原始请求中的流数据
* @return byte流
*/
public static byte[] getCachedBytes() {
return cachedBytes;
}
/**
* 设置原始请求数据
* @param cachedBytes 流
*/
private static void setCachedBytes(byte[] cachedBytes) {
HttpRequestUtils.cachedBytes = cachedBytes;
}
public static String getBody() {
return body;
}
public static void setBody(String body) {
HttpRequestUtils.body = body;
}
}
~~~
### 4. 后续处理思路
使用上述工具后可以在Filter中进行参数处理后拿到最后处理完的参数进行业务处理。由于是二进制流文件上传,所以从request读取输入流,转为byte[],在将byte[]转为MultipartFile,后续就可以拿到文件各属性进行上传操作类。
### 5. 实现MultipartFile
自定义类实现MultipartFile,重写父类必要的方法,从而实现将byte[]转为MultipartFile
~~~
/**
* @author: wangjg on 2019/11/13 15:34
* @description: 自定义CustomMultipartFile用来将二进制文件流转为MultipartFile
* 只需继承MultipartFile并且重写必要的父类方法即可。
* @editored:
*/
public class CustomMultipartFile implements MultipartFile {
/**
* 文件流
*/
private final byte[] fileContent;
/**
* 文件名(带后缀)
*/
private String fileName;
/**
* contentType
*/
private String contentType;
private File file;
private FileOutputStream fileOutputStream;
/**
* 构造方法将二进制流转为文件
* @param fileData 二进制流
* @param name 带后缀的文件名
*/
public CustomMultipartFile(byte[] fileData, String name) {
this.fileContent = fileData;
this.fileName = name;
String destPath = System.getProperty("java.io.tmpdir");
file = new File(destPath + fileName);
}
@Override
public void transferTo(File dest) throws IOException, IllegalStateException {
fileOutputStream = new FileOutputStream(dest);
fileOutputStream.write(fileContent);
}
public void clearOutStreams() throws IOException {
if (null != fileOutputStream) {
fileOutputStream.flush();
fileOutputStream.close();
file.deleteOnExit();
}
}
@Override
public String getName() {
// TODO - implementation depends on your requirements
return null;
}
@Override
public String getOriginalFilename() {
return this.fileName;
}
@Override
public String getContentType() {
// TODO - implementation depends on your requirements
return null;
}
@Override
public boolean isEmpty() {
return fileContent == null || fileContent.length == 0;
}
@Override
public long getSize() {
return fileContent.length;
}
@Override
public byte[] getBytes() throws IOException {
return fileContent;
}
@Override
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(fileContent);
}
}
~~~
### 6. 准换操作
~~~
try {
MultipartFile file;
try {
//表单提交上传文件
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
file = multipartRequest.getFile("uploadFile");
}catch (ClassCastException e){
//二进制流方式上传文件
//一般这里转换出错是由于使用二进制流方式上传文件,提交方式不是form-data表单提交,无法转成MultipartHttpServletRequest。
log.info("开始使用二进制流上传文件...");
byte[] bytes = StreamUtils.copyToByteArray(request.getInputStream());
file = new CustomMultipartFile(bytes, fileName);
}
~~~
### 7. 总结
利用重写的CustomMultipartFile既可以处理表单提交的文件上传,又可以处理二进制流文件上传。最终的结果都是将表单提交的文件或者二进制文件流转化为MultipartFile.后续的文件具体上传操作有多种,此处不再记录。值得注意的是spring-test的包下有个 MockMultipartFile 类同样可以实现将byte数组转为MultipartFile,代码如下:
~~~
byte[] bytes = StreamUtils.copyToByteArray(request.getInputStream());
MultipartFile multipartFile =new MockMultipartFile("file", file.getName(), "text/plain", bytes);
~~~
但是坑爹的是改代码是test包下的类,在本地测试可以使用, 在发布正式版时、根本不会打测试包。所以打包后运行会报错NPE异常,根本找不到MockMultipartFile 类。所以本文的实现方式是非常合适的一种处理方法。
byte[]转为MultipartFile