记录一下实现一个文件下载的实现代码,这里踩过很多坑。首先这里的传入参数objectName应该是文件路径,因为在这个MinioUtil中,文件名就是文件路径(Minio上的路径) ,所以传入的是文件名同时也是文件路径(后面都叫objectName)。代码如下:
@ApiOperation("下载文件")
@GetMapping("/download")
public void download(@RequestParam("objectName") String objectName, HttpServletResponse response){
if (StringUtils.isNotBlank(objectName)) {
try {
if (MinioUtil.existObject(CommonConsts.GLOBAL_BUCKET_NAME, objectName)) {
// 获取文件的二进制内容
InputStream fileStream = MinioUtil.getStream(CommonConsts.GLOBAL_BUCKET_NAME, objectName);
// 设置响应头,指示下载为二进制文件
response.setContentType("application/octet-stream");
response.setCharacterEncoding("UTF-8");
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
response.setHeader("Content-Disposition", "attachment; filename=" + objectName);
response.setHeader("filename", objectName);
// 将文件内容写入响应体
OutputStream outputStream = response.getOutputStream();
IOUtils.copy(fileStream, outputStream);
outputStream.flush();
} else {
// 文件不存在在minio服务器中
response.setContentType("application/json; charset=utf-8");
response.getOutputStream().write(JSONObject.toJSONString(ApiResult.createFail("文件不存在")).getBytes(StandardCharsets.UTF_8));
}
} catch (Exception e) {
log.error("下载文件异常!", e);
response.setContentType("application/json; charset=utf-8");
try {
response.getOutputStream().write(JSONObject.toJSONString(ApiResult.createFail("下载文件异常")).getBytes(StandardCharsets.UTF_8));
} catch (IOException ex) {
log.error("获取响应输入流异常!", ex);
}
}
}
}
首先是方法上的注解GetMapping,一开始用了PostMapping,好像是可以的。但是后面改成了GetMapping再改回PostMapping就不行了,后端报错(Request method ‘GET’ not supported):
就只能用GetMapping或者RequestMapping,虽然这里理论上来讲就是用GetMapping(没有对数据库进行操作,只是执行下载操作)。至于为什么报这个错?它又是怎么判定这是个GET方法的? 这里记录一下,以后遇到解决了再回来看一下。
下面是另一个方法,利用了MinioUtil工具类中定义的下载方法进行下载:
@ApiOperation("下载文件")
@RequestMapping("/download")
public void download(@RequestParam("objectName") String objectName, HttpServletResponse response){
if (StringUtils.isNotBlank(objectName)) {
try {
if (MinioUtil.existObject(CommonConsts.GLOBAL_BUCKET_NAME, objectName)) {
// 下载文件
MinioUtil.download(CommonConsts.GLOBAL_BUCKET_NAME, objectName, response);
} else {
// 文件不存在在minio服务器中
response.setContentType("application/json; charset=utf-8");
response.getOutputStream().write(JSONObject.toJSONString(ApiResult.createFail("文件不存在")).getBytes(StandardCharsets.UTF_8));
}
} catch (Exception e) {
log.error("下载文件异常!", e);
response.setContentType("application/json; charset=utf-8");
try {
response.getOutputStream().write(JSONObject.toJSONString(ApiResult.createFail("下载文件异常")).getBytes(StandardCharsets.UTF_8));
} catch (IOException ex) {
log.error("获取响应输入流异常!", ex);
}
}
}
}
下面是自定义的MinioUtile类:
/**
* minio工具
*
*/
@Slf4j
public class MinioUtil {
private static final String ORIGINAL_FILENAME = "OriginalFilename";
/**
* 建议用ApplicationStartedEvent触发初始创建桶操作;
* 不用处理异常;
*
* @param bucketName
*/
public static void createBucketIfNotExist(String bucketName) throws Exception {
boolean found = getClient().bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!found) {
getClient().makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
}
/**
* 上传文件,建议用关联表名作为bucketName,方便清理无效数据;
* <p>
* 上传文件并使用uuid作为对象名,对象名保留原文件名后缀;
* 同时记录文件的CONTENT_TYPE;
*
* @param file
* @param bucketName
* @return
* @throws Exception
*/
public static String uploadFile(String bucketName, MultipartFile file) throws Exception {
String originalFilename = file.getOriginalFilename();
String extension = FilenameUtils.getExtension(originalFilename);
extension = StringUtils.isEmpty(extension) ? "" : FilenameUtils.EXTENSION_SEPARATOR_STR + extension;
String objectName = StringUtils.join(IdUtil.simpleUUID(), extension);
return uploadFile(bucketName, objectName, file);
}
/**
* 上传文件,建议用关联表名作为bucketName,方便清理无效数据;
* <p>
* 同时记录文件的CONTENT_TYPE;
*
* @param bucketName
* @param objectName
* @param file
* @return
* @throws Exception
*/
public static String uploadFile(String bucketName, String objectName, MultipartFile file) throws Exception {
var headers = new HashMap<String, String>(1);
headers.put(HttpHeaders.CONTENT_TYPE, file.getContentType());
//headers.put(ORIGINAL_FILENAME, file.getOriginalFilename());
try (var stream = file.getInputStream()) {
getClient().putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName)
.stream(stream, file.getSize(), -1)
.headers(headers).build());
return objectName;
}
}
/**
* 上传文件,建议用关联表名作为bucketName,方便清理无效数据;
* <p>
* 同时记录文件的CONTENT_TYPE;
*
* @param bucketName 桶名
* @param objectName 文件对象名
* @param inputStream 输入流
* @param size 可以从此输入流中读取(或跳过)而不阻塞的剩余字节数。
* @param contentType 例如 "application/octet-stream"
*
* @throws Exception
*/
public static void uploadFile(String bucketName, String objectName, InputStream inputStream, int size, String contentType) throws Exception {
getClient().putObject(PutObjectArgs.builder().
bucket(bucketName).
object(objectName)
.stream(inputStream, size, -1)
.contentType(contentType).build());
}
/**
* @Title: uploadFile
* @Desciption: 通过流上传文件
* @param1: bucketName
* @param2: objectName
* @param3: inputStream
* @return: void
*/
public static void uploadFile(String bucketName, String objectName, InputStream inputStream) throws Exception {
getClient().putObject(PutObjectArgs.builder().
bucket(bucketName).
object(objectName)
.stream(inputStream, inputStream.available(), -1)
// .contentType(contentType)
.build());
}
/**
* @Title: uploadFile
* @Desciption: 上传本地文件
* @param1: bucketName
* @param2: objectName
* @param3: fileName
* @return: void
*/
public static void uploadFile(String bucketName, String objectName, String fileName) throws Exception {
getClient().uploadObject(UploadObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.filename(fileName)
.build()
);
}
/**
* 下载文件;
* <p>
* 使用已记录的文件的CONTENT_TYPE;
* 使用对象名作为CONTENT_DISPOSITION的文件名;
*
* @param bucketName
* @param objectName
* @param response
* @throws Exception
*/
public static void download(String bucketName, String objectName, HttpServletResponse response) throws Exception {
try (var stream =
getClient().getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
) {
response.setHeader(HttpHeaders.CONTENT_TYPE, stream.headers().get(HttpHeaders.CONTENT_TYPE));
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
String.format("attachment;filename=\"%s\"", hideRealName(objectName)));
StreamUtils.copy(stream, response.getOutputStream());
}
}
/**
* @Title: download
* @Desciption: 下载文件到指定输出流
* @param1: bucketName
* @param2: objectName
* @param3: os
* @return: void
*/
public static void download(String bucketName, String objectName, OutputStream os) throws Exception {
try (var stream =
getClient().getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
) {
StreamUtils.copy(stream, os);
}
}
public static InputStream getStream(String bucketName, String objectName) throws Exception {
return getClient().getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
}
public static boolean existObject(String bucketName, String objectName){
boolean exist = true;
try {
getClient().statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
} catch (Exception e) {
//文件不存在
exist = false;
}
return exist;
}
/**
* @Title: deleteObject
* @Desciption: 删除文件对象
* @param1: bucketName
* @param2: objectName
* @return: boolean
*/
public static void deleteObject(String bucketName, String objectName) throws Exception {
getClient().removeObject(RemoveObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build()
);
}
/**
* @Title: deleteObjects
* @Desciption: 批量删除
* @param1: bucketName
* @param2: objectName
* @return: void
*/
public static void deleteObjects(String bucketName, List<String> objectNames) throws Exception {
List<DeleteObject> deleteObjects = objectNames.stream().map(DeleteObject::new).toList();
Iterable<Result<DeleteError>> results = getClient().removeObjects(
RemoveObjectsArgs.builder()
.bucket(bucketName)
.objects(deleteObjects)
.build()
);
for (Result<DeleteError> result : results) {
DeleteError error = result.get();
log.error("Error in deleting object " + error.objectName() + "; " + error.message());
}
}
public static void preview(String bucketName, String objectName, HttpServletResponse response) throws Exception {
try (var stream =
getClient().getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
) {
/*response.setHeader(HttpHeaders.CONTENT_TYPE, stream.headers().get(HttpHeaders.CONTENT_TYPE));
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
String.format("inline;filename=\"%s\"", hideRealName(objectName)));
StreamUtils.copy(stream, response.getOutputStream());*/
//转为Base64字符串
response.setContentType("text/plain;charset=utf-8");
String result = new String(Base64.getEncoder().encode(stream.readAllBytes()));
response.getOutputStream().write(result.getBytes(StandardCharsets.UTF_8));
}
}
public static String preview(String bucketName, String objectName) throws Exception {
// 查看文件地址
GetPresignedObjectUrlArgs build = GetPresignedObjectUrlArgs.builder().bucket(bucketName).object(objectName).method(Method.GET).build();
return getClient().getPresignedObjectUrl(build);
}
private static String hideRealName(String objectName) {
String extension = FilenameUtils.getExtension(objectName);
extension = StringUtils.isEmpty(extension) ? "" : FilenameUtils.EXTENSION_SEPARATOR_STR + extension;
String baseName = FilenameUtils.getBaseName(objectName);
return SecureUtil.md5(baseName) + extension;
}
private static MinioClient getClient() throws Exception {
var bean = SpringUtil.getBean(MinioClient.class);
if (bean == null) {
throw new Exception("there is no spring bean MinioClient");
}
return bean;
}
}
其实应该两个方法都可以的,实现了获取文件的二进制内容,然后再将内容写出。其中,用postman或者前端f12调试模式下看到的是没输出/输出乱码,结果分别如下:
实时响应显示返回的不是json格式。
网页返回的是一堆乱码,按照大部分人的解释是,返回的是文件的二进制内容,但是网页尝试读成json格式,所以会显示乱码。
然后就是跟前端联调遇到的比较大的问题。前端一直说这个功能是不行的,因为返回的不对,如上图(一堆乱码),且虽然显示状态200但是并没有下载文件。但是直接在浏览器调用是没问题的(在浏览器中输入相应url,回车后实现了文件以附件的形式下载到本地)。一开始我觉得是调用形式的问题,就是当时这个功能是在一个【用户编辑】的页面下,每个用户对应有一个【文件】,点击这个【文件】可以实现附件下载。但是前端将这个接口放在了访问这个【用户编辑】页面下,即打开【用户编辑】页面就立马调用了接口。然后这个接口其实显示状态是成功的,如图:
但是同时返回那个如上个图,是一对乱码。当时一个是以为返回的值不对,但是正如上述的解释,其实返回的是文件的二进制内容读成了乱码。后面考虑到是否是这个接口的调用形式不对,应该是【用户编辑】的文件那里是先显示该用户对应的文件。然后点击该文件再调用下载接口,实现文件下载。(其实我觉得这里已经接近正确答案了)。但是转念一想,前端说这个接口只是放在【用户编辑】页面调用测试,到时是实现如我说的方法。其实,这里涉及一个前端的知识(应该算前端把),这里不能直接在打开页面的时候就调用这个接口,而是应该用一个新窗口(window.open())调用这个接口,把这个解决方法告诉前端后,功能实现,完成任务。
小菜鸡终于完成了自己实现的第一个接口/(ㄒoㄒ)/~~,再接再厉。