当前位置: 首页 > news >正文

Java NIO 实战

Java NIO 实战:三种文件处理场景的生产写法


场景一:大文件上传

前端传文件过来,后端接收并保存到本地或附件服务。

从 multipart 中读取保存到本地

@PostMapping("/upload")publicStringupload(MultipartFilefile)throwsIOException{StringdestPath="/data/uploads/"+UUID.randomUUID()+"_"+file.getOriginalFilename();// MultipartFile 本身提供了 transferTo,但大文件建议用 FileChannel 更可控try(FileChannelout=FileChannel.open(Paths.get(destPath),StandardOpenOption.WRITE,StandardOpenOption.CREATE,StandardOpenOption.TRUNCATE_EXISTING)){FileChannelin=((FileChannel)file.getInputStream().getChannel());// FileInputStream 获取 Channel// 零拷贝传输longtransferred=0;longfileSize=in.size();while(transferred<fileSize){transferred+=in.transferTo(transferred,fileSize-transferred,out);}}returndestPath;}

边读边写(不依赖 multipart 的 InputStream)

有时候前端不是 multipart 上传,而是直接传二进制流,比如大文件分片上传的场景。

@PostMapping("/upload/raw")publicvoiduploadRaw(HttpServletRequestrequest)throwsIOException{StringdestPath="/data/uploads/"+UUID.randomUUID()+".bin";try(FileChannelout=FileChannel.open(Paths.get(destPath),StandardOpenOption.WRITE,StandardOpenOption.CREATE,StandardOpenOption.TRUNCATE_EXISTING)){// 从 request 的 InputStream 包装成 ChannelReadableByteChannelin=Channels.newChannel(request.getInputStream());ByteBufferbuf=ByteBuffer.allocate(8192);// 8K 缓冲区while(in.read(buf)!=-1){buf.flip();out.write(buf);buf.clear();}}}

上传到 MinIO(不走本地磁盘)

如果你们用的是 MinIO 或 S3 之类的对象存储,不需要落盘:

@PostMapping("/upload/oss")publicStringuploadToOss(MultipartFilefile)throwsIOException{StringobjectName="uploads/"+UUID.randomUUID()+"_"+file.getOriginalFilename();minioClient.putObject(PutObjectArgs.builder().bucket("my-bucket").object(objectName).stream(file.getInputStream(),file.getSize(),-1).contentType(file.getContentType()).build());returnobjectName;}

大文件上传的注意事项

  • 不要file.getBytes()— 大文件会直接撑爆内存
  • 不要FileUtils.copyInputStreamToFile— 本质也是先读到内存再写
  • 分段传— 前端分片 + 后端按 offset 写入(如果文件超过 500M,建议前端分片)
  • 限流— Nginx 层限制请求体大小,防止有人直接传几个 G 过来
  • 临时文件清理— 上传过程中崩溃了,记得兜底删除残留文件

场景二:文件拷贝

把本地一个文件复制到另一个地方。

零拷贝复制(最推荐)

publicstaticvoidcopyFile(Stringsource,Stringtarget)throwsIOException{try(FileChannelin=FileChannel.open(Paths.get(source),StandardOpenOption.READ);FileChannelout=FileChannel.open(Paths.get(target),StandardOpenOption.WRITE,StandardOpenOption.CREATE,StandardOpenOption.TRUNCATE_EXISTING)){longtransferred=0;longfileSize=in.size();while(transferred<fileSize){transferred+=in.transferTo(transferred,fileSize-transferred,out);}}}

带进度的复制(大文件时给前端反馈)

publicstaticvoidcopyFileWithProgress(Stringsource,Stringtarget,ProgressCallbackcallback)throwsIOException{try(FileChannelin=FileChannel.open(Paths.get(source),StandardOpenOption.READ);FileChannelout=FileChannel.open(Paths.get(target),StandardOpenOption.WRITE,StandardOpenOption.CREATE,StandardOpenOption.TRUNCATE_EXISTING)){longtransferred=0;longfileSize=in.size();while(transferred<fileSize){transferred+=in.transferTo(transferred,fileSize-transferred,out);callback.onProgress(transferred,fileSize);}}}@FunctionalInterfaceinterfaceProgressCallback{voidonProgress(longtransferred,longtotal);}

目录下的批量复制

publicstaticvoidcopyDir(StringsourceDir,StringtargetDir)throwsIOException{Files.createDirectories(Paths.get(targetDir));Files.walk(Paths.get(sourceDir)).forEach(sourcePath->{try{Stringrelative=sourceDir.relativize(sourcePath).toString();PathtargetPath=Paths.get(targetDir,relative);if(Files.isDirectory(sourcePath)){Files.createDirectories(targetPath);}else{copyFile(sourcePath.toString(),targetPath.toString());}}catch(IOExceptione){thrownewUncheckedIOException(e);}});}

FileChannel 和 Files.copy 选哪个?

// 方式一:FileChannel.transferTo(零拷贝)try(FileChannelin=...;FileChannelout=...){in.transferTo(0,in.size(),out);}// 方式二:Files.copy(简单,但内部实现取决于平台)Files.copy(sourcePath,targetPath,StandardCopyOption.REPLACE_EXISTING);
  • 同一个机器上的文件复制,两者差别不大。Files.copy底层在 Linux 上也走 sendfile,是一样的
  • 跨机器场景(本地文件系统 → 远程 socket),只能用transferTo
  • 从代码简洁角度看,小文件直接用Files.copy。几百兆以上的用FileChannel.transferTo

场景三:大文件导出

本地文件通过网络传给前端。这是最容易踩坑的场景——很多人直接把文件全读到内存再输出,文件一大就 OOM。

标准写法:FileChannel 直接写到 Response

@GetMapping("/export")publicvoidexport(HttpServletResponseresponse)throwsIOException{Filefile=newFile("/data/reports/monthly-report.xlsx");response.setContentType("application/octet-stream");response.setHeader("Content-Disposition","attachment; filename="+file.getName());response.setContentLengthLong(file.length());try(FileChannelin=FileChannel.open(file.toPath(),StandardOpenOption.READ);WritableByteChannelout=Channels.newChannel(response.getOutputStream())){longtransferred=0;longfileSize=in.size();while(transferred<fileSize){transferred+=in.transferTo(transferred,fileSize-transferred,out);}}}

用 OutputStream 写(如果框架限制只能用 OutputStream)

如果你的框架不支持 Channel,退而求其次用 Buffer 流式写:

@GetMapping("/download")publicvoiddownload(HttpServletResponseresponse)throwsIOException{Pathfile=Paths.get("/data/reports/monthly-report.xlsx");response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");response.setHeader("Content-Disposition","attachment; filename=report.xlsx");response.setContentLengthLong(Files.size(file));try(InputStreamis=Files.newInputStream(file);OutputStreamos=response.getOutputStream()){byte[]buf=newbyte[8192];intlen;while((len=is.read(buf))!=-1){os.write(buf,0,len);}}}

Files.newInputStream返回的是一个FileInputStream,它底层读文件不会把整个文件加载到堆内存,而是走系统调用,每次只读 8K。所以即使文件几个 G,内存占用也是稳定的。

动态生成内容直接导出(不落盘)

有时候文件是实时生成的,比如导出数据库数据为 Excel。这时候不需要先保存成文件再发送,边生成边发:

@GetMapping("/export/excel")publicvoidexportExcel(HttpServletResponseresponse)throwsIOException{response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");response.setHeader("Content-Disposition","attachment; filename=export.xlsx");// 用 SXSSFWorkbook(不缓存数据在内存)try(SXSSFWorkbookworkbook=newSXSSFWorkbook()){Sheetsheet=workbook.createSheet("数据");// 边写行for(inti=0;i<100000;i++){Rowrow=sheet.createRow(i);row.createCell(0).setCellValue("第"+i+"行");}// 直接写到 response 流,不经过本地磁盘workbook.write(response.getOutputStream());}}

大文件导出的注意事项

不要这么做原因
FileUtils.readFileToByteArray整个文件读到堆内存,文件稍大就 OOM
IOUtils.toByteArray同上
byte[] fileBytes = Files.readAllBytes(path)同上
先保存到本地再返回要兜底清理临时文件,而且多了 IO 开销
不设置 ContentLength前端不知道文件多大,不能显示进度条

导出时的超时处理

大文件导出时间较长,Nginx 和 Spring Boot 都有默认超时,要提前配好:

# application.ymlserver:servlet:session:timeout:30m

Nginx 侧:

proxy_read_timeout 600s; proxy_send_timeout 600s;

总结

场景推荐方式核心代码
上传保存FileChannel.transferToin.transferTo(offset, size, out)
文件复制FileChannel.transferToin.transferTo(offset, size, out)
文件导出FileChannel.transferTo到 Responsein.transferTo(offset, size, Channels.newChannel(response.getOutputStream()))

核心思想就一条:数据不经过 Java 堆内存,在内核态完成传输。你只需要管理好 offset 和 while 循环确保全量传输,剩下的交给操作系统。

http://www.jsqmd.com/news/1126747/

相关文章:

  • STM32与XTR116的4-20mA电流环设计实战
  • 嵌入式矩阵键盘设计:硬件去抖动与中断触发方案
  • 第七周学习记录
  • OBS多平台推流终极指南:一键实现多平台同时直播的完整教程
  • 高效电机驱动系统设计与STM32控制优化
  • 【OpenHarmony/HarmonyOs 】从数学学习 App 出发:近场快传、实况窗与全场景智慧学习设计
  • 879644
  • WechatDecrypt终极指南:三步解锁你的微信记忆宝库
  • 5步掌握RimSort:告别RimWorld模组冲突烦恼的终极指南
  • LTC6904与MK20微控制器构建高精度方波发生器
  • Windows任务栏透明化技术实现|TranslucentTB架构解析与应用场景分析
  • ICM-42688-P与PIC18F97J94在工业自动化中的高效协同方案
  • STM32F042K6与13DOF传感器实现低成本高精度定位
  • 基于LP5812与PIC32MZ的智能灯光控制系统设计
  • 呼和浩特老房改造市场乱象 | 增项漏项成投诉重灾区,如何必坑成难点
  • LTC6904与PIC24FJ1024GB610实现高精度方波脉冲生成
  • 终极RimWorld模组管理器:RimSort如何让你告别模组冲突烦恼
  • dirsearch:Web 路径发现工具,安全测试绕不开
  • 2026年AI论文工具推荐,这些软件帮你解决论文写作的各种难题
  • AD74412R与PIC18F86J11工业级信号链优化方案
  • DeepMetier 让 AI 成为你的专业伙伴 面向 C 端与 B 端的综合性智能体平台
  • Gin 12年零破坏API,架构哲学如何练成?
  • ASM330LHH与PIC18F4550运动跟踪系统设计与优化
  • AI DApp 日志诊断:链上失败和前端错误要一起看
  • LT3976与ATmega328的高效电源管理方案解析
  • WindowsCleaner:如何彻底解决C盘空间不足的终极系统清理方案
  • STM32与KMX63实现低功耗手势识别的嵌入式开发实践
  • ParsecVDD:5分钟学会Windows虚拟显示器完整免费方案
  • Windows桌面焕新之旅:用TranslucentTB打造个性任务栏的完整指南
  • Linux服务器入侵排查:从登录日志定位攻击源到系统加固实战