鸡翅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执行前将完整的语句打印出来,可以直接复制到相关的数据库工具中执行,可以更快的定位问题。
第一步需要创建一个拦截器
拦截器需要实现 MyBatisPlus 的 InnerInterceptor,具体的包名为 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 的配置,即下图中的配置

创建配置类接收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();
}
}