WebFlux上传下载Excel文件


前言

最近在做一个API网关产品,要开发一个API批量导入和导出的功能,因为网关使用的技术是spring cloud gateway,核心是WebFlux和reactor异步框架,导入使用的是spring-web的FilePart组件,一般导入使用的MultiPart在这里并不适用,在导出导出功能实现上,和webmvc有很多地方不一样,坑也比较多,网上也有很多类似的帖子及技术博客,但是在编码过程中还是有很多细节要注意,如果不是对这部分技术十分熟悉,其实很难完成这个功能的编码,下面具体介绍下实现过程及遇到的问题,不足之处还望指正。


一、引入相关jar包

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-gateway</artifactId>
	<version>3.0.3</version>
	</dependency>
<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>easyexcel</artifactId>
	<version>2.2.6</version>
</dependency>

二、开发过程

1.Excel上传

1.1 导入参数绑定

导入接口定义:

@ApiOperation(value = "API批量导入", tags = {"API发布"})
@PostMapping(value = "/api/pass/integration/apiis/importInterfaces")
Mono<SPMSRsp<String>> importInterfaces(ImportInterfaceParamDto importInterfaceParamDto) throws IOException;

小提示:如果导入只有文件参数,FilePart直接作为接口参数就可以了,如下这样

@ApiOperation(value = "API批量导入", tags = {"API发布"})
@PostMapping(value = "/api/pass/integration/apiis/importInterfaces")
Mono<SPMSRsp<String>> importInterfaces(@RequestPart("file") FilePart filePart) throws IOException;

如果导入接口还要传递其他参数,和FilePart一起放在接口定义中,这么写是不行的,会获取不到二进制文件,比如下面这样:

@ApiOperation(value = "API批量导入", tags = {"API发布"})
@PostMapping(value = "/api/pass/integration/apiis/importInterfaces")
Mono<SPMSRsp<String>> importInterfaces(@RequestPart("file") FilePart filePart, @RequestBody SPMSReq req) throws IOException;

这个时候就需要将FilePart和其他的参数一起封装成一个对象,然后作为接口参数定义,这样就可以绑定所有参数,如下:

/**
 * @author Mr.bin
 * @version 1.0.0
 * @ClassName RouterParamDto.java
 * @Description API列表导入入参
 * @createTime 2022年01月13日 14:28:00
 */
@Data
@ApiModel(value = "API列表导入入参")
public class ImportInterfaceParamDto extends BaseReqBody {
    /**
     * 创建人ID
     */
    @ApiModelProperty(value = "请求参数",required = true)
    @NotNull
    private SPMSReq req;
        
    /**
     * 文件
     */
    @ApiModelProperty(value = "文件", required = true)
    @NotNull
    private FilePart file;
}

1.2 导入功能实现

先贴一段网上通用的实现方法,作为参考

Path tempFile = Files.createTempFile("test", filePart.filename());
//NOTE 方法一
AsynchronousFileChannel channel =AsynchronousFileChannel.open(tempFile, StandardOpenOption.WRITE);
DataBufferUtils.write(filePart.content(), channel, 0).doOnComplete(() -> {
                    System.out.println("finish");
                }).subscribe();

//NOTE 方法二
filePart.transferTo(tempFile.toFile());

说明一下,方法二是不可行的,因为webflux是异步的,这种使用方式你会发现转换后文件是空的,因为是流处理还是异步的,这种方式看着很简洁,一行代码就可以搞定,其实是个坑,可能不是这种简单的用法,至少就这一行不行,所以得通过DataBuffer的方式去处理,那方法一就没问题了吗?
其实这样还是不行,因为你并不知道什么时候文件会读取结束,所以会出现一个现象,有时候顺利完成读取且可导入成功,这个在数据量比较少的时候没问题,你会觉得为很完美,其实是一个假象,数据量少异步是无法感知的,基本是秒处理,但是有时候就会抛异常,尤其是数量了大的时候,我使用的模板测试数据超过一千行时就已经出现问题,比如抛出下面这样的异常:

Could not open the specified zip entry source stream
...
java.io.EOFException: Unexpected end of ZLIB input stream
...

查看临时文件缓存目录下生成的文件,你会发现已损坏无法打开,像下面这样子,当出现这种情况肯定是程序问题,网上一大堆解释说因为什么意外导致文件被损坏,重新创建一个就好了等等等等,都是瞎扯淡,说一大堆也说不出个所以然来,其实是因为异步读取文件流,还没读完就被中断去执行其他任务,等回去继续执行时造成文件没有正确写入,导致文件损坏不可读取,使用debug追踪代码执行过程,你就能看到这一点
在这里插入图片描述
解决方法呢?方式1是可行的,用DataBuffer方式实现流读取,只不过需要把读取过程阻塞一下,等读取数据结束后,再去解析excel导入数据,这样就没问题了,但是不能直接调用block()去阻塞,否则会报如下异常:

java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-2

可以采用java同步器CountDownLatch实现,或者使用publishOn重启另外一个线程,然后阻塞等待读取数据完成,我是用同步器实现的,如下:

FilePart file = interfaceParamDto.getFile();
// 解析excel文件
Path tempFilePath = Files.createTempFile("import_", ExcelTypeEnum.XLSX.getValue());
AsynchronousFileChannel channel =AsynchronousFileChannel.open(tempFilePath, StandardOpenOption.WRITE);
AtomicReference<File> newFile = new AtomicReference<>();
AtomicReference<List<ImportInterfaceDto>> result = new AtomicReference<>();
CountDownLatch countDownLatch = new CountDownLatch(1);
DataBufferUtils.write(file.content(), channel, 0).doOnComplete(() -> countDownLatch.countDown()).subscribe(DataBufferUtils.releaseConsumer());
ExcelAnalysisListener<ImportInterfaceDto> listener = new ExcelAnalysisListener();
try {
	countDownLatch.await();
	newFile.set(tempFilePath.toFile());
	Assert.isTrue(newFile.get().exists() && newFile.get().length() > 0,MessageUtils.getMessage("message.gateway.publish.file.empty"));
	EasyExcelFactory.read(newFile.get(), ImportInterfaceDto.class, listener).sheet().doRead();
	result.set(listener.getDatas());
	uploadInterfaceDatas(interfaceParamDto, result.get());         		
} catch (Exception e) {
  countDownLatch.countDown();
  e.printStackTrace();
  throw new BusinessException(ReturnCodeEnum.DATA_BAD.getCode(), "upload excel has error:" + e.getMessage());
} finally {
  newFile.get().delete();
}

小提示:要注意哦,使用同步器,当程序出现异常时,必须要释放线程,不然线程会一直阻塞,异常无法正常抛出,请求一直没响应。

2. Excel下载

这里使用的是EasyExcel,导出的方式有很多种,我用的是按模板导出,EasyExcel还是很好用的,定义一个模板对象,使用@ExcelProperty标注表头,excel文件中对应使用{.xxxx}的方式绑定模板对象属性,这样你就可以任意导出了,如下:

DefaultDataBuffer dataBuffer = new DefaultDataBufferFactory().allocateBuffer();
OutputStream outputStream = dataBuffer.asOutputStream();
ClassPathResource classPathResource = new ClassPathResource("templates" + File.separator + "InterfaceExportTemplate.xlsx");
InputStream inputStream = classPathResource.getInputStream();
// 下拉框key转value
interfacelList.stream().forEach(item -> {
    translateBoxVal(item);
});
EasyExcel.write(outputStream, ExportInterfaceDto.class). withTemplate(inputStream).sheet("接口列表").doFill(interfacelList);
Flux<DataBuffer> dataBufferFlux = Flux.create((FluxSink<DataBuffer> emitter) -> {
    emitter.next(dataBuffer);
    emitter.complete();
});
return response.writeWith(dataBufferFlux);

还有一种方式,我从网上抄一个仅供参考:

ZeroCopyHttpOutputMessage zeroCopyResponse = (ZeroCopyHttpOutputMessage) response;
response.getHeaders().set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=parallel.png");
response.getHeaders().setContentType(MediaType.IMAGE_PNG);
Resource resource = new ClassPathResource("parallel.png");
File file = resource.getFile();
return zeroCopyResponse.writeWith(file, 0, file.length());

总结

1、使用WebFlux、reactor做导入,要记住他是一个AIO,是异步的,reactor是一个反应式编程框架,其实说白了就是采用的观察者模式,所以不能按照顺序编程、命令编程的思维去理解它,认为他就是顺序执行的,其实不然;
2、WebFlux的subscribe、flatmap等是属于NonBlocking线程,不能使用block()/blockFirst()/blockLast()去阻塞它,如果需要使用同步功能,可以采用java的同步器,或者使用WebFlux提供的线程池开启另外一个线程,在那个线程阻塞等待处理结果;
2、使用同步器后者线程池阻塞线程时,如果出现异常,记得释放线程资源,让程序可以正常退出,不然会出现异常不能正常返回,请求一直超时位响应

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值