在复杂的企业应用中,多数据源管理是常见需求。本文将介绍如何基于Spring Boot实现优雅的动态数据源切换方案,通过自定义注解和AOP实现透明化切换。

核心设计思路

通过三层结构实现数据源动态路由:
1. 注解层:声明式标记数据源
2. 路由层:基于ThreadLocal的上下文管理
3. 切面层:在方法执行前后自动切换数据源

核心实现代码

1. 数据源注解定义

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {String name() default ""; // 数据源名称
}

2. 动态数据源上下文

public class DynamicDataSourceContext extends AbstractRoutingDataSource {private static String DEFAULT_DATASOURCE_NAME;private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();public DynamicDataSourceContext(String defaultDataSourceName, Map<Object, Object> targetDataSources) {super.setDefaultTargetDataSource(targetDataSources.get(defaultDataSourceName));super.setTargetDataSources(targetDataSources);DEFAULT_DATASOURCE_NAME = defaultDataSourceName;super.afterPropertiesSet(); // 关键初始化}@Overrideprotected Object determineCurrentLookupKey() {return getDataSourceKey(); // 获取当前数据源标识}// 数据源操作工具方法public static void setDataSourceKey(String key) {CONTEXT_HOLDER.set(key);}public static String getDataSourceKey() {return CONTEXT_HOLDER.get(); }public static void clearDataSourceKey() {CONTEXT_HOLDER.remove(); }public static String getDefaultDataSourceName() {return DEFAULT_DATASOURCE_NAME;}
}

3. AOP切面实现

@Aspect
@Component
@Order(-1) // 确保在事务切面前执行
public class DataSourceAspect {@Around("@within(DataSource) || @annotation(DataSource)")public Object around(ProceedingJoinPoint point) throws Throwable {MethodSignature signature = (MethodSignature) point.getSignature();Method method = signature.getMethod();// 优先级:方法注解 > 类注解 > 默认数据源DataSource methodAnno = method.getAnnotation(DataSource.class);DataSource classAnno = method.getDeclaringClass().getAnnotation(DataSource.class);String dataSource = DynamicDataSourceContext.getDefaultDataSourceName();if (methodAnno != null && StringUtils.hasText(methodAnno.name())) {dataSource = methodAnno.name();} else if (classAnno != null && StringUtils.hasText(classAnno.name())) {dataSource = classAnno.name();}try {DynamicDataSourceContext.setDataSourceKey(dataSource);return point.proceed(); // 执行目标方法} finally {DynamicDataSourceContext.clearDataSourceKey(); // 清理数据源标识}}
}

自动配置

在`src/main/resources/META-INF/spring`目录下创建文件:
org.springframework.boot.autoconfigure.AutoConfiguration.imports

com.test.datasourcestater.aspect.DataSourceAspect

使用示例

1、新增动态切换数据源定义

@Configuration
@Component
public class DynamicDataSourceConfig {//默认数据源定义@Resource(name = "defaultDataSource")private DataSource defaultDataSource;//其他数据源定义@Resource(name = "testDataSource")private DataSource testDataSource;@Bean("dynamicDataSource")@Primarypublic DynamicDataSourceContext dynamicDataSource() {Map<Object, Object> targetDataSources = new HashMap<>();//添加默认数据源和其他数据源targetDataSources.put("default",defaultDataSource);targetDataSources.put("testDataSource",testDataSource);return new DynamicDataSourceContext("default", targetDataSources);}@Bean("dataSourceTransactionManager")public DataSourceTransactionManager dataSourceTransactionManager(@Qualifier("dynamicDataSource") DataSource dataSource){DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();dataSourceTransactionManager.setDataSource(dataSource);return dataSourceTransactionManager;}@Bean("jdbcTemplate")public JdbcTemplate jdbcTemplate(@Qualifier("dynamicDataSource") DataSource dataSource){JdbcTemplate jdbcTemplate = new JdbcTemplate();jdbcTemplate.setDataSource(dataSource);return  jdbcTemplate;}
}

其他数据源定义:

@Configuration
@ConfigurationProperties(prefix = "xxx")
@Data
public class TestDataSourceConfig {private String driverClassName;private String url;private String username;private String password;private String validationQuery;private int initialSize;private int maxActive;private int maxIdle;private int minIdle;@Bean("testDataSource")public BasicDataSourceDecrypt basicDataSourceDecrypt(){BasicDataSourceDecrypt basicDataSourceDecrypt = new BasicDataSourceDecrypt();basicDataSourceDecrypt.setUsername(username);basicDataSourceDecrypt.setPassword(password);basicDataSourceDecrypt.setDriverClassName(driverClassName);basicDataSourceDecrypt.setPoolName("xxx");// 数据库连接地址basicDataSourceDecrypt.setJdbcUrl(url);//  最小空闲连接,默认值10,小于0或大于maximum-pool-size,都会重置为maximum-pool-sizebasicDataSourceDecrypt.setMinimumIdle(minIdle);// 最大连接数,小于等于0会被重置为默认值10;大于零小于1会被重置为minimum-idle的值basicDataSourceDecrypt.setMaximumPoolSize(maxActive);// 空闲连接超时时间,默认值600000(10分钟),大于等于max-lifetime且max-lifetime>0,会被重置为0;不等于0且小于10秒,会被重置为10秒。basicDataSourceDecrypt.setIdleTimeout(30000);// 连接最大存活时间,不等于0且小于30秒,会被重置为默认值30分钟.设置应该比mysql设置的超时时间短basicDataSourceDecrypt.setMaxLifetime(360000L);// 连接超时时间:毫秒,小于250毫秒,否则被重置为默认值30秒basicDataSourceDecrypt.setConnectionTimeout(500);// 用于测试连接是否可用的查询语句basicDataSourceDecrypt.setConnectionTestQuery(validationQuery);return basicDataSourceDecrypt;}
}

2、修改原始的DataSourceConfig为默认数据源

  • 将datasource bean 定义名称改成@Bean("defaultDataSource")
  • 将@Qualifier("dataSource")改成@Qualifier("dynamicDataSource") 参考代码,如下:
@Configuration
@ConfigurationProperties(prefix = "xxx")
@Data
public class DataSourceConfig {private String driverClassName;private String url;private String username;private String password;private String validationQuery;private int initialSize;private int maxActive;private int maxIdle;private int minIdle;@Bean("defaultDataSource")public BasicDataSourceDecrypt basicDataSourceDecrypt(){BasicDataSourceDecrypt basicDataSourceDecrypt = new BasicDataSourceDecrypt();basicDataSourceDecrypt.setDriverClassName(driverClassName);basicDataSourceDecrypt.setUsername(username);basicDataSourceDecrypt.setPassword(password);basicDataSourceDecrypt.setPoolName("xx");// 数据库连接地址basicDataSourceDecrypt.setJdbcUrl(url);//  最小空闲连接,默认值10,小于0或大于maximum-pool-size,都会重置为maximum-pool-sizebasicDataSourceDecrypt.setMinimumIdle(minIdle);// 最大连接数,小于等于0会被重置为默认值10;大于零小于1会被重置为minimum-idle的值basicDataSourceDecrypt.setMaximumPoolSize(maxActive);// 空闲连接超时时间,默认值600000(10分钟),大于等于max-lifetime且max-lifetime>0,会被重置为0;不等于0且小于10秒,会被重置为10秒。basicDataSourceDecrypt.setIdleTimeout(60000);// 连接最大存活时间,不等于0且小于30秒,会被重置为默认值30分钟.设置应该比mysql设置的超时时间短basicDataSourceDecrypt.setMaxLifetime(600000);// 连接超时时间:毫秒,小于250毫秒,否则被重置为默认值30秒basicDataSourceDecrypt.setConnectionTimeout(30000);// 用于测试连接是否可用的查询语句basicDataSourceDecrypt.setConnectionTestQuery(validationQuery);return basicDataSourceDecrypt;}@Bean("messageResource")public ResourceBundleMessageSource resourceBundleMessageSource(){ResourceBundleMessageSource messageResource = new ResourceBundleMessageSource();messageResource.setDefaultEncoding("UTF-8");messageResource.setCacheSeconds(0);return  messageResource;}@Bean("dataSourceTransactionManager")public DataSourceTransactionManager dataSourceTransactionManager(@Qualifier("dynamicDataSource") BasicDataSourceDecrypt dataSource){DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();dataSourceTransactionManager.setDataSource(dataSource);return dataSourceTransactionManager;}@Bean("jdbcTemplate")public JdbcTemplate jdbcTemplate(@Qualifier("dynamicDataSource") BasicDataSourceDecrypt dataSource){JdbcTemplate jdbcTemplate = new JdbcTemplate();jdbcTemplate.setDataSource(dataSource);return  jdbcTemplate;}}

3、修改mybatisPlusConfig配置

dataSource注入,改成注入@Resource(name="dynamicDataSource")

@Configuration
@EnableTransactionManagement
public class MybatisPlusConfig {@Resource(name="dynamicDataSource")private DataSource dataSource;@Autowiredprivate MybatisPlusProperties properties;@Autowiredprivate ResourceLoader resourceLoader = new DefaultResourceLoader();@Autowired(required = false)private DatabaseIdProvider databaseIdProvider;@Beanpublic DatabaseIdProvider getDatabaseIdProvider() {DatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();Properties properties = new Properties();databaseIdProvider.setProperties(properties);return databaseIdProvider;}/***    mybatis-plus分页插件*/@Bean("paginationInterceptor")public PaginationInnerInterceptor paginationInterceptor(@Value("${database.type:mysql}") String databaseType) {PaginationInnerInterceptor page = new PaginationInnerInterceptor();page.setDbType(DbType.getDbType(databaseType));return page;}@Bean("optimisticLockerInterceptor")public OptimisticLockerInnerInterceptor optimisticLockerInterceptor() {return new OptimisticLockerInnerInterceptor();}@Bean("mybatisPlusInterceptor")public MybatisPlusInterceptor mybatisPlusInterceptor(@Qualifier("paginationInterceptor") PaginationInnerInterceptor paginationInterceptor,@Qualifier("optimisticLockerInterceptor") OptimisticLockerInnerInterceptor optimisticLockerInterceptor){MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();mybatisPlusInterceptor.addInnerInterceptor(paginationInterceptor);mybatisPlusInterceptor.addInnerInterceptor(optimisticLockerInterceptor);return mybatisPlusInterceptor;}/*** 这里全部使用mybatis-autoconfigure 已经自动加载的资源。不手动指定* 配置文件和mybatis-boot的配置文件同步* @return*/@Bean("sqlSessionFactory")public MybatisSqlSessionFactoryBean mybatisSqlSessionFactoryBean(@Qualifier("globalConfiguration") GlobalConfig globalConfig,@Qualifier("mybatisPlusInterceptor") MybatisPlusInterceptor mybatisPlusInterceptor) {MybatisSqlSessionFactoryBean mybatisPlus = new MybatisSqlSessionFactoryBean();mybatisPlus.setDataSource(dataSource);mybatisPlus.setVfs(SpringBootVFS.class);if (StringUtils.hasText(this.properties.getConfigLocation())) {mybatisPlus.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));}mybatisPlus.setConfiguration(properties.getConfiguration());mybatisPlus.setPlugins(mybatisPlusInterceptor);mybatisPlus.setGlobalConfig(globalConfig);MybatisConfiguration mc = new MybatisConfiguration();mc.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class);mybatisPlus.setConfiguration(mc);if (this.databaseIdProvider != null) {mybatisPlus.setDatabaseIdProvider(this.databaseIdProvider);}if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {mybatisPlus.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());}if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {mybatisPlus.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());}if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {mybatisPlus.setMapperLocations(this.properties.resolveMapperLocations());}return mybatisPlus;}@Bean("globalConfiguration")public GlobalConfig globalConfig(@Qualifier("myMetaObjectHandler") ModelMetaObjectHandler myMetaObjectHandler,@Qualifier("customIdGenerator") CustomerIdGenerator customIdGenerator){GlobalConfig globalConfig = new GlobalConfig();globalConfig.setMetaObjectHandler(myMetaObjectHandler);globalConfig.setIdentifierGenerator(customIdGenerator);return globalConfig;}@Bean("myMetaObjectHandler")public ModelMetaObjectHandler modelMetaObjectHandler(){return new ModelMetaObjectHandler();}@Bean("customIdGenerator")public CustomerIdGenerator customerIdGenerator(){return new CustomerIdGenerator();}
}

4、使用说明

在Controller层、Service层和Dao层的方法或者类加上@DataSource(name="数据源名字")注解,完成数据源的自动切换,其中数据源名字来自DynamicDataSourceConfig中dynamicDataSource方法中定义的数据源

4.1、类级别注解

在类上添加多数据源注解,类中的所有方法都是使用注解中设置的数据源

4.1.1、Controller层用法
@RestController
@DataSource(name = "default")
@RequestMapping("/Order")
public class OrderController{// 所有方法默认使用default数据源@GetMapping("/list")public List<Order> queryAll() {// ...}// 默认使用default数据源@GetMapping("/{id}")public Order selectById(@PathVariable Long id) {// ...}
}
4.1.2、Service层用法

Service层使用多数据源注解时,需使用在@Service修饰的类上多数据源注解才能生效

@Service
@DataSource(name = "default")
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {// 所有方法默认使用default数据源public List<Order> findAll() {// ...}// 默认使用default数据源public Order findById() {// ...}
}
4.1.3、Dao层用法

Dao层使用多数据源注解时,需使用在@Component、@Repository或者@Mapper修饰的Dao层接口上多数据源注解才能生效

@Component
@DataSource(name = "default")
public interface OrderMapper extends BaseMapper<Order> {// 使用default数据源Order getById(@Param("id")Long id);// 使用default数据源List<Order> getList();}

4.2、方法级别注解

在方法上添加多数据源注解,具体方法使用注解中设置的数据源

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{// 使用testDataSource数据源@DataSource(name = "testDataSource")public User getUserById(Long id) {// ...}// 使用default数据源@DataSource(name = "default") public User deleteUserById(Long id) {// ...}// 使用默认数据源(default数据源)public void updateUser(User user) {// ...}
}

Controller层与Dao层用法与类级别注解部分的使用介绍类似,此处不再赘述。

4.3、混合使用

多数据源注解的优先级别:方法级别注解>类级别注解>无注解(默认数据源)

当类和方法中都使用多数据源注解,会按照优先级别选择具体数据源

@Service
@DataSource(name = "testDataSource")
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService{// 继承类注解,使用testDataSource数据源public Product getProduct(Long id) {// ...}// 方法级别注解的优先级最高,优先使用方法注解,使用default数据源@DataSource(name = "default")public void updateProduct(Product product) {// ...}
}

Controller层与Dao层用法与类级别注解部分的使用介绍类似,此处不再赘述。

5、注意事项

⚠️ 重要限制:由于数据源切换基于AOP,与@Transactional注解联用时需注意:

  1. 事务注解应加在数据源注解外层
// ✅ 正确:事务在外层
@Transactional
@DataSource(name = "slave1")
public void transactionalMethod() { /* ... */ }// ❌ 危险:数据源切换可能不生效
@DataSource(name = "slave1")
@Transactional
public void riskyMethod() { /* ... */ }

总结

本文实现的多数据源方案具有以下优势:
1. 非侵入式:通过注解透明切换,不影响业务逻辑
2. 灵活配置:支持方法级和类级数据源指定
3. 线程安全:基于ThreadLocal的上下文管理
4. 易于扩展:可快速添加新数据源

通过这种设计,开发者可以轻松管理多个数据源,特别适用于多租户系统、读写分离、分库分表等复杂场景。完整代码已托管至Gitee,gitee地址:https://gitee.com/mutigmss/multiple-data-source-stater

欢迎在评论区交流使用体验和优化建议!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/86224.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/86224.shtml
英文地址,请注明出处:http://en.pswp.cn/bicheng/86224.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何挑选一款1588PTP时钟同步服务器​

在当今数字化程度极高的时代&#xff0c;高精度时间同步对于众多关键领域的高效、稳定运行起着决定性作用。PTP&#xff08;精确时间协议&#xff09;时钟作为实现高精度时间同步的核心设备&#xff0c;其性能优劣直接关乎系统整体表现。挑选一款合适的 ptp网络同步时钟&#x…

Harmony状态管理 @Local和@Param

深入理解ArkUI中的Param与Local装饰器 引言 在ArkUI的状态管理系统中&#xff0c;Param和Local是两个核心装饰器&#xff0c;它们分别用于处理组件间的数据传递和组件内部状态管理。本文将详细介绍这两个装饰器的使用场景、特性差异以及最佳实践。 Param装饰器&#xff1a;组…

物联网摄像头模块的应用场景

一、智慧城市治理 ‌智能交通优化‌ ‌动态信号控制‌&#xff1a;杭州部署20万物联网摄像头&#xff0c;实时分析车流密度并联动1200个红绿灯&#xff0c;早高峰通行效率提升40%。 ‌违规行为识别‌&#xff1a;搭载GB/T28181协议的摄像头AI抓拍交通违章&#xff0c;车牌识…

k8s Ingress、Service配置各样例大全

目录 壹、k8s Ingress 样例大全&#x1f527; 一、基础路由与 TLS 终止&#x1f504; 二、高级路由控制1. **URL 重写**&#xff08;适用后端服务路径与入口路径不一致&#xff09;2. **多路径路由到不同服务** &#x1f6a6; 三、流量治理策略1. **金丝雀发布&#xff08;灰度…

领域驱动设计(DDD)【10】之DDD战术模式:工厂模式与表意接口模式

文章目录 引言&#xff1a;DDD战术模式的重要性一、DDD中的工厂模式1.1 工厂模式的核心概念1.2 工厂模式的三种实现方式1.2.1 简单工厂方法1.2.2 工厂类1.2.3 抽象工厂模式 1.3 工厂模式的适用场景1.4 实际案例&#xff1a;电商订单系统 二、表意接口模式2.1 表意接口2.2 表意接…

鸿蒙ArkTS---登录逻辑,数据持久化,ArkUI,网络请求等基础内容记录

该内容是在【博学谷】学习过程中的代码记录&#xff0c;如有任何问题请与作者联系。 也欢迎同在学习鸿蒙开发的小伙伴的留言&#xff0c;一同学习&#xff0c;一同进步。 功能实现&#xff08;只记录代码&#xff0c;没有相关配置&#xff0c;跑不起来&#xff09;&#xff…

没有公网ip可以实现跨网p2p互通吗?内网让公网直连访问常用工具

没有公网IP的情况下仍然可以实现P2P通信&#xff0c;但需要借助NAT穿透技术或类似nat123同端口映射等第三方工具实现内网穿透‌。‌‌‌‌ 一、什么是P2P通信&#xff1f; P2P网络&#xff08;Peer-to-Peer Network&#xff09;是一种去中心化的网络架构&#xff0c;其中每个…

云服务器安装宝塔面板(BT Panel)

安装宝塔面板&#xff08;BT Panel&#xff09;是很多服务器管理员常用的操作&#xff0c;尤其适合用于管理网站、数据库、FTP等。以下是基于 Linux 系统&#xff08;推荐 CentOS 或 Ubuntu&#xff09;的宝塔面板安装步骤。 安装前准备 云服务器一台 可以订购服务器 海外云主…

mongoose解析http字段值

最近在使用mongoose开发嵌入式web后端时&#xff0c;会遇到要解析js前端发送过来的http消息&#xff0c;比如传递用户名&#xff0c;密码过来&#xff0c;后端要解析出来并判断是否登录成功。 前端http有两种组装字段的方式。 第一种是 $.ajax({url: /upgradePackage,method: P…

高德地图地址解析获取经纬度失败原因JSAPI

高德地图地址解析获取经纬度失败原因JSAPI 地图加载的时候老是报异常码&#xff0c;地图是可以加载出来的&#xff0c;但是在地图上的操作老是有异常码&#xff0c;找了好久不知道什么问题&#xff0c;异常码会报两种&#xff0c;一种是说什么key的问题&#xff0c;但是我当时…

极速JavaScript:全面性能优化实战指南

在现代Web开发中&#xff0c;JavaScript性能直接影响用户体验。一个优化良好的应用能带来更流畅的交互、更快的加载速度和更低的资源消耗。本文将深入探讨实用的JavaScript性能优化技术&#xff0c;帮助您打造高性能Web应用。 一、性能瓶颈分析与诊断工具 性能问题的常见来源&…

【开源模型】高考数学139分!小米MiMo开源模型:7B参数突出重围

小米 MiMo&#xff1a;7 B 参数撬动推理巅峰&#xff0c;开源模型的技术突围 70 亿参数超越 320 亿对手&#xff0c;高考数学 139 分的背后是训练策略的全面革新。 2025 年 4 月 30 日&#xff0c;小米开源的首个推理大模型 Xiaomi MiMo-7 B 横空出世&#xff0c;以​​仅 7 B …

用vscode破解最新typora1.10.8

1.下载格式化插件防止打开文件一团乱 1&#xff09;下载vscode&#xff1a; Download Visual Studio Code - Mac, Linux, Windows 2&#xff09;vscode下载中文插件重启 如果没变中文&#xff0c;在vscode界面按下&#xff1a; ctrl shift p 调出命令行 再输入&#xff…

在 CI/CD 流程中使用 Jenkins 与 Docker 集成

在 CI/CD 流程中&#xff0c;Jenkins 与 Docker 的集成可以实现自动构建、测试、打包、发布容器镜像&#xff0c;并部署到测试/生产环境。下面是从概念到落地操作的完整集成方案。 一、常见的集成方式有哪些&#xff1f; 方式描述1️⃣ Jenkins 主机安装 DockerJenkins 可以直…

闲庭信步使用SV搭建图像测试平台:第十课——继续说说类

&#xff08;本系列只需要modelsim即可完成数字图像的处理&#xff0c;每个工程都搭建了全自动化的仿真环境&#xff0c;只需要双击top_tb.bat文件就可以完成整个的仿真&#xff0c;大大降低了初学者的门槛&#xff01;&#xff01;&#xff01;&#xff01;如需要该系列的工程…

如何改进复杂推理 - 从提示词设计入手

引言&#xff08;动机&#xff09; 在使用大语言模型&#xff08;如 GPT-4、Claude、DeepSeek 等&#xff09;构建智能问答、辅助决策或复杂任务代理系统时&#xff0c;可能遇到这些问题&#xff1a; 模型回答跳步骤、思路混乱同样问题&#xff0c;模型表现高度不稳定新任务一…

如何解决和各个经销商不同软件对接的问题?汤臣案例分享

一、项目背景 汤臣倍健作为健康产品行业的领军企业&#xff0c;其营销云系统与全国经销商 ERP 系统的数据无缝对接&#xff0c;对于提升业务运营效率和营销精准度至关重要。传统数据集成方法在面对经销商 ERP 系统的多样性和复杂性时&#xff0c;暴露出诸多问题&#xff0c;如…

Wordvice AI:Wordvice 推出的免费,基于先进的 AI 技术帮助用户提升英文写作质量

Wordvice AI&#xff1a;智能写作助手&#xff0c;助力高效英文写作 在当今全球化时代&#xff0c;英文写作已成为众多学生、研究人员、职场人士必备技能。然而&#xff0c;语法错误、表达不流畅、词汇匮乏等问题常困扰着大家。别担心&#xff0c;今天就来给大家介绍一款强大的…

【UE5】如何开发安卓项目的udp客户端

1关于如何打包安卓项目这里就不赘述了 2代码举例。最重要的就是这两句 #if PLATFORM_ANDROID #endif#if PLATFORM_WINDOWS #endif全部代码如下&#xff1a; Button_Sheng.h: // Fill out your copyright notice in the Description page of Project Settings.#pragma once#in…

2025年6月21和22日复习和预习(python)

一、作业内容 &#xff08;一&#xff09;知识点回顾 用户输入处理 使用input()函数获取用户输入的字符串&#xff0c;并存储到变量中。 条件判断语句 if-elif-else结构&#xff1a;根据不同条件执行相应代码块&#xff0c;适用于多分支判断。 语音合成技术 导入pyttsx3库实现…