🎉🎉《Spring Boot实战案例合集》目前已更新192个案例,我们将持续不断的更新。文末有电子书目录。
💪💪永久更新承诺
我们郑重承诺,所有订阅合集的粉丝都将享受永久免费的后续更新服务。
💌💌如何获取
订阅我们的合集《点我订阅》,并通过私信联系我们,我们将第一时间将电子书发送给您。
环境:SpringBoot3.4.2
1. 简介
在 Spring Boot 中高效传输二进制数据(如文件、图片、动态生成内容)需关注 Content-Type 设置、流式处理、缓存控制及分块下载。直接读取文件到内存(如 byte[])适用于小文件,但大文件应使用 InputStreamResource 或 StreamingResponseBody 避免内存溢出。动态内容可通过 HttpServletResponse 直接写入响应流,压缩文件则可用 ZipOutputStream 实时生成。为避免缓存问题,可通过 ETag 或 Last-Modified 实现条件请求。此外,支持 HTTP 范围请求(ResourceRegion)可实现断点续传,显著提升大文件传输体验。本篇文章将结合代码示例,系统化讲解二进制数据传输的核心技术与优化策略。
public ResponseEntity<byte[]> download1() throws IOException {Path path = Paths.get("E:/技术架构.pdf");byte[] pdfData = Files.readAllBytes(path);String fileName = "技术架构.pdf";fileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"").contentType(MediaType.APPLICATION_PDF).body(pdfData);}
public ResponseEntity<byte[]> fetchLogo() throws IOException {ClassPathResource resource = new ClassPathResource("static/7.png") ;byte[] bytes;try (InputStream in = resource.getInputStream()) {bytes = in.readAllBytes();}return ResponseEntity.ok().contentType(MediaType.IMAGE_PNG).contentLength(resource.contentLength()).body(bytes);}
当文件超过几兆时,在发送时将内容全部加载到内存中这将带来很大的风险。首先,这种做法会消耗不必要的内存,并在等待文件读取完成时导致响应卡顿。流式传输允许数据以较小块的形式传输,无需一次性存储全部内容。Spring Boot可通过InputStreamResource、FileSystemResource控制流式传输。如下示例:
public ResponseEntity<InputStreamResource> download2() throws IOException {File file = new File("E:/技术架构.pdf");String fileName = "技术架构.pdf";fileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");InputStreamResource resource = new InputStreamResource(new FileInputStream(file));return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"").contentType(MediaType.APPLICATION_OCTET_STREAM).contentLength(file.length()).body(resource);}
此方法实现了直接流式传输至客户端,不会被复制到内存中。同时客户端可以统计设置的Content-Length判断文件是否接收完成。
("/export/csv")public ResponseEntity<StreamingResponseBody> exportCsv() {StreamingResponseBody stream = outputStream -> {String header = "姓名,年龄,邮箱\n";outputStream.write(header.getBytes(java.nio.charset.StandardCharsets.UTF_8));outputStream.write("pack,33,pack@gmail.com\n".getBytes(java.nio.charset.StandardCharsets.UTF_8));outputStream.write("xg,32,xg@qq.com\n".getBytes(java.nio.charset.StandardCharsets.UTF_8));outputStream.write("pack_xg,40,pack_xg@163.com\n".getBytes(java.nio.charset.StandardCharsets.UTF_8));};return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"users.csv\"").contentType(MediaType.parseMediaType("text/csv; charset=UTF-8")).body(stream) ;}
这避免了临时文件的使用,即使在数据生成过程中也能保持响应流畅。对于需要保持低内存占用的大型报告或日志导出而言,此方法非常实用。
并非所有文件都来自磁盘。许多服务会在内存中生成内容,例如PDF、图像或压缩数据集。当内容在运行时创建时,可直接写入响应流而无需预先保存。如下示例:
@GetMapping("/generate/report")public void generateReport(HttpServletResponse response) throws IOException {response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE) ;String filename = "报告.txt";filename = URLEncoder.encode(filename, StandardCharsets.UTF_8).replaceAll("\\+", "%20");response.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");try (OutputStream out = response.getOutputStream()) {byte[] reportBytes = createText(); // 假设这个方法生成文本内容out.write(reportBytes);out.flush();}}private byte[] createText() {return "这是报告内容\n第二行内容".getBytes(StandardCharsets.UTF_8);}
如上实现没有文件IO操作,这在处理动态生成的文档或分析导出时,这种做法很常见。输出流直接连接到HTTP响应,既降低了延迟,又减少了磁盘访问。
public ResponseEntity<StreamingResponseBody> generateZip() {StreamingResponseBody stream = output -> {try (ZipOutputStream zipOut = new ZipOutputStream(output)) {ZipEntry entry = new ZipEntry("summary.txt");zipOut.putNextEntry(entry);zipOut.write("这里是摘要内容".getBytes(java.nio.charset.StandardCharsets.UTF_8));entry = new ZipEntry("content.txt");zipOut.putNextEntry(entry);zipOut.write("主体内容".getBytes(java.nio.charset.StandardCharsets.UTF_8));zipOut.closeEntry();zipOut.finish() ;}};return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"export.zip\"").contentType(MediaType.APPLICATION_OCTET_STREAM).body(stream);}
浏览器倾向于保留已下载的文件。这对图像或静态资源很有帮助,但对于经常更新的数据(如每日报告或导出文件)则不然。如果响应未告知浏览器更新,用户可能在不知情的情况下下载过时文件。为确保始终提供最新数据,可在响应中直接添加缓存标头。如下示例:
public ResponseEntity<byte[]> download3() throws IOException {Path path = Paths.get("E:/技术架构.pdf");byte[] data = Files.readAllBytes(path);String fileName = "技术架构.pdf";fileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");ZonedDateTime expiresTime = ZonedDateTime.now(ZoneId.systemDefault()).plusSeconds(30);String expiresHeader = expiresTime.format(DateTimeFormatter.RFC_1123_DATE_TIME);return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"").header(HttpHeaders.EXPIRES, expiresHeader).contentType(MediaType.APPLICATION_PDF).body(data);}
当我们需要更加精确的控制缓存时,可通过更具选择性的方法。条件缓存标头(如ETag或Last-Modified)会指示浏览器在复用文件前重新验证其有效性。如下示例:
public ResponseEntity<Resource> getConfigFile(HttpServletRequest request) throws IOException {FileSystemResource resource = new FileSystemResource("E:/config.json");long lastModified = resource.lastModified();String eTag = "\"" + lastModified + "\"";String ifNoneMatch = request.getHeader(HttpHeaders.IF_NONE_MATCH);boolean tagMatches = ifNoneMatch != null && ifNoneMatch.contains(eTag);if (tagMatches) {return ResponseEntity.status(HttpStatus.NOT_MODIFIED).eTag(eTag).lastModified(lastModified).build();}return ResponseEntity.ok().eTag(eTag).lastModified(lastModified).contentType(MediaType.APPLICATION_JSON).body(resource);}
public class CacheConfig implements WebMvcConfigurer {public void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler("/resources/**").addResourceLocations("classpath:/public/").setCacheControl(CacheControl.maxAge(Duration.ofDays(1)));}}
当需要高效传输大文件(如视频、音频、大型软件包)或支持断点续传时,HTTP范围请求可按需下载文件片段。例如视频拖动进度条、下载工具暂停后恢复,或仅传输客户端未缓存的部分,显著节省带宽和时间。如下示例:
public ResponseEntity<ResourceRegion> download4( HttpHeaders headers) throws IOException {// 1.获取系统资源文件FileSystemResource resource = new FileSystemResource("e:/doubao.exe");if (!resource.exists()) {return ResponseEntity.notFound().build();}long contentLength = resource.contentLength();MediaType mediaType = MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM);// 2.处理范围请求(支持多范围请求,但仅返回第一个范围)List<HttpRange> ranges = headers.getRange();if (ranges.isEmpty()) {// 完整文件下载return ResponseEntity.ok().header("Accept-Ranges", "bytes").contentType(mediaType).contentLength(contentLength).body(new ResourceRegion(resource, 0, contentLength));}// 3.严格校验范围有效性(避免越界)HttpRange range = ranges.get(0);long start = range.getRangeStart(contentLength);long end = range.getRangeEnd(contentLength);if (start > end || start < 0 || end >= contentLength) {return ResponseEntity.status(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE).header("Content-Range", "bytes */" + contentLength).build();}// 4.优化分块大小long rangeLength = end - start + 1;long chunkSize = Math.min(10 * 1024 * 1024, rangeLength); // 默认10MB,适应大文件ResourceRegion region = new ResourceRegion(resource, start, chunkSize);return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT).header("Content-Range", "bytes " + start + "-" + (start + chunkSize - 1) + "/" + contentLength).header("Content-Disposition", "attachment; filename=\"" + resource.getFilename() + "\"").contentType(mediaType).body(region);}
性能优化!Spring Boot使用强大的@EntityGraph优化查询
性能优化!Spring Boot 优化JPA插入操作,性能提升N倍,尤其批量操作
Spring Boot全局异常处理:3种方案,第三种性能40%的提升
知道太晚了!实时通信新选择fetch-event-source,真香
Spring Boot 记录Controller接口请求日志7种方式,第六种性能极高
TargetSource炸场!Spring AOP动态换“靶”超神!


