news 2026/9/9 11:13:02

Spring Boot多数据源实战:PostgreSQL与SQL Server动态切换全攻略

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot多数据源实战:PostgreSQL与SQL Server动态切换全攻略

项目跑得好好的,突然产品提了一个需求,说要把 PostgreSQL 里的业务数据和另一台 SQL Server 上的历史数据放到一起看。数据量不是特别大,但每次都开两个连接写代码又麻烦,于是“Spring Boot 多数据源”这几个字就被提上了日程。我正在做的一个数据交换服务就是这么干的,现在把连接 PostgreSQL 和 SQL Server 的实践过程完整整理出来,从依赖配置、动态切换、事务处理到排坑实录,一次讲透。

如果你最近也在搞 Spring Boot 多数据源,或者准备接 PostgreSQL 和 SQL Server 这两个库,这篇文章应该能帮你少走不少弯路。我会先用“为什么”的角度把方案选型讲清楚,再给出可以直接抄的代码和配置,最后把我在实操里踩过的坑一个个列出来。

1. 多数据源需求分析:不是所有项目都适合硬上

1.1 真实业务场景长什么样

多数据源并不是一个炫技的需求,而是现实逼迫的产物。最常见的情况有四种:

  • 老系统用的是 SQL Server,新系统切换到了 PostgreSQL,在过渡阶段需要两边同时读。
  • 业务库在 PostgreSQL,报表库在 SQL Server,服务层要同时查询两边然后聚合结果。
  • 做数据清洗和迁移工具,比如定时从 SQL Server 拉取增量数据写入 PostgreSQL。
  • 多个业务系统之间不开放数据库直连,只允许通过应用层做跨库“联表查询”。

这几种场景都有一个共同特点:你没办法在数据库层面直接做跨库 join,也没办法让 DBA 把数据同步好,最直接的做法就是在同一个 Spring Boot 进程里维护两个数据源。

有人会问,为什么不拆成两个独立服务?两个服务确实更干净,但如果只是为了在一个接口里返回聚合结果,拆服务会增加网络开销、部署成本和事务协调成本。在一段时间内,用一个应用同时连两个数据库,反而是比较划算的方案。

1.2 三种实现思路对比,别一上来就写代码

我在调研和实际落地过程中,见过下面这三种主流做法:

方案实现思路优点缺点
手动获取连接在代码里分别持有两个 DataSource,按业务硬编码调用逻辑直白,无框架依赖侵入性太强,每个方法都要写连接获取和异常处理
多套 SqlSessionFactory 分包扫描按 mapper 包分开绑定不同数据源数据源隔离彻底,事务边界清晰配置量较大,同一个实体类要维护多套 Mapper
AbstractRoutingDataSource + AOP运行时通过路由键切换数据源代码侵入小,原理清晰,主流方案需要自己写上下文和切面,事务处理要小心

我的选择是第三种,也就是 Spring 官方提供的AbstractRoutingDataSource,结合自定义注解和 AOP 实现动态切换。这样做的核心原因有两个:第一,它不需要引入额外重量级框架,底层原理就是路由;第二,所有切换逻辑收敛到一个切面里,业务代码几乎无感,后期维护成本低。

2. 依赖和配置准备:版本兼容是第一道大坑

2.1 版本环境怎么选

先说环境。我当时用的是 Spring Boot 2.7.x + JDK 8,数据库这边 PostgreSQL 13,SQL Server 2019。这个组合比较稳,网上资料也最多。

如果你用 Spring Boot 3.x,那需要 JDK 17,并且 MyBatis-Plus 要升级到 3.5.3 以上才兼容jakarta命名空间。SQL Server 驱动方面,微软的mssql-jdbc从 9.x 开始只支持 Java 8+,到了 12.x 对老版本 SQL Server 的支持做了收窄,如果连接 SQL Server 2008 R2,建议用mssql-jdbc:7.4.0.jre8甚至更低版本。

PostgreSQL 驱动相对简单,直接使用org.postgresql:postgresql:42.6.0就行。它要求 PostgreSQL 8.2 以上,基本都能满足。

2.2 Maven 依赖引入

我使用的是 MyBatis-Plus,所以 pom 里引入了mybatis-plus-boot-starter。如果你不想用 MyBatis-Plus,换成mybatis-spring-boot-starter也可以,核心的数据源逻辑完全一样。

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.6.0</version> </dependency> <dependency> <groupId>com.microsoft.sqlserver</groupId> <artifactId>mssql-jdbc</artifactId> <version>12.2.0.jre8</version> </dependency>

这里有个容易踩的坑:如果连接老版本 SQL Server 2008 R2,mssql-jdbc12.x 很可能尝试连接时直接报“驱动程序不支持此 SQL Server 版本”。所以本地开发要先确认目标库版本,再决定驱动版本。不要盲目用最新的驱动。

2.3 application.yml 双数据源配置

Spring Boot 常规配置只认spring.datasource.url,但多数据源时我们会用自定义前缀spring.datasource.primaryspring.datasource.secondary,配合@ConfigurationProperties绑定。

spring: datasource: primary: jdbc-url: jdbc:postgresql://localhost:5432/business_db username: postgres password: postgres driver-class-name: org.postgresql.Driver hikari: pool-name: HikariPool-PG maximum-pool-size: 10 minimum-idle: 2 secondary: jdbc-url: jdbc:sqlserver://localhost:1433;DatabaseName=history_db;encrypt=true;trustServerCertificate=true username: sa password: YourStrongPass driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver hikari: pool-name: HikariPool-MSSQL maximum-pool-size: 5 minimum-idle: 1

注意两个地方:

第一,自定义前缀的 HikariCP 配置,字段必须用jdbc-url,不能用url。因为 Spring Boot 的DataSourceBuilder在绑定HikariDataSource时,只认jdbc-url,写成url会导致连接池启动时报错或者根本读不到连接地址。这个细节坑了好几个人。

第二,SQL Server 连接串里面的参数用分号分隔,而不是?接 query string。DatabaseName=history_db指定库名;encrypt=true;trustServerCertificate=true是微软驱动对 SSL 加密的应对方案,开发环境先这样放开,生产环境请使用真实证书。

2.4 Spring Boot 高版本注意事项

现在不少人直接上 Spring Boot 3.x,结果发现老一套代码跑不起来。这里单独说几句。

Spring Boot 3 从javax迁移到了jakarta,如果你用 MyBatis-Plus 3.4.x,会直接抛 ClassNotFound。所以要么老老实实待在 Boot 2.7.x + JDK 8,要么把 MyBatis-Plus、SQL Server 驱动、PostgreSQL 驱动全部升级到对应的高版本。

另外,Spring Boot 3.x 里配置 SQL Server 连接串时,同样要注意新版驱动默认要求 TLS 加密。很多人在本地连 SQL Server 报“The driver could not establish a secure connection to SQL Server by using Secure Sockets Layer (SSL) encryption”,就是因为没有加trustServerCertificate=true

3. 核心实现:用 AbstractRoutingDataSource 做动态数据源

3.1 路由原理:其实就是一张 Map 加一个线程变量

AbstractRoutingDataSource是 Spring 提供的一个抽象类,内部维护了两个关键字段:targetDataSourcesdefaultTargetDataSourcetargetDataSources是一个 Map,key 是路由键,value 是真实的数据源对象。

当调用getConnection()时,Spring 会先调用determineCurrentLookupKey()拿当前路由键,然后用这个键去 Map 里找真正要用的数据源。我们要做的事情很清晰:

  • 定义两个路由键,分别对应 PostgreSQL 和 SQL Server。
  • 用一个 ThreadLocal 保存当前线程的路由键。
  • 在业务方法执行前设置路由键,执行后清理。
  • 用 AOP 统一处理“设置”和“清理”两个动作。

3.2 先定义枚举和线程上下文

public enum DataSourceType { PRIMARY, SECONDARY }
public class DataSourceContextHolder { private static final ThreadLocal<DataSourceType> CONTEXT = new ThreadLocal<>(); public static void set(DataSourceType dataSourceType) { CONTEXT.set(dataSourceType); } public static DataSourceType get() { return CONTEXT.get() == null ? DataSourceType.PRIMARY : CONTEXT.get(); } public static void clear() { CONTEXT.remove(); } }

ThreadLocal是这里的关键。因为 Web 请求的每个线程都独立保存上下文,不会互相污染;同时 Spring Boot 的线程池会复用线程,所以一定要在 finally 里clear(),否则下一次复用线程时会串库。

3.3 编写动态数据源类

public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.get(); } }

这段代码非常短,但所有“选择哪个库”的逻辑都在这里。返回DataSourceType.PRIMARY就会用 PostgreSQL,返回DataSourceType.SECONDARY就会用 SQL Server。

3.4 组装 DataSource Bean

接下来把两个真实数据源和动态数据源注册到 Spring 容器中。

@Configuration public class DataSourceConfig { @Bean @ConfigurationProperties("spring.datasource.primary") public DataSource primaryDataSource() { return DataSourceBuilder.create().build(); } @Bean @ConfigurationProperties("spring.datasource.secondary") public DataSource secondaryDataSource() { return DataSourceBuilder.create().build(); } @Bean @Primary public DynamicDataSource dynamicDataSource() { DynamicDataSource dynamicDataSource = new DynamicDataSource(); Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DataSourceType.PRIMARY, primaryDataSource()); targetDataSources.put(DataSourceType.SECONDARY, secondaryDataSource()); dynamicDataSource.setTargetDataSources(targetDataSources); dynamicDataSource.setDefaultTargetDataSource(primaryDataSource()); return dynamicDataSource; } }

这里有两个关键点:

  • dynamicDataSource必须标注@Primary,否则 Spring 容器里有多个DataSourceBean,MyBatis 自动配置和事务管理器都不知道该用哪一个。
  • targetDataSources的 key 用枚举对象本身即可,因为determineCurrentLookupKey()返回的也是同一个枚举对象。

3.5 自定义注解和 AOP 切面

为了让业务代码无感,我会定义@DataSource注解,然后用 AOP 在方法调用前后自动切换数据源。

@Documented @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface DataSource { DataSourceType value() default DataSourceType.PRIMARY; }

切面这里我推荐用@Around,比@Before+@After更安全,因为它可以在 finally 里保证清理上下文,即使方法抛异常也不会漏。

@Aspect @Component @Order(-1) public class DataSourceAspect { @Around("@annotation(dataSource)") public Object around(ProceedingJoinPoint joinPoint, DataSource dataSource) throws Throwable { DataSourceContextHolder.set(dataSource.value()); try { return joinPoint.proceed(); } finally { DataSourceContextHolder.clear(); } } }

@Order(-1)的意思是这个切面的优先级要比事务切面更高。这样在事务管理器开始事务之前,路由键就已经设置好了。

3.6 在 Service 层使用

@Service public class OrderQueryService { @Autowired private OrderMapper orderMapper; @Autowired private OrderArchiveMapper orderArchiveMapper; @DataSource(DataSourceType.PRIMARY) public Order getOrderFromPg(Long orderId) { return orderMapper.selectById(orderId); } @DataSource(DataSourceType.SECONDARY) public List<OrderArchive> getArchivesFromSqlServer(Long userId) { return orderArchiveMapper.selectList( new LambdaQueryWrapper<OrderArchive>() .eq(OrderArchive::getUserId, userId)); } }

使用的时候,只需要在方法上加注解,方法内部不用写任何数据源切换代码。PostgreSQL 的操作走PRIMARY,SQL Server 的操作走SECONDARY,互不干扰。

这里要提醒一个 AOP 经典问题:同一个类内部调用带@DataSource的方法,注解不会生效。比如this.getOrderFromPg(orderId),因为 Spring AOP 是基于代理的,内部调用绕过了代理对象。解决办法是把不同数据源的方法拆到不同的 Bean 里,或者用AopContext.currentProxy()

4. 参数调整和事务问题:不能只配置完就完事

4.1 连接池参数怎么给两个库分配

多数据源环境下,两个连接池是互相独立的,所以参数也要分开设计。不要图省事给两个库配一样的连接数。

PostgreSQL 作为主业务库,并发通常较高,我一般会设置maximum-pool-size: 10minimum-idle: 2connection-timeout: 30000。SQL Server 作为历史库或报表库,查询频率低,连接数给5就够。连接池大小不是越多越好,每一条连接都占着内存和数据库资源,配合实际 QPS 估算才是正路。

HikariCP 还有一个常用参数pool-name,建议给每个连接池取不同的名字,这样在日志和监控里能一眼看出当前请求用的是哪个库的连接。

4.2 MyBatis 如何自动使用动态数据源

只要我们在DataSourceConfig里定义了DynamicDataSource并标注了@Primary,MyBatis-Plus 的自动配置就会把DynamicDataSource作为唯一的DataSource注入到SqlSessionFactory里。

这就意味着我们不需要为 PostgreSQL 和 SQL Server 分别创建SqlSessionFactory。MyBatis 持有一个动态数据源引用,每次执行 SQL 的时候根据路由键选择真实连接。

如果你用的不是 MyBatis-Plus,而是原生 MyBatis,也是一样的原理。只要DataSourceBean 唯一,MyBatis 自动配置就会生效。

如果项目比较复杂,比如 PostgreSQL 和 SQL Server 两边的表结构差异很大,并且你想让 Mapper 接口按包分开扫描,那才需要配置多个SqlSessionFactory。这种情况一般配两个DataSource,而不是一个动态数据源。两种姿势没有绝对好坏,看团队熟悉程度和维护成本。

4.3 事务管理器与跨数据源事务

多数据源最需要注意的是事务,这不是开个玩笑,我见过不少人在这一步翻车。

默认情况下,Spring Boot 会创建一个绑定到唯一DataSourceDataSourceTransactionManager。由于我们只有一个DynamicDataSource,所以事务管理器绑定到动态数据源上,本身没问题。但你要理解它背后发生了什么:

  • 事务开始时,事务管理器从DynamicDataSource获取连接。
  • DynamicDataSource.getConnection()会调用determineCurrentLookupKey()
  • 如果当前线程已经被@DataSource切面设置了SECONDARY,那么事务连接来自 SQL Server。
  • 这个连接会被绑定到当前线程,直到事务提交或回滚。

所以这里有一个很容易被忽略的限制:同一个事务里的连接一旦确定,就不会因为你中途切换数据源而切换。如果业务方法先查了 PostgreSQL,又在同一个事务里(通过内部方法切换)去查 SQL Server,后半段查询拿到的仍然是 PostgreSQL 的连接,整条链路会乱掉。

解决办法有三种:

  • 把跨库操作拆成两个独立 Service 方法,分别管理事务。
  • 使用@Transactional时,保证一个事务内只操作一个数据源。
  • 如果业务上确实需要强一致性跨库事务,需要引入 JTA 或 Seata,用本地应用多数据源方案是无法保证的。

如果你需要配置事务管理器,可以显式声明:

@Configuration public class TransactionConfig { @Bean @Primary public PlatformTransactionManager transactionManager(DynamicDataSource dynamicDataSource) { return new DataSourceTransactionManager(dynamicDataSource); } }

5. 实操中常见的坑与排查实录

5.1 PostgreSQL 启动时报“无法创建锁文件 /var/run/postgresql/.s.pgsql.5432.lock: 权限不够”

这个问题热词里出现了很多次,本质上不是 Spring Boot 的问题,而是 PostgreSQL 进程没有权限在/var/run/postgresql目录下创建 Unix Socket 文件。

我当时的处理方式分两步:

  • 如果服务器上已经有postgres系统用户,用这个用户启动 PostgreSQL,而不要用 root 或当前普通用户。

  • 如果目录权限不对,执行:

    sudo mkdir -p /var/run/postgresql sudo chown postgres:postgres /var/run/postgresql sudo chmod 1777 /var/run/postgresql
  • 如果实在不想碰系统目录,还可以在postgresql.conf里修改:

    unix_socket_directories = '/tmp'

然后重启数据库。Spring Boot 连接的是 TCP 端口,锁文件只影响 Unix Socket 连接,但很多本地测试工具会走 Socket,所以这个问题必须处理掉。

5.2 SQL Server 用户 'sa' 登录失败

刚开始配置 SQL Server 数据源时,最常见的是sa登录失败。Spring Boot 里报错通常是:

Login failed for user 'sa'. ClientConnectionId:xxxx

这基本都是在 SQL Server 侧没开混合认证模式,或者sa账号本身是禁用状态。排查步骤:

  • 打开 SQL Server Management Studio,右键实例进入“属性”。
  • 在“安全性”页签,把服务器身份验证改为“SQL Server 和 Windows 身份验证模式”。
  • 展开“安全性 -> 登录名 -> sa”,右键进入属性。
  • 在“状态”页签启用登录,并在“常规”页签重置密码。
  • 最后重启 SQL Server 服务,混合认证才会生效。

另外,SQL Server 默认可能只监听 1433 端口,如果你连接的是命名实例,需要先启动 SQL Server Browser 服务。端口不对时,错误信息里会带上Named Pipes Provider: Could not open a connection to SQL Server,和热词里的[08001]很像。

5.3 SQL Server ODBC 驱动报 08001,Named Pipes Provider 连不上

虽然 Spring Boot 用的是 JDBC,不是 ODBC,但很多人在用 DBeaver、DataGrip 或 Windows 工具时遇到过这个[08001] [microsoft][odbc driver 17 for sql server]named pipes provider错误。

出现这个错,说明客户端尝试用 Named Pipes 协议连接,但服务器端没开对应协议,或者主机名/实例名解析有问题。解决办法是把连接串改成直连端口的形式,避免依赖 SQL Browser:

jdbc:sqlserver://192.168.1.100:1433;DatabaseName=history_db;encrypt=true;trustServerCertificate=true

如果数据库实例不是默认实例,比如192.168.1.100\SQLEXPRESS,且无法直连端口,则要确认 SQL Server Browser 服务是启动状态,并且在防火墙里放行 UDP 1434。

5.4 数据源切换不生效怎么办

动态数据源看起来代码不多,但切换不生效的问题非常典型。

我会从这几个方向排查:

  • 确认DataSourceAspect被 Spring 管理,也就是类上有没有@Component
  • 确认业务方法是通过代理对象调用的,而不是同类内部this.xxx()调用的。
  • 确认ThreadLocal有没有在方法结束后被清理。可以用@Around+finally,这样最稳。
  • 如果同一个请求里需要多个数据源切换,可以考虑在 Service 方法内部通过DataSourceContextHolder.set()手动切换,并在 finally 里清理。
  • 打开日志,在DataSourceContextHolder.set()clear()里打印当前线程名称和数据源类型,排查串库。

我一般会加一行日志:

logger.debug("switch datasource thread={}, type={}", Thread.currentThread().getName(), dataSourceType);

这样线上问题定位很快。

5.5 Spring Boot 版本太高与 MyBatis-Plus 多数据源插件

如果你不想自己写 AOP,MyBatis-Plus 官方提供了dynamic-datasource-spring-boot-starter,使用起来更省事:

<dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>3.5.2</version> </dependency>

使用这个插件后,可以直接在 Service 方法上写@DS("primary")@DS("secondary"),配置类也简化不少。但我个人建议先理解AbstractRoutingDataSource的原理,再决定是否用插件。因为插件的底层核心仍然是路由,跟本文的思路一样,只是把上下文管理和切面包得好了一点。遇到问题时不理解原理,很容易被“灵异现象”卡住。

另外,Spring Boot 3.x 用户注意插件的版本兼容性。dynamic-datasource-spring-boot-starter3.5.x 对 Spring Boot 3 的支持已经比较成熟,但 3.4.x 及以下版本可能报错。升级前先看官方 Release 说明。

6. 最后的实操建议

多数据源连接 PostgreSQL 和 SQL Server 这条路,我走过几次之后,总结出几个小习惯:

先拿 DBeaver 或 DataGrip 把两个库的连接串、账号密码全部验证一遍,再写 Spring Boot 代码。很多时候连接不上不是代码问题,而是数据库端口、账号权限、SSL 设置的问题。连接串能通过工具连接,基本能省掉 80% 的排查时间。

连接池参数一定要分开配,宁可先往小了配,也不要无脑给 50。尤其是 SQL Server 历史库,并发不高时连接数给 5 就够了。连接数太大反而容易把数据库连接吃满,影响其他业务。

如果是 Spring Boot 3.x 用户,建议在 pom 里锁定驱动版本,不要用依赖传递隐式带的版本。SQL Server 驱动新旧版本差异很大,PostgreSQL 驱动相对稳,但也不能完全不看。

多数据源的事务尽量保持简单。一个事务里只碰一个库,跨库操作拆开做最终一致,是当前业务系统里比较稳妥的做法。不要指望在一个事务里先更新 PostgreSQL 再更新 SQL Server,然后统一回滚,那是分布式事务的领域,不是普通动态数据源能解决的。

最后再分享一个小技巧:在动态数据源类里临时加一行打印,把determineCurrentLookupKey()的返回值打出来,再去看数据库监控里的连接来源,排障效率会高很多。等系统稳定了再把这行日志调成 trace 级别,随时可以打开。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 11:12:50

图表设计系统化实战:从信息秩序到高质量架构图的完整流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/9 11:11:22

自学软件测试总半途而废?从学习路线到项目实战一次讲透

自学软件测试&#xff0c;为什么总是半途而废&#xff1f;“为什么自学软件测试很难坚持下去&#xff1f;”——这几乎是每个刚踏入这个领域的人都会问的问题。我见过太多人下载了一堆视频教程&#xff0c;收藏了十几篇面试题合集&#xff0c;甚至报了七八个网课&#xff0c;然…

作者头像 李华
网站建设 2026/9/9 11:10:55

性能测试实战指南:从JMeter脚本设计到瓶颈定位全流程解析

1. 从“能跑”到“扛得住”&#xff1a;性能测试解决的根本问题 先聊个直白的话题。很多团队做性能测试&#xff0c;上来就打开JMeter&#xff0c;添加线程组、填几个并发数&#xff0c;然后点启动&#xff0c;盯着聚合报告里的数字发呆。跑完一看平均响应时间80ms&#xff0c;…

作者头像 李华
网站建设 2026/9/9 11:09:00

用GitHub Actions自动清理过期测试用例,拯救失控的CI

如果你的测试套件已经跑过几千条用例&#xff0c;你迟早会意识到一件事&#xff1a;测试用例的数量只会增加&#xff0c;不会自己减少。我刚接手一个中型项目时就撞上了这个问题——CI 单次全量跑完要 40 多分钟&#xff0c;其中相当一部分时间花在那些早就没人关心的用例上&am…

作者头像 李华
网站建设 2026/9/9 11:08:37

OV5645 MIPI CSI-2 YUV图像采集驱动:从协议解析到FPGA/Linux实现

简介&#xff1a;OV5645 MIPI YUV驱动是一份面向手机及平板等嵌入式设备的摄像头驱动源码包&#xff0c;主要服务嵌入式驱动开发、摄像头调试以及系统移植相关工程师&#xff1b;该驱动围绕OV5645图像传感器在移动行业处理器接口下的YUV图像输出&#xff0c;完整覆盖传感器初始…

作者头像 李华
网站建设 2026/9/9 11:06:50

手机短信导出全攻略:从Android到iOS的原理与实操

1. 内容整体设计与思路拆解1.1 短信导出到底在解决什么问题先说个实际的场景&#xff1a;我手头有一台用了四年的安卓手机&#xff0c;里面躺着两万多条短信&#xff0c;有银行验证码、快递取件通知、老同学叙旧、家人的叮嘱&#xff0c;还有几段跟客户谈事的完整记录。某天手机…

作者头像 李华