承上篇上传文件之后,本文就主要介绍下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 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 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 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工具类,依赖如下
commons-io
commons-io
2.4
经过测试,当文件小于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,就没有实现这个方法,那么就不可以使用这种方式创建数组。