阿里云 OSS 从零到实战:概念、配置与 Spring Boot 集成指南

阿里云 OSS 从零到实战:概念、配置与 Spring Boot 集成指南

一、OSS 核心概念

1.1 什么是 OSS

OSS(Object Storage Service,对象存储服务)是一种海量、安全、低成本的云存储服务。简单理解就是一个无限容量的网盘,通过 API 上传和下载文件。

与传统文件存储的区别:

维度 本地磁盘/NAS OSS 对象存储
容量 有限,需要扩容 无限
访问方式 文件路径(/usr/local/file.txt) HTTP URL
可靠性 单机故障丢失 多副本冗余,99.9999999999%
并发访问 有限 支持高并发读写
CDN 加速 不支持 天然支持
成本 硬件+运维 按量付费

1.2 核心术语

复制代码
Endpoint (接入点)
    │
    ▼
Bucket (存储空间)
    │
    ├── folder1/
    │     ├── file1.xlsx
    │     └── file2.pdf
    │
    └── folder2/
          └── image.png
术语 说明 类比
Endpoint OSS 服务的访问域名,按地域区分 数据库的 host:port
Bucket 存储空间,是文件的容器 数据库中的一个 database
Object 存储的基本单元,即文件 数据库中的一行记录
Object Key 文件在 Bucket 中的唯一标识(路径+文件名) 文件路径
AccessKey ID / Secret 访问凭证(身份认证) 数据库的用户名/密码
Region 地域(如 cn-qingdao, cn-hangzhou) 数据中心位置

1.3 Endpoint 格式

复制代码
格式:<BucketName>.<Endpoint>
示例:xxx-img-xxx.oss-cn-xxxx.aliyuncs.com

内网访问:xxx-img-xxx.oss-cn-xxx-internal.aliyuncs.com(同地域免流量费)

1.4 Object Key(文件路径)

OSS 没有真正的文件夹概念,"文件夹"只是 Key 中的前缀:

复制代码
upload/2026/06/10/abc123.xlsx     ← 完整的 Object Key
├── upload/2026/06/10/            ← "文件夹"(实际是Key前缀)
└── abc123.xlsx                   ← "文件名"

1.5 访问权限

权限 说明
private 默认,需要签名 URL 才能访问
public-read 任何人可读,写入需认证
public-read-write 任何人可读写(不推荐)

生产环境通常设为 private,程序通过 SDK 生成带签名的临时 URL 供用户下载。


注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

二、首次申请和开通 OSS

2.1 开通步骤

  1. 登录阿里云控制台 → 搜索"对象存储 OSS"
  2. 点击"立即开通"(按量付费,不用的时候几乎不花钱)
  3. 进入 OSS 管理控制台

2.2 创建 Bucket

  1. 点击"创建 Bucket"
  2. 填写信息:
    • Bucket 名称 :全局唯一,如 my-app-files
    • 地域:选择离服务器最近的地域(如 cn-qingdao)
    • 存储类型:标准存储(频繁访问)/ 低频访问 / 归档
    • 读写权限:私有(推荐)
    • 同城冗余:生产环境建议开启
  3. 点击确认创建

2.3 获取 AccessKey

  1. 鼠标悬停右上角头像 → "AccessKey 管理"
  2. 创建 AccessKey(会得到 AccessKey ID 和 AccessKey Secret)
  3. 重要:Secret 只显示一次,必须保存好

安全建议:不要使用主账号 AccessKey,创建 RAM 子账号并只授予 OSS 权限。

2.4 记录关键信息

创建完成后你会拥有:

复制代码
Endpoint:          oss-cn-xxx.aliyuncs.com
Bucket:            my-app-files
AccessKey ID:      xxxx
AccessKey Secret:  xxxx
访问 URL 前缀:     https://my-app-files.oss-cn-xxx.aliyuncs.com/

三、Spring Boot 集成 OSS

3.1 方式一:阿里云官方 SDK(通用)

依赖

xml 复制代码
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.17.4</version>
</dependency>

3.3 配置(application.yml)

yaml 复制代码
# 参考工程中的配置格式
jsh:
  oss:
    endpoint: oss-cn-xxx.aliyuncs.com
    access-key-id: xx
    access-key-secret: xxx
    bucket-name: ylh-img-test
    oss-url: https://image.ylhtest.com    # CDN 域名或 Bucket 公网域名
    max-size: 52480000                     # 上传大小限制(50MB)

3.4 配置项含义详解

配置项 含义 示例
endpoint OSS 服务接入点域名 oss-cn-qingdao.aliyuncs.com
access-key-id 认证 ID LTAI5tXXXX
access-key-secret 认证密钥 p9e27XXXX
bucket-name 存储空间名称 my-app-files
oss-url 文件访问的基础 URL(可以是 CDN 域名) https://image.example.com
max-size 允许上传的最大文件大小(字节) 52428800(50MB)

四、完整示例:Spring Boot + 阿里云 OSS 文件上传下载

4.1 依赖

xml 复制代码
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.17.4</version>
</dependency>

4.2 配置

yaml 复制代码
oss:
  endpoint: oss-cn-xxx.aliyuncs.com
  access-key-id: your-access-key-id
  access-key-secret: your-access-key-secret
  bucket-name: my-app-files
  url-prefix: https://my-app-files.oss-cn-qingdao.aliyuncs.com

4.3 配置类

java 复制代码
package com.example.config;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 阿里云 OSS 客户端配置.
 */
@Configuration
public class OssConfig {

  @Value("${oss.endpoint}")
  private String endpoint;

  @Value("${oss.access-key-id}")
  private String accessKeyId;

  @Value("${oss.access-key-secret}")
  private String accessKeySecret;

  /**
   * 创建 OSS 客户端 Bean.
   * OSS 客户端是线程安全的,全局共享一个实例即可.
   */
  @Bean(destroyMethod = "shutdown")
  public OSS ossClient() {
    return new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
  }
}

4.4 工具类

java 复制代码
package com.example.util;

import com.aliyun.oss.OSS;
import com.aliyun.oss.model.OSSObject;
import com.aliyun.oss.model.PutObjectRequest;
import jakarta.annotation.Resource;
import java.io.File;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

/**
 * OSS 文件操作工具类.
 */
@Slf4j
@Component
public class OssUtil {

  @Resource
  private OSS ossClient;

  @Value("${oss.bucket-name}")
  private String bucketName;

  @Value("${oss.url-prefix}")
  private String urlPrefix;

  /**
   * 上传 MultipartFile(Web 表单文件).
   *
   * @param file 上传的文件
   * @param bizFolder 业务文件夹名(如 "import", "export")
   * @return 文件访问 URL
   */
  public String uploadMultipartFile(MultipartFile file, String bizFolder) {
    String objectKey = generateObjectKey(bizFolder, getExtension(file.getOriginalFilename()));
    try (InputStream is = file.getInputStream()) {
      ossClient.putObject(bucketName, objectKey, is);
      String url = urlPrefix + "/" + objectKey;
      log.info("文件上传成功,objectKey:{}, url:{}", objectKey, url);
      return url;
    } catch (Exception e) {
      log.error("文件上传失败", e);
      throw new RuntimeException("文件上传失败", e);
    }
  }

  /**
   * 上传本地 File 对象.
   *
   * @param file 本地文件
   * @param bizFolder 业务文件夹名
   * @return 文件访问 URL
   */
  public String uploadFile(File file, String bizFolder) {
    String objectKey = generateObjectKey(bizFolder, getExtension(file.getName()));
    PutObjectRequest request = new PutObjectRequest(bucketName, objectKey, file);
    ossClient.putObject(request);
    String url = urlPrefix + "/" + objectKey;
    log.info("文件上传成功,objectKey:{}, url:{}", objectKey, url);
    return url;
  }

  /**
   * 上传字节流.
   *
   * @param inputStream 输入流
   * @param bizFolder 业务文件夹名
   * @param extension 文件扩展名(如 "xlsx")
   * @return 文件访问 URL
   */
  public String uploadStream(InputStream inputStream, String bizFolder, String extension) {
    String objectKey = generateObjectKey(bizFolder, extension);
    ossClient.putObject(bucketName, objectKey, inputStream);
    String url = urlPrefix + "/" + objectKey;
    log.info("文件上传成功,objectKey:{}, url:{}", objectKey, url);
    return url;
  }

  /**
   * 下载文件,返回输入流.
   *
   * @param objectKey 文件在 OSS 中的 Key
   * @return 文件输入流(调用方需要关闭)
   */
  public InputStream downloadFile(String objectKey) {
    OSSObject ossObject = ossClient.getObject(bucketName, objectKey);
    return ossObject.getObjectContent();
  }

  /**
   * 通过完整 URL 下载文件.
   *
   * @param fileUrl 文件完整 URL
   * @return 文件输入流
   */
  public InputStream downloadByUrl(String fileUrl) {
    // 从 URL 中提取 objectKey
    String objectKey = fileUrl.replace(urlPrefix + "/", "");
    return downloadFile(objectKey);
  }

  /**
   * 删除文件.
   *
   * @param objectKey 文件在 OSS 中的 Key
   */
  public void deleteFile(String objectKey) {
    ossClient.deleteObject(bucketName, objectKey);
    log.info("文件删除成功,objectKey:{}", objectKey);
  }

  /**
   * 生成唯一的 Object Key.
   * 格式:bizFolder/yyyy/MM/dd/uuid.extension
   * 例如:import/2026/06/10/a1b2c3d4.xlsx
   */
  private String generateObjectKey(String bizFolder, String extension) {
    String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
    String fileName = UUID.randomUUID().toString().replace("-", "") + "." + extension;
    return bizFolder + "/" + datePath + "/" + fileName;
  }

  /**
   * 获取文件扩展名.
   */
  private String getExtension(String filename) {
    if (filename == null || !filename.contains(".")) {
      return "unknown";
    }
    return filename.substring(filename.lastIndexOf(".") + 1).toLowerCase();
  }
}

4.5 业务使用示例

java 复制代码
package com.example.service.impl;

import com.example.util.OssUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

@Slf4j
@Service
public class FileBusinessService {

  @Resource
  private OssUtil ossUtil;

  /**
   * 场景一:接收用户上传的 Excel 文件,存到 OSS.
   */
  public String handleFileUpload(MultipartFile file) {
    // 直接上传到 OSS 的 "import" 文件夹下
    return ossUtil.uploadMultipartFile(file, "import");
    // 返回值如:https://my-app-files.oss-cn-xxx.aliyuncs.com/import/2026/06/10/xxx.xlsx
  }

  /**
   * 场景二:程序生成 Excel 文件(如导入错误报告),上传到 OSS.
   */
  public String generateAndUploadErrorReport(java.util.List<String> errors) {
    File tempFile = null;
    try {
      // 1. 在内存中生成 Excel
      XSSFWorkbook workbook = new XSSFWorkbook();
      var sheet = workbook.createSheet("错误信息");
      var headerRow = sheet.createRow(0);
      headerRow.createCell(0).setCellValue("行号");
      headerRow.createCell(1).setCellValue("错误原因");

      for (int i = 0; i < errors.size(); i++) {
        var row = sheet.createRow(i + 1);
        row.createCell(0).setCellValue(i + 1);
        row.createCell(1).setCellValue(errors.get(i));
      }

      // 2. 写入临时文件
      tempFile = File.createTempFile("error_report_", ".xlsx");
      try (FileOutputStream fos = new FileOutputStream(tempFile)) {
        workbook.write(fos);
      }
      workbook.close();

      // 3. 上传到 OSS
      return ossUtil.uploadFile(tempFile, "error-reports");

    } catch (Exception e) {
      throw new RuntimeException("生成错误报告失败", e);
    } finally {
      // 4. 清理临时文件
      if (tempFile != null && tempFile.exists()) {
        tempFile.delete();
      }
    }
  }

  /**
   * 场景三:从 OSS 下载文件提供给用户.
   */
  public void downloadFromOss(String fileUrl, HttpServletResponse response) {
    try (InputStream is = ossUtil.downloadByUrl(fileUrl)) {
      response.setContentType("application/octet-stream");
      response.setHeader("Content-Disposition", "attachment;filename=download.xlsx");

      byte[] buffer = new byte[8192];
      int len;
      var os = response.getOutputStream();
      while ((len = is.read(buffer)) != -1) {
        os.write(buffer, 0, len);
      }
      os.flush();
    } catch (Exception e) {
      throw new RuntimeException("文件下载失败", e);
    }
  }
}

五、OSS 常见使用场景

场景 Object Key 设计 说明
用户上传的原始文件 upload/原始文件名或UUID.xlsx 保留用于异步处理
导入失败错误报告 error-reports/2026/06/10/xxx.xlsx 前端展示下载链接
导出的数据文件 export/2026/06/10/xxx.xlsx 大文件异步生成后通知用户下载
用户头像 avatar/用户ID.jpg 公开读,CDN 加速
合同/票据打印件 contracts/2026/06/xxx.pdf 私有,签名 URL 访问

六、生产环境最佳实践

6.1 安全

  • 使用 RAM 子账号,只授予最小权限(如只能操作特定 Bucket)
  • AccessKey 不硬编码在代码中,放在环境变量或配置中心(如 Nacos)
  • Bucket 设为 private,通过后端接口代理访问或生成临时签名 URL

6.2 签名 URL(临时授权访问)

对于 private 文件,生成有时效的访问 URL:

java 复制代码
// 生成有效期 1 小时的签名 URL
Date expiration = new Date(System.currentTimeMillis() + 3600 * 1000);
URL signedUrl = ossClient.generatePresignedUrl(bucketName, objectKey, expiration);

6.3 生命周期管理

配置自动清理规则,避免存储费用无限增长:

  • 临时文件(error-reports/)7 天后自动删除
  • 导出文件(export/)30 天后转低频存储,90 天后删除
  • 在 OSS 控制台 → Bucket → 基础设置 → 生命周期 中配置

6.4 内网访问

如果应用服务器和 OSS 在同一地域,使用内网 Endpoint 可以:

  • 免除流量费
  • 速度更快、更稳定
yaml 复制代码
# 公网
endpoint: oss-cn-qingdao.aliyuncs.com
# 内网(同地域 ECS 使用)
endpoint: oss-cn-qingdao-internal.aliyuncs.com

6.5 大文件处理

文件大小 推荐方式
< 5MB 普通上传(PutObject)
5MB ~ 5GB 分片上传(Multipart Upload)
> 5GB 分片上传(必须)

分片上传的好处:

  • 断点续传
  • 并行上传多个分片提速
  • 单个分片失败只需重传该分片

七、费用说明

计费项 说明 大致费用
存储费 按存储容量计费 标准存储 ≈ 0.12 元/GB/月
流量费 外网下行流量 ≈ 0.50 元/GB(内网免费)
请求费 PUT/GET 请求次数 PUT 0.01 元/万次,GET 0.01 元/万次
数据取回费 低频/归档存储取回 低频 0.075 元/GB

对于导入导出场景(文件存几天就删,主要走内网),费用非常低。


八、总结

步骤 操作
1. 开通 阿里云控制台开通 OSS 服务
2. 创建 Bucket 选地域、命名、设权限
3. 获取凭证 创建 RAM 子账号,获取 AccessKey
4. 引入依赖 aliyun-sdk-oss 或公司封装库
5. 配置 endpoint、bucket、accessKey 写入 YAML
6. 编码 创建 OSSClient Bean → 封装工具类 → 业务调用
7. 安全 私有 Bucket + 签名 URL + RAM 最小权限
8. 运维 生命周期规则 + 内网访问 + 监控告警
相关推荐
茉莉玫瑰花茶1 小时前
综合案例 - AI 智能租房助手 [ 4 ]
数据库·python·ai·langgraph
可乐ea1 小时前
【Spring Boot + MyBatis|第4篇】MyBatis 动态 SQL:if、where、foreach 使用详解
java·spring boot·后端·sql·mybatis
ULIi096kr1 小时前
MySQL查看表创建时间、修改时间、最后更新时间(精准排查僵尸表)
数据库·mysql
阿里-于怀1 小时前
阿里云 Agent Infra 上长出的约束基建
阿里云·云计算·agent·starops
折哥的程序人生 · 物流技术专研1 小时前
Tomcat 严重警告:JDBC 驱动未注销 + 工作线程泄漏 —— 原因、影响与彻底修复(生产级终极指南)
java·运维·数据库·mysql·oracle·tomcat
初圣魔门首席弟子1 小时前
Qt C++ 项目实战:修改共享头文件后的高效增量编译与快速发布流程
数据库
wb043072011 小时前
仓库搬家不停业——从阿明的“在线换仓库“,看数据库迁移与 Schema 演进的实战方法论
数据库·adb·架构
lx188548698961 小时前
Redis大Key阻塞:单线程CPU100%的致命陷阱
数据库·redis·缓存
IT策士2 小时前
Redis 从入门到精通:位图、HyperLogLog、GEO
数据库·redis·缓存