Contents

鸡翅Club-02

本篇要点

  • 工厂 + 策略模式实现操作的分别处理
  • 分页处理封装
  • 空值全局处理
  • SQL翻译器
  • Nacos实现动态Bean加载
  • 适配器模式配置Minio

工厂+策略模式实现操作的分别处理

策略 + 工厂 模式主要解决当一段业务在运行时需要根据某个 类型/枚举/状态 选择不同算法或处理逻辑时,用大量 if-else / switch 会导致代码难维护、难扩展。通过把算法封装为策略并用工厂按需生成,可消除条件分支、遵守开闭原则、提高可读性与可测试性。

在鸡哥的项目中就是使用题目的新增与查询作为例子来讲解的,题目的类型包含单选、多选、判断等等类型,直接在新增的时候采用 if-else / switch-case 进行判断就要写很多层,以后继续增加类型还需要去修改代码,非常不方便。这种情况就非常适合使用工厂加策略的模式处理。

模式的组成部分

工厂 + 策略模式的组成部分,最常见的5个角色如下:

  • 策略接口(Handler Interface):规定所有的策略都必须实现统一的方法,如 add()update() 等。
  • 具体策略(Handler):每个类封装一种题型的专属算法或规则,实现接口定义的方法。
  • 策略工厂(Factory):负责按照操作类型枚举返回对应的 Handler 实例,屏蔽创建与管理的细节,可以做缓存或则注入。
  • 类型标注(Enum):操作类型枚举,作为工厂查表的 key,让映射关系清晰。
  • 调用方(Client):只做流程编排,也就是获取 Handler -> 调用统一接口,不关心内部实现。

模式的具体实现

本文将会通过工厂 + 策略模式实现AI对话模型选择的处理,在市面上存在很多种模型,但是每家的模型的返回数据格式可能都不同(虽然我实际使用发现几乎每一家都兼容openai的返回格式🤪),非常适合来实现这次工厂策略模式。

完整的Demo代码在 AIStrategyFactoryDemo

类型标注枚举

主要就是标注作用,区分不同的策略类

@Getter
public enum AiTypeEnum {

    DS("deepseek-chat", "DeepSeek官网模型调用"),
    QW_TURBO("qwen-turbo", "阿里云百炼平台的Qwen Turbo模型调用");

    private final String modelName;
    private final String desc;

    AiTypeEnum(String modelName, String desc) {
        this.modelName = modelName;
        this.desc = desc;
    }

    public static AiTypeEnum getModelTypeEnum(String modelName) {
        for (AiTypeEnum aiTypeEnum : AiTypeEnum.values()) {
            if (aiTypeEnum.getModelName().equals(modelName)) {
                return aiTypeEnum;
            }
        }
        return null;
    }
}

策略接口

策略接口主要用于定义策略需要实现的方法,不同的策略类去实现这个接口类并重写接口定义的方法,从而达到不同策略类完成不同的业务操作的效果

public interface AiTypeHandler {

    /**
     * 枚举身份的识别,就是获取当前的调用的模型是什么类型的
     * @return
     */
    AiTypeEnum getHandlerType();

    /**
     * 实际的对话方法 每类型的模型对话方式实现内容不同
     * @param dto
     */
    ChatVO chat(GeneratorPromptDTO dto);
}

策略的具体实现类

public class DeepSeekTypeHandler implements AiTypeHandler {

    /**
     * 获取DeepSeek的模型类型枚举
     * @return
     */
    @Override
    public AiTypeEnum getHandlerType() {
        return AiTypeEnum.DS;
    }

    /**
     * 实际的对话方法
     * @param dto
     */
    @Override
    public ChatVO chat(GeneratorPromptDTO dto) {
      // TODO 具体的对话实现方法......
    }
}

策略工厂

通过在SpringBoot中实现InitializingBean接口并重写afterPropertiesSet方法,可以在Bean属性设置完成后执行自定义逻辑,例如初始化枚举类型和相应的策略实现类的映射关系。此外还提供一个根据枚举类型获取相应的策略的公共方法。

public class AiTypeHandlerFactory implements InitializingBean {

    private final List<AiTypeHandler> aiTypeHandlerList;

    private final Map<AiTypeEnum, AiTypeHandler> handlerMap = new HashMap<>();

    /**
     * 根据类型获取AI对话的处理器
     * @param aiType
     * @return
     */
    public AiTypeHandler getHandler(AiTypeEnum aiType) {
        AiTypeHandler handler = handlerMap.get(aiType);
        if (handler == null) {
            throw new IllegalArgumentException("No handler found for ai type: " + aiType);
        }
        return handler;
    }

    /**
     * 初始化枚举类与策略处理器的对应关系
     */
    @Override
    public void afterPropertiesSet() {
        for (AiTypeHandler aiTypeHandler : aiTypeHandlerList) {
            handlerMap.put(aiTypeHandler.getHandlerType(), aiTypeHandler);
        }
    }
}

策略的调用方

在实际的业务实现类中,首先需要注入策略工厂,然后通过不同的枚举类型获取到对应的策略实现类,然后调用其实现的方法即可。之后需要添加新的业务,只需要添加枚举类型和创建新的策略实现类即可,完全不需要修改业务实现类的代码。

public class AiStrategyFactoryServiceImpl implements AiStrategyFactoryService {

    /**
     * 注入AI类型处理器工厂
     */
    private final AiTypeHandlerFactory aiTypeHandlerFactory;

    /**
     * 调用AI模型的统一入口
     * @param dto
     * @return
     */
    @Override
    public String chatByAi(GeneratorPromptDTO dto) {

        // 1. 获取策略器
        AiTypeHandler handler = aiTypeHandlerFactory.getHandler(dto.getAiType());

        // 2. 调用对应的处理器进行对话
        try {
            return handler.chat(dto).getContent();
        } catch (Exception e) {
            log.error("AI对话失败,类型:{},错误信息:{}", dto.getAiType(), e.getMessage());
            return "AI对话失败,请稍后再试。";
        }
    }
}

分页处理封装

鸡哥的分页封装是基于手动分页实现的,没有依赖插件,关键的实体类分别是请求实体类和统一的分页数据返回类,我这里记录的是我优化后的实体类

请求实体类

@Data
@Accessors(chain = true)
public class PageInfo implements Serializable {
    private static final long serialVersionUID = 1L;
    private static final int DEFAULT_PAGE_NO = 1;
    private static final int DEFAULT_PAGE_SIZE = 20;
    private static final int MAX_PAGE_SIZE = 1000; // 最大每页条数限制

    /**
     * 当前页码
     */
    private Integer pageNo = DEFAULT_PAGE_NO;

    /**
     * 每页显示条数
     */
    private Integer pageSize = DEFAULT_PAGE_SIZE;

    /**
     * 获取有效的页码
     * @return 有效页码(最小为1)
     */
    public Integer getPageNo() {
        return (pageNo == null || pageNo < 1) ? DEFAULT_PAGE_NO : pageNo;
    }

    /**
     * 获取有效的每页条数
     * @return 有效的每页条数(在1-MAX_PAGE_SIZE之间)
     */
    public Integer getPageSize() {
        if (pageSize == null || pageSize < 1) {
            return DEFAULT_PAGE_SIZE;
        }
        return Math.min(pageSize, MAX_PAGE_SIZE);
    }

    /**
     * 计算偏移量(用于数据库分页查询)
     * @return (pageNo - 1) * pageSize
     */
    public Integer getOffset() {
        return (getPageNo() - 1) * getPageSize();
    }

    /**
     * 计算当前页的最大记录数
     * @return 当前页最多显示的记录数
     */
    public Integer getLimit() {
        return getPageSize();
    }
}

统一的分页数据返回类

@Data
public class PageResult<T> implements Serializable {
    private static final long serialVersionUID = 1L;
    private static final int DEFAULT_PAGE_NO = 1;
    private static final int DEFAULT_PAGE_SIZE = 20;

    /**
     * 当前页码
     */
    private Integer pageNo = DEFAULT_PAGE_NO;

    /**
     * 每页显示条数
     */
    private Integer pageSize = DEFAULT_PAGE_SIZE;

    /**
     * 总记录数
     */
    private Integer total = 0;

    /**
     * 总页数
     */
    private Integer totalPages = 0;

    /**
     * 当前页的数据
     */
    private List<T> records = Collections.emptyList();

    /**
     * 当前页的第一条数据在数据库中的起始位置
     */
    private Integer start = 1;

    /**
     * 当前页的最后一条数据在数据库中的结束位置
     */
    private Integer end = 0;

    /**
     * 设置当前页的数据
     * @param records 当前页数据列表
     */
    public void setRecords(List<T> records) {
        this.records = records != null ? records : Collections.emptyList();
        if (!this.records.isEmpty()) {
            setTotal(this.records.size());
        }
    }

    /**
     * 设置总记录数并计算相关分页信息
     * @param total 总记录数
     */
    public void setTotal(Integer total) {
        if (total == null || total < 0) {
            total = 0;
        }
        this.total = total;

        // 计算总页数
        if (this.pageSize > 0) {
            this.totalPages = (total + this.pageSize - 1) / this.pageSize;
        } else {
            this.totalPages = 0;
        }

        // 计算起始和结束位置
        this.start = (this.pageSize > 0 ? (this.pageNo - 1) * this.pageSize : 0) + 1;
        this.end = Math.min(this.start - 1 + this.pageSize, total);
    }

    /**
     * 设置每页显示条数
     * @param pageSize 每页条数
     */
    public void setPageSize(Integer pageSize) {
        this.pageSize = pageSize != null && pageSize > 0 ? pageSize : DEFAULT_PAGE_SIZE;
    }

    /**
     * 设置当前页码
     * @param pageNo 当前页码
     */
    public void setPageNo(Integer pageNo) {
        this.pageNo = pageNo != null && pageNo > 0 ? pageNo : DEFAULT_PAGE_NO;
    }

    /**
     * 判断是否有上一页
     * @return 是否有上一页
     */
    public boolean hasPrevious() {
        return this.pageNo > 1;
    }

    /**
     * 判断是否有下一页
     * @return 是否有下一页
     */
    public boolean hasNext() {
        return this.pageNo < this.totalPages;
    }
}

使用方法

在具体的业务实现类可以通过下列方法使用

@Service
public class NoteServiceImpl implements NoteService {

    @Autowired
    private NoteMapper noteMapper;

    @Override
    public PageResult<Note> listNotes(PageInfo pageInfo) {
        // 1. 构建分页结果对象
        PageResult<Note> pageResult = new PageResult<>();
        pageResult.setPageNo(pageInfo.getPageNo());
        pageResult.setPageSize(pageInfo.getPageSize());

        // 2. 查询总记录数
        int total = noteMapper.countNotes();
        pageResult.setTotal(total);

        // 3. 查询当前页数据
        if (total > 0) {
            List<Note> notes = noteMapper.selectNotes(
                pageInfo.getOffset(),
                pageInfo.getLimit()
            );
            pageResult.setRecords(notes);
        }

        return pageResult;
    }
}

空值全局处理

在鸡哥的课程中通过继承 WebMvcConfigurationSupport 这个类可以进行一些 Spring MVC 的配置,但是该方式会覆盖 Spring Boot 的默认 MVC 配置,可能导致一些自动配置失效。这里记录的是优化后的,可以在这里配置将数据返回给前端时忽略值为 null 的字段,并且在这里也可以配置其他的全局处理,如全局日期返回格式。

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        // 移除默认的Jackson转换器
        converters.removeIf(c -> c instanceof MappingJackson2HttpMessageConverter);
        // 添加自定义的转换器
        converters.add(mappingJackson2HttpMessageConverter());
    }

    @Bean
    public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        converter.setObjectMapper(objectMapper());
        return converter;
    }

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();

        // 空值处理
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

        // 日期处理
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

        // 其他配置
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);

        return objectMapper;
    }
}

如果某些字段即便值为 null 但是也需要返回给前端,可以在实体类中给该字段加上 @JsonInclude,这样他就不会被这个配置影响

/**
 * 下一题
 * 相当于 @JsonInclude(JsonInclude.Include.ALWAYS)
 */
@JsonInclude
private Long nextSubjectId;

SQL翻译器

MyBatisPlus 版本:3.4.0 JDK 版本:1.8

MyBatis自带的SQL打印方式很不友好,可以自定义一个拦截器配置到MyBatisPlus中,在SQL执行前将完整的语句打印出来,可以直接复制到相关的数据库工具中执行,可以更快的定位问题。

第一步需要创建一个拦截器

拦截器需要实现 MyBatisPlusInnerInterceptor,具体的包名为 com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor,实现的这个接口中包含两个方法,分别是

  • beforeQuery:用于实现查询SQL在执行之前的操作
  • beforeUpdate:用于实现数据变更SQL在执行之前的操作,这里的数据变更包含增删改

除此之外还包含一些需要的参数,均为 org.apache.ibatis 包下的类

  • Executor:MyBatis 的 SQL 执行器
  • MappedStatement:封装了 SQL 映射语句的对象
  • Object parameter: 传递给 SQL 的参数对象
  • RowBounds:用于分页的对象
  • ResultHandler:结果处理器
  • BoundSql:封装了要执行的 SQL 语句和参数信息的对象,是关键方法
public class MybatisPlusAllSqlLog implements InnerInterceptor {

    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    }

    @Override
    public void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {
      BoundSql boundSql = ms.getBoundSql(parameter);
    }
}

有了这两个方法以及一系列参数后,就可以编写实际的SQL翻译逻辑了。

SQL翻译逻辑的编写

首先就需要在 BoundSql 这个参数中拿到传递给SQL的参数对象和参数映射列表

Object parameterObject = boundSql.getParameterObject();
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();

然后可以将真实SQL中的多个空格全局替换成一个,使得填充参数后的SQL格式美观

String sql = boundSql.getSql().replaceAll("[\\s]+", " ");

参数的映射关系 parameterMappings 不为空就可以进行参数填充了,通过TypeHandlerRegistry 获取类型处理器注册器,用于进行Java类型和数据库类型的转换,如果根据 parameterObject.getClass()可以找到对应的类型,则说明是简单的参数,直接进行填充即可

// getParameterValue 是一个格式化的方法
if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
    sql = sql.replaceFirst("\\?",
            Matcher.quoteReplacement(getParameterValue(parameterObject)));
}

如果为复杂的参数,例如实体类,则依次处理每个参数映射,并替换相应的 ? 占位符号

MetaObject metaObject = configuration.newMetaObject(parameterObject);
for (ParameterMapping parameterMapping : parameterMappings) {
    String propertyName = parameterMapping.getProperty();
    Object value = null;

    if (metaObject.hasGetter(propertyName)) {
        value = metaObject.getValue(propertyName);
    } else if (boundSql.hasAdditionalParameter(propertyName)) {
        value = boundSql.getAdditionalParameter(propertyName);
    }

    String paramValue = value != null ? formatParameter(value) : MISSING_PARAM;
    sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(paramValue));
}

完整的代码见下:

/**
 * MyBatis-Plus SQL 日志拦截器
 * 用于打印完整的 SQL 语句,包括参数值和格式化后的 SQL
 */
public class MybatisPlusAllSqlLog implements InnerInterceptor {

    private static final Logger log = LoggerFactory.getLogger("sys-sql");
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    private static final String LINE_SEPARATOR = System.lineSeparator();
    private static final String MISSING_PARAM = "[缺失参数]";

    /**
     * 查询操作前的拦截方法
     */
    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter,
                          RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        logSqlInfo(boundSql, ms, parameter, "Query");
    }

    /**
     * 更新操作前的拦截方法
     */
    @Override
    public void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameter);
        logSqlInfo(boundSql, ms, parameter, "Update");
    }

    /**
     * 记录 SQL 相关信息
     */
    private void logSqlInfo(BoundSql boundSql, MappedStatement ms, Object parameter, String operationType) {
        try {
            StringBuilder logBuilder = new StringBuilder();
            logBuilder.append(LINE_SEPARATOR)
                    .append("===================SQL执行信息===================").append(LINE_SEPARATOR)
                    .append("操作类型: ").append(operationType).append(LINE_SEPARATOR)
                    .append("执行时间: ").append(LocalDateTime.now().format(DATE_FORMATTER)).append(LINE_SEPARATOR)
                    .append("Mapper方法: ").append(ms.getId()).append(LINE_SEPARATOR);

            if (parameter != null) {
                logBuilder.append("参数信息: ").append(parameter).append(LINE_SEPARATOR);
            }

            String formattedSql = formatSql(ms.getConfiguration(), boundSql);
            logBuilder.append("完整SQL: ").append(LINE_SEPARATOR).append(formattedSql).append(LINE_SEPARATOR)
                    .append("===============================================");

            log.info(logBuilder.toString());
        } catch (Exception e) {
            log.error("SQL日志记录异常: {}", e.getMessage(), e);
        }
    }

    /**
     * 格式化 SQL 语句
     */
    private String formatSql(Configuration configuration, BoundSql boundSql) {
        String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();

        if (CollectionUtils.isEmpty(parameterMappings) || parameterObject == null) {
            return sql;
        }

        return processParameters(configuration, boundSql, sql, parameterObject, parameterMappings);
    }

    /**
     * 处理 SQL 参数
     */
    private String processParameters(Configuration configuration, BoundSql boundSql,
                                   String sql, Object parameterObject, List<ParameterMapping> parameterMappings) {
        TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();

        if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            return sql.replaceFirst("\\?", Matcher.quoteReplacement(formatParameter(parameterObject)));
        }

        MetaObject metaObject = configuration.newMetaObject(parameterObject);
        return replaceSqlParameters(sql, parameterMappings, metaObject, boundSql);
    }

    /**
     * 替换 SQL 中的参数
     */
    private String replaceSqlParameters(String sql, List<ParameterMapping> parameterMappings,
                                      MetaObject metaObject, BoundSql boundSql) {
        for (ParameterMapping parameterMapping : parameterMappings) {
            String propertyName = parameterMapping.getProperty();
            Object value = null;

            if (metaObject.hasGetter(propertyName)) {
                value = metaObject.getValue(propertyName);
            } else if (boundSql.hasAdditionalParameter(propertyName)) {
                value = boundSql.getAdditionalParameter(propertyName);
            }

            String paramValue = value != null ? formatParameter(value) : MISSING_PARAM;
            sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(paramValue));
        }

        // 美化 SQL
        return beautifySql(sql);
    }

    /**
     * 格式化参数值
     */
    private String formatParameter(Object parameter) {
        if (parameter == null) {
            return "NULL";
        }

        if (parameter instanceof String) {
            return "'" + escapeString(parameter.toString()) + "'";
        }

        if (parameter instanceof Date) {
            return "'" + DATE_FORMATTER.format(LocalDateTime.now()) + "'";
        }

        if (parameter instanceof Boolean) {
            return parameter.toString().toUpperCase(Locale.ROOT);
        }

        return parameter.toString();
    }

    /**
     * 转义字符串中的特殊字符
     */
    private String escapeString(String str) {
        return str.replace("'", "''");
    }

    /**
     * 美化 SQL 语句
     */
    private String beautifySql(String sql) {
        sql = sql.replaceAll("(?i)select", LINE_SEPARATOR + "SELECT")
                .replaceAll("(?i)from", LINE_SEPARATOR + "FROM")
                .replaceAll("(?i)where", LINE_SEPARATOR + "WHERE")
                .replaceAll("(?i)and", LINE_SEPARATOR + "  AND")
                .replaceAll("(?i)or", LINE_SEPARATOR + "  OR")
                .replaceAll("(?i)group by", LINE_SEPARATOR + "GROUP BY")
                .replaceAll("(?i)having", LINE_SEPARATOR + "HAVING")
                .replaceAll("(?i)order by", LINE_SEPARATOR + "ORDER BY")
                .replaceAll("(?i)left join", LINE_SEPARATOR + "LEFT JOIN")
                .replaceAll("(?i)right join", LINE_SEPARATOR + "RIGHT JOIN")
                .replaceAll("(?i)inner join", LINE_SEPARATOR + "INNER JOIN");

        return sql;
    }
}

最后只需要在MyBatisPlus中注册这个拦截器即可

@Configuration
public class MybatisConfiguration {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new MybatisPlusAllSqlLog());
        return mybatisPlusInterceptor;
    }

}

Nacos实现动态Bean加载

  • SpringBoot版本: 2.4.2
  • Spring Cloud Alibaba版本: 2021.1
  • Spring Cloud版本: 2020.0.6
  • Nacos版本: 2.0.1

Nacos实现动态Bean其实十分简单,只需要在 Pom 文件中正确的引入相关配置后在Java程序的配置类和Bean实例上使用 @RefreshScope 就可以实现动态Bean的加载,此时在nacos中发布配置时,无需重启项目就可以使用新的配置。

集成Naocs

配置Naocs

集成Naocs需要在Pom文件中导入两个核心的配置

<!-- Nacos 配置中心 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

<!-- Spring Cloud Bootstrap -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

在这之前是需要 SpringCloud 相关依赖的

<dependencyManagement>
      <dependencies>
          <!-- Spring Cloud 依赖管理 -->
          <dependency>
              <groupId>org.springframework.cloud</groupId>
              <artifactId>spring-cloud-dependencies</artifactId>
              <version>${spring-cloud.version}</version>
              <type>pom</type>
              <scope>import</scope>
          </dependency>

          <!-- Spring Cloud Alibaba 依赖管理 -->
          <dependency>
              <groupId>com.alibaba.cloud</groupId>
              <artifactId>spring-cloud-alibaba-dependencies</artifactId>
              <version>${spring-cloud-alibaba.version}</version>
              <type>pom</type>
              <scope>import</scope>
          </dependency>
      </dependencies>
  </dependencyManagement>

接着就需要在项目的 appliaction.yml 同级目录下编写一个 bootstrap.yml 文件,这个文件会在 application.yml 加载之前先去 Nacos 中读取配置。

spring:
  application:
    name: jc-club-oss-dev
  profiles:
    active: dev
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848 # Nacos 配置中心地址
        prefix: ${spring.application.name} # 配置文件前缀,使用应用名变量,也就是 dev
        group: DEFAULT_GROUP # 配置分组,默认为 DEFAULT_GROUP。
        namespace: # 指定命名空间,当前为空,表示使用默认命名空间。
        file-extension: yaml #  配置文件格式为 yaml。
        username: nacos # 账号
        password: nacos # 密码

这段配置会读取Naocs中名称为 jc-club-oss-dev 且分组为 DEFAULT_GROUP 的配置,即下图中的配置

https://aetherial.cn/oss/aetherial/2025/08/4e8285c6-da64-4d82-bd83-dabd8c3299d6_image.png

创建配置类接收Nacos中的配置

成功读取配置后需要创建相关的配置类读取Nacos中的配置,关键注解是 @RefreshScope 具体如下:

@Configuration
@RefreshScope
public class NacosConfig {

    @Value("${service.type}")
    private String StringType; // 从 Nacos 获取存储类型配置

    @Bean
    @RefreshScope
    public String nacosService() {
        return "配置为:" + StringType;
    }
}

这样配置后,在nacos中当配置发生变化时,重新创建被该注解标记的 Bean。具体的流程是:配置更新 -> 销毁旧的 Bean -> 重新调用 nacosService() -> 创建新的 Bean,至此,就完成了动态Bean的加载。

适配器模式配置Minio

了解了Nacos实现动态Bean的加载,就可以直接运用到Minio的配置中。在项目中,如果OSS存储服务会出现频繁变更的情况下,就可以使用Nacos的动态Bean配合适配器的设计模式实现动态无感的切换OSS服务产商。

第一步需要创建一个适配器接口,在这个接口中定义每一个适配器都需要实现的方法,适配器实现这个接口并完成相关方法的重写,需要几个OSS服务产商就创建几个适配器实现类

public interface StorageAdapter {

    /**
     * 上传文件
     */
    void uploadFile(MultipartFile uploadFile, String bucket, String objectName);

    /**
     * 获取文件下载地址
     */
    String getUrl(String bucket, String objectName);
}

第二步需要配合Naocs实现Minio配置的动态Bean加载。在构造函数中初始化适配器工厂的映射,并在Bean的实例中根据Naocs中动态加载的配置返回对应的适配器

@Configuration
@RefreshScope
public class StorageConfig {

    @Value("${storage.service.type}")
    private String storageType; // 读取nacos中的Minio服务类型

    private final Map<StorageType, StorageAdapterFactory> storageFactories;

    /**
     * 存储适配器工厂映射
     */
    private final Map<StorageType, StorageAdapterFactory> storageFactories;

    /**
     * 构造函数,初始化存储适配器工厂映射
     */
    public StorageConfig() {
        this.storageFactories = new EnumMap<>(StorageType.class);
        this.storageFactories.put(StorageType.MINIO, MinioStorageAdapter::new);
        this.storageFactories.put(StorageType.ALIYUN, AliStorageAdapter::new);
    }

    /**
     * 获取存储服务适配器
     * @return
     */
    @Bean
    @RefreshScope
    public StorageAdapter storageService() {
        StorageType type = StorageType.fromCode(storageType);
        StorageAdapterFactory factory = storageFactories.get(type);

        if (factory == null) {
            throw new IllegalArgumentException("未找到对应的文件存储处理器: " + storageType);
        }

        return factory.create();
    }
}