一、OSS配置类及相关代码

1. OSS配置属性类(OSSProperties.java)
package com.zgb.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * OSS配置属性类,用于绑定nacos或yml中的OSS配置
 * 自动映射配置文件中前缀为"zgb.oss"的配置项
 */
@Data
@Component
@ConfigurationProperties(prefix = "zgb.oss")
public class OSSProperties {

    /**
     * OSS服务端点(Endpoint),不同地域不同,例如:oss-cn-beijing.aliyuncs.com
     */
    private String endpoint;

    /**
     * 阿里云访问密钥ID(AccessKey ID)
     */
    private String accessKeyId;

    /**
     * 阿里云访问密钥Secret(AccessKey Secret)
     */
    private String accessKeySecret;

    /**
     * OSS存储空间名称(Bucket Name)
     */
    private String bucketName;

    /**
     * 文件访问基础URL(可选,若配置则用于拼接文件访问路径)
     * 例如:https://bucketName.endpoint/
     */
    private String baseUrl;

    /**
     * 默认文件存储目录(可选,如未指定则存储在根目录)
     */
    private String defaultDir;
}
2. OSS配置类(OSSConfig.java)
package com.zgb.config;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * OSS客户端配置类,用于创建阿里云OSS客户端实例
 */
@Configuration
@RequiredArgsConstructor
public class OSSConfig {

    private final OSSProperties ossProperties;

    /**
     * 创建OSS客户端实例(单例)
     *
     * @return OSS客户端对象,用于操作阿里云OSS服务
     */
    @Bean
    public OSS ossClient() {
        return new OSSClientBuilder()
                .build(
                        ossProperties.getEndpoint(),
                        ossProperties.getAccessKeyId(),
                        ossProperties.getAccessKeySecret()
                );
    }
}
3. OSS工具类(OSSUtil.java)
package com.zgb.util;

import com.aliyun.oss.OSS;
import com.aliyun.oss.model.*;
import com.zgb.config.OSSProperties;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * OSS工具类,封装阿里云OSS的常用操作(上传、下载、删除、查询等)
 */
@Component
@RequiredArgsConstructor
public class OSSUtil {

    private final OSS ossClient;
    private final OSSProperties ossProperties;

    /**
     * 上传文件到OSS
     *
     * @param file       待上传的文件(MultipartFile类型)
     * @param customDir 自定义存储目录(可选,若为null则使用默认目录)
     * @return 上传成功后的文件访问URL
     * @throws IOException 当文件输入流获取失败时抛出
     */
    public String uploadFile(MultipartFile file, String customDir) throws IOException {
        // 1. 处理存储目录:优先使用自定义目录,否则使用默认目录
        String dir = customDir != null ? customDir : ossProperties.getDefaultDir();
        // 2. 生成文件名:UUID + 原文件名(避免重名)
        String originalFilename = file.getOriginalFilename();
        String fileName = UUID.randomUUID().toString() + "_" + originalFilename;
        // 3. 拼接完整存储路径(目录 + 日期目录 + 文件名,按日期分类存储)
        String dateDir = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        String objectName = dir + "/" + dateDir + "/" + fileName;

        // 4. 上传文件
        try (InputStream inputStream = file.getInputStream()) {
            PutObjectRequest request = new PutObjectRequest(ossProperties.getBucketName(), objectName, inputStream);
            // 设置文件ContentType(自动识别文件类型)
            ObjectMetadata metadata = new ObjectMetadata();
            metadata.setContentType(file.getContentType());
            request.setMetadata(metadata);
            ossClient.putObject(request);
        }

        // 5. 拼接访问URL并返回
        return buildFileUrl(objectName);
    }

    /**
     * 下载OSS文件(获取文件输入流)
     *
     * @param objectName OSS中的文件完整路径(如:dir/20240929/file.jpg)
     * @return 文件输入流,需调用方自行关闭
     */
    public InputStream downloadFile(String objectName) {
        GetObjectRequest request = new GetObjectRequest(ossProperties.getBucketName(), objectName);
        OSSObject ossObject = ossClient.getObject(request);
        return ossObject.getObjectContent();
    }

    /**
     * 删除OSS中的文件
     *
     * @param objectName OSS中的文件完整路径(如:dir/20240929/file.jpg)
     * @return 删除成功返回true,否则返回false
     */
    public boolean deleteFile(String objectName) {
        if (ossClient.doesObjectExist(ossProperties.getBucketName(), objectName)) {
            ossClient.deleteObject(ossProperties.getBucketName(), objectName);
            return true;
        }
        return false;
    }

    /**
     * 批量删除OSS中的文件
     *
     * @param objectNames 文件路径列表(如:["dir/file1.jpg", "dir/file2.jpg"])
     * @return 成功删除的文件数量
     */
    public int batchDeleteFiles(List<String> objectNames) {
        if (objectNames == null || objectNames.isEmpty()) {
            return 0;
        }
        DeleteObjectsRequest request = new DeleteObjectsRequest(ossProperties.getBucketName())
                .withKeys(objectNames);
        DeleteObjectsResult result = ossClient.deleteObjects(request);
        return result.getDeletedObjects().size();
    }

    /**
     * 查询指定目录下的文件列表
     *
     * @param prefix 目录前缀(如:"dir/20240929/",查询该目录下的所有文件)
     * @return 文件路径列表(OSS中的完整路径)
     */
    public List<String> listFiles(String prefix) {
        List<String> fileList = new ArrayList<>();
        // 设置查询参数
        ListObjectsRequest request = new ListObjectsRequest(ossProperties.getBucketName())
                .withPrefix(prefix)
                .withMaxKeys(1000); // 最多返回1000条

        ObjectListing listing;
        do {
            listing = ossClient.listObjects(request);
            // 遍历文件并添加到列表
            for (OSSObjectSummary summary : listing.getObjectSummaries()) {
                fileList.add(summary.getKey());
            }
            // 分页查询(如果有更多结果)
            request.setMarker(listing.getNextMarker());
        } while (listing.isTruncated());

        return fileList;
    }

    /**
     * 构建文件访问URL
     *
     * @param objectName OSS中的文件完整路径
     * @return 文件可访问的URL
     */
    private String buildFileUrl(String objectName) {
        // 若配置了baseUrl,则直接拼接;否则使用默认格式:https://bucket.endpoint/objectName
        if (ossProperties.getBaseUrl() != null && !ossProperties.getBaseUrl().isEmpty()) {
            return ossProperties.getBaseUrl() + objectName;
        } else {
            return "https://" + ossProperties.getBucketName() + "." + ossProperties.getEndpoint() + "/" + objectName;
        }
    }
}

二、配置文件(application.yml)

# OSS配置
zgb:
  oss:
    # OSS服务端点(根据地域选择,参考阿里云文档)
    endpoint: oss-cn-beijing.aliyuncs.com
    # 阿里云AccessKey ID(需替换为实际值)
    accessKeyId: your-access-key-id
    # 阿里云AccessKey Secret(需替换为实际值)
    accessKeySecret: your-access-key-secret
    # 存储空间名称(需替换为实际Bucket名称)
    bucketName: your-bucket-name
    # 文件访问基础URL(可选,如:https://your-bucket-name.oss-cn-beijing.aliyuncs.com/)
    baseUrl: 
    # 默认文件存储目录(如:upload/files/,结尾建议加"/")
    defaultDir: upload/files

三、单元测试(OSSUtilTest.java)

package com.zgb.util;

import com.aliyun.oss.OSS;
import com.zgb.config.OSSProperties;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

/**
 * OSS工具类单元测试
 */
@SpringBootTest
public class OSSUtilTest {

    @Autowired
    private OSSUtil ossUtil;

    @Autowired
    private OSS ossClient;

    @Autowired
    private OSSProperties ossProperties;

    // 测试用文件路径
    private String testObjectName;

    /**
     * 测试前准备:创建一个测试文件
     */
    @BeforeEach
    public void setUp() throws IOException {
        // 创建测试文件
        MultipartFile testFile = new MockMultipartFile(
                "test.txt",
                "test.txt",
                "text/plain",
                "Hello OSS!".getBytes()
        );
        // 上传文件并记录路径(从URL中提取objectName)
        String fileUrl = ossUtil.uploadFile(testFile, "test");
        testObjectName = extractObjectName(fileUrl);
    }

    /**
     * 测试后清理:删除测试文件
     */
    @AfterEach
    public void tearDown() {
        if (testObjectName != null) {
            ossClient.deleteObject(ossProperties.getBucketName(), testObjectName);
        }
    }

    /**
     * 测试文件上传
     */
    @Test
    public void testUploadFile() throws IOException {
        // 1. 创建测试文件
        MultipartFile file = new MockMultipartFile(
                "upload-test.txt",
                "upload-test.txt",
                "text/plain",
                "Upload test content".getBytes()
        );

        // 2. 执行上传
        String fileUrl = ossUtil.uploadFile(file, "test/upload");
        assertNotNull(fileUrl, "文件上传失败,URL为空");

        // 3. 验证文件是否存在
        String objectName = extractObjectName(fileUrl);
        assertTrue(ossClient.doesObjectExist(ossProperties.getBucketName(), objectName), "上传的文件不存在");

        // 4. 清理测试文件
        ossClient.deleteObject(ossProperties.getBucketName(), objectName);
    }

    /**
     * 测试文件下载
     */
    @Test
    public void testDownloadFile() throws IOException {
        // 执行下载
        try (InputStream is = ossUtil.downloadFile(testObjectName)) {
            assertNotNull(is, "文件下载失败,输入流为空");

            // 验证文件内容
            byte[] content = is.readAllBytes();
            assertEquals("Hello OSS!", new String(content), "文件内容不一致");
        }
    }

    /**
     * 测试文件删除
     */
    @Test
    public void testDeleteFile() {
        // 执行删除
        boolean result = ossUtil.deleteFile(testObjectName);
        assertTrue(result, "文件删除失败");

        // 验证文件已删除
        assertFalse(ossClient.doesObjectExist(ossProperties.getBucketName(), testObjectName), "文件未被删除");

        // 重置testObjectName(避免tearDown重复删除)
        testObjectName = null;
    }

    /**
     * 测试批量删除文件
     */
    @Test
    public void testBatchDeleteFiles() throws IOException {
        // 1. 准备多个测试文件
        MultipartFile file1 = new MockMultipartFile("file1.txt", "file1.txt", "text/plain", "1".getBytes());
        MultipartFile file2 = new MockMultipartFile("file2.txt", "file2.txt", "text/plain", "2".getBytes());
        String url1 = ossUtil.uploadFile(file1, "test/batch");
        String url2 = ossUtil.uploadFile(file2, "test/batch");
        List<String> objectNames = List.of(extractObjectName(url1), extractObjectName(url2));

        // 2. 执行批量删除
        int deletedCount = ossUtil.batchDeleteFiles(objectNames);
        assertEquals(2, deletedCount, "批量删除数量不正确");

        // 3. 验证文件已删除
        for (String name : objectNames) {
            assertFalse(ossClient.doesObjectExist(ossProperties.getBucketName(), name), "文件未被删除:" + name);
        }
    }

    /**
     * 测试文件列表查询
     */
    @Test
    public void testListFiles() {
        // 执行查询(查询test目录下的文件)
        List<String> fileList = ossUtil.listFiles("test/");
        assertFalse(fileList.isEmpty(), "查询不到文件列表");
        assertTrue(fileList.contains(testObjectName), "文件列表中不包含测试文件");
    }

    /**
     * 从文件URL中提取OSS的objectName
     */
    private String extractObjectName(String fileUrl) {
        String base = "https://" + ossProperties.getBucketName() + "." + ossProperties.getEndpoint() + "/";
        if (fileUrl.startsWith(base)) {
            return fileUrl.substring(base.length());
        }
        if (ossProperties.getBaseUrl() != null && fileUrl.startsWith(ossProperties.getBaseUrl())) {
            return fileUrl.substring(ossProperties.getBaseUrl().length());
        }
        throw new IllegalArgumentException("无法从URL中提取objectName:" + fileUrl);
    }
}

说明

  1. 依赖说明:需在pom.xml中添加阿里云OSS SDK依赖:
    <dependency>
        <groupId>com.aliyun.oss</groupId>
        <artifactId>aliyun-sdk-oss</artifactId>
        <version>3.16.1</version>
    </dependency>
    
  2. 安全性:AccessKey建议通过Nacos配置中心管理,避免硬编码在代码中。
  3. 异常处理:实际生产环境中需根据业务需求完善异常处理(如网络异常、权限不足等)。
  4. 扩展性:可根据需要添加更多功能(如文件分片上传、签名URL生成、文件元信息获取等)。

更多推荐