找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1870

积分

0

好友

252

主题
发表于 昨天 19:12 | 查看: 2| 回复: 0

本文旨在讲解如何构建一个企业级的OSS(对象存储服务)Spring Boot Starter,实现开箱即用,为项目快速赋能。该Starter基于Amazon S3协议设计,能够无缝适配市面上主流的对象存储服务,例如阿里云OSS、腾讯云COS、七牛云OSS以及Minio等。

文章核心步骤概览

  • 什么是OSS?
  • OSS在项目中的使用场景
  • 什么是Amazon S3
  • 寻找并引入核心依赖
  • 编写配置属性类OssProperties
  • 定义操作接口OssTemplate
  • 实现OssTemplate接口
  • 创建自动配置类OssAutoConfiguration
  • 配置spring.factories文件
  • 执行install打包至本地仓库
  • 创建测试工程进行验证

什么是OSS?

OSS(Object Storage Service),即对象存储服务。它是一种通过HTTP API来存储和检索非结构化数据(对象)的云服务。简单来说,就是将系统所需的各类文件(如图片、文档、音视频)上传到云端存储,该服务提供上传、下载、预览、权限控制、版本管理以及数据生命周期管理等一站式功能。

OSS在项目中的使用

对象存储在当今的绝大多数项目中已成为不可或缺的基础组件,其主要应用场景如下图所示。

OSS对象存储系统架构图

  1. 资源集中管理:项目通常使用OSS来集中管理图片、文件、音频等静态资源,提供统一的上传、下载、预览和删除接口,并实施精细的权限控制与生命周期策略。
  2. 前端项目部署:利用OSS的静态网站托管功能,可以直接部署前端项目。

什么是Amazon S3

Amazon Simple Storage Service(Amazon S3)是AWS最早推出的云服务之一。经过多年发展,其API协议在对象存储领域已成为事实上的行业标准。

其主要特性包括:

  1. 提供统一的REST/SOAP接口访问数据。
  2. 采用简单的键值对(对象名-数据)存储模型。
  3. 存储容量无限,单个对象大小最高可达5TB,支持动态扩容。
  4. 性能优异,每个存储桶(Bucket)每秒可承受高达3500次写请求或5500次读请求。
  5. 具备完善的版本控制与权限管理能力。
  6. 支持灵活的数据生命周期管理策略。

由于S3功能的完备性和普适性,目前市面上主流的OSS服务都提供了对Amazon S3协议的兼容支持。本文的核心,正是讲解如何基于Amazon S3的Java SDK,封装我们自己的、可适配多平台的Spring Boot Starter。

主流云服务对S3的兼容性

  • 阿里云OSS兼容S3
    阿里云OSS S3兼容性文档截图
  • 七牛云对象存储兼容S3
    七牛云S3兼容接口公告截图
  • 腾讯云COS兼容S3
    腾讯云COS S3兼容配置指南截图
  • Minio兼容S3
    Minio官方S3兼容介绍截图

为什么选择基于Amazon S3实现Starter?

核心原因在于适配性、可迁移性和可扩展性。如果直接对接各云厂商的专属SDK,当需要从阿里云迁移到腾讯云,或同时支持多种存储后端时,业务代码将面临大量修改。而基于统一的S3协议进行封装,后续切换存储提供商时,通常只需修改配置信息(如Endpoint、AccessKey),无需调整业务逻辑代码,极大地提升了组件的通用性和可维护性。

第一步:创建Spring Boot项目

首先,我们需要创建一个基础的Spring Boot项目,项目名定为 oss-spring-boot-starter

使用IDE创建Spring Boot项目

项目创建成功后,结构如下图所示,接下来我们将开始具体的开发工作。

新创建的Spring Boot项目结构

第二步:引入核心依赖

打开Maven中央仓库,搜索 aws-java-sdk-s3
仓库地址:https://mvnrepository.com/

Maven仓库搜索aws-java-sdk-s3结果

选择第一个结果进入,这里我们以 1.12.423 版本为例进行演示。

AWS Java SDK S3特定版本页面

对应的Maven依赖如下:

<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-java-sdk-s3</artifactId>
    <version>1.12.423</version>
</dependency>

本项目完整的POM文件

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.9</version>
        <relativePath/><!-- lookup parent from repository -->
    </parent>
    <groupId>com.qing</groupId>
    <artifactId>oss-spring-boot-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>oss-spring-boot-starter</name>
    <description>Demo oss-spring-boot-starter</description>
    <properties>
        <java.version>1.8</java.version>
        <aws.version>1.12.423</aws.version>
        <hutool.version>5.8.5</hutool.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-java-sdk-s3</artifactId>
            <version>${aws.version}</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-core</artifactId>
            <version>${hutool.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>RELEASE</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
</project>

第三步:编写配置属性类 - OssProperties

这个类用于映射配置文件中的OSS相关属性。@ConfigurationProperties 注解报红可以先忽略,后续步骤会解决。@Data 是Lombok注解,用于自动生成getter和setter方法。

  • @ConfigurationProperties(prefix = "oss"):将应用配置文件中以 oss 为前缀的属性绑定到该类的字段上。

这意味着使用我们最终Starter的项目,只需要在 application.ymlapplication.properties 中配置如下属性即可:

oss.endpoint=xxx
oss.accessKey=xxx
oss.secretKey=xxx
/**
 * @Author JiaQIng
 * @Description Oss配置类
 * @ClassName OssProperties
 * @Date 2023/3/18 17:51
 **/
@Data
@ConfigurationProperties(prefix = "oss")
public class OssProperties {
    /**
     * 对象存储服务的URL
     */
    private String endpoint;

    /**
     * 区域
     */
    private String region;

    /**
     * true path-style nginx 反向代理和S3默认支持 pathStyle模式 {http://endpoint/bucketname}
     * false supports virtual-hosted-style 阿里云等需要配置为 virtual-hosted-style 模式{http://bucketname.endpoint}
     * 只是url的显示不一样
     */
    private Boolean pathStyleAccess = true;

    /**
     * Access key
     */
    private String accessKey;

    /**
     * Secret key
     */
    private String secretKey;

    /**
     * 最大连接数,默认:100
     */
    private Integer maxConnections = 100;
}

第四步:定义操作接口 - OssTemplate

OssTemplate 是我们定义的OSS操作模板接口。将其定义为接口是为了遵循“面向接口编程”的原则,提高扩展性。其他开发者使用我们的Starter时,可以通过实现此接口来自定义或增强某些操作。

以下代码定义了一系列基本的OSS操作方法:

/**
 * @Author JiaQIng
 * @Description oss操作模板
 * @ClassName OssTemplate
 * @Date 2023/3/18 18:15
 **/
public interface OssTemplate {

    /**
     * 创建bucket
     * @param bucketName bucket名称
     */
    void createBucket(String bucketName);

    /**
     * 获取所有的bucket
     * @return
     */
    List<Bucket> getAllBuckets();

    /**
     * 通过bucket名称删除bucket
     * @param bucketName
     */
    void removeBucket(String bucketName);

    /**
     * 上传文件
     * @param bucketName bucket名称
     * @param objectName 文件名称
     * @param stream 文件流
     * @param contextType 文件类型
     * @throws Exception
     */
    void putObject(String bucketName, String objectName, InputStream stream, String contextType) throws Exception;

    /**
     * 上传文件
     * @param bucketName bucket名称
     * @param objectName 文件名称
     * @param stream 文件流
     * @throws Exception
     */
    void putObject(String bucketName, String objectName, InputStream stream) throws Exception;

    /**
     * 获取文件
     * @param bucketName bucket名称
     * @param objectName 文件名称
     * @return S3Object
     */
    S3Object getObject(String bucketName, String objectName);

    /**
     * 获取对象的url
     * @param bucketName
     * @param objectName
     * @param expires
     * @return
     */
    String getObjectURL(String bucketName, String objectName, Integer expires);

    /**
     * 通过bucketName和objectName删除对象
     * @param bucketName
     * @param objectName
     * @throws Exception
     */
    void removeObject(String bucketName, String objectName) throws Exception;

    /**
     * 根据文件前置查询文件
     * @param bucketName bucket名称
     * @param prefix 前缀
     * @param recursive 是否递归查询
     * @return S3ObjectSummary 列表
     */
    List<S3ObjectSummary> getAllObjectsByPrefix(String bucketName, String prefix, boolean recursive);
}

第五步:实现 OssTemplate 接口

接下来,我们需要实现 OssTemplate 接口,内部通过调用 AmazonS3 SDK 提供的方法来完成具体操作。Amazon S3 提供了非常丰富的API,这里仅实现部分核心方法,实际项目中可按需扩展。

AmazonS3 官方API文档地址:https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateBucket.html

注解说明

  • @RequiredArgsConstructor:Lombok注解,为 final 字段生成构造函数,替代 @Autowired 进行注入。
  • @SneakyThrows:Lombok注解,自动抛出受检异常,避免代码中显式编写 throws
/**
 * @Author JiaQIng
 * @Description OssTemplate的实现类
 * @ClassName OssTemplateImpl
 * @Date 2023/3/18 19:02
 **/
@RequiredArgsConstructor
public class OssTemplateImpl implements OssTemplate {

    private final AmazonS3 amazonS3;

    /**
     * 创建Bucket
     * @param bucketName bucket名称
     */
    @Override
    @SneakyThrows
    public void createBucket(String bucketName) {
        if ( !amazonS3.doesBucketExistV2(bucketName) ) {
            amazonS3.createBucket((bucketName));
        }
    }

    /**
     * 获取所有的buckets
     * @return
     */
    @Override
    @SneakyThrows
    public List<Bucket> getAllBuckets() {
        return amazonS3.listBuckets();
    }

    /**
     * 通过Bucket名称删除Bucket
     * @param bucketName
     */
    @Override
    @SneakyThrows
    public void removeBucket(String bucketName) {
        amazonS3.deleteBucket(bucketName);
    }

    /**
     * 上传对象
     * @param bucketName bucket名称
     * @param objectName 文件名称
     * @param stream 文件流
     * @param contextType 文件类型
     */
    @Override
    @SneakyThrows
    public void putObject(String bucketName, String objectName, InputStream stream, String contextType) {
        putObject(bucketName, objectName, stream, stream.available(), contextType);
    }
    /**
     * 上传对象
     * @param bucketName bucket名称
     * @param objectName 文件名称
     * @param stream 文件流
     */
    @Override
    @SneakyThrows
    public void putObject(String bucketName, String objectName, InputStream stream) {
        putObject(bucketName, objectName, stream, stream.available(), "application/octet-stream");
    }

    /**
     * 通过bucketName和objectName获取对象
     * @param bucketName bucket名称
     * @param objectName 文件名称
     * @return
     */
    @Override
    @SneakyThrows
    public S3Object getObject(String bucketName, String objectName) {
        return amazonS3.getObject(bucketName, objectName);
    }

    /**
     * 获取对象的url
     * @param bucketName
     * @param objectName
     * @param expires
     * @return
     */
    @Override
    @SneakyThrows
    public String getObjectURL(String bucketName, String objectName, Integer expires) {
        Date date = new Date();
        Calendar calendar = new GregorianCalendar();
        calendar.setTime(date);
        calendar.add(Calendar.DAY_OF_MONTH, expires);
        URL url = amazonS3.generatePresignedUrl(bucketName, objectName, calendar.getTime());
        return url.toString();
    }

    /**
     * 通过bucketName和objectName删除对象
     * @param bucketName
     * @param objectName
     */
    @Override
    @SneakyThrows
    public void removeObject(String bucketName, String objectName) {
        amazonS3.deleteObject(bucketName, objectName);
    }

    /**
     * 根据bucketName和prefix获取对象集合
     * @param bucketName bucket名称
     * @param prefix 前缀
     * @param recursive 是否递归查询
     * @return
     */
    @Override
    @SneakyThrows
    public List<S3ObjectSummary> getAllObjectsByPrefix(String bucketName, String prefix, boolean recursive) {
        ObjectListing objectListing = amazonS3.listObjects(bucketName, prefix);
        return objectListing.getObjectSummaries();
    }

    /**
     *
     * @param bucketName
     * @param objectName
     * @param stream
     * @param size
     * @param contextType
     * @return
     */
    @SneakyThrows
    private PutObjectResult putObject(String bucketName, String objectName, InputStream stream, long size,
                                     String contextType)  {

        byte[] bytes = IOUtils.toByteArray(stream);
        ObjectMetadata objectMetadata = new ObjectMetadata();
        objectMetadata.setContentLength(size);
        objectMetadata.setContentType(contextType);
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        // 上传
        return amazonS3.putObject(bucketName, objectName, byteArrayInputStream, objectMetadata);

    }
}

第六步:创建自动配置类 - OssAutoConfiguration

OssAutoConfiguration 是Spring Boot自动装配的核心,它负责创建 AmazonS3 客户端Bean和 OssTemplate Bean。

OssAutoConfiguration代码截图

关键注解解析

  • @RequiredArgsConstructor:同上。
  • @EnableConfigurationProperties(OssProperties.class):启用对 OssProperties 配置类的支持。
  • @Bean:声明一个由Spring容器管理的Bean。
  • @ConditionalOnMissingBean:当Spring容器中不存在指定类型的Bean时,才创建当前Bean。这保证了用户可以在自己的项目中自定义同类型Bean来覆盖默认实现。
  • @ConditionalOnBean(AmazonS3.class):当容器中存在 AmazonS3 类型的Bean时,才创建当前Bean。确保 OssTemplate 依赖于 AmazonS3 客户端。
/**
 * @Author JiaQIng
 * @Description oss配置bean
 * @ClassName OssAConfiguration
 * @Date 2023/3/18 18:23
 **/
@Configuration
@RequiredArgsConstructor
@EnableConfigurationProperties(OssProperties.class)
public class OssAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public AmazonS3 ossClient(OssProperties ossProperties) {
        // 客户端配置,主要是全局的配置信息
        ClientConfiguration clientConfiguration = new ClientConfiguration();
        clientConfiguration.setMaxConnections(ossProperties.getMaxConnections());
        // url以及region配置
        AwsClientBuilder.EndpointConfiguration endpointConfiguration = new AwsClientBuilder.EndpointConfiguration(
                ossProperties.getEndpoint(), ossProperties.getRegion());
        // 凭证配置
        AWSCredentials awsCredentials = new BasicAWSCredentials(ossProperties.getAccessKey(),
                ossProperties.getSecretKey());
        AWSCredentialsProvider awsCredentialsProvider = new AWSStaticCredentialsProvider(awsCredentials);
        // build amazonS3Client客户端
        return AmazonS3Client.builder().withEndpointConfiguration(endpointConfiguration)
                .withClientConfiguration(clientConfiguration).withCredentials(awsCredentialsProvider)
                .disableChunkedEncoding().withPathStyleAccessEnabled(ossProperties.getPathStyleAccess()).build();
    }

    @Bean
    @ConditionalOnBean(AmazonS3.class)
    public OssTemplate ossTemplate(AmazonS3 amazonS3){
        return new OssTemplateImpl(amazonS3);
    }
}

关于 ClientConfiguration 对象

ClientConfiguration 是Amazon S3客户端的全局配置对象,包含了连接超时、最大连接数、重试策略等诸多配置项。下图展示了其部分默认配置。

ClientConfiguration默认配置截图

理解这些配置的作用对于避免生产环境下的潜在问题至关重要,建议有需要的同学深入阅读官方文档进行配置调优。

第七步:创建 spring.factories 文件

resources 目录下创建 META-INF 文件夹,并在其中新建 spring.factories 文件。这是Spring Boot“约定大于配置”理念的体现,用于声明自动配置类,使其能够被Spring Boot应用在启动时自动加载。

文件内容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.qing.oss.OssAutoConfiguration

第八步:打包并安装到本地Maven仓库

在打包之前,我们需要对项目做一些清理和调整:

  1. 删除Spring Boot工程的启动类、配置文件。
  2. 删除测试包(src/test)。
  3. 最关键的一步:从 pom.xml 中移除 spring-boot-maven-plugin 插件,否则执行 install 时会报错。

Maven打包因spring-boot-maven-plugin报错

需要移除的插件配置部分:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

清理调整后,项目的最终结构应如下图所示:

清理后的starter项目结构

现在,可以执行 mvn clean install 命令,将我们的 oss-spring-boot-starter 打包并安装到本地Maven仓库。

Maven install成功日志

安装成功后,可以在本地仓库的对应目录下找到生成的JAR文件。

本地仓库中的starter Jar包

第九步:创建测试工程进行验证

1. 创建测试Spring Boot工程

新建一个普通的Spring Boot工程作为测试项目。

测试Spring Boot项目结构

2. 引入我们自制的Starter依赖

在测试项目的 pom.xml 中,首先定义版本属性:

<properties>
    <oss.version>0.0.1-SNAPSHOT</oss.version>
</properties>

然后添加依赖:

<dependency>
    <groupId>com.qing</groupId>
    <artifactId>oss-spring-boot-starter</artifactId>
    <version>${oss.version}</version>
</dependency>

刷新Maven后,可以在依赖列表中看到我们引入的starter。

项目依赖中已加入自制的starter

3. 解决源码(Sources)包问题

你可能会发现引入的依赖没有附加源码(Sources),这会影响IDE中的代码提示和跳转。为了解决这个问题,我们需要在 starter项目的pom文件 中加入 maven-source-plugin 插件,然后重新执行 install

<build>
    <plugins>
        <!-- 在打好的jar包中保留javadoc注释,实际会另外生成一个xxxxx-sources.jar -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-source-plugin</artifactId>
            <executions>
                <execution>
                    <id>attach-sources</id>
                    <goals>
                        <goal>jar</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

重新安装后,在测试项目中刷新依赖,就可以看到附带源码的JAR了。

附带源码的依赖截图

4. 添加配置文件

在测试项目的 application.propertiesapplication.yml 中,配置OSS连接信息。以下以Minio为例:

oss.endpoint=http://your-minio-server:9000
oss.accessKey=your-access-key
oss.secretKey=your-secret-key
# region非必填,根据实际情况配置
# oss.region=us-east-1

5. 编写并执行测试

创建一个简单的测试类,注入 OssTemplate 并调用其方法。

@SpringBootTest
class TestOssSpringBpptStarterApplicationTests {
    @Autowired
    private OssTemplate ossTemplate;

    @Test
    void contextLoads() {
        ossTemplate.createBucket("oss02");
    }
}

执行测试方法,如果控制台输出成功且无报错,则表明starter工作正常。

测试方法执行成功截图

最后,可以登录到Minio的管理控制台进行确认,发现名为 oss02 的存储桶已被成功创建。

Minio控制台显示新创建的oss02桶

总结与后记

本文详细演示了如何从零开始构建一个基于Amazon S3协议的企业级OSS对象存储 Spring Boot Starter。该组件实现了开箱即用,并能适配阿里云OSS、腾讯云COS、七牛云OSS、Minio等多种兼容S3协议的服务,有效提升了项目在集成对象存储功能时的开发效率和可维护性。

通过这个实战案例,我们不仅完成了一个实用的工具,也深入实践了Spring Boot自动配置、Conditional条件化Bean装配等核心机制。希望这个案例能为你设计和封装自己的 开源实战 组件提供清晰的思路和参考。

如果你想深入探讨 Java 后端开发、微服务架构或更多技术实践,欢迎在 云栈社区 与其他开发者交流分享你的经验和见解。




上一篇:深入解析分布式锁四大方案:数据库、Redis、Redission与ZooKeeper源码及高并发场景选型
下一篇:Spark UI深度解析:从Jobs到Tasks的性能调优实战指南
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-4 03:40 , Processed in 0.477915 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表