【Java面试篇】Spring中@Transactional注解事务失效的常见场景
文章目录
- `@Transactional`注解的失效场景
- ☁️前言
- 🍀前置知识
- 🍁场景一:`@Transactional`应用在非 public 修饰的方法上
- 🍁场景二: `propagation` 属性设置错误
- 🍁场景三:`rollbackFor`属性设置错误
- 🍁场景四:方法调用导致`@Transactional`失效
- 🍁场景五:异常捕获导致`@Transactional`失效
- 🍁场景六:数据库引擎不支持事务
- 🍁场景七:未启用事务
- 🍁场景八:Bean没有纳入Spring容器管理
- 🍁场景九:事务方法启动新线程进行异步操作
- 🍃总结
@Transactional注解的失效场景
☁️前言
最初学习Spring时,B站的杨老师就说过“在工作中不要过于依赖
@Transactional注解实现事务,我们不仅要掌握注解实现事务,还需要掌握通过配置文件实现事务”,当时他没有明确说为什么,现在我应该是大致了解了,因为@Transactional注解对于新手而言是存在很多坑的,在很多情况下@Transactional注解都会失效,现在就让我们来详细学习哪些情况下@Transactional注解实现的事务会失效吧😄。PS:关于如何通过配置文件实现事务请参考Spring学习笔记
🍀前置知识
-
事务:指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。可以理解事务就是一段代码块或者一行SQL,这段代码或这行SQL会更新数据库,事务具有基本的ACID特性,以此保障数据的安全性。
-
事务管理:由事务管理器1、恢复管理器2、锁管理器3、死锁管理器4、缓存管理器5构成
-
事务管理的作用:管理事务相关的资源;更容易处理复杂的事务;简化事务相关的操作,让程序员更关注业务
-
Spring中提供了两种事务管理机制:
- 编程式事务:是指在代码中手动的管理事务的提交、回滚等操作,代码侵入性比较强。(Spring提供了TransactionTemplate模板,利用该模板我们可以通过编程的方式实现事务管理,而无需关注资源获取、复用、释放、事务同步及异常处理等操作。相对于声明式事务来说,这种方式相对麻烦一些,但是好在更为灵活,我们可以将事务管理的范围控制的更为精确)
- 声明式事务:基于
AOP面向切面的,它将具体业务与事务处理部分解耦,代码侵入性很低。而声明式事务有两种方式实现,方式一是基于@Transaction注解实现,方式二是基于XML实现。(Spring事务管理的亮点在于声明式事务管理,它允许我们通过声明的方式,在IoC配置中指定事务的边界和事务属性,Spring会自动在指定的事务边界上应用事务属性。相对于编程式事务来说,这种方式十分的方便,只需要在需要做事务管理的方法上,增加@Transactional注解,以声明事务特征即可)
大多数 Spring 框架的用户选择声明式事务管理,因为它对应用代码的影响最小, 因此更符合一个无侵入的轻量级容器的思想。声明式事务管理要优于编程式事务 管理,虽然比编程式事务管理(这种方式允许你通过代码控制事务)少了一点灵 活性。
-
@Transactional:可以作用在接口、类、方法
- 作用于接口:不推荐这种使用方法,因为一旦标注在
Interface上并且配置了Spring AOP 使用CGLib动态代理,将会导致@Transactional注解失效 - 作用于类:当把
@Transactional注解放在类上时,代表这个类所有公共(public)非静态(static)的方法都将启用事务功能,且都会被 Spring 的事务管理器进行管理 - 作用于方法:当把
@Transactional配置在方法上,该方法被当成一个独立的事务,且被事务管理器管理。当类配置了@Transactional,方法也配置了@Transactional,此时方法的事务会覆盖类的事务配置信息
- 作用于接口:不推荐这种使用方法,因为一旦标注在
-
@Transactional的属性

-
propagation属性propagation代表事务的传播行为,默认值为Propagation.REQUIRED,其他的属性信息如下:
-
Propagation.REQUIRED:如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。 也就是说如果A方法和B方法都添加了注解,在默认传播模式下,A方法内部调用B方法,会把两个方法的事务合并为一个事务 -
Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行 -
Propagation.MANDATORY:如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常 -
Propagation.REQUIRES_NEW:重新创建一个新的事务,如果当前存在事务,暂停当前的事务。( 当类A中的 a 方法用默认Propagation.REQUIRED模式,类B中的 b方法加上采用Propagation.REQUIRES_NEW模式,然后在 a 方法中调用 b方法操作数据库,然而 a方法抛出异常后,b方法并没有进行回滚,因为Propagation.REQUIRES_NEW会暂停 a方法的事务 ) -
Propagation.NOT_SUPPORTED:以非事务的方式运行,如果当前存在事务,暂停当前的事务 -
Propagation.NEVER:以非事务的方式运行,如果当前存在事务,则抛出异常 -
Propagation.NESTED:和Propagation.REQUIRED效果一样
-
-
isolation属性事务的隔离级别,默认值为
Isolation.DEFAULT-
Isolation.DEFAULT:使用底层数据库默认的隔离级别。 -
Isolation.READ_UNCOMMITTED:读取未提交数据(会出现脏读, 不可重复读) 基本不使用 -
Isolation.READ_COMMITTED:读取已提交数据(会出现不可重复读和幻读) -
Isolation.REPEATABLE_READ:可重复读(会出现幻读) -
Isolation.SERIALIZABLE:串行化
-
-
timeout属性:事务的超时时间,默认值为 -1。如果超过该时间限制但事务还没有完成,则自动回滚事务 -
readOnly属性:指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置readonly为true -
rollbackFor属性:用于指定能够触发事务回滚的异常类型,可以指定多个异常类型 -
noRollbackFor属性:抛出指定的异常类型,不回滚事务,也可以指定多个异常类型
-
🍁场景一:@Transactional应用在非 public 修饰的方法上
@Transactional注解修饰的方法必须是public修饰的,同样的@Transactional修饰类时,也只有类中使用pulbic修饰的方法才能成为事务。须知:使用@Transactional修饰的方法,必须是public修饰、非static修饰、非final修饰的,一个不满足就会导致事务失效
原因:
由于Spring的事务是通过AOP实现的,在AOP代理时,事务拦截器TransactionInterceptor在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CGlib动态代理时的事务拦截器)的 intercept 方法或 JdkDynamicAopProxy (JDK动态代理时的代理对象)的 invoke 方法都会间接调用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute 方法,获取Transactional 注解的事务配置信息,如下图所示:

备注:在老版本的Spring中,这个需要十分注意,因为当我们在非pulic方法上加@Transactional,它在编译阶段是没有任何报错信息的,但是新版的Spring是能够在编译阶段就能够进行报错,所以只要是使用较新版本的版本,基本没可能会犯这种错误,比如我当前使用的Spring版本是5.2.15,直接就在编译阶段报错了:

🍁场景二: propagation 属性设置错误
当我们将
propagation属性的值设置为一下几种取值就会导致事务失效:
Propagation.NOT_SUPPORTED:以非事务的方式运行,如果当前存在事务,暂停当前的事务
Propagation.NEVER:以非事务的方式运行,如果当前存在事务,则抛出异常
🍁场景三:rollbackFor属性设置错误
Spring默认抛出了未检查
unchecked异常(继承自RuntimeException的异常)或者Error才回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定rollbackFor属性;若在目标方法中抛出的异常是rollbackFor指定的异常的子类,事务同样会回滚。
[
Spring底层通过getDepth方法来判断出现异常是否需要进行事务回滚
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-njPEUQUq-1677830697622)(D:/%E7%94%A8%E6%88%B7/ghp/Pictures/Typora/image-20230205223102898.png)]](https://img-blog.csdnimg.cn/a7009bcb00164d938d6788fbf48373bb.png)
🍁场景四:方法调用导致@Transactional失效
同一个类中,A方法是非事务性方法,但是B方法是事务性方法,此时A调用B就会导致B的事务失效。
原因:
这个和场景一的原因是类似的,事务的实现是基于AOP的,而AOP的实现又是基于动态代理的,而动态代理的本质就算对方法的增强,如果想要使用增强的方法(也就是想要使用事务方法),就必须是通过代理对象去触发目标对象的方法,这个我相信只要学过设计模式都是能够理解的。
解决方案:
通过
AopContext.currentProxy()这个API获取当前类的代理对象
示例:
@Service
Class ServiceImpl implements IService{// 普通方法@Overridepublic Result A() {......IService proxy = (Iservice) AopContext.currentProxy();return proxy.B();}// 事务方法@Override@Transactionalpublic Result B() {......return Result.ok();}
}
推荐阅读:每日一个设计模式之【代理模式】
🍁场景五:异常捕获导致@Transactional失效
当一个事务方法中抛出了异常,此时该异常通过
try...catch进行了捕获,此时就会导致该方法的事务注解@Transactional失效
示例:
@Resource
private IBService bService;@Service
Class AServiceImpl implements IAService{@Transactionalpublic Result A(Student s) {try {bService.save(s);} catch (Exception e) {e.printStackTrace();}return Result.ok();}
}
此时会报错org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
原因:因为bService执行save方法过程中出现了异常,所以bService告诉事务管理器,当前事务需要被rollback,但是aService中使用try...catch捕获了异常,它认为当前事务并没有发生异常,程序是处于正常状态,于是aService就告诉事务管理器,当前事务需要被commit,结果事务管理器发现收到两个矛盾的信号,它也搞不清是该rollback还是该commit,于是就抛了个UnexpectedRollbackException异常。
也就是说Spring中,事务是在方法调用时开始的,业务方法执行完毕后才执行rollback或commit操作,事务是否被回滚取决于是否抛出异常,且该异常是否满足场景三(也就是说抛出的异常是否有被rollbackFor指定,或rollbackFor指定异常的子类)。如果一定要使用try..catch时,一定要抛出异常(且抛出的异常必须满足场景三,一般直接抛一个运行时异常就可以了 throw new RuntimeException(),运行时异常是rollbackFor默认指定的异常),而不只是打印异常信息。
综上所诉:在Service层中,方法中最好不要随便写try...catch,如果写了则一定要手动抛异常
🍁场景六:数据库引擎不支持事务
Spring的事务本质还是得靠数据库引擎的支持,如果数据库引擎不支持事务,那么Spring就算使用事务也是白搭。常用的MySQL数据库默认使用支持事务的
innodb引擎。一旦数据库引擎切换成不支持事务的myisam,那事务就从根本上失效了。当然相信这个问题出现的概率很小,但并不代码没有,我们还是需要有一定了解的
注意:从 MySQL 5.5.5 开始的默认存储引擎是:InnoDB,之前默认的都是:MyISAM。也就是说是从MySQL5.5.5开始,MySQL才支持事务
🍁场景七:未启用事务
想要
@Transactional注解实现声明式事务,首先就需要开启事务,开启事务就三步:
- 配置事务管理器
- 开启事务的注解驱动
- 使用
@TransactionalPS:这个事件对于向我这种初学者来说概率还是存在的,对于老手应该不太可能会出现这种低级错误了😄
开启事务相关配置:
<!--配置事务管理器--><bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource" ref="dataSourceRef"></property></bean><!--开启事务的注解驱动,将事务管理器中的环绕通知作用到连接点,连接点使用@Transactional进行标识transaction-manager属性用于指定事务管理器,默认是transactionManager这个id名--><tx:annotation-driven transaction-manager="transactionManager"/>
备注:在SpringBoot中,只需要在启动类上添加@EnableAspectJAutoProxy(exposeProxy = true)这个注解,就能使用基于注解实现的声明式事务了,等价于上面的配置
🍁场景八:Bean没有纳入Spring容器管理
Spring的事务管理核心是动态代理,不是动态代理的Bean是无法进行被Spring进行事务管理的
将Bean纳入Spring容器管理的方法:
-
方式一:配置XML
<bean id="xxx" class="xxx.xxx.xxx"></bean> -
方式二:添加注解,比如:@Controller、@Service、@Repository、@Conponent……
🍁场景九:事务方法启动新线程进行异步操作
spring 的事务是通过LocalThread来保证线程安全的,事务和当前线程绑定,此时开启新线程执行业务,这个新线程的业务就会事务失效,因为事务是基于动态代理的,要想有事务,需要被动态代理。这里提供一种解决方法,可以将新的业务单独封装成一个方法,然后改方法上添加一个@Transactional,或者将这个无法单独抽取到一个类中,将该类交给IOC容器进行管理,这样就能让新线程的业务具有事务了
🍃总结
@Transactional使用事务具有几个基本的要求:
- 必须开启注解事务
- 事务所在类,必须要交给IOC容器进行管理
- 事务所在目标,必须要进行动态代理
- 事务所在方法必须要是public
总而言之,使用@Transactional来启动事务,有很多坑,对于简单的业务还是推荐直接使用注解进行事务管理,对于复杂的业务还是推荐使用XML进行事务管理

参考文章:
- 一口气说出 6种,@Transactional注解的失效场景 - 掘金 (juejin.cn)
- 咱们从头到尾说一次 Spring 事务管理(器) - 掘金 (juejin.cn)
- spring 事务失效的 12 种场景-CSDN博客
在此致谢(^^ゞ🌹🌹🌹
事务管理器:负责产生事务并为其分配事务标识 ↩︎
恢复管理器:子事务提交时,负责将子事务的日志链接到父事务的日志上,确保事务的一致性原则 ↩︎
锁管理器:事务申请锁时,负责判断锁的相容性 ↩︎
死锁管理器:负责检测死锁 ↩︎
缓存管理器:提供对事务标识的缓存 ↩︎
相关文章:
【Java面试篇】Spring中@Transactional注解事务失效的常见场景
文章目录Transactional注解的失效场景☁️前言🍀前置知识🍁场景一:Transactional应用在非 public 修饰的方法上🍁场景二: propagation 属性设置错误🍁场景三:rollbackFor属性设置错误dz…...
【C】分配内存的函数
#include <stdlib.h>//分配所需的内存空间,并返回一个指向它的指针。 void *malloc(size_t size);//分配所需的内存空间,并返回一个指向它的指针。并且calloc负责把这块内存空间用字节0填//充,而malloc并不负责把分配的内存空间清零 vo…...
IDEA 断点总是进入class文件没有进入源文件解决
前言 idea 断点总是进入class文件没有进入源文件解决 问题 在源文件里打了断点,断点模式启动时却进入了class文件里的断点,而没有进入到java源文件里的断点。 比如:我在 A.java 里打了断点,调试时却进入到了 jar 包里的 A.clas…...
【flink】 flink入门教程demo 初识flink
文章目录通俗解释什么是flink及其应用场景flink处理流程及核心APIflink代码快速入门flink重要概念什么是flink? 刚接触这个词的同学 可能会觉得比较难懂,网上搜教程 也是一套一套的官话, 如果大家熟悉stream流,那或许会比较好理解…...
LeetCode 1487. 保证文件名唯一
【LetMeFly】1487.保证文件名唯一 力扣题目链接:https://leetcode.cn/problems/making-file-names-unique/ 给你一个长度为 n 的字符串数组 names 。你将会在文件系统中创建 n 个文件夹:在第 i 分钟,新建名为 names[i] 的文件夹。 由于两个…...
详细剖析|袋鼠云数栈前端框架Antd 3.x 升级 4.x 的踩坑之路
袋鼠云数栈从2016年发布第⼀个版本开始,就始终坚持着以技术为核⼼、安全为底线、提效为⽬标、中台为战略的思想,坚定不移地⾛国产化信创路线,不断推进产品功能迭代、技术创新、服务细化和性能升级。 在数栈过去的产品迭代中受限于当前组件的…...
【C++PrimerPlus】第三章 处理数据
文章目录前言内容目录3.1 简单变量3.1.2 变量名3.1.2 整形3.1.3 整形short,int,long,long long3.1.4 无符号类型3.1.5 选择整形类型3.1.6 整形字面值3.1.7 C如何确定常量的类型3.1.8 char类型:字符和小整数3.1.9 bool类型3.2 const修饰符3.3浮点数3.3.1 书写浮点数3…...
【基础算法】单链表的OJ练习(1) # 反转链表 # 合并两个有序链表 #
文章目录前言反转链表合并两个有序链表写在最后前言 上一章讲解了单链表 -> 传送门 <- ,后面几章就对单链表进行一些简单的题目练习,目的是为了更好的理解单链表的实现以及加深对某些函数接口的熟练度。 本章带来了两个题目。一是反转链表&#x…...
离散数学笔记(1)命题逻辑
文章目录1.命题符号化及联结词基本概念本节题型2.命题公式及分类基本概念本节题型1.命题符号化及联结词 基本概念 命题的定义:能够判断真假的陈述句称为命题。 备注:感叹句、疑问句、祈使句和类似于xy>5之类真值不唯一的句子都不是命题。 真值的真假…...
IDEA Android 网格布局(GridLayout)示例(计算器界面布局)
网格布局(GridLayout) 示例程序效果(实现类似vivo手机自带计算器UI) 真机和模拟器运行效果: 简述: GridLayout(网格布局)和TableLayout(表格布局)有类似的地方,通俗来讲可以理解为…...
【蓝桥杯嵌入式】拓展板之数码管显示
文章目录硬件电路连接方式函数实现文章福利硬件电路 通过上述原理图,可知拓展板上的数码管是一个共阴数码管,也就是说某段数码管接上高电平时,就会点亮。 上述原理图还给出一个提示,即:三个数码管分别与三个74HC59…...
Web Spider案例 网洛克 第三题 AAEncode加密 练习(七)
声明 此次案例只为学习交流使用,抓包内容、敏感网址、数据接口均已做脱敏处理,切勿用于其他非法用途; 文章目录声明一、资源推荐二、逆向目标三、抓包分析 & 下断分析逆向3.1 抓包分析3.2 下断分析逆向拿到混淆JS代码3.3 AAEncode解决方…...
【javaScript面试题】2023前端最新版javaScript模块,高频24问
🥳博 主:初映CY的前说(前端领域) 🌞个人信条:想要变成得到,中间还有做到! 🤘本文核心:博主收集的关于javaScript的面试题 目录 一、2023javaScript面试题精选 1.js的数据类型…...
Hadoop集群启动从节点没有DataNode
一、问题背景 之前启动hadoop集群的时候都没有问题,今天启动hadoop集群的时候,从节点的DataNode没有启动起来。 二、解决思路 遇见节点起不来的情况,可以去看看当前节点的日志文件 我进入当前从节点的hadoop安装目录的Logs文件下去查看日…...
FIFO IP Core
FIFO IP Core 先进先出的缓存器常常被用于数据的缓存,或者高速异步数据交互(跨时钟信号传递)和RAM和ROM的区别是没有地址线,无法指定地址 写时钟(Write Clock Domain),读时钟写复位(wr_rst),读…...
从FPGA说起的深度学习(四)
这是新的系列教程,在本教程中,我们将介绍使用 FPGA 实现深度学习的技术,深度学习是近年来人工智能领域的热门话题。在本教程中,旨在加深对深度学习和 FPGA 的理解。用 C/C 编写深度学习推理代码高级综合 (HLS) 将 C/C 代码转换为硬…...
pytorch入门7--自动求导和神经网络
深度学习网上自学学了10多天了,看了很多大神的课总是很快被劝退。终于,遇到了一位对小白友好的刘二大人,先附上链接,需要者自取:https://b23.tv/RHlDxbc。 下面是课程笔记。 一、自动求导 举例说明自动求导。 torch中的…...
QT 之wayland 事件处理分析基于qt5wayland5.14.2
1. Qt wayland 初始化 接收鼠标/案件,触摸屏等事件事件 QWaylandNativeInterface : public QPlatformNativeInterface 在QWaylandNativeInterface 继承qpa 接口类QPlatformNativeInterface; 1.1 初始化鼠标: void *QWaylandNativeInterface::nativeR…...
【this 和 super 的区别】
在 Java 中,this 和 super 都是关键字,表示当前对象和父类对象。 this 关键字可以用于以下几种情况: 引用当前对象的成员变量,方法和构造方法,用于区分局部变量和成员变量重名的情况; 调用当前类的另外一…...
K8s:Monokle Desktop 一个集Yaml资源编写、项目管理、集群管理的 K8s IDE
写在前面 Monokle Desktop 是 kubeshop 推出的一个开源的 K8s IDE相关项目还有 Monokle CLI 和 Monokle Cloud相比其他的工具,Monokle Desktop 功能较全面,涉及 k8s 管理的整个生命周期博文内容:Monokle Desktop 下载安装,项目管理…...
生成xcframework
打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式,可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...
python打卡day49
知识点回顾: 通道注意力模块复习空间注意力模块CBAM的定义 作业:尝试对今天的模型检查参数数目,并用tensorboard查看训练过程 import torch import torch.nn as nn# 定义通道注意力 class ChannelAttention(nn.Module):def __init__(self,…...
ubuntu搭建nfs服务centos挂载访问
在Ubuntu上设置NFS服务器 在Ubuntu上,你可以使用apt包管理器来安装NFS服务器。打开终端并运行: sudo apt update sudo apt install nfs-kernel-server创建共享目录 创建一个目录用于共享,例如/shared: sudo mkdir /shared sud…...
微软PowerBI考试 PL300-选择 Power BI 模型框架【附练习数据】
微软PowerBI考试 PL300-选择 Power BI 模型框架 20 多年来,Microsoft 持续对企业商业智能 (BI) 进行大量投资。 Azure Analysis Services (AAS) 和 SQL Server Analysis Services (SSAS) 基于无数企业使用的成熟的 BI 数据建模技术。 同样的技术也是 Power BI 数据…...
从零实现富文本编辑器#5-编辑器选区模型的状态结构表达
先前我们总结了浏览器选区模型的交互策略,并且实现了基本的选区操作,还调研了自绘选区的实现。那么相对的,我们还需要设计编辑器的选区表达,也可以称为模型选区。编辑器中应用变更时的操作范围,就是以模型选区为基准来…...
SCAU期末笔记 - 数据分析与数据挖掘题库解析
这门怎么题库答案不全啊日 来简单学一下子来 一、选择题(可多选) 将原始数据进行集成、变换、维度规约、数值规约是在以下哪个步骤的任务?(C) A. 频繁模式挖掘 B.分类和预测 C.数据预处理 D.数据流挖掘 A. 频繁模式挖掘:专注于发现数据中…...
【快手拥抱开源】通过快手团队开源的 KwaiCoder-AutoThink-preview 解锁大语言模型的潜力
引言: 在人工智能快速发展的浪潮中,快手Kwaipilot团队推出的 KwaiCoder-AutoThink-preview 具有里程碑意义——这是首个公开的AutoThink大语言模型(LLM)。该模型代表着该领域的重大突破,通过独特方式融合思考与非思考…...
Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
均衡后的SNRSINR
本文主要摘自参考文献中的前两篇,相关文献中经常会出现MIMO检测后的SINR不过一直没有找到相关数学推到过程,其中文献[1]中给出了相关原理在此仅做记录。 1. 系统模型 复信道模型 n t n_t nt 根发送天线, n r n_r nr 根接收天线的 MIMO 系…...
听写流程自动化实践,轻量级教育辅助
随着智能教育工具的发展,越来越多的传统学习方式正在被数字化、自动化所优化。听写作为语文、英语等学科中重要的基础训练形式,也迎来了更高效的解决方案。 这是一款轻量但功能强大的听写辅助工具。它是基于本地词库与可选在线语音引擎构建,…...
