WebFlux 上传文件
- 1. 表单上传方式
- 1.1 Spring MVC
- 1.2 Spring WebFlux
- 2. 二进制流
- 2.1 Spring MVC
- 2.2 Spring WebFlux
开发环境:jdk 11
WebFlux:jdk 8+
1. 表单上传方式
1.1 Spring MVC
- multipart大小限制
spring:
servlet:
multipart:
max-file-size: 512MB
max-request-size: 1024MB
- controller
private static final Path BASE_PATH = Paths.get("./uploads");
@PostMapping("/uploadForm")
public String uploadForm(@RequestPart("files") MultipartFile[] files) throws IOException {
for (MultipartFile file : files) {
String fileName = UUID.randomUUID() + "-" + file.getOriginalFilename();
final Path path = BASE_PATH.resolve(fileName);
try (InputStream inputStream = file.getInputStream()) {
Files.write(path, inputStream.readAllBytes());
}
}
// 处理上传的二进制数据
return "success";
}
- 客户端
String url = "http://localhost:8080/upload";
final File file = new File("D:\\test\\2023032201-205680e2622740b5bb888b7e4801ebf0.mp4");
RestTemplate restTemplate = new RestTemplate();
// 设置请求头和请求体
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
final Resource resource = new FileSystemResource(file);
final MultiValueMap<String, Object> valueMap = new LinkedMultiValueMap<>();
valueMap.add("files", resource);
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(valueMap, headers);
ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, requestEntity, String.class);
System.out.println(responseEntity.getStatusCode());
1.2 Spring WebFlux
- multipart大小限制
spring:
codec:
# DataBuffer方式
max-in-memory-size: 512MB
webflux:
multipart:
max-in-memory-size: 512MB
# 默认非流式
streaming: false
- controller
private static final Path BASE_PATH = Paths.get("./uploads");
@PostMapping("/upload")
public Mono<List<String>> upload(@RequestPart("files") Flux<FilePart> files) {
return files.flatMap(filePart -> {
String fileName = UUID.randomUUID() + "-" + filePart.filename();
final Path path = BASE_PATH.resolve(fileName);
final File dest = path.toFile();
return filePart.transferTo(dest).thenReturn(dest.getName());
}).collectList();
}
或者
private static final Path BASE_PATH = Paths.get("./uploads");
// 不推荐此种方式(文件名丢失)
@PostMapping("/upload")
public Mono<List<String>> upload(@RequestPart("files") Flux<DataBuffer> files) {
return files.flatMap(dataBuffer -> {
String fileName = UUID.randomUUID() + ".dat";
final Path path = BASE_PATH.resolve(fileName);
return DataBufferUtils.write(Mono.just(dataBuffer), path, StandardOpenOption.CREATE)
.then(Mono.just(fileName))
.onErrorResume(ex -> {
log.error("上传失败", ex);
return Mono.just("fail: " + ex.getMessage());
});
}).collectList();
}
- 客户端
String url = "http://localhost:8080/upload";
final File file = new File("D:\\test\\2023032201-205680e2622740b5bb888b7e4801ebf0.mp4");
final MediaType contentType = MediaType.MULTIPART_FORM_DATA;
WebClient webClient = WebClient.builder().build();
webClient.post()
.uri(url)
.contentType(contentType)
.body(BodyInserters.fromMultipartData("files", new FileSystemResource(file)))
.exchangeToMono(response -> response.bodyToMono(String.class))
.subscribe(System.out::println);
// 无限期等待结果(线上不允许使用)
Thread.currentThread().join();
值得一说的是
FilePart
和FormFieldPart
均继承自Part
,两者是平级关系。需要说明的是,客户端传输的org.springframework.core.io.Resource
file part 对应的并不一定是FilePart
类型,也有可能是FormFieldPart
,两者最主要的区别是,FilePart
可以取到原始的文件名,FormFieldPart
无法取得原始的文件名,这也是合理的,因为Resource
有个派生类ByteArrayResource
是基于内存的不存在文件名。
@see org.springframework.http.client.MultipartBodyBuilder
2. 二进制流
文件名参数的上传可以以post请求的url参数(注意)的形式进行上传,也可以在请求头header上传输。
URL参数编解码:
js:
encodeURI(str)
java:
URLEncoder.encode(str, "UTF-8")
和URLDecoder.decode(encodedStr, "UTF-8")
2.1 Spring MVC
- 服务端
private static final Path BASE_PATH = Paths.get("./uploads");
@PostMapping("/upload")
public String upload(@RequestBody Resource resource) throws IOException {
// 此处Resource类型一定是ByteArrayResource,哪怕客户端传输的是FileSystemResource
String fileName = UUID.randomUUID() + ".dat";
final Path path = BASE_PATH.resolve(fileName);
try (InputStream inputStream = resource.getInputStream()) {
Files.write(path, inputStream.readAllBytes());
}
// 处理上传的二进制数据
return "success";
}
或者
private static final Path BASE_PATH = Paths.get("./uploads");
@PostMapping("/upload")
public String upload(@RequestBody byte[] bytes) throws IOException {
String fileName = UUID.randomUUID() + ".dat";
final Path path = BASE_PATH.resolve(fileName);
Files.write(path, bytes);
// 处理上传的二进制数据
return "success";
}
使用
HttpHeaders.CONTENT_DISPOSITION
请求头传输文件名
@PostMapping("/upload")
public String upload(HttpServletRequest request, @RequestBody Resource resource) throws IOException {
String originalFilename = extractFileName(request);
String fileName;
if (Objects.isNull(originalFilename)) {
fileName = UUID.randomUUID() + ".dat";
} else {
fileName = UUID.randomUUID() + "-" + originalFilename;
}
final Path path = BASE_PATH.resolve(fileName);
try (InputStream inputStream = resource.getInputStream()) {
Files.write(path, inputStream.readAllBytes());
}
// 处理上传的二进制数据
return "success";
}
/**
* 从请求头提取文件名
*
* @param request 请求对象
* @return 文件名
*/
private String extractFileName(HttpServletRequest request) {
final String disposition = request.getHeader(HttpHeaders.CONTENT_DISPOSITION);
if (Objects.nonNull(disposition)) {
Pattern pattern = Pattern.compile("filename=\"(.+)\"");
Matcher matcher = pattern.matcher(disposition);
if(matcher.find()) {
return matcher.group(1);
}
}
return null;
}
- 客户端
public static void main(String[] args) throws IOException {
String url = "http://localhost:8080/upload";
final File file = new File("D:\\test\\2023032201-205680e2622740b5bb888b7e4801ebf0.mp4");
RestTemplate restTemplate = new RestTemplate();
// 设置请求头和请求体
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.set(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment; filename=\"%s\"", file.getName()));
try (FileInputStream inputStream = new FileInputStream(file)) {
HttpEntity<ByteArrayResource> requestEntity = new HttpEntity<>(new ByteArrayResource(inputStream.readAllBytes()), headers);
ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, requestEntity, String.class);
System.out.println(responseEntity.getStatusCode());
}
}
值得一说的是
MultipartFile
和Resource
均继承自InputStreamSource
,两者是平级关系,MultipartFile
用于接受表单文件参数,而Resource
可用于接受二进制文件流。
2.2 Spring WebFlux
@PostMapping("/upload")
public Mono<String> upload(@RequestBody Mono<Resource> resourceMono) {
return resourceMono.flatMap(resource -> {
String fileName = UUID.randomUUID() + ".dat";
final Path path = BASE_PATH.resolve(fileName);
try (InputStream inputStream = resource.getInputStream()) {
Files.write(path, inputStream.readAllBytes());
} catch (IOException e) {
log.error("上传失败", e);
return Mono.error(e);
}
return Mono.just(fileName);
});
}
或者
@PostMapping("/upload")
public Mono<String> upload(@RequestBody Mono<byte[]> bytesMono) {
return bytesMono.flatMap(bytes -> {
String fileName = UUID.randomUUID() + ".dat";
final Path path = BASE_PATH.resolve(fileName);
try {
Files.write(path, bytes);
} catch (IOException e) {
log.error("上传失败", e);
return Mono.error(e);
}
return Mono.just(fileName);
});
}
- 客户端
String url = "http://localhost:8080/upload";
final File file = new File("D:\\test\\2023032201-205680e2622740b5bb888b7e4801ebf0.mp4");
final MediaType contentType = MediaType.APPLICATION_OCTET_STREAM;
WebClient webClient = WebClient.builder().build();
try (final FileInputStream inputStream = new FileInputStream(file)) {
final InputStreamResource resource = new InputStreamResource(inputStream);
final String encode = URLEncoder.encode(file.getName(), Charset.defaultCharset());
webClient.post()
.uri(url)
.contentType(contentType)
.header(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment; filename=\"%s\"", encode))
.body(BodyInserters.fromResource(resource))
.exchangeToMono(response -> response.bodyToMono(String.class))
.subscribe(System.out::println);
// 无限期等待结果(线上不允许使用)
Thread.currentThread().join();
}
使用
HttpHeaders.CONTENT_DISPOSITION
请求头传输文件名
@PostMapping("/upload")
public Mono<String> upload(@RequestBody Mono<byte[]> bytesMono, ServerHttpRequest request) {
return bytesMono.flatMap(bytes -> {
String originalFilename = extractFileName(request);
String fileName;
if (Objects.isNull(originalFilename)) {
fileName = UUID.randomUUID() + ".dat";
} else {
final String decode = URLDecoder.decode(originalFilename, Charset.defaultCharset());
fileName = UUID.randomUUID() + "-" + decode;
}
final Path path = BASE_PATH.resolve(fileName);
try {
Files.write(path, bytes);
} catch (IOException e) {
log.error("上传失败", e);
return Mono.error(e);
}
return Mono.just(fileName);
});
}
/**
* 从请求头提取文件名
*
* @param request 请求对象
* @return 文件名
*/
private String extractFileName(ServerHttpRequest request) {
final String disposition = request.getHeaders().getFirst(HttpHeaders.CONTENT_DISPOSITION);
if (Objects.nonNull(disposition)) {
Pattern pattern = Pattern.compile("filename=\"(.+)\"");
Matcher matcher = pattern.matcher(disposition);
if(matcher.find()) {
return matcher.group(1);
}
}
return null;
}