byte[]转为MultipartFile

Scroll Down

1.背景

最近有个文件上传的需求,客户端上传二进制流,服务端接收该二进制文件流进行文件处理。因为项目中有一层参数过滤器,读取过一次request中的参数。接口中需再次从request读取流转为byte数组,最后转为MultipartFile进行文件验证上传操作。request中参数多次读取已在之前的文章中实现多次读取request里面的参数值。本文在原来的的基础上稍加改造,在处理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<String, Object> commonHttpRequestParamConvert(HttpServletRequest httpServletRequest) {
        Map<String, Object> params = new HashMap<>();
        try {
            //用来保存请求数据流
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            Map<String, String[]> 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 类。所以本文的实现方式是非常合适的一种处理方法。