前言

目前网络上的所谓流上传的例子,大多数并非真正的流式上传,要不就是用MultipartFile的getInputStream,要不就是用了默认的ByteArrayOutputStream上传方式。前者是占用了磁盘资源,后者实际上用了缓存流,占用了内存资源。假如一个业务应用系统,想通过自己的后端上传,用了这两种方式,无异于让一个业务应用系统不纯粹,还需要考虑文件资源的问题。所以这篇文章介绍目前小编采取的上传方案,以及着重地把restTemplate流式上传说一下。

上传方案

目前小编上传文件主要采取3种方案,分别是通过url下载链接、跟业务解耦地对象存储服务、直连应用系统流式上传。

url链接

这种方式非常直接,由用户提供一个支持下载的url链接来上传文件
优点:这样能做到快速相应和减少资源浪费。
缺点:因为链接文件不可控,并且很多公司的文件系统不支持外网直连,所以建议内网环境下,在内部应用交互场景中采用。

对象存储服务

用户直接把文件上传到对象存储服务,上传不经过应用系统
优点:能够跟我们的应用系统解耦,上传文件可以直接上传到对象存储服务上,完全不需要考虑自身应用承受文件资源的压力。这也是建议同学们在应用系统中采用的方案。
缺点:调用过程相对复杂,假如是跟后端对接的话,会被后端的同学诟病,这也是小编为什么还支持直连上传的原因。

restTemplate 上传javafile resttemplate传文件流_上传

流式上传

用户直接以文件流的方式进行传输
优点:不占用业务应用服务器的磁盘资源和内存资源
缺点:会受到业务应用服务器的带宽和请求连接数限制,上传速度并非最优,并且与业务应用系统耦合。

以下是服务端的写法以及后端如何对接

前端对接服务端的服务端写法

##后端接收代码
	@RequestMapping(value = "/upload", method = RequestMethod.POST)
	public Long upload(HttpServletRequest request , @Validated UploadParam upload) throws Exception {
        return documentAggService.upload(request,upload);
	}

这里以postman上传为例:

restTemplate 上传javafile resttemplate传文件流_文件上传_02

后端对后端

没有经过配置优化的内存缓存流上传测试情况,上传大文件,从图上发现,很快就把内存给占用完了

restTemplate 上传javafile resttemplate传文件流_缓存_03

这是小编采取了流式上传的测试情况,java程序配置了堆内存最大1G,然后上传大文件,发现没有任何内存飙升的情况,通过手动gc也可以进行回收,证明了没有使用内存做缓存流。

restTemplate 上传javafile resttemplate传文件流_缓存_04

##配置文件
spring:
  servlet:
    multipart:
      max-file-size: -1
      max-request-size: -1
## 核心在于以下,经过测试,不会存在临时文件以及占用内存做缓存流
connection.setChunkedStreamingMode(20*1024);
factory.setBufferRequestBody(false);
## 两种方式上传远端都需要以下配置

	## 先定义factory
	@Bean(name = "streamFactory")
    public SimpleClientHttpRequestFactory simpleClientHttpRequestFactory() {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory() {
            @Override
            protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException {
                if (connection instanceof HttpsURLConnection) {
                    ((HttpsURLConnection) connection).setHostnameVerifier((hostname, session) -> true);
                }
                //指定流的大小,当内容达到这个值的时候就把流输出
                connection.setChunkedStreamingMode(20*1024);
                super.prepareConnection(connection, httpMethod);
            }
        };
        //设置不使用缓存流的形式,假如开启之后,会占用内存来缓存流
        factory.setBufferRequestBody(false);
        factory.setConnectTimeout(150000);
        factory.setReadTimeout(150000);
        return factory;
    }
    
	## 再定义restTemplate
	@Bean(name = "streamUploadRestTemplate")
    public RestTemplate streamUploadRestTemplate(SimpleClientHttpRequestFactory streamFactory) {
        RestTemplate restTemplate = new RestTemplate(streamFactory);
        List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();
        for (HttpMessageConverter<?> httpMessageConverter : messageConverters) {
            if (httpMessageConverter instanceof StringHttpMessageConverter) {
                // 将默认字符集从"ISO-8859-1"变成"UTF-8"
                ((StringHttpMessageConverter) httpMessageConverter).setDefaultCharset(Charset.forName("UTF-8"));
            }
        }
        return restTemplate;
    }
远端服务器是表单接收的场景
## 上传到远端
    public String uploadFile(HttpServletRequest request) {
        // 构建远程头
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.MULTIPART_FORM_DATA);
        headers.setConnection("keep-alive");
        Resource resource = null;
        try {
            resource = new CommonInputStreamResource(request.getInputStream());
        } catch (IOException e) {
            throw new ClientException(ExceptionMessage.Upload.UPLOAD_GET_INPUT_STREAM_ERROR);
        }
        MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
        param.add("file", resource);
        // 远程请求
        Optional<UploadResponse> response = Optional.of(streamUploadRestTemplate.postForObject("http://localhost:8888/upload",
                new HttpEntity<MultiValueMap<String, Object>>(param, headers), UploadResponse.class));
        return response.map(UploadResponse::getData).map(UploadData::getId)
                .orElseThrow(() -> new ClientException(response.map(UploadResponse::getMsg)
                        .orElseGet(() -> response.map(UploadResponse::getMessage).orElseGet(() -> ExceptionMessage.DocumentFeign.FILE_CALL_FAIL_MSG))));
    }

	##上面用到的CommonInputStreamResource
	public class CommonInputStreamResource extends InputStreamResource {
    private int length;
 
    public CommonInputStreamResource(InputStream inputStream) {
        super(inputStream);
    }
 
    public CommonInputStreamResource(InputStream inputStream, int length) {
        super(inputStream);
        this.length = length;
    }
 
    @Override
    public String getFilename() {
        return "temp";
    }
    @Override
    public long contentLength() {
        int estimate = length;
        return estimate == 0 ? 1 : estimate;
    }
}
远端服务器是流式接收的场景
@RequestMapping(value = "/upload2", method = RequestMethod.POST )
    public Long upload2(HttpServletRequest request , UploadParam upload) throws IOException {
        // 构建远程头
        HttpHeaders headers = new HttpHeaders();
        RequestCallback requestCallback = request1 -> {
            request1.getHeaders()
                    .setAccept(Arrays.asList(MediaType.ALL));
            request1.getHeaders().setContentType(MediaType.APPLICATION_OCTET_STREAM);
            request1.getHeaders().setConnection("keep-alive");
            StreamUtils.copy(request.getInputStream(), request1.getBody());
        };
        String s = streamUploadRestTemplate.execute("http://localhost:8888/upload?fileName=123.rvt", HttpMethod.POST, requestCallback, ResponseExtractor -> {
        //做一些相应相关的操作
//            ResponseExtractor.getStatusCode()
            InputStream inputStream = ResponseExtractor.getBody();
            return IOUtils.toString(inputStream, Charset.forName("UTF-8"));
        });
    }

总结

一般应用服务没有很多文件上传的需求,所以很多同学都不需要考虑这个问题。假如各位同学的系统上有很多、很大文件上传的需求,并且还没有头绪如何优化,相信小编的这篇文章能给你们带来启发/帮助。