springboot 2.x 实现minio文件上传下载

1.背景
公司早前使用的是fastdfs文件存储服务器,并且是以单点的形式存在了多年。不管从维护性和未来的扩展性考虑,决定进行升级更换,调研之后,发现minio比较适合公司现状,也是未来的趋势。

2.升级minio,如果兼容fastdfs数据?
由于fastdfs的文件不能直接迁移至minio中使用,文件链接地址存储在数据库中,格式也大不相同,只能进行逐步手动迁移的思路进行。
(1)对于有时效性的文件,比如要求存在3个月,半年的这种文件,可以不进行迁移,保留fastdfs服务器的运行,提供文件链接下载功能。
(2)重新生成上传的文件,系统均对接至minio服务器上,保证新文件均落在minio上。
(3)系统内部进行文件下载,不能局限于minio提供的下载方法,应该编写通用方法,通过文件链接进行文件下载。因为有可能通过fastdfs或者minio进行下载;
(4)对于系统内部的文件,这些不确定文件时效性的,或者’永久’需要的,如果少量文件,可以手工进行文件迁移并修改数据库文件链接地址。量多考虑程序批量迁移。
(5)数据迁移完成,如果fastdfs上的文件不需要提供下载,便可以停止fastdfs的维护。

3.代码实现
服务端
创建一个公共微服务,提供文件上传下载等操作。
maven依赖,忽略基本springcloud依赖:

<!--通过文件类型得到contenttype-->
		<dependency>
            <groupId>com.j256.simplemagic</groupId>
            <artifactId>simplemagic</artifactId>
            <version>1.16</version>
        </dependency>
        <!--minio版本-->
        <dependency>
            <groupId>io.minio</groupId>
            <artifactId>minio</artifactId>
            <version>7.1.2</version>
        </dependency>
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.11</version>
        </dependency>

以下为yml配置文件,忽略注册中心等基础配置:

minio:
  endpoint: https://xx.xxxxxx.xx
  access-key: minio
  secret-key: minio
  bucket-name: public
  gateway: ${minio.endpoint}${minio.bucket-name}/

加载配置类:

@Configuration
@ConfigurationProperties(prefix = "minio")
@Data
public class MinioConfig {
    public String endpoint;
    public String accessKey;
    public String secretKey;
    public String bucketName;
    public String gateway;
}

接口类:


public interface IFileService {

    /**
    * @description 文件上传
    * @param file:待上传文件
    **/
    String uploadFile(MultipartFile file);

    /**
     * @Description 上传文件到自定义桶,bucketName为空时,上传到默认桶(配置项public)
     * @Param [bucketName, file]
     */
    String uploadFileByBucket(String bucketName, MultipartFile file);


    /**
     * @Description 文件上传,通过文件流和contentType
     * @Param [bucketName,fileInputStream, fileType,contentType]
     */
    String uploadFileByByte(String bucketName,InputStream fileInputStream,String fileType, String contentType);

    /**
    * @description 文件下载(读取)
    * @param filePath: 文件路径
    **/
    byte[] downloadFile(String filePath);
}

实现类:


@Service
public class FileServiceImpl implements IFileService {

    private final Logger logger = LoggerFactory.getLogger(FileServiceImpl.class);

    @Autowired
    private MinioConfig minioConfig;
    /**
     * url分隔符
     */
    public static final String URI_DELIMITER = "/";

    /**
     * 定义一个单例的MinioClient对象
     */
    private static MinioClient minioClient;

    /**
     * @Description 获取客户端单例对象
     * @Param []
     */
    private MinioClient getInstance() {
        if (minioClient == null) {
            minioClient = MinioClient.builder().endpoint(minioConfig.endpoint).credentials(minioConfig.accessKey, minioConfig.secretKey).build();
        }
        return minioClient;
    }

    /**
     * @Description 文件上传
     * @Param [file]
     */
    @Override
    public String uploadFile(MultipartFile file) {
        return putObject(null, file);
    }

    /**
     * @Description 自定义桶上传文件,当为null,上传默认桶public
     * @Param [bucketName, file]
     */
    @Override
    public String uploadFileByBucket(String bucketName, MultipartFile file) {
        return putObject(bucketName, file);
    }

    /**
     * jsonObject包含参数
     * 1.bytes为basr64bianma后的byte数组
     * 2.fileType上传文件的后缀扩展名,例如'pdf'
     * 3.contentType文件上传类型,例如‘application/pdf’
     * 4.bucketName桶名称,为空则默认桶public
     */
    @Override
    public String uploadFileByByte(String bucketName,InputStream fileInputStream, String fileType, String contentType) {
        return putObject(bucketName, fileInputStream, fileType, contentType);
    }

    /**
     * @Description 文件下载
     * @Param [filePath 通过文件链接得到字节数组]
     */
    @Override
    public byte[] downloadFile(String filePath) {
        logger.info("下载文件:{}",filePath);
        try {
            URL url = new URL(filePath);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            //设置超时间为3秒
            conn.setConnectTimeout(3 * 1000);
            //设置请求头
            conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.110 Safari/537.36");
            //得到输入流
            InputStream inputStream = conn.getInputStream();
            //获取自己数组
            byte[] bs = readInputStream(inputStream);
            return bs;
        } catch (Exception e) {
            throw new RuntimeException("下载文件异常:",e);
        }

    }

    /**
     * 从输入流中获取字节数组
     *
     * @param inputStream
     * @return
     * @throws IOException
     */
    public static byte[] readInputStream(InputStream inputStream) {
        byte[] buffer = new byte[1024];
        int len = 0;
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try {
            while ((len = inputStream.read(buffer)) != -1) {
                bos.write(buffer, 0, len);
            }
            return bos.toByteArray();
        } catch (Exception e) {
            throw new RuntimeException("读取文件流异常:", e);
        } finally {
            if (bos != null) {
                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * @Description 根据桶名,上传文件
     * @Param [bucketName, multipartFile]
     */
    private String putObject(String bucketName, MultipartFile multipartFile) {
        logger.info("开始上传文件,文件桶名:{},文件名:{}",bucketName,multipartFile.getOriginalFilename());
        try {
            // 获取网关地址
            String gateway = getGateway(bucketName);
            // 若桶名不存在
            if (bucketName == null) {
                // 上传默认桶
                bucketName = minioConfig.bucketName;
                // 默认网关地址
                gateway = minioConfig.gateway;
            }
            minioClient = getInstance();
            // UUID重命名
            String fileName = UUID.randomUUID().toString().replace("-", "") + "." + getSuffix(multipartFile.getOriginalFilename());
            // 年/月/日/file
            String finalPath = new StringBuilder()
                    .append(getDateFolder())
                    .append(URI_DELIMITER)
                    .append(fileName).toString();
            minioClient.putObject(PutObjectArgs.builder()
                    .stream(multipartFile.getInputStream(), multipartFile.getSize(), PutObjectArgs.MIN_MULTIPART_SIZE)
                    .object(finalPath)
                    .contentType(multipartFile.getContentType())
                    .bucket(bucketName)
                    .build());
             finalPath = gateway + finalPath;
            logger.info("返回文件地址:" + finalPath);
            return finalPath;
        } catch (Exception e) {
            logger.error("文件上传出现异常:{}", e.getMessage());
            throw new RuntimeException("文件上传出现异常:", e);
        }
    }

    /**
     * @Description 根据桶名,文件流,文件扩张名,文件上传类型,例如‘application/pdf’
     * @Param [bucketName, fileInputStream, fileType, contentType]
     */
    private String putObject(String bucketName, InputStream fileInputStream, String fileType, String contentType) {

        try {
            // 获取网关地址
            String gateway = getGateway(bucketName);
            // 若桶名不存在
            if (bucketName == null) {
                // 上传默认桶
                bucketName = minioConfig.bucketName;
                // 默认网关地址
                gateway = minioConfig.gateway;
            }

            minioClient = getInstance();

            //构造文件上传路径
            String finalPath = setUpdaloadFilePath(fileType);
            logger.info("文件上传路径为" + finalPath);
            minioClient.putObject(PutObjectArgs.builder()
                    .stream(fileInputStream, fileInputStream.available(), PutObjectArgs.MIN_MULTIPART_SIZE)
                    .object(finalPath)
                    .contentType(contentType)
                    .bucket(bucketName)
                    .build());
            logger.info("返回文件地址:" + gateway + finalPath);
            return gateway + finalPath;
        } catch (Exception e) {
            throw new RuntimeException("文件上传出现异常", e);
        }
    }

    /**
     * 获取年月日[2020/09/01]
     *
     * @return
     */
    private static String getDateFolder() {
        Date d = new Date();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
        return sdf.format(d);
    }

    /**
     * 获取文件后缀
     *
     * @param fileName
     * @return
     */
    protected static String getSuffix(String fileName) {
        int index = fileName.lastIndexOf(".");
        if (index != -1) {
            String suffix = fileName.substring(index + 1);
            if (!suffix.isEmpty()) {
                return suffix;
            }
        }
        throw new IllegalArgumentException("非法文件名称:" + fileName);
    }

    /**
     * @Description 获取自定义桶的网关地址
     * @Param [bucketName]
     */
    private String getGateway(String bucketName) {
        String tempEndpoint = minioConfig.endpoint;
        if (!tempEndpoint.endsWith(URI_DELIMITER)) {
            tempEndpoint += URI_DELIMITER;
        }
        return tempEndpoint + bucketName + URI_DELIMITER;
    }

    /**
     * @Description 构造上传文件的上传路径
     * @Param [fileType 文件扩展名]
     */
    private String setUpdaloadFilePath(String fileType){
        if(StringUtils.isBlank(fileType)){
            fileType = "";
        }
        // UUID重命名
        String fileName = UUID.randomUUID().toString().replace("-", "") + "." + fileType;
        // 年/月/日/file
        String finalPath = new StringBuilder()
                .append(getDateFolder())
                .append(URI_DELIMITER)
                .append(fileName).toString();
        return finalPath;
    }
}

controller层:


@RestController
@RequestMapping("/fileService")
public class FileController {

    private final Logger logger = LoggerFactory.getLogger(FileController.class);

    @Autowired
    private FileServiceImpl fileService;

    /**
     * @Description 上传文件
     * @Param [file]
     */
    @PostMapping(value = "/uploadFile", produces = {MediaType.APPLICATION_JSON_UTF8_VALUE},consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public String uploadFile(@RequestPart(value = "file") MultipartFile file) {
        return fileService.uploadFile(file);
    }

    /**
     * @Description 自定义桶上传文件
     * @Param [bucketName, file]
     */
    @PostMapping(value = "/uploadFileByBucket", produces = {MediaType.APPLICATION_JSON_UTF8_VALUE},consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public String uploadFileByBucket(@RequestParam(value = "bucketName") String bucketName,@RequestPart(value = "file") MultipartFile file) {
        return fileService.uploadFileByBucket(bucketName, file);
    }


    /**
     * jsonObject包含参数
     * 1.bytes必填,为basr64编码后的byte数组,org.apache.commons.codec.binary.Base64.encodeBase64String
     * 2.fileType必填,上传文件的后缀扩展名,例如'pdf'
     * 3.contentType可为空,文件上传类型,例如‘application/pdf',如果该参数不存在,则工具类中寻
     * 4.bucketName可为空,桶名称为空则默认桶public
     */
    @PostMapping(value = "/uploadFileByByte")
    public String uploadFileByByte(@RequestBody JSONObject jsonObject) {
        String bytes = jsonObject.getString("bytes");
        String fileType = jsonObject.getString("fileType");
        String contentType = jsonObject.getString("contentType");
        String bucketName = jsonObject.getString("bucketName");
        InputStream input = new ByteArrayInputStream(Base64.decodeBase64(bytes));
        // 如果没有contentType,则在工具类中寻找
        if(StringUtils.isBlank(contentType)){
            ContentInfo info = ContentInfoUtil.findExtensionMatch(fileType);
            if(info == null){
                logger.error("未找到fileType:{},对应的contentType,文件上传失败",fileType);
                throw new IllegalArgumentException("未找到fileType:"+fileType+",对应的contentType,文件上传失败");
            }
            contentType = info.getMimeType();
        }
        return fileService.uploadFileByByte(bucketName,input, fileType,contentType);
    }


    /**
     * @Description 下载文件返回字节数组
     * @Param [filePath]
     */
    @GetMapping("/readFile")
    public byte[] readFile(@RequestParam String filePath) {
        return fileService.downloadFile(filePath);
    }
}

重点内容说明:
1.接口注解中指定MediaType

produces = {MediaType.APPLICATION_JSON_UTF8_VALUE},consumes = MediaType.MULTIPART_FORM_DATA_VALUE

2.文件对象参数使用注解

@RequestPart

3.接口uploadFileByByte,通过文件字节数组上传文件,需要参数fileType文件类型与contentType。通过文件类型得到文件的contentType,例如pdf文件的contentType为application/pdf,推荐一个工具包

		<dependency>
            <groupId>com.j256.simplemagic</groupId>
            <artifactId>simplemagic</artifactId>
            <version>1.16</version>
        </dependency>

通过以下方法即可得到:

ContentInfo info = ContentInfoUtil.findExtensionMatch(fileType);
String contentType = info.getMimeType();

客户端
feign配置:

@FeignClient(value = "file-service-api", configuration = FileService.MultipartSupportConfig.class)
public interface FileService {

    @PostMapping(value = "/fileService/uploadFile", produces = {MediaType.APPLICATION_JSON_UTF8_VALUE},consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    String uploadFile(@RequestPart(value = "file") MultipartFile file);

    @PostMapping(value = "/fileService/uploadFileByBucket", produces = {MediaType.APPLICATION_JSON_UTF8_VALUE},consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    String uploadFileByBucket(@RequestParam(value = "bucketName") String bucketName,@RequestPart(value = "file") MultipartFile file);

    @PostMapping(value = "/fileService/uploadFileByByte")
    String uploadFileByByte(@RequestBody JSONObject jsonObject);

    @GetMapping("/fileService/readFile")
    byte[] readFile(@RequestParam String filePath);

    @Configuration
    class MultipartSupportConfig  {
        @Autowired
        private ObjectFactory<HttpMessageConverters> messageConverters;

        @Bean
        @Primary
        @Scope("prototype")
        public Encoder feignFormEncoder() {
            return new SpringFormEncoder(new SpringEncoder(messageConverters));
        }
    }
}

重点内容说明:
1.接口注解中指定MediaType

produces = {MediaType.APPLICATION_JSON_UTF8_VALUE},consumes = MediaType.MULTIPART_FORM_DATA_VALUE

2.文件对象参数使用注解

@RequestPart

3.feign本身不支持文件传输,需要配置Configuration中的内容才可支持,但仅支持单个文件传输。

注意事项
1.配置文件中,其中minio.endpoint配置minio的地址域名,结合nginx使用,注意nginx配置只能配置root目录,即匹配根路径。附nginx配置示例:

events {
    worker_connections  1024;
}
http {
client_max_body_size       20m;
include mime.types;
default_type application/octet-stream;

upstream minio-server {
	server 192.168.xxx.xxx:9000 weight=25 max_fails=2 fail_timeout=30s;
	server 192.168.xxx.xxx:9000 weight=25 max_fails=2 fail_timeout=30s;
	server 192.168.xxx.xxx:9000 weight=25 max_fails=2 fail_timeout=30s;
	server 192.168.xxx.xxx:9000 weight=25 max_fails=2 fail_timeout=30s;
}

server {
    listen       80;
    server_name  localhost;

    charset utf-8;

    location /{
		proxy_set_header Host $http_host;
		proxy_set_header X-Forwarded-For $remote_addr;
		client_body_buffer_size 10M;
		client_max_body_size 1G;
		proxy_buffers 1024 4k;
		proxy_read_timeout 300;
		proxy_next_upstream error timeout http_404;
		proxy_pass http://minio-server;
	}

}
}

2.文件服务接口返回的文件链接地址,是不带时效性的,即永久可用。需要修改minio服务器桶策略,可读可写。否则文件链接最大时效性为7天。
在这里插入图片描述
3.安全方面考虑,正式环境需要关闭minio的web端管理,在minio启动脚本中指定参数即可:

export MINIO_BROWSER=off

但是为了方便运维人员,还是需要web管理端,所以可以通过网关的形式访问,启动对应的网关服务即可,这样即使关闭了web管理端,但是运维人员仍然可以通过网关地址进行访问。

4.minio对于大于5MB的文件上传会自动进行分片处理,对于客户端是无感的,若需要前端进行分片上传,可使用tus协议或uppy文件上传组件处理。

参考文档:
http://docs.minio.org.cn/docs

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值