目前网络上的所谓流上传的例子,大多数并非真正的流式上传,要不就是用MultipartFile的getInputStream,要不就是用了默认的ByteArrayOutputStream上传方式。前者是占用了磁盘资源,后者实际上用了缓存流,占用了内存资源。假如一个业务应用系统,想通过自己的后端上传,用了这两种方式,无异于让一个业务应用系统不纯粹,还需要考虑文件资源的问题。所以这篇文章介绍目前小编采取的上传方案,以及着重地把restTemplate流式上传说一下。
目前小编上传文件主要采取3种方案,分别是通过url下载链接、跟业务解耦地对象存储服务、直连应用系统流式上传。
这种方式非常直接,由用户提供一个支持下载的url链接来上传文件
优点:这样能做到快速相应和减少资源浪费。
缺点:因为链接文件不可控,并且很多公司的文件系统不支持外网直连,所以建议内网环境下,在内部应用交互场景中采用。
用户直接把文件上传到对象存储服务,上传不经过应用系统
优点:能够跟我们的应用系统解耦,上传文件可以直接上传到对象存储服务上,完全不需要考虑自身应用承受文件资源的压力。这也是建议同学们在应用系统中采用的方案。
缺点:调用过程相对复杂,假如是跟后端对接的话,会被后端的同学诟病,这也是小编为什么还支持直连上传的原因。
用户直接以文件流的方式进行传输
优点:不占用业务应用服务器的磁盘资源和内存资源
缺点:会受到业务应用服务器的带宽和请求连接数限制,上传速度并非最优,并且与业务应用系统耦合。
以下是服务端的写法以及后端如何对接
##后端接收代码
@RequestMapping(value = "/upload", method = RequestMethod.POST)
public Long upload(HttpServletRequest request , @Validated UploadParam upload) throws Exception {
return documentAggService.upload(request,upload);
}
没有经过配置优化的内存缓存流上传测试情况,上传大文件,从图上发现,很快就把内存给占用完了
。
这是小编采取了流式上传的测试情况,java程序配置了堆内存最大1G,然后上传大文件,发现没有任何内存飙升的情况
,通过手动gc也可以进行回收,证明了没有使用内存做缓存流。
##配置文件
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"));
});
}
一般应用服务没有很多文件上传的需求,所以很多同学都不需要考虑这个问题。假如各位同学的系统上有很多、很大文件上传的需求,并且还没有头绪如何优化,相信小编的这篇文章能给你们带来启发/帮助。