一、导入依赖
<!-- MinIO 客户端 -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.7</version>
</dependency>
<!-- OkHttp 是一个高效的网络库 -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.3</version>
</dependency>
二、添加yaml配置
server:
port: 3333
spring:
servlet:
multipart:
max-request-size: 200MB
max-file-size: 200MB
minio:
url: http://127.0.0.1:19000 #换成自己的minio服务端地址
accessKey: minioadmin # 用户名
secretKey: minioadmin # 密码
bucketName: demo # bucketName指的就是之前创建的MinIO桶Bucket
三、创建配置类
import io.minio.MinioClient;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @ClassName MinioConfig
* @Description 文件
* @Date 2024/9/21 10:21
*/
@Data
@Configuration
public class MinioConfig {
/** 访问地址 */
@Value("${minio.url}")
private String endpoint;
/** 唯一标识账户 */
@Value("${minio.accessKey}")
private String accessKey;
/** 账户密码 */
@Value("${minio.secretKey}")
private String secretKey;
/** 默认储存桶 */
@Value("${minio.bucketName}")
private String bucketName;
/**
* 标记此方法为一个 Bean,Spring 会在上下文中管理这个 Bean
*/
@Bean
public MinioClient minioClient(){
使用 MinioClient 的构建器模式创建一个 MinioClient 实例
return MinioClient.builder()
//设置 Minio 服务的端点地址
.endpoint(endpoint)
// 设置访问 Minio 服务所需的访问密钥和秘密密钥
.credentials(accessKey, secretKey)
// 构建并返回 MinioClient 实例
.build();
}
}
四、创建工具类
java
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import sun.misc.BASE64Decoder;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.*;
/**
* Minio工具类
*/
@Log4j2
@Component
@RequiredArgsConstructor
public class MinioUtils {
/**
* 定义一个私有的、不可变的 MinioClient 实例变量 minioClient
*/
private final MinioClient minioClient;
/**
* 启动SpringBoot容器的时候初始化Bucket(桶)
* 如果没有Bucket(桶)则创建
*
* @param bucketName Bucket(桶)名称
*/
@SneakyThrows(Exception.class)
private void createBucket(String bucketName) {
if (!bucketExists(bucketName)) {
//使用 minioClient 的 makeBucket 方法创建一个新的桶。
//MakeBucketArgs.builder().bucket(bucketName).build() 创建了一个请求参数对象,指定了要创建的桶的名称。
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
}
/**
* 判断Bucket(桶)是否存在
*
* @param bucketName Bucket(桶)名称
* @return true:存在 false:不存在
*/
@SneakyThrows(Exception.class)
public boolean bucketExists(String bucketName) {
//调用 minioClient 对象的 bucketExists 方法,检查指定的桶是否存在。
//BucketExistsArgs.builder().bucket(bucketName).build() 创建了一个请求参数对象,用于传递桶名称。
return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
}
/**
* 获得Bucket(桶)的策略
*
* @param bucketName Bucket(桶)名称
* @return 指定桶的访问策略
*/
@SneakyThrows(Exception.class)
public String getBucketPolicy(String bucketName) {
//通过 MinIO 客户端 API 获取指定桶的访问策略。
return minioClient.getBucketPolicy(
//使用构建者模式创建一个 GetBucketPolicyArgs 对象,以便设置请求参数。
GetBucketPolicyArgs
.builder()
//设置要获取策略的桶的名称。
.bucket(bucketName)
//构建并返回 GetBucketPolicyArgs 对象,该对象将作为参数传递给 getBucketPolicy 方法。
.build()
);
}
/**
* 获得所有Bucket(桶)列表
*
* @return 所有桶的列表
*/
@SneakyThrows(Exception.class)
public List<Bucket> getAllBuckets() {
//调用 MinIO 客户端的 listBuckets 方法,返回当前用户拥有的所有桶的列表。
return minioClient.listBuckets();
}
/**
* 根据bucketName获取其相关信息
*
* @param bucketName Bucket(桶)名称
* @return Optional<Bucket>
*/
@SneakyThrows(Exception.class)
public Optional<Bucket> getBucket(String bucketName) {
//查找并返回指定名称的桶,如果不存在,则返回 Optional.empty()。
//调用 getAllBuckets() 方法获取所有桶,然后使用 Java 8 的 Stream API 进行处理。
//filter 方法用于筛选出名称与 bucketName 匹配的桶。
//findFirst 方法用于返回第一个匹配的桶,返回值是一个 Optional<Bucket>,表示可能存在的桶。
return getAllBuckets().stream().filter(bucket -> bucket.name().equals(bucketName)).findFirst();
}
/**
* 根据bucketName删除Bucket(桶) true:删除成功 false:删除失败
*
* @param bucketName Bucket(桶)名称
*/
@SneakyThrows(Exception.class)
public void removeBucket(String bucketName) {
//调用 MinIO 客户端的 removeBucket 方法,通过构建 RemoveBucketArgs 对象来指定要删除的桶名。
minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
}
/**
* 判断文件是否存在
*
* @param bucketName Bucket(桶)名称
* @param objectName 文件名称
* @return true:存在 false:不存在
*/
public boolean isObjectExist(String bucketName, String objectName) {
boolean exist = true;
try {
//调用 MinIO 客户端的 statObject 方法,构建 StatObjectArgs 对象以检查对象的状态。如果对象存在,该方法不会抛出异常。
minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
} catch (Exception e) {
log.error("【Minio工具类】>>>> 判断文件是否存在, 异常:", e);
exist = false;
}
return exist;
}
/**
* 判断文件夹是否存在
*
* @param bucketName Bucket(桶)名称
* @param objectName 文件夹名称
* @return true:存在 false:不存在
*/
public boolean isFolderExit(String bucketName, String objectName) {
boolean exist = true;
try {
//使用 listObjects 方法列出指定桶中以 objectName 为前缀的对象。recursive(false) 表示不进行递归查找。
Iterable<Result<Item>> results = minioClient.listObjects(
ListObjectsArgs
.builder()
.bucket(bucketName)
.prefix(objectName)
.recursive(false)
.build()
);
//遍历结果,检查每个 Item 是否为目录且名称与 objectName 匹配。如果找到匹配的目录,将 exist 设为 true。
for (Result<Item> result : results) {
Item item = result.get();
if (item.isDir() && objectName.equals(item.objectName())) {
exist = true;
}
}
} catch (Exception e) {
log.error("【Minio工具类】>>>> 判断文件夹是否存在, 异常:", e);
exist = false;
}
return exist;
}
/**
* 根据文件前置查询文件
*
* @param bucketName 存储桶
* @param prefix 前缀
* @param recursive 是否使用递归查询
* @return MinioItem 列表
*/
@SneakyThrows(Exception.class)
public List<Item> getAllObjectsByPrefix(String bucketName,
String prefix,
boolean recursive) {
List<Item> list = new ArrayList<>();// 创建一个空的 Item 列表,用于存储结果
Iterable<Result<Item>> objectsIterator = minioClient.listObjects(// 获取指定桶中以 prefix 开头的对象列表
ListObjectsArgs.builder()// 构建 ListObjectsArgs 对象
.bucket(bucketName)// 设置桶名
.prefix(prefix)// 设置对象前缀
.recursive(recursive)// 设置是否递归查找
.build());// 构建参数
if (objectsIterator != null) { // 检查对象迭代器是否不为 null
for (Result<Item> o : objectsIterator) { // 遍历迭代器中的每个结果
Item item = o.get();// 获取当前结果中的 Item 对象
list.add(item);// 将 Item 添加到列表中
}
}
return list;// 返回包含所有找到的 Item 的列表
}
/**
* 获取文件流
*
* @param bucketName 存储桶
* @param objectName 文件名
* @return 二进制流
*/
@SneakyThrows(Exception.class)
public InputStream getObject(String bucketName, String objectName) {
return minioClient.getObject(// 调用 MinIO 客户端的 getObject 方法,获取指定对象的输入流
GetObjectArgs.builder()// 构建 GetObjectArgs 对象
.bucket(bucketName)// 设置桶名
.object(objectName)// 设置对象名
.build()); // 构建参数并调用 getObject
}
/**
* 断点下载
*
* @param bucketName 存储桶
* @param objectName 文件名称
* @param offset 起始字节的位置
* @param length 要读取的长度
* @return 二进制流
*/
@SneakyThrows(Exception.class)
public InputStream getObject(String bucketName, String objectName, long offset, long length) {
return minioClient.getObject(// 调用 MinIO 客户端的 getObject 方法,获取指定对象的输入流
GetObjectArgs.builder()// 构建 GetObjectArgs 对象
.bucket(bucketName) // 设置桶名
.object(objectName)// 设置对象名
.offset(offset)// 设置读取的起始偏移量
.length(length) // 设置要读取的字节长度
.build());// 构建参数并调用 getObject
}
/**
* 获取路径下文件列表
*
* @param bucketName 存储桶
* @param prefix 文件名称
* @param recursive 是否递归查找,false:模拟文件夹结构查找
* @return 二进制流
*/
public Iterable<Result<Item>> listObjects(String bucketName, String prefix, boolean recursive) {
return minioClient.listObjects(// 调用 MinIO 客户端的 listObjects 方法,返回对象列表
ListObjectsArgs.builder() // 构建 ListObjectsArgs 对象
.bucket(bucketName) // 设置桶名
.prefix(prefix)// 设置对象前缀
.recursive(recursive)// 设置是否递归查找
.build());// 构建参数并调用 listObjects
}
/**
* 使用MultipartFile进行文件上传
*
* @param bucketName 存储桶
* @param file 文件名
* @param objectName 对象名
* @param contentType 类型
* @return
*/
@SneakyThrows(Exception.class)
public ObjectWriteResponse uploadFile(String bucketName, MultipartFile file, String objectName, String contentType) {
InputStream inputStream = file.getInputStream(); // 获取文件的输入流,以便上传
return minioClient.putObject(// 调用 MinIO 客户端的 putObject 方法,上传文件
PutObjectArgs.builder()// 构建 PutObjectArgs 对象
.bucket(bucketName)// 设置桶名
.object(objectName)// 设置对象名
.contentType(contentType)// 设置文件的内容类型
.stream(inputStream, inputStream.available(), -1) // 设置输入流及其可用字节数,-1 表示不限制
.build());// 构建参数并调用 putObject
}
/**
* 图片上传
*
* @param bucketName 存储桶
* @param imageBase64 图像的 Base64 编码字符串
* @param imageName 接收图像的原始名称
* @return
*/
public ObjectWriteResponse uploadImage(String bucketName, String imageBase64, String imageName) {
if (!StringUtils.isEmpty(imageBase64)) {// 检查 Base64 字符串是否非空
InputStream in = base64ToInputStream(imageBase64);// 将 Base64 字符串转换为输入流
String newName = System.currentTimeMillis() + "_" + imageName + ".jpg";// 生成新文件名,包含当前时间戳和原始名称
String year = String.valueOf(new Date().getYear());// 获取当前年份
String month = String.valueOf(new Date().getMonth());// 获取当前月份
return uploadFile(bucketName, year + "/" + month + "/" + newName, in); // 调用 uploadFile 方法上传文件,路径为 年/月/新文件名
}
return null;// 如果 Base64 字符串为空,返回 null
}
// BASE64Decoder在jdk8以上的版本移除了,报错最简单解决换成jdk8就行了
public static InputStream base64ToInputStream(String base64) {
ByteArrayInputStream stream = null;// 声明 ByteArrayInputStream 变量,用于保存字节流
try {
// 使用 BASE64Decoder 解码 Base64 字符串并去除首尾空格,得到字节数组
byte[] bytes = new BASE64Decoder().decodeBuffer(base64.trim());
// 将字节数组转换为 ByteArrayInputStream,以便返回
stream = new ByteArrayInputStream(bytes);
} catch (Exception e) {
e.printStackTrace();
}
return stream;// 返回字节输入流,如果出错则返回 null
}
/**
* 上传本地文件
*
* @param bucketName 存储桶
* @param objectName 对象名称
* @param fileName 本地文件路径
* @return
*/
@SneakyThrows(Exception.class)
public ObjectWriteResponse uploadFile(String bucketName, String objectName, String fileName) {
return minioClient.uploadObject(// 调用 MinIO 客户端的 uploadObject 方法进行文件上传
UploadObjectArgs.builder()// 使用 UploadObjectArgs 构建上传参数
.bucket(bucketName)// 设置存储桶名称
.object(objectName)// 设置对象名称(在存储桶中的文件名)
.filename(fileName)// 设置要上传的本地文件名
.build()); // 构建并返回 UploadObjectArgs 对象,然后执行上传
}
/**
* 通过流上传文件
*
* @param bucketName 存储桶
* @param objectName 文件对象
* @param inputStream 文件流
* @return
*/
@SneakyThrows(Exception.class)
public ObjectWriteResponse uploadFile(String bucketName, String objectName, InputStream inputStream) {
return minioClient.putObject( // 调用 MinIO 客户端的 putObject 方法进行文件上传
PutObjectArgs.builder()// 使用 PutObjectArgs 构建上传参数
.bucket(bucketName)// 设置存储桶名称
.object(objectName)// 设置对象名称(在存储桶中的文件名)
.stream(inputStream, inputStream.available(), -1)// 设置输入流及其大小,-1 表示使用默认值
.build()); // 构建并返回 PutObjectArgs 对象,然后执行上传
}
/**
* 创建文件夹或目录
*
* @param bucketName 存储桶
* @param objectName 目录路径
* @return
*/
@SneakyThrows(Exception.class)
public ObjectWriteResponse createDir(String bucketName, String objectName) {
return minioClient.putObject(// 调用 MinIO 客户端的 putObject 方法进行上传
PutObjectArgs.builder()// 使用 PutObjectArgs 构建上传参数
.bucket(bucketName)// 设置存储桶名称
.object(objectName) // 设置对象名称(在存储桶中的"目录"名称)
.stream(new ByteArrayInputStream(new byte[]{}), 0, -1)// 使用空的字节数组流,表示创建一个空对象,0 表示长度,-1 表示使用默认值
.build());// 构建并返回 PutObjectArgs 对象,然后执行上传
}
/**
* 获取文件信息, 如果抛出异常则说明文件不存在
*
* @param bucketName 存储桶
* @param objectName 文件名称
* @return
*/
@SneakyThrows(Exception.class)
public String getFileStatusInfo(String bucketName, String objectName) {
return minioClient.statObject( // 调用 MinIO 客户端的 statObject 方法获取对象状态信息
StatObjectArgs.builder() // 使用 StatObjectArgs 构建请求参数
.bucket(bucketName) // 设置存储桶名称
.object(objectName)// 设置对象名称
.build() // 构建并返回 StatObjectArgs 对象
).toString();// 调用 toString 方法,将获取的状态信息转换为字符串并返回
}
/**
* 拷贝文件
*
* @param bucketName 存储桶
* @param objectName 文件名
* @param srcBucketName 目标存储桶
* @param srcObjectName 目标文件名
*/
@SneakyThrows(Exception.class)
public ObjectWriteResponse copyFile(String bucketName, String objectName, String srcBucketName, String srcObjectName) {
return minioClient.copyObject(// 调用 MinIO 客户端的 copyObject 方法进行对象复制
CopyObjectArgs.builder()// 使用 CopyObjectArgs 构建复制参数
.source(CopySource.builder()// 设置复制源
.bucket(bucketName) // 源存储桶名称
.object(objectName)// 源对象名称
.build())// 构建并返回 CopySource 对象
.bucket(srcBucketName)// 设置目标存储桶名称
.object(srcObjectName) // 设置目标对象名称
.build() // 构建并返回 CopyObjectArgs 对象
);// 执行对象复制并返回 ObjectWriteResponse
}
/**
* 删除文件
*
* @param bucketName 存储桶
* @param objectName 文件名称
*/
@SneakyThrows(Exception.class)
public void removeFile(String bucketName, String objectName) {
minioClient.removeObject(// 调用 MinIO 客户端的 removeObject 方法以删除指定对象
RemoveObjectArgs.builder()// 使用 RemoveObjectArgs 构建删除参数
.bucket(bucketName) // 设置要删除对象的存储桶名称
.object(objectName)// 设置要删除的对象名称
.build());// 构建并返回 RemoveObjectArgs 对象,并执行删除操作
}
/**
* 批量删除文件
*
* @param bucketName 存储桶
* @param keys 需要删除的文件列表
* @return
*/
public void removeFiles(String bucketName, List<String> keys) {
List<DeleteObject> objects = new LinkedList<>();// 创建一个 LinkedList 用于存储待删除对象
keys.forEach(s -> {// 遍历对象键列表
objects.add(new DeleteObject(s)); // 将每个键封装成 DeleteObject 并添加到列表中
try {
// 尝试执行删除操作
removeFile(bucketName, s);// 调用 removeFile 方法删除指定对象
} catch (Exception e) {
log.error("【Minio工具类】>>>> 批量删除文件,异常:", e);
}
});
}
/**
* 获取文件外链
* 使用了 .expiry(expires) 方法,指定了预签名 URL 的过期时间。
* 适用于需要限制 URL 有效期的场景。
* 适用于需要设定过期时间的情况
*
* @param bucketName 存储桶
* @param objectName 文件名
* @param expires 过期时间 <=7 秒 (外链有效时间(单位:秒))
* @return url
*/
@SneakyThrows(Exception.class)
public String getPresignedObjectUrl(String bucketName, String objectName, Integer expires) {
// 构建获取预签名对象 URL 的参数
GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()// 使用 GetPresignedObjectUrlArgs 的构建器
.expiry(expires)// 设置 URL 的过期时间(以秒为单位)
.bucket(bucketName) // 设置存储桶名称
.object(objectName)// 设置对象名称
.build();// 构建并返回 GetPresignedObjectUrlArgs 对象
return minioClient.getPresignedObjectUrl(args);// 调用 MinIO 客户端获取预签名 URL 并返回
}
/**
* 获得文件外链
* 使用了 .method(Method.GET) 方法,指定了 HTTP 请求方法为 GET。
* 适用于需要明确请求类型的场景,但没有设置过期时间。
* 适用于无需设定过期时间的访问
*
* @param bucketName
* @param objectName
* @return url
*/
@SneakyThrows(Exception.class)
public String getPresignedObjectUrl(String bucketName, String objectName) {
// 构建获取预签名对象 URL 的参数
GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()// 使用 GetPresignedObjectUrlArgs 的构建器
.bucket(bucketName) // 设置存储桶名称
.object(objectName)// 设置对象名称
.method(Method.GET)// 设置对象名称
.build();// 构建并返回 GetPresignedObjectUrlArgs 对象
return minioClient.getPresignedObjectUrl(args);// 调用 MinIO 客户端获取预签名 URL 并返回
}
/**
* 将URLDecoder编码转成UTF8
*
* @param str
* @return
* @throws UnsupportedEncodingException
*/
public String getUtf8ByURLDecoder(String str) throws UnsupportedEncodingException {
// 将字符串中的不合法 URL 编码转换为合法编码
String url = str.replaceAll("%(?![0-9a-fA-F]{2})", "%25");// 使用正则表达式,将不符合 URL 编码规范的百分号(%)替换为 %25
return URLDecoder.decode(url, "UTF-8");// 使用 URLDecoder 解码字符串,返回 UTF-8 编码的结果
}
}
一些解释
1.@Log4j2:这是一个 Lombok 提供的注解,用于自动生成一个 Log4j2 日志记录器。使用此注解后,你可以直接调用 log 对象来记录日志,而无需手动创建日志实例。
2.@Component:这是 Spring 框架的注解,表示该类是一个 Spring 组件,Spring 会自动检测并将其注册为一个 Bean。可以用于自动装配和依赖注入。
3.@RequiredArgsConstructor:也是 Lombok 提供的注解,自动生成一个构造函数,该构造函数接受所有被 final 修饰的字段或带有 @NonNull 注解的字段。这简化了依赖注入的过程,使代码更加简洁。
4.@SneakyThrows(Exception.class) :是 Lombok 提供的一个注解,用于在方法上处理检查性异常。
作用:
自动处理异常:在使用 @SneakyThrows 注解的方法中,如果抛出任何类型的检查性异常(即编译器要求显式捕获或声明的异常),Lombok 会自动将其包装成运行时异常(RuntimeException),从而避免了需要显式捕获或声明这些异常的麻烦。
注意事项:
尽管 @SneakyThrows 使代码更简洁,但在调试或异常处理时,要小心隐式处理异常可能导致的问题。如果可能,考虑在应用程序中适当处理异常,以提高代码的可读性和可维护性。
5.Optional是Java 8引入的一个新的容器对象,它提供了非常丰富的API,主要是为了解决空指针异常的问题。Optional类允许你创建一个可能为null的值的容器,从而避免了直接使用null值可能导致的空指针异常。它提供了多种方法来操作这个容器,包括判断值是否存在(isPresent())、获取值(get())、转换值(map())、过滤值(filter())等。
6.OkHttp 是一个高效的 HTTP 客户端库,用于发送和接收网络请求,它支持同步和异步调用,自动处理常见的网络问题如重定向、HTTPS握手和连接池。它广泛用于 Android 和 Java 应用程序中,以提高网络通信的效率和稳定性。
五、创建controller层
java
/**
* Minio控制层
*/
@Log4j2
@RestController
@RequestMapping("/minio")
public class MinioController {
@Autowired
private MinioUtils minioUtils;
@Autowired
private MinioConfig minioConfig;
/**
* 文件上传
*
* @param file
*/
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
try {
//文件名
String fileName = file.getOriginalFilename();
String newFileName = System.currentTimeMillis() + "." + StringUtils.substringAfterLast(fileName, ".");
//类型
String contentType = file.getContentType();
minioUtils.uploadFile(minioConfig.getBucketName(), file, newFileName, contentType);
return "上传成功";
} catch (Exception e) {
log.error("上传失败",e);
return "上传失败";
}
}
/**
* 删除
*
* @param fileName
*/
@DeleteMapping("/")
public void delete(@RequestParam("fileName") String fileName) {
minioUtils.removeFile(minioConfig.getBucketName(), fileName);
}
/**
* 获取文件信息
*
* @param fileName
* @return
*/
@GetMapping("/info")
public String getFileStatusInfo(@RequestParam("fileName") String fileName) {
return minioUtils.getFileStatusInfo(minioConfig.getBucketName(), fileName);
}
/**
* 获取文件外链
*
* @param fileName
* @return
*/
@GetMapping("/url")
public String getPresignedObjectUrl(@RequestParam("fileName") String fileName) {
return minioUtils.getPresignedObjectUrl(minioConfig.getBucketName(), fileName);
}
/**
* 文件下载
*
* @param fileName
* @param response
*/
@GetMapping("/download")
public void download(@RequestParam("fileName") String fileName, HttpServletResponse response) {
try {
// 从 MinIO 获取指定的对象(文件)的输入流
InputStream fileInputStream = minioUtils.getObject(minioConfig.getBucketName(), fileName);
// 设置响应头,指明这是一个附件下载,并指定下载文件的名称
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
// 设置响应的内容类型为强制下载
response.setContentType("application/force-download");
// 设置响应的字符编码为 UTF-8
response.setCharacterEncoding("UTF-8");
// 将输入流中的文件内容复制到响应输出流中
IOUtils.copy(fileInputStream, response.getOutputStream());
response.flushBuffer(); // 刷新响应输出流
} catch (Exception e) {
log.error("下载失败");
System.out.println(e);
}
}