Minio结合SpringBoot搭建文件服务器
项目地址:宫静雨/microservice_spc - 码云 - 开源中国 (gitee.com)
子工程:minio
本文章先编写了项目代码,然后再配置部署Minio和SpringBoot服务。
如果有需要,可以先直接看使用Docker部署项目,先把Minio部署起来,docker-compose.yaml文件中把springboot项去掉就可以了。
新建SpringBoot项目并配置
配置pom.xml
由于我采用的是父子工程创建的,所以该pom.xml中没有SpringBoot版本。
主要是依赖(dependencies)和build,其他的可以自行选择。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>minio</artifactId>
<groupId>com.gjy</groupId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.2.1</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
<build>
<finalName>minio-service</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.gjy.minio.MinioApp</mainClass>
<layout>ZIP</layout>
<includeSystemScope>true</includeSystemScope>
<addResources>true</addResources>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
配置application.yml
这里的端口号和项目名可以改成自己的。minio的配置主要参考docker-compose.yaml中minio的配置。
server:
port: 18000
spring:
application:
name: minio-service
minio:
username: minio
password: minio123
address: http://192.168.200.162:9000
启动类MinioApp
package com.gjy.minio;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MinioApp {
public static void main(String[] args) {
SpringApplication.run(MinioApp.class, args);
}
}
添加配置类
MinioYaml
此类的作用是为了获取配置文件中的值。
package com.gjy.minio.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@ConfigurationProperties(prefix = "minio")
@Component
@Data
public class MinioYaml {
private String address;
private String username;
private String password;
}
MinioConfig
package com.gjy.minio.config;
import com.gjy.minio.properties.MinioYaml;
import io.minio.MinioClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
@Configuration
public class MinioConfig {
private static final Logger log = LoggerFactory.getLogger(MinioConfig.class);
@Resource
private MinioYaml minioYaml;
@Bean(name = "minio")
public MinioClient minio() {
log.info("minio yaml: {}", minioYaml);
return MinioClient.builder()
.endpoint(minioYaml.getAddress())
.credentials(minioYaml.getUsername(), minioYaml.getPassword())
.build();
}
}
添加实体类
先添加实体类,避免编写服务类的时候报错。
BucketInfo
package com.gjy.minio.domain;
import lombok.Data;
import java.io.Serializable;
import java.time.ZonedDateTime;
@Data
public class BucketInfo implements Serializable {
private String name;
private ZonedDateTime creationDate;
}
BucketConvert
package com.gjy.minio.convert;
import com.gjy.minio.domain.BucketInfo;
import io.minio.messages.Bucket;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
@Component(value = "bucketConvert")
public class BucketConvert {
public List<BucketInfo> convert(List<Bucket> bucket) {
return bucket.stream()
.map(b -> {
BucketInfo info = new BucketInfo();
info.setName(b.name());
info.setCreationDate(b.creationDate());
return info;
})
.collect(Collectors.toList());
}
}
AjaxResult
package com.gjy.minio.domain;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class AjaxResult implements Serializable {
private Integer code;
private String message;
private Object data;
}
FileInfo
package com.gjy.minio.domain;
import lombok.Data;
import java.io.Serializable;
@Data
public class FileInfo implements Serializable {
private String name;
private String url;
public FileInfo() {
}
public FileInfo(String name, String url) {
this.name = name;
this.url = url;
}
public static FileInfo empty() {
return new FileInfo("", "");
}
}
FileItem
package com.gjy.minio.domain;
import lombok.Data;
import java.io.Serializable;
@Data
public class FileItem implements Serializable {
private String name;
public FileItem() {
}
public FileItem(String name) {
this.name = name;
}
}
BucketRequest
package com.gjy.minio.domain.vo;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
@Data
public class BucketRequest implements Serializable {
@NotBlank
private String bucket;
@NotBlank
private String operation;
private String type;
}
BucketResponse
package com.gjy.minio.domain.vo;
import lombok.Data;
import java.io.Serializable;
@Data
public class BucketResponse implements Serializable {
private Object result;
}
添加功能类
MinioService
package com.gjy.minio.service;
import com.gjy.minio.domain.BucketInfo;
import com.gjy.minio.domain.FileInfo;
import com.gjy.minio.domain.FileItem;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
public interface MinioService {
boolean existsBucket(String bucket);
boolean createBucket(String bucket, String type);
boolean deleteBucket(String bucket);
boolean deleteBucketForce(String bucket);
List<BucketInfo> getBuckets();
FileInfo uploadFile(String bucket, MultipartFile file);
List<FileInfo> uploadFile(String bucket, List<MultipartFile> files);
void downloadFile(String bucket, String filename, HttpServletResponse response);
FileInfo previewFile(String bucket, String filename);
boolean deleteFile(String bucket, String filename);
List<FileItem> getFiles(String bucket);
}
MinioServiceImpl
package com.gjy.minio.service.imp;
import com.gjy.minio.convert.BucketConvert;
import com.gjy.minio.domain.BucketInfo;
import com.gjy.minio.domain.FileInfo;
import com.gjy.minio.domain.FileItem;
import com.gjy.minio.service.MinioService;
import com.google.common.collect.Lists;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Item;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
public class MinioServiceImpl implements MinioService {
private static final Logger log = LoggerFactory.getLogger(MinioServiceImpl.class);
private static final String BUCKET_PARAM = "${bucket}";
/**
* bucket权限-读写, 可以生产永久可访问的路径
*/
private static final String READ_WRITE = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:GetBucketLocation\",\"s3:ListBucket\",\"s3:ListBucketMultipartUploads\"],\"Resource\":[\"arn:aws:s3:::" + BUCKET_PARAM + "\"]},{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:DeleteObject\",\"s3:GetObject\",\"s3:ListMultipartUploadParts\",\"s3:PutObject\",\"s3:AbortMultipartUpload\"],\"Resource\":[\"arn:aws:s3:::" + BUCKET_PARAM + "/*\"]}]}";
/**
* bucket权限-只读, 只能生成最多7天的临时访问路径
*/
private static final String WRITE_ONLY = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:GetBucketLocation\",\"s3:ListBucketMultipartUploads\"],\"Resource\":[\"arn:aws:s3:::" + BUCKET_PARAM + "\"]},{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:AbortMultipartUpload\",\"s3:DeleteObject\",\"s3:ListMultipartUploadParts\",\"s3:PutObject\"],\"Resource\":[\"arn:aws:s3:::" + BUCKET_PARAM + "/*\"]}]}";
@Resource
private MinioClient minio;
@Resource
private BucketConvert bucketConvert;
@Override
public boolean existsBucket(String bucket) {
try {
return minio.bucketExists(BucketExistsArgs.builder().bucket(bucket).build());
} catch (Exception e) {
log.info("判断bucket[{}] 失败: {}", bucket, e.getMessage());
return false;
}
}
@Override
public boolean createBucket(String bucket, String type) {
try {
minio.makeBucket(MakeBucketArgs.builder().bucket(bucket).build());
if (type.equals("READ_WRITE")) {
minio.setBucketPolicy(SetBucketPolicyArgs.builder()
.bucket(bucket)
.config(READ_WRITE.replace(BUCKET_PARAM, bucket))
.build());
} else {
minio.setBucketPolicy(SetBucketPolicyArgs.builder()
.bucket(bucket)
.config(WRITE_ONLY.replace(BUCKET_PARAM, bucket))
.build());
}
return true;
} catch (Exception e) {
log.info("创建bucket[{}] 失败: {}", bucket, e.getMessage());
return false;
}
}
@Override
public boolean deleteBucket(String bucket) {
try {
minio.removeBucket(RemoveBucketArgs.builder().bucket(bucket).build());
return true;
} catch (Exception e) {
log.info("删除bucket[{}] 失败: {}", bucket, e.getMessage());
return false;
}
}
@Override
public boolean deleteBucketForce(String bucket) {
try {
getFiles(bucket).stream().map(FileItem::getName).forEach(n -> {
try {
minio.removeObject(RemoveObjectArgs.builder().bucket(bucket).object(n).build());
} catch (Exception e) {
log.error("删除bucket[{}], 删除文件[{}], 失败: {}", bucket, n, e.getMessage());
}
});
minio.removeBucket(RemoveBucketArgs.builder().bucket(bucket).build());
return true;
} catch (Exception e) {
log.info("删除bucket[{}] 失败: {}", bucket, e.getMessage());
return false;
}
}
@Override
public List<BucketInfo> getBuckets() {
try {
return bucketConvert.convert(minio.listBuckets());
} catch (Exception e) {
log.error("获取bucket 失败: {}", e.getMessage());
return Lists.newArrayList();
}
}
@Override
public FileInfo uploadFile(String bucket, MultipartFile file) {
try {
InputStream inputStream = file.getInputStream();
String originalFilename = file.getOriginalFilename();
long fileSize = file.getSize();
String filename = UUID.randomUUID().toString().replaceAll("-", "");
minio.putObject(PutObjectArgs.builder().bucket(bucket).object(filename).stream(inputStream, fileSize, -1).contentType(file.getContentType()).build());
return previewFile(bucket, filename);
} catch (Exception e) {
return FileInfo.empty();
}
}
@Override
public List<FileInfo> uploadFile(String bucket, List<MultipartFile> files) {
return files.stream().map(f -> uploadFile(bucket, f)).collect(Collectors.toList());
}
@Override
public void downloadFile(String bucket, String filename, HttpServletResponse response) {
try {
GetObjectResponse object = minio.getObject(GetObjectArgs.builder()
.bucket(bucket)
.object(filename).build());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int len;
while ((len = object.read(buf)) != -1) {
baos.write(buf, 0, len);
}
baos.flush();
byte[] bytes = baos.toByteArray();
response.setHeader("Content-Disposition", "attachment;filename=" +
URLEncoder.encode(filename, StandardCharsets.UTF_8.name()));
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(bytes);
outputStream.flush();
outputStream.close();
baos.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public FileInfo previewFile(String bucket, String filename) {
try {
String url = minio.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.bucket(bucket).object(filename).method(Method.GET).build());
return new FileInfo(filename, url);
} catch (Exception e) {
log.error("{}", e.getMessage());
return FileInfo.empty();
}
}
@Override
public boolean deleteFile(String bucket, String filename) {
try {
minio.removeObject(RemoveObjectArgs.builder()
.bucket(bucket)
.object(filename).build());
return true;
} catch (Exception e) {
return false;
}
}
@Override
public List<FileItem> getFiles(String bucket) {
try {
Iterable<Result<Item>> results = minio.listObjects(ListObjectsArgs.builder().recursive(true).bucket(bucket).build());
List<FileItem> list = Lists.newArrayList();
results.forEach(r -> {
try {
Item item = r.get();
String name = item.objectName();
list.add(new FileItem(name));
} catch (Exception e) {
log.error("获取文件失败: {}", e.getMessage());
}
});
return list;
} catch (Exception e) {
return Lists.newArrayList();
}
}
}
MinioController
package com.gjy.minio.controller;
import com.gjy.minio.domain.AjaxResult;
import com.gjy.minio.domain.BucketInfo;
import com.gjy.minio.domain.FileInfo;
import com.gjy.minio.domain.vo.BucketRequest;
import com.gjy.minio.domain.vo.BucketResponse;
import com.gjy.minio.service.MinioService;
import com.google.common.collect.Lists;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
@RestController
@RequestMapping("/minio")
public class MinioController {
@Resource
private MinioService minioService;
private static final List<String> BUCKET_OPERATIONS = Lists.newArrayList("exists", "create", "delete", "delete_force", "get");
@PostMapping("/bucket")
public AjaxResult bucket(@RequestBody BucketRequest request) {
AjaxResult r;
BucketResponse response = new BucketResponse();
boolean bucket;
switch (request.getOperation()) {
case "exists":
bucket = minioService.existsBucket(request.getBucket());
response.setResult(bucket);
r = AjaxResult.builder().code(200).message("OK").data(response).build();
break;
case "create":
bucket = minioService.createBucket(request.getBucket(), request.getType());
response.setResult(bucket);
r = AjaxResult.builder().code(200).message("OK").data(response).build();
break;
case "delete":
bucket = minioService.deleteBucket(request.getBucket());
response.setResult(bucket);
r = AjaxResult.builder().code(200).message("OK").data(response).build();
break;
case "delete_force":
bucket = minioService.deleteBucketForce(request.getBucket());
response.setResult(bucket);
r = AjaxResult.builder().code(200).message("OK").data(response).build();
break;
case "get":
List<BucketInfo> buckets = minioService.getBuckets();
response.setResult(buckets);
r = AjaxResult.builder().code(200).message("OK").data(response).build();
break;
default:
r = AjaxResult.builder().code(400).message("参数错误,operator仅支持: " + BUCKET_OPERATIONS).build();
}
return r;
}
@PostMapping("/upload")
public AjaxResult uploadFile(@RequestPart String bucket,
@RequestPart MultipartFile file) {
FileInfo info = minioService.uploadFile(bucket, file);
return AjaxResult.builder().code(200).message("OK").data(info).build();
}
@PostMapping("/uploads")
public AjaxResult uploadFiles(@RequestPart String bucket,
@RequestPart List<MultipartFile> files) {
List<FileInfo> info = minioService.uploadFile(bucket, files);
return AjaxResult.builder().code(200).message("OK").data(info).build();
}
@PostMapping("/preview")
public AjaxResult previewFile(@RequestParam String bucket,
@RequestParam String filename) {
FileInfo info = minioService.previewFile(bucket, filename);
return AjaxResult.builder().code(200).message("OK").data(info).build();
}
@PostMapping("/delete")
public AjaxResult deleteFile(@RequestParam String bucket,
@RequestParam String filename) {
boolean info = minioService.deleteFile(bucket, filename);
return AjaxResult.builder().code(200).message("OK").data(info).build();
}
@PostMapping("/download")
public void downloadFile(@RequestParam String bucket,
@RequestParam String filename,
HttpServletResponse response) {
minioService.downloadFile(bucket, filename, response);
}
}
使用Docker部署项目
编写Dockerfile
此处的minio-service.jar是在pom.xml中build的finalName配置的,可以更改为自己喜欢的名称。
FROM openjdk:8
ADD minio-service.jar /minio-service.jar
EXPOSE 18000
ENTRYPOINT ["java","-jar","/minio-service.jar"]
编写docker-compose.yaml
version: '3.8'
services:
minio:
image: minio/minio
ports:
- "9000:9000"
- "9090:9090"
volumes:
- /data/minio:/data
environment:
- MINIO_ACCESS_KEY=minio
- MINIO_SECRET_KEY=minio123
command: server /data --console-address ":9090" -address ":9000"
springboot:
build:
context: .
dockerfile: Dockerfile
ports:
- "18000:18000"
depends_on:
- minio
执行
-
将Dockerfile,docker-compose.yaml,minio-service.jar上传至服务器
-
执行命令启动项目
// 后台运行
docker-compose up -d
-
停止项目
docker-compsoe down
-
如果想改了SpringBoot项目文件,需要先删除minio-server镜像
docker rmi minio-service-springboot:latest
-
不用担心minio服务关掉会丢失数据,前提是在服务器新建目录 /data/minio
mkdir -p /data/minio
测试用例
可以在Gitee上下载查看
minio/minio-service-test.html · 宫静雨/microservice_spc - 码云 - 开源中国 (gitee.com)
附录
虚拟机安装CentOS
可以参考这篇文章