使用SpringWebFlux和Spring中提供的静态文件恢复文件下载
我相信在整个互联网上没有答案,因为它可能非常复杂,但我会继续问 基本上,我希望在多个Spring应用程序之间进行交叉通信。它们中的每一个都以静态的方式服务于资源。其他应用程序实例可以根据请求下载这些文件(目前我正在通过HTTP传输文件),从而利用此服务。由于这个问题,我能够下载文件 现在我想提升我的代码,以便在连接问题或应用程序在给定的超时时间内暂时不可用的情况下,我能够继续下载文件。我在中配置了使用SpringWebFlux和Spring中提供的静态文件恢复文件下载,spring,reactive-programming,spring-webflux,project-reactor,spring-webclient,Spring,Reactive Programming,Spring Webflux,Project Reactor,Spring Webclient,我相信在整个互联网上没有答案,因为它可能非常复杂,但我会继续问 基本上,我希望在多个Spring应用程序之间进行交叉通信。它们中的每一个都以静态的方式服务于资源。其他应用程序实例可以根据请求下载这些文件(目前我正在通过HTTP传输文件),从而利用此服务。由于这个问题,我能够下载文件 现在我想提升我的代码,以便在连接问题或应用程序在给定的超时时间内暂时不可用的情况下,我能够继续下载文件。我在中配置了WebClient超时 现在,我认为这样的代码实际上可以让我处理暂时不可用的服务: final As
WebClient
超时
现在,我认为这样的代码实际上可以让我处理暂时不可用的服务:
final AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(targetPath, StandardOpenOption.WRITE);
Flux<DataBuffer> fileData = Mono.just(filePath)
.map(file -> targetPath.toFile().exists() ? targetPath.toFile().length() : 0)
.map(bytes -> webClient
.get()
.uri(uri)
.accept(MediaType.APPLICATION_OCTET_STREAM)
.header("Range", String.format("bytes=%d-", bytes))
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse -> Mono.error(new CustomException("4xx error")))
.onStatus(HttpStatus::is5xxServerError, clientResponse -> Mono.error(new CustomException("5xx error")))
.bodyToFlux(DataBuffer.class)
)
.flatMapMany(Function.identity());
DataBufferUtils
.write(fileData , fileChannel)
.map(DataBufferUtils::release)
.doOnError(throwable -> {
try {
fileChannel.force(true);
} catch (IOException e) {
e.printStackTrace();
}
})
.retry(3)
.doOnComplete(() -> {
try {
fileChannel.force(true);
} catch (IOException e) {
e.printStackTrace();
}
})
.doOnError(e -> !(e instanceof ChannelException), e -> {
try {
Files.deleteIfExists(targetPath);
} catch (IOException exc) {
exc.printStackTrace();
}
})
.doOnError(ChannelException.class, e -> {
try {
Files.deleteIfExists(targetPath);
} catch (IOException exc) {
exc.printStackTrace();
}
})
.doOnTerminate(() -> {
try {
fileChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
})
.blockLast();
以及稍后在同一堆栈跟踪中:
2019-10-25T15:41:53.602+0200 [WARN] [xxx] [N/A:N/A] [i.n.c.AbstractChannelHandlerContext] { thread=reactor-http-nio-4 } An exception 'reactor.core.Exceptions$BubblingException: reactor.netty.http.client.PrematureCloseException: Connection prematurely closed DURING response' [enable DEBUG level for full stacktrace] was thrown by a user handler's exceptionCaught() method while handling the following exception:
reactor.core.Exceptions$BubblingException: reactor.netty.http.client.PrematureCloseException: Connection prematurely closed DURING response
at reactor.core.Exceptions.bubble(Exceptions.java:154)
at reactor.core.publisher.Operators.onErrorDropped(Operators.java:512)
at reactor.netty.channel.FluxReceive.onInboundError(FluxReceive.java:343)
at reactor.netty.channel.ChannelOperations.onInboundError(ChannelOperations.java:399)
at reactor.netty.http.client.HttpClientOperations.onInboundClose(HttpClientOperations.java:258)
at reactor.netty.channel.ChannelOperationsHandler.channelInactive(ChannelOperationsHandler.java:121)
异常本身并不是什么大问题,但关键是我的下载不会在启动应用程序后恢复。
所以,是的,我的问题是,我如何才能恢复下载,以及我应该/可以如何处理这里的异常情况?嗨,下面是我发现的实现 基于此,以下是我的文件下载代码:
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ZeroCopyHttpOutputMessage;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import java.io.File;
import java.io.IOException;
@RestController
@RequestMapping("/")
public class DownloadContorller {
@GetMapping
public Mono<Void> downloadByWriteWith(ServerHttpResponse response) throws IOException {
ZeroCopyHttpOutputMessage zeroCopyResponse = (ZeroCopyHttpOutputMessage) response;
response.getHeaders().set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=large-file.so");
response.getHeaders().setContentType(MediaType.APPLICATION_OCTET_STREAM);
Resource resource = new ClassPathResource("large-file.so");
File file = resource.getFile();
return zeroCopyResponse.writeWith(file, 0, file.length());
}
}
导入org.springframework.core.io.ClassPathResource;
导入org.springframework.core.io.Resource;
导入org.springframework.http.HttpHeaders;
导入org.springframework.http.MediaType;
导入org.springframework.http.ZeroCopyHttpOutputMessage;
导入org.springframework.http.server.reactive.ServerHttpResponse;
导入org.springframework.web.bind.annotation.GetMapping;
导入org.springframework.web.bind.annotation.RequestMapping;
导入org.springframework.web.bind.annotation.RestController;
导入reactor.core.publisher.Mono;
导入java.io.File;
导入java.io.IOException;
@RestController
@请求映射(“/”)
公共类下载控制器{
@GetMapping
公共Mono downloadByWriteWith(ServerHttpResponse响应)引发IOException{
ZeroCopyHttpOutputMessage zeroCopyResponse=(ZeroCopyHttpOutputMessage)响应;
response.getHeaders().set(HttpHeaders.CONTENT_处置,“附件;文件名=大文件.so”);
response.getHeaders().setContentType(MediaType.APPLICATION_OCTET_STREAM);
Resource Resource=newclasspathresource(“large file.so”);
File File=resource.getFile();
返回zeroCopyResponse.writeWith(file,0,file.length());
}
}
我使用wget-c对它进行了测试http://localhost:8080/
并且我能够恢复从中断处下载的内容。我用电脑测试了一下
我希望它有帮助。代码有两个问题。首先是在开始时确定文件大小的方法。使用
AtomicLong
的正确方法与在DataBufferUtils.write()中一样。第二个问题是远程调用之间的retry()
。在处理带有固定回退的远程调用时,使用retryWhen()
似乎是一种很好的方式。总而言之,下面是一段代码,它使我能够在连接/应用程序出现问题时从正确的字节恢复文件下载
final AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(targetPath, StandardOpenOption.WRITE);
AtomicLong fileSize = new AtomicLong(targetPath.toFile().length());
Flux<DataBuffer> fileDataStream = webClient
.get()
.uri(remoteXFerServiceTargetHost)
.accept(MediaType.APPLICATION_OCTET_STREAM)
.header("Range", String.format("bytes=%d-", fileSize.get()))
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse -> Mono.error(new CustomException("4xx error")))
.onStatus(HttpStatus::is5xxServerError, clientResponse -> Mono.error(new CustomException("5xx error")))
.bodyToFlux(DataBuffer.class);
DataBufferUtils
.write(fileData , fileChannel)
.map(DataBufferUtils::release)
.doOnError(throwable -> {
try {
fileChannel.force(true);
} catch (IOException e) {
e.printStackTrace();
}
})
.retryWhen(Retry.any().fixedBackoff(Duration.ofSeconds(5)).retryMax(5))
.doOnComplete(() -> {
try {
fileChannel.force(true);
} catch (IOException e) {
e.printStackTrace();
}
})
.doOnError(e -> !(e instanceof ChannelException), e -> {
try {
Files.deleteIfExists(targetPath);
} catch (IOException exc) {
exc.printStackTrace();
}
})
.doOnError(ChannelException.class, e -> {
try {
Files.deleteIfExists(targetPath);
} catch (IOException exc) {
exc.printStackTrace();
}
})
.doOnTerminate(() -> {
try {
fileChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
})
.blockLast();
final AsynchronousFileChannel fileChannel=AsynchronousFileChannel.open(targetPath,StandardOpenOption.WRITE);
AtomicLong fileSize=新的AtomicLong(targetPath.toFile().length());
Flux fileDataStream=webClient
.get()
.uri(remoteXFerServiceTargetHost)
.accept(MediaType.APPLICATION\u八位字节\u流)
.header(“范围”,String.format(“字节=%d-”,fileSize.get())
.retrieve()
.onStatus(HttpStatus::is4xclientorror,clientResponse->Mono.error(新自定义异常(“4xx错误”))
.onStatus(HttpStatus::is5xxServerError,clientResponse->Mono.error(新的CustomException(“5xx错误”))
.bodyToFlux(数据缓冲级);
数据缓冲器
.write(文件数据、文件通道)
.map(数据缓冲区::发布)
.doon错误(可丢弃->{
试一试{
fileChannel.force(true);
}捕获(IOE异常){
e、 printStackTrace();
}
})
.retryWhen(Retry.any().fixedBackoff(持续时间为秒(5)).retryMax(5))
.doOnComplete(()->{
试一试{
fileChannel.force(true);
}捕获(IOE异常){
e、 printStackTrace();
}
})
.doon错误(e->!(e通道异常实例),e->{
试一试{
Files.deleteIfExists(targetPath);
}捕获(IOException){
exc.printStackTrace();
}
})
.doon错误(ChannelException.class,e->{
试一试{
Files.deleteIfExists(targetPath);
}捕获(IOException){
exc.printStackTrace();
}
})
.doointerate(()->{
试一试{
fileChannel.close();
}捕获(IOE异常){
e、 printStackTrace();
}
})
.blockLast();
也许吧,但遗憾的是,这不是反应式编程。这段代码不考虑任何异常处理或与它有关的任何事情,这是必不可少的。这是反应性的,如返回Mono,并且在其间没有阻塞操作,它将给您事件循环,它也是一个更干净的方式,不在您的代码中与您的代码相同的地方进行异常处理。建议您在控制器建议中执行此操作。最后,您编写代码的方式是功能性的,解决方案中提到的代码是必需的,但两者都是被动的。Spr
final AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(targetPath, StandardOpenOption.WRITE);
AtomicLong fileSize = new AtomicLong(targetPath.toFile().length());
Flux<DataBuffer> fileDataStream = webClient
.get()
.uri(remoteXFerServiceTargetHost)
.accept(MediaType.APPLICATION_OCTET_STREAM)
.header("Range", String.format("bytes=%d-", fileSize.get()))
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse -> Mono.error(new CustomException("4xx error")))
.onStatus(HttpStatus::is5xxServerError, clientResponse -> Mono.error(new CustomException("5xx error")))
.bodyToFlux(DataBuffer.class);
DataBufferUtils
.write(fileData , fileChannel)
.map(DataBufferUtils::release)
.doOnError(throwable -> {
try {
fileChannel.force(true);
} catch (IOException e) {
e.printStackTrace();
}
})
.retryWhen(Retry.any().fixedBackoff(Duration.ofSeconds(5)).retryMax(5))
.doOnComplete(() -> {
try {
fileChannel.force(true);
} catch (IOException e) {
e.printStackTrace();
}
})
.doOnError(e -> !(e instanceof ChannelException), e -> {
try {
Files.deleteIfExists(targetPath);
} catch (IOException exc) {
exc.printStackTrace();
}
})
.doOnError(ChannelException.class, e -> {
try {
Files.deleteIfExists(targetPath);
} catch (IOException exc) {
exc.printStackTrace();
}
})
.doOnTerminate(() -> {
try {
fileChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
})
.blockLast();