龙哥网

龙哥网

SpringBoot下载文件的实现及速度对比_java
2022-03-01

目录
  • 前言
  • 文件来源
  • 文件下载
    • 1、OutputStream形式
    • 2、ResponseEntity形式
  • 两种方式下载速度比较
    • 后话

      前言

      承上篇上传文件之后,本文就主要介绍下SpringBoot下下载文件的方式,大致有两种Outputstream与ResponseEntity,并大概看一下速度对比

      文件来源

      这里还是以GridFS为例,主要演示的还是从mongo下载下来的文件,如果是本地服务器上的文件,前端传以文件路径直接获取流即可,如下:

      InputStream in = new FileInputStream(System.getProperty("user.dir") + filePath);

      接下来就演示下使用GridFsTemplate下载文件,mongo的配置其实上篇已经贴过了,这里就直接贴代码了,具体的就不做解释了

      @Service
      @Slf4j
      public class MongoConfig extends AbstractMongoConfiguration {
       
          @Autowired
          private MongoTemplate mongoTemplate;
          @Autowired
          private GridFSBucket gridFSBucket;
       
          @Override
          public MongoClient mongoClient() {
              MongoClient mongoClient = getMongoClient();
              return mongoClient;
          }
       
          public MongoClient getMongoClient() {
              // MongoDB地址列表
              List<ServerAddress> serverAddresses = new ArrayList<>();
              serverAddresses.add(new ServerAddress("10.1.61.101:27017"));
              // 连接认证
              MongoCredential credential = MongoCredential.createCredential("root", "admin", "Root_123".toCharArray());
              MongoClientOptions.Builder builder = MongoClientOptions.builder();
       
              //最大连接数
              builder.connectionsPerHost(10);
              //最小连接数
              builder.minConnectionsPerHost(0);
              //超时时间
              builder.connectTimeout(1000*3);
              // 一个线程成功获取到一个可用数据库之前的最大等待时间
              builder.maxWaitTime(5000);
              //此参数跟connectionsPerHost的乘机为一个线程变为可用的最大阻塞数,超过此乘机数之后的所有线程将及时获取一个异常.eg.connectionsPerHost=10 and threadsAllowedToBlockForConnectionMultiplier=5,最多50个线程等级一个链接,推荐配置为5
              builder.threadsAllowedToBlockForConnectionMultiplier(5);
              //最大空闲时间
              builder.maxConnectionIdleTime(1000*10);
              //设置池连接的最大生命时间。
              builder.maxConnectionLifeTime(1000*10);
              //连接超时时间
              builder.socketTimeout(1000*10);
       
              MongoClientOptions myOptions = builder.build();
              MongoClient mongoClient = new MongoClient(serverAddresses, credential, myOptions);
              return mongoClient;
          }
       
          @Override
          protected String getDatabaseName() {
              return "notifyTest";
          }
       
          /**
           * 获取另一个数据库
           * @return
           */
          public String getFilesDataBaseName() {
              return "notifyFiles";
          }
       
          /**
           * 用于切换不同的数据库
           * @return
           */
          public MongoDbFactory getDbFactory(String dataBaseName) {
              MongoDbFactory dbFactory = null;
              try {
                  dbFactory = new SimpleMongoDbFactory(getMongoClient(), dataBaseName);
              } catch (Exception e) {
                  log.error("Get mongo client have an error, please check reason...", e.getMessage());
              }
              return dbFactory;
          }
       
          /**
           * 获取文件存储模块
           * @return
           */
          public GridFsTemplate getGridFS() {
              return new GridFsTemplate(getDbFactory(getFilesDataBaseName()), mongoTemplate.getConverter());
          }
       
          @Bean
          public GridFSBucket getGridFSBuckets() {
              MongoDatabase db = getDbFactory(getFilesDataBaseName()).getDb();
              return GridFSBuckets.create(db);
          }
       
          /**
           * 为了解决springBoot2.0之后findOne方法返回类更改所新增 将GridFSFile 转为 GridFsResource
           * @param gridFsFile
           * @return
           */
          public GridFsResource convertGridFSFile2Resource(GridFSFile gridFsFile) {
              GridFSDownloadStream gridFSDownloadStream = gridFSBucket.openDownloadStream(gridFsFile.getObjectId());
              return new GridFsResource(gridFsFile, gridFSDownloadStream);
          }
      }

      对比上篇配置,新增加的两个方法主要为了应对SpringBoot2.x之后,GridFsTemplate的findOne()方法返回从GridFSDBFile改为GridFSFile,导致文件下载时不能使用以前的GridFSDBFile 操作流了,所以加了转换操作

      文件下载

      分别把两种方式的下载实现贴出来

      1、OutputStream形式

          @RequestMapping(value = "/download2", method = RequestMethod.GET)
          public void downLoad2(HttpServletResponse response, String id) {
              userService.download2(response, id);
          }

      controller层如上,只是测试所以很简略,因为是流的形式所以并不需要指定输出格式,下面看下service层实现

          /**
           * 以OutputStream形式下载文件
           * @param response
           * @param id
           */
          @Override
          public void download2(HttpServletResponse response, String id) {
              GridFsTemplate gridFsTemplate = new GridFsTemplate(mongoConfig.getDbFactory(mongoConfig.getFilesDataBaseName()), mongoTemplate.getConverter());
              // 由于springBoot升级到2.x 之后 findOne方法返回由 GridFSDBFile 变为 GridFSFile 了,导致下载变得稍微有点繁琐
              GridFSFile gridFSFile = gridFsTemplate.findOne(new Query(Criteria.where("_id").is(id)));
              String fileName = gridFSFile.getFilename();
              GridFsResource gridFsResource = mongoConfig.convertGridFSFile2Resource(gridFSFile);
              // 从此处开始计时
              long startTime = System.currentTimeMillis();
              InputStream in = null;
              OutputStream out = null;
              try {
                  // 这里需对中文进行转码处理
                  fileName = new String(fileName.getBytes("utf-8"), "ISO-8859-1");
                  // 告诉浏览器弹出下载对话框
                  response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
                  byte[] buffer = new byte[1024];
                  int len;
                  // 获得输出流
                  out = response.getOutputStream();
                  in = gridFsResource.getInputStream();
                  while ((len = in.read(buffer)) > 0) {
                     out.write(buffer, 0 ,len);
                  }
              } catch (IOException e) {
                  log.error("transfer in error .");
              } finally {
                  try {
                      if (null != in)
                          in.close();
                      if (null != out)
                          out.close();
                      log.info("download file with stream total time : {}", System.currentTimeMillis() - startTime);
                  } catch (IOException e){
                      log.error("close IO error .");
                  }
              }
          }

      可以看到篇幅较长,注释也已经都在代码里了

      2、ResponseEntity形式

          @RequestMapping(value = "/download", method = RequestMethod.GET, produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
          public Object downLoad(String id) {
              return userService.download(id);
          }

      controller需要指定输出格式application/octet-stream,标明是以流的形式下载文件,下面看下service层

          /**
           * 以ResponseEntity形式下载文件
           * @param id
           * @return
           */
          @Override
          public ResponseEntity<byte[]> download(String id) {
              GridFsTemplate gridFsTemplate = new GridFsTemplate(mongoConfig.getDbFactory(mongoConfig.getFilesDataBaseName()), mongoTemplate.getConverter());
              // 由于springBoot升级到2.x 之后 findOne方法返回由 GridFSDBFile 变为 GridFSFile 了,导致下载变得稍微有点繁琐
              GridFSFile gridFSFile = gridFsTemplate.findOne(new Query(Criteria.where("_id").is(id)));
              String fileName = gridFSFile.getFilename();
              GridFsResource gridFsResource = mongoConfig.convertGridFSFile2Resource(gridFSFile);
              // 从此处开始计时
              long startTime = System.currentTimeMillis();
              try {
                  InputStream in = gridFsResource.getInputStream();
                  // 请求体
                  byte[] body = IOUtils.toByteArray(in);
                  // 请求头
                  HttpHeaders httpHeaders = new HttpHeaders();
                  // 这里需对中文进行转码处理
                  fileName = new String(fileName.getBytes("utf-8"), "ISO-8859-1");
                  // 告诉浏览器弹出下载对话框
                  httpHeaders.add("Content-Disposition", "attachment;filename=" + fileName);
                  ResponseEntity<byte[]> responseEntity = new ResponseEntity<>(body, httpHeaders, HttpStatus.OK);
                  log.info("download file total with ResponseEntity time : {}", System.currentTimeMillis() - startTime);
                  return responseEntity;
              } catch (IOException e) {
                  log.error("transfer in error .");
              }
              return null;
          }

      上面用到了IOUtils工具类,依赖如下

      <dependency>
          <groupId>commons-io</groupId>
          <artifactId>commons-io</artifactId>
          <version>2.4</version>
      </dependency>

      两种方式下载速度比较

      经过测试,当文件小于1m内两种方式速度差不多,然后我测了5m的文件,结果如下:

      可以看到OutputStream略慢一点点,当文件再大时这边也并没有作测试,总之本人推荐使用ResponseEntity形式下载文件~

      后话

      如果只是想显示某个路径下的图片而并不需要下载,那么采用如下形式:

          @RequestMapping(value = "/application/file/show", method = RequestMethod.GET, produces = MediaType.IMAGE_PNG_VALUE)
          public Object downloadFile(@RequestParam("path") String filePath) {
              try {
                  InputStream in = new FileInputStream(System.getProperty("user.dir") + filePath);
                  byte[] bytes = new byte[in.available()];
                  in.read(bytes);
                  return bytes;
              } catch (IOException e) {
                  log.error("transfer byte error");
                  return buildMessage(ResultModel.FAIL, "show pic error");
              }
          }

      需要注意上述的available()方法,该方法是返回输入流中所包含的字节数,方便在读写操作时就能得知数量,能否使用取决于实现了InputStream这个抽象类的具体子类中有没有实现available这个方法。

      如果实现了那么就可以取得大小,如果没有实现那么就获取不到。

      例如FileInputStream就实现了available方法,那么就可以用new byte[in.available()];这种方式。

      但是,网络编程的时候Socket中取到的InputStream,就没有实现这个方法,那么就不可以使用这种方式创建数组。

      免责声明
      本站部分资源来源于互联网 如有侵权 请联系站长删除
      龙哥网是优质的互联网科技创业资源_行业项目分享_网络知识引流变现方法的平台为广大网友提供学习互联网相关知识_内容变现的方法。