乐趣区

关于java:一个简单易用的文件上传方案

当初 OSS 服务算是一个根底服务了,很多云服务厂商都有提供这样的服务,价格也不贵,松哥本人的 www.javaboy.org 用的就是相似的服务。

不过对于中小公司来说,除了购买 OSS 服务之外,也能够本人搭建业余的文件服务器,本人搭建专门的文件服务器的话,已经比拟业余的做法是 FastDFS,松哥之前也专门为之录过视频发在 B 站上,感兴趣的小伙伴能够自行查看。不过 FastDFS 搭建比拟麻烦,非常容易出错,所以对各位小伙伴来说多多少少有一点门槛。

松哥在之前的文章录制的一些我的项目视频中,如果波及到文件上传,基本上都是保留在我的项目本地,这种形式比拟省事,然而安全性不高。

所以,明天给大伙介绍一个较好的玩意 MinIO,看看这个工具带给咱们什么惊喜。

1. MinIO 简介

MinIO 是一个基于 Apache License v2.0 开源协定的对象存储服务,它兼容亚马逊 S3 云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器 / 虚拟机镜像等,而一个对象文件能够是任意大小,从几 KB 到最大 5T 不等。

MinIO 是一个十分轻量的服务,能够很简略的和其余利用的联合,相似 NodeJS, Redis 或者 MySQL。

简略来说,能够应用 MinIO 来搭建一个对象存储服务,而且 MinIO 的 Java 客户端和亚马逊的 S3 云存储服务客户端接口兼容,换句话说,你会往 MinIO 上存数据,就会往 S3 上存数据。

MinIO 的特点:

  1. 兼容 Amazon S3:能够应用 MinIO SDK,MinIO Client,AWS SDK 和 AWS CLI 拜访 MinIO 服务器。
  2. 较强的数据保护能力:MinIO 应用 Minio Erasure Code 来避免硬件故障。
  3. 高度可用:MinIO 服务器能够容忍分布式设置中高达 (N/2)-1 节点故障。
  4. 反对 Lambda 计算。
  5. 具备加密和防篡改性能:MinIO 为加密数据提供了机密性,完整性和真实性保障,而且性能开销微不足道。应用 AES-256-GCM,ChaCha20-Poly1305 和 AES-CBC 反对服务器端和客户端加密。
  6. 可对接后端存储:除了 MinIO 本人的文件系统,还反对 DAS、JBODs、NAS、Google 云存储和 Azure Blob 存储。

2. MinIO 装置

不废话了,连忙装一个体验一把吧。

为了省事,咱们就间接用 docker 来装置吧,如果你对 docker 还不相熟,公众号后盾回复 docker 获取松哥的 docker 教程。

咱们执行如下命令,装置 MinIO:

docker run -p 9000:9000 -p 9001:9001 -d minio/minio server /data --console-address ":9000" --address ":9001"

这个启动命令中配置了两个端口:console-address 是后盾治理的网页端口;address 则是 API 通信端口。以下面的启动脚本为例,我的项目启动胜利后,网页上的拜访端口是 9000,如果咱们通过 Java 代码上传文件,通信端口则是 9001。

我的项目启动胜利后,浏览器地址栏输出 http://127.0.0.1:9000/login 即可拜访到 MinIO 的后端页面:

默认的登录用户名和明码均为 minioadmin

登录胜利之后,咱们首先创立一个 bucket,未来咱们上传的文件都处于 bucket 之中,如下:

创立胜利之后,咱们还须要设置一下桶的读取权限,确保文件未来上传胜利之后能够读取到,点击左上角的设置按钮进行设置,如下:

设置实现后,接下来咱们就能够往这个桶中上传资源了,如下图:

上传实现后,就能够看到刚刚上传的文件了:

上传胜利后,点击文件,而后点击左边的 Share 按钮会弹出来文件的拜访链接,因为咱们曾经设置了文件可读,因而能够不必管这里的链接有效期了,间接通过门路的后面局部就能够拜访到刚刚上传的图片了,如下:

当初文件就可上传可拜访了。是不是比 FastDFS 容易多了!

不过后面这种装置形式其实有点小问题,因为咱们没有为 docker 容器设置数据卷,所以如果你把 docker 容器不小心删除了,那么数据也就没了!

所以咱们要设置数据卷。

修改后的 docker 脚本如下:

docker run -p 9000:9000 -p 9001:9001 -d --name minio -v /Users/sang/minio/data:/data -v /Users/sang/minio/config:/root/.minio -e "MINIO_ROOT_USER=javaboy" -e "MINIO_ROOT_PASSWORD=123@45678" minio/minio server /data --console-address ":9000" --address ":9001"

次要是加了数据卷映射性能,将 MinIO 的数据和配置文件映射到宿主机上, 这样未来即便容器删除了,数据也都还在。

留神下面也自定义了登录用户名和明码。

依照下面的命令,从新创立容器之后,咱们也创立一个桶并上传文件,上传胜利之后,咱们就能够在本地对应的文件夹看到咱们上传的文件,如下:

3. 整合 Spring Boot

接下来咱们再来看看在 Spring Boot 中如何玩 MinIO。

首先咱们创立一个 Spring Boot 我的项目,引入 Web 依赖,如下:

我的项目创立胜利之后,咱们再来手动增加一下 MinIO 的依赖,如下:

<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.2.1</version>
</dependency>

这里我尝试用了最新的版本,然而仿佛有一些 BUG,我也没有深究,就换了 8.2.1 这个版本,这个版本是 OK 的。

接下来咱们来配置一下 application.yaml,配置一下文件上传所须要的根本信息:

minio:
  endpoint: http://localhost:9001
  accessKey: javaboy
  secretKey: 123@45678
  nginxHost: http://local.javaboy.org:9001

这里四个属性:

  1. endpoint:这是 MinIO 的 API 通信地址。
  2. accessKey 和 secretKey 是通信的用户名和明码,这跟网页上登录时候的用户名明码统一。
  3. nginxHost:这个配置用来生成上传文件的拜访门路。对于这个门路,有的小伙伴可能会有疑难,nginxHost 不就是 endpoint 吗?为什么还要独自配置?因为对于文件服务器而言,咱们上传文件是通过 MinIO,然而拜访的时候不肯定通过 MinIO,咱们可能会本人搭建一个 Nginx 服务器,通过 Nginx 服务器来拜访上传后的资源,大家晓得 Nginx 十分擅长于做这个事件,效率十分高。所以这里的 nginxHost 其实是指 Nginx 的拜访门路。

接下来咱们提供一个 MinioProperties 来接管这里的四个属性,如下:

@ConfigurationProperties(prefix = "minio")
public class MinioProperties {
    /**
     * 连贯地址
     */
    private String endpoint;
    /**
     * 用户名
     */
    private String accessKey;
    /**
     * 明码
     */
    private String secretKey;
    /**
     * 域名
     */
    private String nginxHost;

    public String getEndpoint() {return endpoint;}

    public void setEndpoint(String endpoint) {this.endpoint = endpoint;}

    public String getAccessKey() {return accessKey;}

    public void setAccessKey(String accessKey) {this.accessKey = accessKey;}

    public String getSecretKey() {return secretKey;}

    public void setSecretKey(String secretKey) {this.secretKey = secretKey;}

    public String getNginxHost() {return nginxHost;}

    public void setNginxHost(String nginxHost) {this.nginxHost = nginxHost;}
}

将 application.yaml 中相干的配置注入到这个配置类中来。

接下来咱们须要提供一个 MinIOClient,通过这个客户端工具能够操作 MinIO,如下:

@Configuration
@EnableConfigurationProperties(MinioProperties.class)
public class MinioConfig {

    @Autowired
    private MinioProperties minioProperties;

    /**
     * 获取 MinioClient
     */
    @Bean
    public MinioClient minioClient() {return MinioClient.builder()
                .endpoint(minioProperties.getEndpoint())
                .credentials(minioProperties.getAccessKey(), minioProperties.getSecretKey())
                .build();}

}

这个也没啥好说的,传入通信地址以及用户名明码,就能够构建出一个 MinioClient 进去。

当文件上传胜利之后,咱们能够通过 MinIO 去拜访,也能够通过 Nginx 拜访,所以接下来咱们就须要提供一个类,来封装这两个地址:

public class UploadResponse {
    private String minIoUrl;

    private String nginxUrl;

    public UploadResponse() {}

    public UploadResponse(String minIoUrl, String nginxUrl) {
        this.minIoUrl = minIoUrl;
        this.nginxUrl = nginxUrl;
    }

    public String getMinIoUrl() {return minIoUrl;}

    public void setMinIoUrl(String minIoUrl) {this.minIoUrl = minIoUrl;}

    public String getNginxUrl() {return nginxUrl;}

    public void setNginxUrl(String nginxUrl) {this.nginxUrl = nginxUrl;}
}

再来提供一个 MinIO 文件上传工具类:

@Component
public class MinioUtil {

    @Autowired
    private MinioProperties minioProperties;

    @Autowired
    private MinioClient client;

    /**
     * 创立 bucket
     */
    public void createBucket(String bucketName) throws Exception {if (!client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {client.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        }
    }

    /**
     * 上传文件
     */
    public UploadResponse uploadFile(MultipartFile file, String bucketName) throws Exception {
        // 判断文件是否为空
        if (null == file || 0 == file.getSize()) {return null;}
        // 判断存储桶是否存在  不存在则创立
        createBucket(bucketName);
        // 文件名
        String originalFilename = file.getOriginalFilename();
        // 新的文件名 = 存储桶文件名_工夫戳. 后缀名
        assert originalFilename != null;
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
        String fileName = bucketName + "_" +
                System.currentTimeMillis() + "_" + format.format(new Date()) + "_" + new Random().nextInt(1000) +
                originalFilename.substring(originalFilename.lastIndexOf("."));
        // 开始上传
        client.putObject(PutObjectArgs.builder().bucket(bucketName).object(fileName).stream(file.getInputStream(), file.getSize(), -1)
                        .contentType(file.getContentType())
                        .build());
        String url = minioProperties.getEndpoint() + "/" + bucketName + "/" + fileName;
        String urlHost = minioProperties.getNginxHost() + "/" + bucketName + "/" + fileName;
        return new UploadResponse(url, urlHost);
    }

    /**
     * 获取全副 bucket
     *
     * @return
     */
    public List<Bucket> getAllBuckets() throws Exception {return client.listBuckets();
    }

    /**
     * 依据 bucketName 获取信息
     *
     * @param bucketName bucket 名称
     */
    public Optional<Bucket> getBucket(String bucketName) throws IOException, InvalidKeyException, NoSuchAlgorithmException, InsufficientDataException, InvalidResponseException, InternalException, ErrorResponseException, ServerException, XmlParserException, ServerException {return client.listBuckets().stream().filter(b -> b.name().equals(bucketName)).findFirst();}

    /**
     * 依据 bucketName 删除信息
     *
     * @param bucketName bucket 名称
     */
    public void removeBucket(String bucketName) throws Exception {client.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
    }

    /**
     * 获取⽂件外链
     *
     * @param bucketName bucket 名称
     * @param objectName ⽂件名称
     * @param expires    过期工夫 <=7
     * @return url
     */
    public String getObjectURL(String bucketName, String objectName, Integer expires) throws Exception {return client.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder().bucket(bucketName).object(objectName).expiry(expires).build());
    }

    /**
     * 获取⽂件
     *
     * @param bucketName bucket 名称
     * @param objectName ⽂件名称
     * @return ⼆进制流
     */
    public InputStream getObject(String bucketName, String objectName) throws Exception {return client.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
    }

    /**
     * 上传⽂件
     *
     * @param bucketName bucket 名称
     * @param objectName ⽂件名称
     * @param stream     ⽂件流
     * @throws Exception https://docs.minio.io/cn/java-client-api-reference.html#putObject
     */
    public void putObject(String bucketName, String objectName, InputStream stream) throws
            Exception {client.putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(stream, stream.available(), -1).contentType(objectName.substring(objectName.lastIndexOf("."))).build());
    }

    /**
     * 上传⽂件
     *
     * @param bucketName  bucket 名称
     * @param objectName  ⽂件名称
     * @param stream      ⽂件流
     * @param size        ⼤⼩
     * @param contextType 类型
     * @throws Exception https://docs.minio.io/cn/java-client-api-reference.html#putObject
     */
    public void putObject(String bucketName, String objectName, InputStream stream, long
            size, String contextType) throws Exception {client.putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(stream, size, -1).contentType(contextType).build());
    }

    /**
     * 获取⽂件信息
     *
     * @param bucketName bucket 名称
     * @param objectName ⽂件名称
     * @throws Exception https://docs.minio.io/cn/java-client-api-reference.html#statObject
     */
    public StatObjectResponse getObjectInfo(String bucketName, String objectName) throws Exception {return client.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
    }

    /**
     * 删除⽂件
     *
     * @param bucketName bucket 名称
     * @param objectName ⽂件名称
     * @throws Exception https://docs.minio.io/cn/java-client-apireference.html#removeObject
     */
    public void removeObject(String bucketName, String objectName) throws Exception {client.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build());
    }
}

都是一些惯例的 API 调用,我就不逐行解释了,接下来咱们来一个文件上传接口:

@RestController
public class FileUploadController {
    @Autowired
    MinioUtil minioUtil;

    @PostMapping("/upload")
    public String fileUpload(MultipartFile file) throws Exception {UploadResponse bucket01 = minioUtil.uploadFile(file, "bucket01");
        System.out.println("bucket01.getMinIoUrl() =" + bucket01.getMinIoUrl());
        System.out.println("bucket01.getNginxUrl() =" + bucket01.getNginxUrl());
        return bucket01.getMinIoUrl();}
}

好啦,功败垂成。

接下来启动 Spring Boot 我的项目,而后调用这个接口上传文件,上传胜利后,控制台会打印如下信息:

这就示意文件上传胜利了。

4. 配置 nginx

后面提到了 MinIO 能够联合 Nginx 来应用,那咱们这里就来配一配 Nginx 看看。

为了省事,Nginx 我也抉择装置到 docker 容器中,然而后面装置 MinIO 时,咱们曾经做了数据卷映射,即上传到 MinIO 的文件实际上是保留在宿主机的,所以当初也得给 Nginx 配置数据卷,未来让 Nginx 也去 /Users/sang/minio/data 门路下查找文件。

Nginx 装置指令如下:

docker run --name nginx01 -p 8888:80 -v /Users/sang/minio/data:/usr/share/nginx/html:ro -d nginx

这里两个关键点:

  1. 设置 Nginx 端口为 8888。
  2. 将 MinIO 映射到宿主机的数据卷,再次挂载到 Nginx 下来。

大家晓得,默认状况下,当咱们拜访 Nginx 的时候,Nginx 给咱们展现进去的数据其实就是 /usr/share/nginx/html 目录下的,当初该目录其实就相当于我宿主机的 /Users/sang/minio/data 目录,所以我当初都不必批改 Nginx 的配置了,装好之后间接应用 Nginx 即可。

好啦,接下来咱们批改一下 application.yaml,如下:

minio:
  endpoint: http://localhost:9001
  accessKey: javaboy
  secretKey: 123@45678
  nginxHost: http://local.javaboy.org:8888

改完之后,再次上传文件,此时打印进去的文件拜访门路如下:

当初咱们通过这个 Nginx 门路也能拜访到刚刚上传的文件了。

5. 小结

好啦,明天就和小伙伴们分享一下 MinIO 的用法,并联合 Nginx 搭建了一个简略的文件服务器,感兴趣的小伙伴能够试试哦。

公众号江南一点雨后盾回复 minio_demo,获取本文源码下载链接。

退出移动版