阿里云oss使用

阿里云Oss

一、阿里云 OSS 是什么?

阿里云对象存储服务(Object Storage Service,简称 OSS) 是阿里云提供的海量、安全、低成本、高可靠的云存储服务。

核心概念:

  • 存储空间(Bucket):存储对象的容器,每个对象都必须包含在某个存储空间中
  • 对象(Object):OSS 存储的基本单元,可以是任意类型的文件
  • 地域(Region):OSS 数据中心的物理位置
  • 访问域名(Endpoint):OSS 对外服务的访问地址
  • AccessKey:访问 OSS 的密钥,包括 AccessKeyId 和 AccessKeySecret

OSS 的主要优势:

  1. 高可靠性:数据持久性不低于 99.9999999999%
  2. 高可用性:服务可用性不低于 99.995%
  3. 安全性强:支持多种加密和权限控制方式
  4. 成本低廉:按实际使用量付费,无最低消费
  5. 易于扩展:存储容量无限扩展

二、准备工作

1. 开通 OSS 服务

  • 登录阿里云控制台
  • 搜索并开通 OSS 服务
  • 创建 Bucket(存储空间)

2. 获取 AccessKey

  • 进入阿里云控制台 → 访问控制 RAM
  • 创建用户并授予 OSS 相关权限
  • 获取 AccessKeyId 和 AccessKeySecret

三、Spring Boot 集成 OSS

1. 添加依赖

Maven:

xml

<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.15.1</version>
</dependency>

或者使用阿里云官方 Starter(推荐):

xml

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alicloud-oss</artifactId>
    <version>2.2.0.RELEASE</version>
</dependency>

2. 配置文件(application.yml)

yaml

# 阿里云 OSS 配置
alibaba:
  cloud:
    access-key: your-access-key-id        # 替换为你的 AccessKeyId
    secret-key: your-access-key-secret    # 替换为你的 AccessKeySecret
    oss:
      endpoint: oss-cn-hangzhou.aliyuncs.com  # 替换为你的 Bucket 所在区域的 Endpoint
      bucket: your-bucket-name            # 替换为你的 Bucket 名称
      cname: false                        # 是否使用自定义域名
      path-style-access: false            # 是否使用路径风格访问

# 自定义配置(可选)
oss:
  max-size: 10485760        # 文件大小限制:10MB
  allowed-extensions: .jpg,.png,.gif,.pdf,.doc,.docx  # 允许的文件扩展名
  callback-url: http://your-domain.com/callback  # 回调地址(如果需要)
  dir-prefix: images/        # 文件存储目录前缀

四、配置类详解

1. 基础配置类

java

package com.example.config;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Data
@Configuration
@ConfigurationProperties(prefix = "alibaba.cloud.oss")
public class AliyunOSSConfig {
    
    private String endpoint;
    private String accessKey;
    private String secretKey;
    private String bucket;
    
    /**
     * 创建 OSS 客户端
     */
    @Bean
    public OSS ossClient() {
        return new OSSClientBuilder().build(endpoint, accessKey, secretKey);
    }
}

2. 完整配置类(推荐)

java

package com.example.config;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.common.comm.Protocol;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Data
@Configuration
@ConfigurationProperties(prefix = "oss")
public class OSSConfig {
    
    // OSS 基本配置
    private String endpoint;
    private String accessKey;
    private String secretKey;
    private String bucket;
    
    // 业务配置
    private Long maxSize;                    // 文件大小限制
    private String allowedExtensions;        // 允许的文件类型
    private String callbackUrl;              // 回调地址
    private String dirPrefix;                // 目录前缀
    
    // 线程池配置(用于异步上传)
    private int corePoolSize = 5;
    private int maxPoolSize = 20;
    private int queueCapacity = 100;
    
    /**
     * 创建 OSS 客户端
     */
    @Bean
    public OSS ossClient() {
        // 创建客户端配置
        com.aliyun.oss.ClientBuilderConfiguration config = 
            new com.aliyun.oss.ClientBuilderConfiguration();
        
        // 设置协议(HTTP 或 HTTPS)
        config.setProtocol(Protocol.HTTPS);
        // 设置连接超时时间(默认50秒)
        config.setConnectionTimeout(50000);
        // 设置Socket超时时间(默认50秒)
        config.setSocketTimeout(50000);
        // 设置最大连接数(默认1024)
        config.setMaxConnections(1024);
        
        return new OSSClientBuilder().build(endpoint, accessKey, secretKey, config);
    }
    
    /**
     * 创建文件上传线程池
     */
    @Bean("ossThreadPool")
    public ThreadPoolExecutor ossThreadPool() {
        return new ThreadPoolExecutor(
            corePoolSize,
            maxPoolSize,
            60L,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(queueCapacity),
            new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略:由调用线程处理
        );
    }
    
    /**
     * 获取允许的文件扩展名数组
     */
    public String[] getAllowedExtensionArray() {
        if (allowedExtensions == null || allowedExtensions.trim().isEmpty()) {
            return new String[0];
        }
        return allowedExtensions.split(",");
    }
}

五、服务类实现

1. OSS 服务接口

java

package com.example.service;

import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.List;

public interface OSSService {
    
    /**
     * 上传文件
     */
    String uploadFile(MultipartFile file);
    
    /**
     * 上传文件到指定目录
     */
    String uploadFile(MultipartFile file, String directory);
    
    /**
     * 上传文件(自定义文件名)
     */
    String uploadFile(InputStream inputStream, String fileName);
    
    /**
     * 批量上传文件
     */
    List<String> uploadFiles(List<MultipartFile> files);
    
    /**
     * 下载文件
     */
    InputStream downloadFile(String fileName);
    
    /**
     * 删除文件
     */
    boolean deleteFile(String fileName);
    
    /**
     * 批量删除文件
     */
    boolean deleteFiles(List<String> fileNames);
    
    /**
     * 检查文件是否存在
     */
    boolean doesFileExist(String fileName);
    
    /**
     * 获取文件访问URL
     */
    String getFileUrl(String fileName);
    
    /**
     * 获取文件访问URL(带过期时间)
     */
    String getFileUrl(String fileName, long expiration);
}

2. OSS 服务实现类

java

package com.example.service.impl;

import com.aliyun.oss.OSS;
import com.aliyun.oss.model.*;
import com.example.config.OSSConfig;
import com.example.service.OSSService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;

@Slf4j
@Service
public class OSSServiceImpl implements OSSService {
    
    @Autowired
    private OSS ossClient;
    
    @Autowired
    private OSSConfig ossConfig;
    
    @Autowired
    private ThreadPoolExecutor ossThreadPool;
    
    /**
     * 上传文件
     */
    @Override
    public String uploadFile(MultipartFile file) {
        return uploadFile(file, ossConfig.getDirPrefix());
    }
    
    /**
     * 上传文件到指定目录
     */
    @Override
    public String uploadFile(MultipartFile file, String directory) {
        // 参数校验
        if (file == null || file.isEmpty()) {
            throw new IllegalArgumentException("文件不能为空");
        }
        
        // 文件大小校验
        if (file.getSize() > ossConfig.getMaxSize()) {
            throw new IllegalArgumentException("文件大小不能超过 " + ossConfig.getMaxSize() / 1024 / 1024 + "MB");
        }
        
        // 文件类型校验
        String originalFilename = file.getOriginalFilename();
        String fileExtension = getFileExtension(originalFilename);
        if (!isAllowedExtension(fileExtension)) {
            throw new IllegalArgumentException("不支持的文件类型: " + fileExtension);
        }
        
        try {
            // 生成唯一文件名
            String fileName = generateFileName(directory, fileExtension);
            
            // 创建上传请求
            PutObjectRequest putObjectRequest = new PutObjectRequest(
                ossConfig.getBucket(), 
                fileName, 
                file.getInputStream()
            );
            
            // 设置对象元数据
            ObjectMetadata metadata = new ObjectMetadata();
            metadata.setContentLength(file.getSize());
            metadata.setContentType(getContentType(fileExtension));
            putObjectRequest.setMetadata(metadata);
            
            // 上传文件
            ossClient.putObject(putObjectRequest);
            
            log.info("文件上传成功: {}", fileName);
            return fileName;
            
        } catch (IOException e) {
            log.error("文件上传失败: {}", e.getMessage(), e);
            throw new RuntimeException("文件上传失败", e);
        }
    }
    
    /**
     * 异步上传文件
     */
    @Async
    public CompletableFuture<String> uploadFileAsync(MultipartFile file) {
        return CompletableFuture.supplyAsync(() -> uploadFile(file), ossThreadPool);
    }
    
    /**
     * 上传文件(自定义文件名)
     */
    @Override
    public String uploadFile(InputStream inputStream, String fileName) {
        try {
            PutObjectRequest putObjectRequest = new PutObjectRequest(
                ossConfig.getBucket(), 
                fileName, 
                inputStream
            );
            
            ossClient.putObject(putObjectRequest);
            log.info("文件上传成功: {}", fileName);
            return fileName;
            
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
            } catch (IOException e) {
                log.error("关闭流失败: {}", e.getMessage());
            }
        }
    }
    
    /**
     * 批量上传文件
     */
    @Override
    public List<String> uploadFiles(List<MultipartFile> files) {
        return files.stream()
            .map(this::uploadFile)
            .collect(Collectors.toList());
    }
    
    /**
     * 下载文件
     */
    @Override
    public InputStream downloadFile(String fileName) {
        if (!doesFileExist(fileName)) {
            throw new IllegalArgumentException("文件不存在: " + fileName);
        }
        
        OSSObject ossObject = ossClient.getObject(ossConfig.getBucket(), fileName);
        return ossObject.getObjectContent();
    }
    
    /**
     * 删除文件
     */
    @Override
    public boolean deleteFile(String fileName) {
        try {
            ossClient.deleteObject(ossConfig.getBucket(), fileName);
            log.info("文件删除成功: {}", fileName);
            return true;
        } catch (Exception e) {
            log.error("文件删除失败: {}", fileName, e);
            return false;
        }
    }
    
    /**
     * 批量删除文件
     */
    @Override
    public boolean deleteFiles(List<String> fileNames) {
        try {
            DeleteObjectsRequest deleteObjectsRequest = new DeleteObjectsRequest(ossConfig.getBucket())
                .withKeys(fileNames);
            
            ossClient.deleteObjects(deleteObjectsRequest);
            log.info("批量删除文件成功: {}", fileNames);
            return true;
        } catch (Exception e) {
            log.error("批量删除文件失败: {}", fileNames, e);
            return false;
        }
    }
    
    /**
     * 检查文件是否存在
     */
    @Override
    public boolean doesFileExist(String fileName) {
        try {
            return ossClient.doesObjectExist(ossConfig.getBucket(), fileName);
        } catch (Exception e) {
            log.error("检查文件是否存在失败: {}", fileName, e);
            return false;
        }
    }
    
    /**
     * 获取文件访问URL
     */
    @Override
    public String getFileUrl(String fileName) {
        // 设置URL过期时间为1小时
        return getFileUrl(fileName, 3600);
    }
    
    /**
     * 获取文件访问URL(带过期时间)
     */
    @Override
    public String getFileUrl(String fileName, long expiration) {
        try {
            Date expirationDate = new Date(System.currentTimeMillis() + expiration * 1000);
            URL url = ossClient.generatePresignedUrl(ossConfig.getBucket(), fileName, expirationDate);
            return url.toString();
        } catch (Exception e) {
            log.error("生成文件URL失败: {}", fileName, e);
            return null;
        }
    }
    
    // ========== 私有方法 ==========
    
    /**
     * 生成唯一文件名
     */
    private String generateFileName(String directory, String extension) {
        String uuid = UUID.randomUUID().toString().replace("-", "");
        String timestamp = String.valueOf(System.currentTimeMillis());
        
        if (StringUtils.isNotBlank(directory)) {
            if (!directory.endsWith("/")) {
                directory += "/";
            }
            return directory + timestamp + "_" + uuid + extension;
        }
        
        return timestamp + "_" + uuid + extension;
    }
    
    /**
     * 获取文件扩展名
     */
    private String getFileExtension(String filename) {
        if (StringUtils.isBlank(filename)) {
            return "";
        }
        int lastDotIndex = filename.lastIndexOf(".");
        return lastDotIndex >= 0 ? filename.substring(lastDotIndex).toLowerCase() : "";
    }
    
    /**
     * 检查文件扩展名是否允许
     */
    private boolean isAllowedExtension(String extension) {
        if (StringUtils.isBlank(extension)) {
            return false;
        }
        
        String[] allowedExtensions = ossConfig.getAllowedExtensionArray();
        if (allowedExtensions.length == 0) {
            return true; // 如果没有配置限制,则允许所有类型
        }
        
        return Arrays.asList(allowedExtensions).contains(extension.toLowerCase());
    }
    
    /**
     * 获取Content-Type
     */
    private String getContentType(String fileExtension) {
        switch (fileExtension.toLowerCase()) {
            case ".jpg":
            case ".jpeg":
                return "image/jpeg";
            case ".png":
                return "image/png";
            case ".gif":
                return "image/gif";
            case ".pdf":
                return "application/pdf";
            case ".doc":
                return "application/msword";
            case ".docx":
                return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
            default:
                return "application/octet-stream";
        }
    }
}

六、控制器类

java

package com.example.controller;

import com.example.service.OSSService;
import com.example.utils.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@Slf4j
@RestController
@RequestMapping("/api/oss")
public class OSSController {
    
    @Autowired
    private OSSService ossService;
    
    /**
     * 单文件上传
     */
    @PostMapping("/upload")
    public Result<String> uploadFile(@RequestParam("file") MultipartFile file,
                                    @RequestParam(value = "directory", required = false) String directory) {
        try {
            String fileName;
            if (directory != null) {
                fileName = ossService.uploadFile(file, directory);
            } else {
                fileName = ossService.uploadFile(file);
            }
            
            String fileUrl = ossService.getFileUrl(fileName);
            return Result.success("文件上传成功", fileUrl);
            
        } catch (Exception e) {
            log.error("文件上传失败: {}", e.getMessage());
            return Result.error(e.getMessage());
        }
    }
    
    /**
     * 多文件上传
     */
    @PostMapping("/upload/batch")
    public Result<List<String>> uploadFiles(@RequestParam("files") List<MultipartFile> files) {
        try {
            List<String> fileNames = ossService.uploadFiles(files);
            List<String> fileUrls = fileNames.stream()
                .map(ossService::getFileUrl)
                .collect(java.util.stream.Collectors.toList());
            
            return Result.success("文件上传成功", fileUrls);
            
        } catch (Exception e) {
            log.error("批量文件上传失败: {}", e.getMessage());
            return Result.error(e.getMessage());
        }
    }
    
    /**
     * 删除文件
     */
    @DeleteMapping("/delete")
    public Result<Boolean> deleteFile(@RequestParam("fileName") String fileName) {
        try {
            boolean result = ossService.deleteFile(fileName);
            return result ? 
                Result.success("文件删除成功", true) : 
                Result.error("文件删除失败");
                
        } catch (Exception e) {
            log.error("文件删除失败: {}", e.getMessage());
            return Result.error(e.getMessage());
        }
    }
    
    /**
     * 获取文件URL
     */
    @GetMapping("/url")
    public Result<String> getFileUrl(@RequestParam("fileName") String fileName) {
        try {
            String fileUrl = ossService.getFileUrl(fileName);
            return Result.success("获取成功", fileUrl);
        } catch (Exception e) {
            log.error("获取文件URL失败: {}", e.getMessage());
            return Result.error(e.getMessage());
        }
    }
}

七、工具类和异常处理

1. 统一返回结果

java

package com.example.utils;

import lombok.Data;
import java.io.Serializable;

@Data
public class Result<T> implements Serializable {
    private int code;
    private String message;
    private T data;
    private long timestamp;
    
    public Result() {
        this.timestamp = System.currentTimeMillis();
    }
    
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMessage("success");
        result.setData(data);
        return result;
    }
    
    public static <T> Result<T> success(String message, T data) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMessage(message);
        result.setData(data);
        return result;
    }
    
    public static <T> Result<T> error(String message) {
        Result<T> result = new Result<>();
        result.setCode(500);
        result.setMessage(message);
        return result;
    }
    
    public static <T> Result<T> error(int code, String message) {
        Result<T> result = new Result<>();
        result.setCode(code);
        result.setMessage(message);
        return result;
    }
}

2. 全局异常处理

java

package com.example.handler;

import com.example.utils.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    /**
     * 文件大小超限异常
     */
    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public Result<?> handleMaxUploadSizeExceededException(MaxUploadSizeExceededException e) {
        log.warn("文件大小超过限制: {}", e.getMessage());
        return Result.error(413, "文件大小超过限制");
    }
    
    /**
     * 非法参数异常
     */
    @ExceptionHandler(IllegalArgumentException.class)
    public Result<?> handleIllegalArgumentException(IllegalArgumentException e) {
        log.warn("参数错误: {}", e.getMessage());
        return Result.error(400, e.getMessage());
    }
    
    /**
     * 其他异常
     */
    @ExceptionHandler(Exception.class)
    public Result<?> handleException(Exception e) {
        log.error("系统异常: {}", e.getMessage(), e);
        return Result.error(500, "系统繁忙,请稍后重试");
    }
}

八、测试和使用

1. 前端调用示例

html

<!-- 单文件上传 -->
<form id="uploadForm">
    <input type="file" name="file" />
    <input type="text" name="directory" placeholder="存储目录(可选)" />
    <button type="submit">上传</button>
</form>

<script>
// 单文件上传
document.getElementById('uploadForm').addEventListener('submit', async (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);
    
    const response = await fetch('/api/oss/upload', {
        method: 'POST',
        body: formData
    });
    
    const result = await response.json();
    console.log('上传结果:', result);
});

// 多文件上传
async function uploadMultipleFiles(files) {
    const formData = new FormData();
    files.forEach(file => {
        formData.append('files', file);
    });
    
    const response = await fetch('/api/oss/upload/batch', {
        method: 'POST',
        body: formData
    });
    
    return await response.json();
}
</script>

2. 单元测试

java

package com.example.service;

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.nio.charset.StandardCharsets;

@SpringBootTest
class OSSServiceImplTest {
    
    @Autowired
    private OSSService ossService;
    
    @Test
    void testUploadFile() {
        // 创建模拟文件
        MultipartFile file = new MockMultipartFile(
            "test.txt", 
            "test.txt", 
            "text/plain", 
            "Hello OSS".getBytes(StandardCharsets.UTF_8)
        );
        
        try {
            String fileName = ossService.uploadFile(file, "test/");
            System.out.println("上传成功,文件名: " + fileName);
            
            // 获取文件URL
            String fileUrl = ossService.getFileUrl(fileName);
            System.out.println("文件URL: " + fileUrl);
            
            // 删除测试文件
            ossService.deleteFile(fileName);
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

九、注意事项

  1. 安全性:AccessKey 要妥善保管,不要硬编码在代码中
  2. 权限控制:使用 RAM 子账号并授予最小必要权限
  3. 网络超时:根据文件大小合理设置超时时间
  4. 异常处理:做好网络异常、权限异常等处理
  5. 资源释放:及时关闭 InputStream 等资源
  6. 费用控制:监控存储量和访问量,避免意外费用

总结

通过以上配置和代码,你可以在 Spring Boot 项目中完整地集成阿里云 OSS 服务,实现文件的上传、下载、删除等功能。这种设计具有良好的扩展性和可维护性,适合在生产环境中使用。

关键点:

  • 使用配置类统一管理 OSS 参数
  • 服务类封装所有 OSS 操作
  • 控制器提供 RESTful API
  • 完善的异常处理和日志记录
  • 支持异步上传提高性能
posted @ 2025-12-14 16:00  binlicoder  阅读(71)  评论(0)    收藏  举报