业务需求:前端集成pdf.js 实现在线阅读pdf 文件,但pdf 文件过大时(大于100M)会出现浏览器内存溢出导出程序崩溃的场景发生。针对这个情况,后端给出的解决方案是:分片加载pdf 文件流。

约定:与前端约定在header 头部参数中追加rang 参数:表示需要加载文件片数,后台动态计算起始字节位置,流式输出指定文件内容。

SpringBoot源码:

pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.single</groupId><artifactId>Single</artifactId><version>0.0.1-SNAPSHOT</version><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.9.RELEASE</version><relativePath /> <!-- lookup parent from repository --></parent><properties><java.version>1.8</java.version><maven-jar-plugin.version>3.1.1</maven-jar-plugin.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

application 程序启动窗口

package com.single;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class SingleApplication {public static void main(String[] args) {// TODO Auto-generated method stubSpringApplication.run(SingleApplication.class, args);}}

Controller

package com.single.controller;import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;/*** * @ClassName: FileController* @Description: 大文件下載* @author: zzg*/@Controller
@RequestMapping("/api/file")
public class FileController {public static final Logger logger = LoggerFactory.getLogger(FileController.class);/*** 下载文件* * @param identNo*            文件唯一识别码(经URLEncoder编码)* @param res*/@RequestMapping(value = "/showpdf", method = RequestMethod.GET)public void showpdf(HttpServletRequest req, HttpServletResponse res) {try {this.download(req, res);} catch (Exception e) {// e.printStackTrace();logger.error("错误信息:{}", e.getMessage(), e);}}/*** 下载文件* * @param identNo*            文件存储路径(经base64加密和URLEncoder编码)* @param res* @throws IOException */public void download(HttpServletRequest request, HttpServletResponse response) throws IOException {// 从文件存储服务器下载文件到本地File file = new File("D:/test.pdf");BufferedInputStream bis = null;OutputStream os = null;BufferedOutputStream bos = null;InputStream is = null;try {is = new FileInputStream(file);bis = new BufferedInputStream(is);os = response.getOutputStream();bos = new BufferedOutputStream(os);// 下载的字节范围int startByte, endByte, totalByte;if (request != null && request.getHeader("range") != null) {// 起始字节大小String[] range = request.getHeader("range").replaceAll("[^0-9\\-]", "").split("-");// 文件总大小totalByte = is.available();// 下载起始位置startByte = Integer.parseInt(range[0]);// 下载结束位置if (range.length > 1) {endByte = Integer.parseInt(range[1]);} else {endByte = totalByte - 1;}// 返回http状态response.setStatus(206);} else {// 正常下载// 文件总大小totalByte = is.available();// 下载起始位置startByte = 0;// 下载结束位置endByte = totalByte - 1;// 返回http状态response.setHeader("Accept-Ranges", "bytes");response.setStatus(206);}// 需要下载字节数int length = endByte - startByte;// 响应头response.setHeader("Accept-Ranges", "bytes");response.setHeader("Content-Range", "bytes " + startByte + "-" + endByte + "/" + totalByte);response.setContentType("Content-Type: application/octet-stream");response.setContentLength(length);// 响应内容bis.skip(startByte);int len = 0;byte[] buff = new byte[1024 * 64];while ((len = bis.read(buff, 0, buff.length)) != -1) {if (length <= len) {bos.write(buff, 0, length);break;} else {length -= len;bos.write(buff, 0, len);}}} catch (IOException e) {System.out.println("下载中断!");logger.error("错误信息:{}", e.getMessage(), e);} finally {bos.close();os.close();bis.close();is.close();}}}

Filter

package com.single.filter;import java.io.IOException;import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;/*** * @ClassName:  CorsFilter   * @Description: SpringBoot 跨域处理拦截器* @author: 世纪伟图 -zzg* @date:   2021年11月1日 下午3:06:38   *     * @Copyright: 2021 www.digipower.cn*/@Component
public class CROSFilter implements Filter {public static final Logger logger = LoggerFactory.getLogger(CROSFilter.class);@Overridepublic void init(FilterConfig filterConfig) throws ServletException {// TODO Auto-generated method stub}@Overridepublic void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException {HttpServletResponse response = (HttpServletResponse) res;  HttpServletRequest reqs = (HttpServletRequest) req;  /** 跨域设置允所有请求跨域 * 如果允许指定的客户端跨域设置: http://127.0.0.1:8020*/response.setHeader("Access-Control-Allow-Origin","*");  response.setHeader("Access-Control-Allow-Credentials", "true");  response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");  response.setHeader("Access-Control-Max-Age", "3600");  response.setHeader("Access-Control-Allow-Headers", "Content-Type");  if (((HttpServletRequest) req).getMethod().equals("OPTIONS")) {response.getWriter().println("ok");return;}chain.doFilter(req, res);  }@Overridepublic void destroy() {// TODO Auto-generated method stub}}

application.properties

# 指定服务端口
server.port=9092
# 指定日志文件配置
logging.config=classpath:logback.xml

logback.xml

<?xml version="1.0" encoding="UTF-8"?> <!-- 从高到地低 OFF 、 FATAL 、 ERROR 、 WARN 、 INFO 、 DEBUG 、 TRACE 、 ALL -->
<!-- 日志输出规则  根据当前ROOT 级别,日志输出时,级别高于root默认的级别时  会输出 -->
<!-- 以下  每个配置的 filter 是过滤掉输出文件里面,会出现高级别文件,依然出现低级别的日志信息,通过filter 过滤只记录本级别的日志-->  <!-- 属性描述 scan:性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,
默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。   debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
<configuration scan="true" scanPeriod="60 seconds" debug="false">  <!-- 定义日志文件 输入位置 -->  <property name="log_dir" value="/logs/single" />  <!-- 日志最大的历史 30天 -->  <property name="maxHistory" value="30"/>  <!-- ConsoleAppender 控制台输出日志 -->  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">  <!-- 对日志进行格式化 -->  <encoder>  <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger -%msg%n</pattern>  </encoder>  </appender>  <!-- ERROR级别日志 -->  <!-- 滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 RollingFileAppender-->  <appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">  <!-- 过滤器,只记录WARN级别的日志 -->  <filter class="ch.qos.logback.classic.filter.LevelFilter">  <level>ERROR</level>  <onMatch>ACCEPT</onMatch>  <onMismatch>DENY</onMismatch>  </filter>  <!-- 最常用的滚动策略,它根据时间来制定滚动策略.既负责滚动也负责出发滚动 -->  <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">  <!--日志输出位置  可相对、和绝对路径 -->  <fileNamePattern>${log_dir}/%d{yyyy-MM-dd}/error.log</fileNamePattern>  <!-- 可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件假设设置每个月滚动,且<maxHistory>是6,  则只保存最近6个月的文件,删除之前的旧文件。注意,删除旧文件是,那些为了归档而创建的目录也会被删除-->  <maxHistory>${maxHistory}</maxHistory>  </rollingPolicy>  <encoder>  <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>  </encoder>  </appender>  <!-- WARN级别日志 appender -->  <appender name="WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">  <!-- 过滤器,只记录WARN级别的日志 -->  <filter class="ch.qos.logback.classic.filter.LevelFilter">  <level>WARN</level>  <onMatch>ACCEPT</onMatch>  <onMismatch>DENY</onMismatch>  </filter>  <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">  <fileNamePattern>${log_dir}/%d{yyyy-MM-dd}/warn.log  </fileNamePattern>  <maxHistory>${maxHistory}</maxHistory>  </rollingPolicy>  <encoder>  <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>  </encoder>  </appender>  <!-- INFO级别日志 appender -->  <appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">  <!-- 过滤器,只记录INFO级别的日志 -->  <filter class="ch.qos.logback.classic.filter.LevelFilter">  <level>INFO</level>  <onMatch>ACCEPT</onMatch>  <onMismatch>DENY</onMismatch>  </filter>  <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">  <fileNamePattern>${log_dir}/%d{yyyy-MM-dd}/info.log  </fileNamePattern>  <maxHistory>${maxHistory}</maxHistory>  </rollingPolicy>  <encoder>  <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>  </encoder>  </appender>  <!-- DEBUG级别日志 appender -->  <appender name="DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender">  <filter class="ch.qos.logback.classic.filter.LevelFilter">  <level>DEBUG</level>  <onMatch>ACCEPT</onMatch>  <onMismatch>DENY</onMismatch>  </filter>  <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">  <fileNamePattern>${log_dir}/%d{yyyy-MM-dd}/debug.log  </fileNamePattern>  <maxHistory>${maxHistory}</maxHistory>  </rollingPolicy>  <encoder>  <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>  </encoder>  </appender>  <logger name="org.springframework.web"/><logger name="com.single" /><!-- root级别   DEBUG -->  <root level="ERROR">  <!-- 控制台输出 -->  <appender-ref ref="STDOUT" />  <!-- 文件输出 -->  <appender-ref ref="ERROR" />  <appender-ref ref="INFO" />  <appender-ref ref="WARN" />  <appender-ref ref="DEBUG" />  </root>
</configuration>

PostMan 模拟工具效果展示:

基本功能已经完成,静待前端工程师的对接。

SpringBoot 之 PDF大文件分片加载(后端)相关推荐

  1. fread读取整个文件_qt如何实现大文件的加载和显示

    最近研究了下如何用qt的原生控件来加载和显示大文件(>1G),分享下一些摸索经验. 下文源码: compilelife/loginsight​github.com 文件的内存映射 在开始qt部分 ...

  2. nginx配置解决vue单页面打包文件大,首次加载慢的问题

    nginx配置解决vue单页面打包文件大,首次加载慢的问题 参考文章: (1)nginx配置解决vue单页面打包文件大,首次加载慢的问题 (2)https://www.cnblogs.com/golo ...

  3. iOS 利用AFNetworking实现大文件分片上传

    概述 一说到文件上传,想必大家都并不陌生,更何况是利用AFNetworking(PS:后期统称AF)来做,那更是小菜一碟.比如开发中常见的场景:头像上传,九宫格图片上传...等等,这些场景无一不使用到 ...

  4. iOS 开发之 pdf 文档的加载与浏览的 4 种方式

    原文链接:http://www.jianshu.com/p/1d4305a02ea5 在我们的开发中,有些像电子书类型的 app 的开发会涉及到 pdf 文档的加载与展示.由于笔者项目中正好涉及到这块 ...

  5. linux文件 内存映射 锁,linux – mmap:将映射文件立即加载到内存中吗?

    不,是的,也许吧.这取决于. 调用mmap通常只意味着对应用程序而言,映射文件的内容将映射到其地址空间,就像文件已加载到那里一样.或者,好像该文件确实存在于内存中,就好像它们是同一个(包括更改被写回磁 ...

  6. 高效使用Bitmaps(一) 大Bitmap的加载

    转自:http://my.oschina.net/rengwuxian/blog/182885 高效使用Bitmaps有什么好处? 我 们常常提到的"Android程序优化",通常 ...

  7. .NET Core Web APi大文件分片上传研究

    [导读]前两天发表利用FormData进行文件上传.NET和.NET Core Web APi FormData多文件上传,然后有人问要是大文件几个G上传怎么搞,常见的不就是分片再搞下断点续传,动动手 ...

  8. Vue项目中遇到了大文件分片上传的问题

    Vue项目中遇到了大文件分片上传的问题,之前用过webuploader,索性就把Vue2.0与webuploader结合起来使用,封装了一个vue的上传组件,使用起来也比较舒爽. 上传就上传吧,为什么 ...

  9. extjs5(03--项目中文件的加载过程)

    上一节中用sencha工具自动创建了一个项目,并且可以在浏览器中查看.现在我们来看看js类加载过程.如下图所示: 1、首先:浏览器中输入 localhost:1841 ,调用 index.html; ...

最新文章

  1. C#综合揭秘——Entity Framework 并发处理详解
  2. 全球链界科技发展大会_如何成为科技界的团队合作者
  3. 为什么Python发展这么快,有哪些优势?
  4. 【Spring】【JUnit】单元测试
  5. elementui的el-tree第一次加载无法展开和选中的问题
  6. dubbo控制中心部署,权重配置,以及管控台中各个配置的简单查看
  7. ABAP web service运行时的细节调试
  8. react学习路线图,学习react就是有捷径
  9. cms核心功能_如何根据这些重要功能选择合适的CMS
  10. mysql show 存储过程_mysql 存储过程 show errors
  11. 69. 二叉树的层次遍历Python实现
  12. Spark的event事件监听器LiveListenerBus和特质SparkListenerBus以及特质ListenerBus
  13. Qt捕捉窗口关闭事件
  14. java 启动连接hsql
  15. 时序逻辑电路的基础知识
  16. Node.js安装详细步骤教程(Windows版)
  17. 攻防世界WEB进阶之mfw
  18. 从配置 Kivy、Buildozer 到 Android app 运行
  19. 数据库实验一:数据库与数据表定义(1)—— 数据库相关操作
  20. 泰坦尼克号生存预测(超详细)

热门文章

  1. android nfc驱动,移植NFC驱动到android系统
  2. python——面向对象的三大特性:封装,继承,多态
  3. pwscf与wannier90 Hands-On实战训练(一)——费米面计算为例
  4. 【题解】剔除多余括号
  5. turtle实现一团乱麻和甜甜圈
  6. window.open()打开新窗口被浏览器拦截
  7. android 软键盘的从属关系,Android控件属性大全
  8. 【计算机毕设】Java课程设计
  9. Docker技术入门与实战 第二版-学习笔记-7-数据管理(volume)
  10. SimpleITK使用——3. 常见操作