SpringBoot 之 PDF大文件分片加载(后端)

Posted 在奋斗的大道

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot 之 PDF大文件分片加载(后端)相关的知识,希望对你有一定的参考价值。

业务需求:前端集成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 stub
		SpringApplication.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, totals;
			if (request != null && request.getHeader("range") != null) {
				// 片数
				String range = request.getHeader("range").replaceAll("[^0-9\\\\-]", "");
				// 文件总大小
				totalByte = is.available();
				// 文件总片数
				totals = totalByte%(1024 * 64)==0 ? totalByte/(1024* 64):totalByte/(1024* 64) + 1;
				// 计算起始位置和结束位置
				int sheet = Integer.valueOf(range);
				startByte = sheet - 1 == 0 ? 0:(sheet - 1)*1024* 64 + 1;
				endByte = sheet*1024* 64;
				
				// 返回http状态
				response.setStatus(206);
			} else {
				// 正常下载
				// 文件总大小
				totalByte = is.available();
				// 文件总片数
				totals = 1;
				// 下载起始位置
				startByte = 0;
				// 下载结束位置
				endByte = totalByte - 1;
				// 返回http状态
				response.setHeader("Accept-Ranges", "bytes");
				response.setStatus(200);
			}
			// 需要下载字节数
			int length = endByte - startByte;
			// 响应头
			response.setHeader("Accept-Ranges", "bytes");
			response.setHeader("Content-Range", "bytes " + startByte + "-" + endByte + "/" + totalByte +"/" + totals);
			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);
	
	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		// TODO Auto-generated method stub
		
	}

	@Override
	public 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);  
	}

	@Override
	public 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大文件分片加载(后端)的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot实现浏览器端大文件分片上传

SpringBoot WebUploader大文件分片上传

上传文件慢,SpringBoot分片上传文件

上传文件慢,SpringBoot分片上传文件

上传文件慢,SpringBoot分片上传文件

SpringBoot 本地大文件分隔及其合并