重学SpringBoot系列之整合分布式文件系统
Posted 大忽悠爱忽悠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了重学SpringBoot系列之整合分布式文件系统相关的知识,希望对你有一定的参考价值。
重学SpringBoot系列之整合分布式文件系统
文件本地上传与提供访问服务
本章的核心内容是为大家介绍分布式文件系统,用于存储应用的图片、word、excel、pdf等文件。在开始介绍分布式文件系统之前,为大家介绍一下使用本机存储来存放文件资源。
二者的核心实现过程是一样的:
上传文件,保存文件(本节是本地磁盘)
返回文件HTTP访问服务路径给前端,进行上传之后的效果展示
复习
服务端接收上传的目的是提供文件的访问服务,那么对于SpringBoot而言,有哪些可以提供文件访问的静态资源目录呢?
- classpath:/META-INF/resources/ ,
- classpath:/static/ ,
- classpath:/public/ ,
- classpath:/resources/
这是之前的章节,我们为大家介绍的内容,从这里看出这里的静态资源都在classpath下。那么就出现问题:
- 应用的文件资源不能和项目代码分开存储(你见过往github上传代码,还附带项目文件数据的么?)
- 项目打包困难,当上传的文件越来越多,项目的打包jar越来越大。
- 代码与文件数据不能分开存储,就意味着文件数据的备份将变得复杂
文件上传目录自定义配置
怎么解决上述问题?别忘记了spring boot 为我们提供了使用spring.resources.static-locations配置自定义静态文件的位置。
web:
upload-path: D:/data/
spring:
resources:
static-locations: classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/,file:$web.upload-path
- 配置
web.upload-path
为与项目代码分离的静态资源路径,即:文件上传保存根路径 - 配置
spring.resources.static-locations
,除了带上Spring Boot
默认的静态资源路径之外,加上file:$web.upload-path
指向外部的文件资源上传路径。该路径下的静态资源可以直接对外提供HTTP
访问服务。
spring.resources.static-locations的配置会覆盖springboot默认的四个静态资源配置
文件上传的Controller实现
详情看代码注释
@RestController
public class FileUploadController
//绑定文件上传路径到uploadPath
@Value("$web.upload-path")
private String uploadPath;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd/");
@PostMapping("/upload")
public String upload(MultipartFile uploadFile,
HttpServletRequest request) throws IOException
// 在 uploadPath 文件夹中通过日期对上传的文件归类保存
// 比如:/2019/06/06/cf13891e-4b95-4000-81eb-b6d70ae44930.png
String format = sdf.format(new Date());
File folder = new File(uploadPath + format);
if (!folder.isDirectory())
folder.mkdirs();
// 对上传的文件重命名,避免文件重名
String oldName = uploadFile.getOriginalFilename();
String newName = UUID.randomUUID().toString()
+ oldName.substring(oldName.lastIndexOf("."), oldName.length());
// 文件保存
uploadFile.transferTo(new File(folder, newName));
// 返回上传文件的访问路径
//https://localhost:8888/2020/10/18/a9a05df4-6615-4bb5-b859-a3f9bf4bfae0.jpg
//request.getScheme() 返回当前链接使用的协议;比如,一般应用返回http;SSL返回https;
//request.getServerName()可以返回当前页面所在的服务器的名字,就是上面例子中的“localhost"
String filePath = request.getScheme() + "://" + request.getServerName()
+ ":" + request.getServerPort() + "/" + format + newName;
return filePath;
写一个模拟的文件上传页面,进行测试
把该upload.html文件放到classpath:public目录下,对外提供访问。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="uploadFile" value="请选择上传文件">
<input type="submit" value="保存">
</form>
</body>
</html>
访问测试、点击“选择文件”,之后保存
文件被保存到服务端的web.upload-path指定的资源目录下
浏览器端响应结果如下,返回一个文件HTTP访问路径:
使用该HTTP访问路径,在浏览器端访问效果如下。证明我们的文件已经成功上传到服务端,以后需要访问该图片就通过这个HTTP URL就可以了。
MinIO简介与选型介绍
目前可用于文件存储的网络服务选择有很多,比如阿里云OSS、七牛云、腾讯云等等,但是收费都有点小贵。
为什么使用MInIO替换了FastDFS
MinIO 是一个基于Apache License v2.0开源协议的对象存储服务。它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。
理由一:安装部署(运维)复杂度
如果将一个fastDFS分布式服务部署完成,需要具备以下的知识
- linux基础的目录操作
- 常用的分布式主从原理
- C语言代码的编译
- nginx安装部署
- nginx插件的使用(防盗链)
如果仅仅是上面的这些基础知识,安排几个程序员学一学还好说。主要是fastdfs的部署结构之复杂,如果我长时间不回顾,自己都会忘了这复杂的架构是怎么回事。
当我看到MinIO的安装过程之后,以及分布式的部署命令之后(分布式MinIO快速入门),放弃fastDFS的决心就已经做出了一大半。
说白了:FastDFS的部署不过是零件的组装过程,需要你去理解fastDFS的架构设计,才能够正确的安装部署。MinIO在安装的过程是黑盒的,你不用去深入关注它的架构,也不需要你进行零件组装,基本上可以做到开箱即用。普通的技术人员就能够参与后期运维。
理由二:文档
我觉得从我知道fastDFS开始,也有十年了。竟然没有官方文档,所有的文档全是某某公司的自己总结的文档,或者是某某网友自己总结的文档。
从这点上看fastDFS真的是一败涂地,当然阿里余庆大神在做这个项目的时候可能也没有考虑到后来会有这么多人用。即使用的人多了,在余庆大神眼里可能觉得只是自己开发的一个小玩具,没有继续深入运营的必要。
理由三:开源项目运营组织
fastdfs是阿里余庆做的一个个人项目,在一些互联网创业公司中有应用,没有官网,不活跃,6个contributors。目前已经很少做更新。
MinIO目前是由2014年在硅谷创立的公司MinIO.Inc运营的开源项目,社区论坛的活跃度目前也非常的不错。
理由四:UI界面
我们都知道fastDFS默认是不带UI界面的,看看MinIO的界面吧。这个界面不需要你单独的部署,和服务端一并安装。开箱即用,爱了爱了。
理由五:性能
MinIO号称是世界上速度最快的对象存储服务器。在标准硬件上,对象存储的读/写速度最高可以达到183 GB/s和171 GB/s。关于fastDFS我曾经单线程测试写了20万个文件,总共200G,大约用时10个小时。总体上是很难达到MinIO“号称的”以G为单位的每秒读写速度。
理由六:容器化支持
MinIO提供了与k8s、etcd、docker等容器化技术深度集成方案,可以说就是为了云环境而生的。这点是FastDFS不具备的。
理由七:丰富的SDK支持
fastDFS目前提供了 C 和 Java SDK ,以及 php 扩展 SDK。下图是MinIO提供的SDK支持,MinIO几乎提供了所有主流开发语言的SDK以及文档。
理由八:AWS S3标准兼容
Amazon的S3 API是对象存储领域的事实标准。MinIO是S3兼容性的事实上的标准,是第一个采用API和第一个添加对S3 Select支持的标准之一。包括微软Azure在内的750多家公司使用MinIO的S3网关,这一数字超过了业内其他公司的总和。
什么意思?就是说你现在为了节约成本使用MinIO,等你的公司壮大了、有钱了。不想自己运维基础设施了,你就可以把对象存储放到云上,只要云厂商支持S3标准(比如阿里云OSS、七牛云等),你的应用程序是不需要重新开发的。
MinIO的安装与基础用法
MInIO在linux服务器上安装
MInIO的安装有很多方法、单实例的、集群分布式的、docker部署的、支持k8s的,我们这里只给大家介绍最简单的一种安装方式:linux单节点安装。
因为我们课程的主要目的不是为大家讲MinIO,我们的课程主要目的是在Spring Boot应用中集成MinIO的API,操作MInIO进行对象存储,也就是下2节的内容。
如果希望对MinIO有深入的掌握,访问MinIO官网:https://min.io
下载及准备工作
下载地址:https://min.io/download#/linux
二进制源码安装:
cd /root/minio //自定义一个minio软件操作目录
wget https://dl.min.io/server/minio/release/linux-amd64/minio
chmod +x minio
创建minio文件存储目录及日志目录
mkdir -p /usr/local/data/minio;
mkdir -p /usr/local/logs/minio;
docker安装minio
1.从Docker Hub查找镜像 minio镜像
docker search minio
能够查找到镜像 minio/minio 排在第一位
2.minio安装 (使用docker安装)
docker pull minio/minio
创建容器,并启动
docker run --name minio \\
-p 9000:9000 \\
-p 9090:9090 \\
-d --restart=always \\
-e "MINIO_ROOT_USER=admin" \\
-e "MINIO_ROOT_PASSWORD=admin123" \\
-v /usr/local/minio/data:/data \\
-v /usr/local/minio/config:/root/.minio \\
minio/minio server /data \\
--console-address '0.0.0.0:9090'
注意,这里要单独设置console的端口,不然会报错,且无法访问
这种安装方式 MinIO 自定义 Access 和 Secret 密钥要覆盖 MinIO 的自动生成的密钥
登录客户端(浏览器):注意—>此处的端口,是你设置的console的端口:9090
此处的用户名密码为启动服务时,设置的用户名密码:admin admin123:
docker安装minio可以参考最新版本的官方文档
启动MinIO
将下面的内容保存为**/root/minio/run.sh**,与minio下载文件放在同一个目录下面。并为其赋予执行权限chmod u+x run.sh
#!/bin/bash
export MINIO_ACCESS_KEY=dhy
export MINIO_SECRET_KEY=123456
# nohup启动服务 指定文件存放路径 /root/data 还有设置日志文件路径 /root/minio/log
nohup ./minio server /root/data/minio > /root/logs/minio/minio.log 2>&1 &
开启防火请端口,对外提供服务
minio默认的服务端口是9000,需要开放防火墙端口。下面的命令是CentOS7的防火墙端口开放命令:
firewall-cmd --zone=public --add-port=9000/tcp --permanent
firewall-cmd --reload
# 查看是否开放
firewall-cmd --query-port=9000/tcp
访问 http://虚拟机ip:9000/ 进行登录。下图是MinIO的登陆界面
登陆的用户名和密码,使用MINIO_ACCESS_KEY和MINIO_SECRET_KEY的配置值。登录成功之后的首页
MinIO系统的基本用法
创建 bucket
登录之后在浏览器上面,点击右下角“红色的加号”创建 bucket 来存储对象。我们要了解什么是bucket,说白了就是上传的对象文件的分类
- 你可以按这个图片或者其他资源文件属于哪个系统创建一个bucket,比如说我创建一个boot-launch的bucket给boot-launch应用使用
- 你也可以按照资源的类型去创建bucket,比如:image,video,audio分别放在不同的bucket中存放
上传资源
bucket 创建好之后,我们就可以向这个bucket里面上传资源文件对象了。点击下图中的按钮
比如我上传了一张png的图片,上传后列表内展示效果如下:
在资源后面的四个按钮分别是:资源分享、预览、下载、删除。
资源分享
MinIO 默认的策略是分享地址的有效时间最多是7天。我们点击Copy link可以获得资源的访问链接
永久资源分享
MinIO 默认的策略是分享地址的有效时间最多是7天,要突破这种限制,可以在 bucket 中进行策略设置。
点击对应的 bucket ,edit policy
添加策略*.*,Read Only
,如下:
如此就放开了访问,并且没有时间限制,同时只需要按http://$MINIO_HOST:$MINIO_PORT/$bucketName/$fileName
的格式可直接访问资源(不需要进行分享操作)。
在 html 文件中引用静态资源
通过上面的设置与运行,MinIO 作为静态资源服务器已经完成,可以写个 html 来引用 MinIO 中的静态资源。如下是测试的 html 里面的图片、视频、音频均使用 MinIO 的资源地址。
格式:
<img src="http://$MINIO_HOST:$MINIO_PORT/image/test.jpg" alt="图片">
实例:
<img src="http://192.168.161.3:9000/boot-launch/java9-streamapi.png" alt="图片">
整合MinIO的JavaSDK
整合MinIO
pom.xml引入:
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>7.1.0</version>
</dependency>
application.yml,服务信息要和我们上一节安装的MinIO服务一致,这样我们才能正常连接测试:
# MinIo文件服务器
minio:
endpoint: http://服务器ip:9000
accessKey: dhy
secretKey: 123456
MinIoProperties.java 配置实体,将上文配置文件属性装载到实体配置对象中:
@Data
@ConfigurationProperties(prefix = "minio")
public class MinIOProperties
private String endpoint;
private String accessKey;
private String secretKey;
写一个工具类,这个工具类只为大家演示了基础的API。更多的API请参考官方文档:https://docs.min.io/cn/java-client-api-reference.html
@Component
@Configuration
@EnableConfigurationProperties(MinIOProperties.class)
public class MinIOTemplate
private MinIOProperties minIo;
public MinIOTemplate(MinIOProperties minIo)
this.minIo = minIo;
private MinioClient instance;
@PostConstruct //minio操作对象实例化
public void init()
instance = MinioClient.builder()
.endpoint(minIo.getEndpoint())
.credentials(minIo.getAccessKey(), minIo.getSecretKey())
.build();
/**
* 判断 bucket是否存在
*/
public boolean bucketExists(String bucketName)
throws IOException, InvalidKeyException, InvalidResponseException,
InsufficientDataException, NoSuchAlgorithmException,
ServerException, InternalException, XmlParserException,
InvalidBucketNameException, ErrorResponseException
return instance.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
/**
* 创建 bucket
*/
public void makeBucket(String bucketName) throws IOException, InvalidResponseException,
InvalidKeyException, NoSuchAlgorithmException, ServerException,
ErrorResponseException, XmlParserException, InvalidBucketNameException,
InsufficientDataException, InternalException, RegionConflictException
boolean isExist = this.bucketExists(bucketName);
if(!isExist)
instance.makeBucket(MakeBucketArgs.builder()
.bucket(bucketName)
.build());
/**
* 文件上传
* @param bucketName bucket名称
* @param objectName 对象名称,文件名称
* @param filepath 文件路径
*/
public ObjectWriteResponse putObject(String bucketName, String objectName, String filepath)
throws IOException, InvalidKeyException, InvalidResponseException,
InsufficientDataException, NoSuchAlgorithmException, ServerException,
InternalException, XmlParserException, InvalidBucketNameException, ErrorResponseException
return instance.uploadObject(
UploadObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.filename(filepath).build());
/**
* 文件上传
* @param bucketName bucket名称
* @param objectName 对象名称,文件名称
* @param inputStream 文件输入流
*/
public ObjectWriteResponse putObject(String bucketName, String objectName, InputStream inputStream)
throws IOException, InvalidKeyException, InvalidResponseException,
InsufficientDataException, NoSuchAlgorithmException, ServerException,
InternalException, XmlParserException, InvalidBucketNameException, ErrorResponseException
return instance.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName).stream(
inputStream, -1, 10485760)
.build());
/**
* 删除文件
* @param bucketName bucket名称
* @param objectName 对象名称
*/
public void removeObject(String bucketName, String objectName)
throws IOException, InvalidKeyException, InvalidResponseException, InsufficientDataException,
NoSuchAlgorithmException, ServerException, InternalException, XmlParserException,
InvalidBucketNameException, ErrorResponseException
instance.removeObject(RemoveObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build());
测试
@SpringBootTest
public class MinIOTest
@Resource
MinIOTemplate minTemplate;
//测试创建bucket
@Test
void testCreateBucket() throws Exception
minTemplate.makeBucket("test");
//测试上传文件对象
@Test
void testPutObject() throws Exception
ObjectWriteResponse response = minTemplate.putObject("test",
"base/dhy.png",
"C:\\\\Users\\\\a\\\\Pictures\\\\dhy.png");
System.out.println(response.object());
//测试删除文件对象
@Test
void testDeleteObject() throws Exception
minTemplate.removeObject("test",
"base/dhy.png");
第一个测试用例创建一个名称为test的bucket
第二个测试用例上传了一个图片文件对象“base/dh
以上是关于重学SpringBoot系列之整合分布式文件系统的主要内容,如果未能解决你的问题,请参考以下文章