文件下载java实现

记录一下实现一个文件下载的实现代码,这里踩过很多坑。首先这里的传入参数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ㄒ)/~~,再接再厉。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值