当前位置: 首页 > news >正文

基于注解切换、Hikari实现的SpringBoot动态数据源(支持JNDI)

实现效果

先说效果,要实现方法级别注解切换当前数据源,不设置注解时走默认数据源,同时支持JNDI源。

总体思路

Spring框架中存在一个抽象类AbstractRoutingDataSource,他是一个可以动态选择当前DataSource的路由类,我们就是要从这里入手,重新实现数据源的切换选择逻辑。然后借助注解和切面,将当前需要的数据源名称放在ThreadLocal中,需要时从当前线程取得即可完成数据源的切换。
注解部分比较简单不再详说,看AbstractRoutingDataSource。该类文档写的非常全面,自行翻译一下就可以看懂。主要看其中的几个关键方法。

setTargetDataSources

类中存在一个成员变量targetDataSources,结合之后的setTargetDataSources方法可知,这里用来保存目标数据源。
根据注释我们可以知道,targetDataSources的key可以是数据源的名字,value是相应数据源的实例。
当然这里也可是使用其他的保存方式,然后自行改写用来查找数据源的determineCurrentLookupKey方法,默认场景就足够我们使用了。所以我们要构建一个Map出来,其中key用来区分数据源的名字,value放入对应数据源的实例,有几个数据源就放几个进去。

	@Nullableprivate Map<Object, Object> targetDataSources;/*** Specify the map of target DataSources, with the lookup key as key.* The mapped value can either be a corresponding {@link javax.sql.DataSource}* instance or a data source name String (to be resolved via a* {@link #setDataSourceLookup DataSourceLookup}).* <p>The key can be of arbitrary type; this class implements the* generic lookup process only. The concrete key representation will* be handled by {@link #resolveSpecifiedLookupKey(Object)} and* {@link #determineCurrentLookupKey()}.*/public void setTargetDataSources(Map<Object, Object> targetDataSources) {this.targetDataSources = targetDataSources;}

setDefaultTargetDataSource

上面说了如何设置当前数据源,那如果在开发的时候每一个方法都要声明一下使用哪个源就太麻烦了,所以Spring提供了一个方法用来设置默认的数据源,没啥可说的,传入DataSource实例就好了。

	@Nullableprivate Object defaultTargetDataSource;/*** Specify the default target DataSource, if any.* <p>The mapped value can either be a corresponding {@link javax.sql.DataSource}* instance or a data source name String (to be resolved via a* {@link #setDataSourceLookup DataSourceLookup}).* <p>This DataSource will be used as target if none of the keyed* {@link #setTargetDataSources targetDataSources} match the* {@link #determineCurrentLookupKey()} current lookup key.*/public void setDefaultTargetDataSource(Object defaultTargetDataSource) {this.defaultTargetDataSource = defaultTargetDataSource;}

determineCurrentLookupKey

在设置好数据源之后,接下来这几个寻路方法则是能实现动态数据源切换的重点。afterPropertiesSet方法对我们以配置的数据源进行校验;如果我们在第一步配置数据源map的时候对key有特殊处理则要自己实现抽象方法resolveSpecifiedLookupKey,告诉Spring应该怎么解析这个key值;determineTargetDataSource则最终确定要使用哪一个数据源,其中有一个方法determineCurrentLookupKey需要关注,这个方法会返回当前要使用的数据源名字,但他是个抽象方法,所以我们需要给他重写一下,改为从当前线程获取数据源名称

	@Overridepublic void afterPropertiesSet() {if (this.targetDataSources == null) {throw new IllegalArgumentException("Property 'targetDataSources' is required");}this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());this.targetDataSources.forEach((key, value) -> {Object lookupKey = resolveSpecifiedLookupKey(key);DataSource dataSource = resolveSpecifiedDataSource(value);this.resolvedDataSources.put(lookupKey, dataSource);});if (this.defaultTargetDataSource != null) {this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);}}@Overridepublic Connection getConnection(String username, String password) throws SQLException {return determineTargetDataSource().getConnection(username, password);}/*** Retrieve the current target DataSource. Determines the* {@link #determineCurrentLookupKey() current lookup key}, performs* a lookup in the {@link #setTargetDataSources targetDataSources} map,* falls back to the specified* {@link #setDefaultTargetDataSource default target DataSource} if necessary.* @see #determineCurrentLookupKey()*/protected DataSource determineTargetDataSource() {Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");Object lookupKey = determineCurrentLookupKey();DataSource dataSource = this.resolvedDataSources.get(lookupKey);if (dataSource == null && (this.lenientFallback || lookupKey == null)) {dataSource = this.resolvedDefaultDataSource;}if (dataSource == null) {throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");}return dataSource;}/*** Determine the current lookup key. This will typically be* implemented to check a thread-bound transaction context.* <p>Allows for arbitrary keys. The returned key needs* to match the stored lookup key type, as resolved by the* {@link #resolveSpecifiedLookupKey} method.*/@Nullableprotected abstract Object determineCurrentLookupKey();

代码实现

思路理顺了,代码写起来就比较快,直接贴最后代码,部分地方保留了注释。

数据源切换注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** 数据源切换注解,默认为primary*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TargetDataSource {DataSourceEnum value() default DataSourceEnum.PRIMARY;
}

数据源切换切面

这里需要特别提醒一下,事务注解@Transactional默认处于切面代理的最后一个,所以我们需要保证数据源切换注解优先级要高于事务注解

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;/*** 数据源切换切面*/
@Aspect
@Component
@Order(1)
public class DynamicDataSourceAspect {private final static Logger log = LoggerFactory.getLogger(DynamicDataSourceAspect.class);@Before(value = "@annotation(targetDataSource)")public void beforePointCut(TargetDataSource targetDataSource) {log.debug("数据源切换为 " + targetDataSource.value().getDataSourceName());DynamicDataSourceContextHolder.setDataSource(targetDataSource.value().getDataSourceName());}@After(value = "@annotation(targetDataSource)")public void afterPointCut(TargetDataSource targetDataSource) {log.debug("数据源恢复为 " + DataSourceEnum.PRIMARY.getDataSourceName());DynamicDataSourceContextHolder.clearDataSource();}
}

数据源枚举类

/*** 数据源枚举类*/
public enum DataSourceEnum {PRIMARY("primary"), SECONDARY("secondary");private final String dataSourceName;public String getDataSourceName() {return dataSourceName;}DataSourceEnum(String dataSourceName) {this.dataSourceName = dataSourceName;}
}

数据源上下文保持类

/*** 数据源上下文线程持有类*/
public class DynamicDataSourceContextHolder {/*** 存放当前线程使用的数据源类型信息*/private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();public static void setDataSource(String dataSourceType) {CONTEXT_HOLDER.set(dataSourceType);}public static String getDataSource() {return CONTEXT_HOLDER.get();}public static void clearDataSource() {CONTEXT_HOLDER.remove();}
}

AbstractRoutingDataSource自定义实现

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;import javax.sql.DataSource;
import java.util.Map;/*** 动态数据源切换类** @author liuenqi*/
public class DynamicDataSource extends AbstractRoutingDataSource {@Overrideprotected Object determineCurrentLookupKey() {return DynamicDataSourceContextHolder.getDataSource();}public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {// 默认数据源super.setDefaultTargetDataSource(defaultTargetDataSource);// 所有目标数据源super.setTargetDataSources(targetDataSources);// 后处理super.afterPropertiesSet();}
}

数据源注册

注意使用jndi源的时候需要加一个特定前缀。

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;/*** 多数据源注册类*/
public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {private DataSource primaryDataSource;private DataSource secondaryDataSource;@Overridepublic void setEnvironment(Environment environment) {initPrimaryDataSource(environment);initSecondaryDataSource(environment);}/*** 组装主数据源参数,兼容jdbc-url与jndi** @param env Environment*/private void initPrimaryDataSource(Environment env) {Map<String, String> paramMap = new HashMap<>(4);if (StringUtils.isNotBlank(env.getProperty("spring.datasource.primary.url"))) {paramMap.put("url", env.getProperty("spring.datasource.primary.url"));paramMap.put("userName", env.getProperty("spring.datasource.primary.username"));paramMap.put("password", env.getProperty("spring.datasource.primary.password"));paramMap.put("driverClassName", env.getProperty("spring.datasource.primary.driver-class-name"));} else {paramMap.put("jndi", env.getProperty("spring.datasource.primary.jndi-name"));}primaryDataSource = buildDataSource(paramMap);}/*** 组装辅数据源参数,兼容jdbc-url与jndi** @param env Environment*/private void initSecondaryDataSource(Environment env) {if (StringUtils.isNotBlank(env.getProperty("spring.datasource.secondary.url"))) {Map<String, String> paramMap = new HashMap<>(4);paramMap.put("url", env.getProperty("spring.datasource.secondary.url"));paramMap.put("userName", env.getProperty("spring.datasource.secondary.username"));paramMap.put("password", env.getProperty("spring.datasource.secondary.password"));paramMap.put("driverClassName", env.getProperty("spring.datasource.secondary.driver-class-name"));secondaryDataSource = buildDataSource(paramMap);} else if (StringUtils.isNotBlank(env.getProperty("spring.datasource.secondary.jndi-name"))) {Map<String, String> paramMap = new HashMap<>(2);paramMap.put("jndi", env.getProperty("spring.datasource.secondary.jndi-name"));secondaryDataSource = buildDataSource(paramMap);}}@Overridepublic void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {Map<Object, Object> targetDataSource = new HashMap<>(2);targetDataSource.put("primary", primaryDataSource);if (Objects.nonNull(secondaryDataSource)) {targetDataSource.put("secondary", secondaryDataSource);}// 为DynamicDataSource构造参数,注意参数顺序ConstructorArgumentValues constructorArgumentValues = new ConstructorArgumentValues();constructorArgumentValues.addGenericArgumentValue(primaryDataSource);constructorArgumentValues.addGenericArgumentValue(targetDataSource);// 构造bean放入IOCGenericBeanDefinition beanDefinition = new GenericBeanDefinition();beanDefinition.setBeanClass(DynamicDataSource.class);beanDefinition.setConstructorArgumentValues(constructorArgumentValues);beanDefinition.setSynthetic(true);registry.registerBeanDefinition("dataSource", beanDefinition);}/*** 使用HikariDataSource** @param paramMap {"url":"JDBC-URL","userName":"数据库用户名","password":"密码","driverClassName":"驱动名","jndi":"jndi源"}* @return HikariDataSource*/private DataSource buildDataSource(Map<String, String> paramMap) {HikariConfig hikariConfig = new HikariConfig();if (paramMap.containsKey("url")) {hikariConfig.setJdbcUrl(paramMap.get("url"));hikariConfig.setUsername(paramMap.get("userName"));hikariConfig.setPassword(paramMap.get("password"));hikariConfig.setDriverClassName(paramMap.get("driverClassName"));} else {hikariConfig.setDataSourceJNDI("java:comp/env/" + paramMap.get("jndi"));}return new HikariDataSource(hikariConfig);}
}

application.yml配置

spring:datasource:primary:url: jdbc:mysql://xxxxxusername: xxxxpassword: xxxxxdriver-class-name: com.mysql.cj.jdbc.Driversecondary: jndi-name: jdbc/db

相关文章:

基于注解切换、Hikari实现的SpringBoot动态数据源(支持JNDI)

实现效果 先说效果&#xff0c;要实现方法级别注解切换当前数据源&#xff0c;不设置注解时走默认数据源&#xff0c;同时支持JNDI源。 总体思路 Spring框架中存在一个抽象类AbstractRoutingDataSource&#xff0c;他是一个可以动态选择当前DataSource的路由类&#xff0c;我…...

Java中的动态链接VS操作系统动态链接

在操作系统OS中为了优化内存的使用会采用一种动态链接方式&#xff0c;一个文件想要在操作系统中运行必须经过编译、汇编译、链接、装载等步骤。可以参考Java程序是怎么跑起来的。本篇主要讲解Java栈帧中动态链接部分与操作系统的的动态链接的区别与联系 操纵系统为什么需要动态…...

深入理解Linux虚拟内存管理(七)

系列文章目录 Linux 内核设计与实现 深入理解 Linux 内核 Linux 设备驱动程序 Linux设备驱动开发详解 深入理解Linux虚拟内存管理&#xff08;一&#xff09; 深入理解Linux虚拟内存管理&#xff08;二&#xff09; 深入理解Linux虚拟内存管理&#xff08;三&#xff09; 深入理…...

GSR II 智能速度辅助系统的型式认证和系统作为独立技术单元的型式认证测试流程和技术要求

智能速度辅助系统ISA的型式认证和系统作为独立技术单元的型式认证测试流程和技术要求 补充欧洲议会和欧洲理事会第2019/2144号条例,为机动车智能速度辅助系统的型式认证和这些系统作为独立技术单元的型式认证规定了详细的测试程序和技术要求,并修订该条例的附件二 (1)(EU…...

工厂方法模式(五)

过气的&#xff0c;终究是过气了 上一章简单介绍了工厂模式(四), 如果没有看过,请观看上一章 一.工厂方法模式 工厂方法模式&#xff0c;通过定义工厂父类负责定义创建对象的公共接口&#xff0c;而子类则负责生成具体的对象。 将类的实例化&#xff08;具体产品的创建&…...

力扣笔记(每日随机一题)——最佳买卖股票时机含冷冻期

问题&#xff08;中等&#xff09; 给定一个整数数组prices&#xff0c;其中第 prices[i] 表示第 i 天的股票价格 。​ 设计一个算法计算出最大利润。在满足以下约束条件下&#xff0c;你可以尽可能地完成更多的交易&#xff08;多次买卖一支股票&#xff09;: 卖出股票后&a…...

yolov5 6.1 关于 tensorrt 加速的使用以及问题说明

文章目录 1. 参考连接2. 使用说明2.1 导出加速模型2.1 使用加速模型2.2 加速参数对比 3. 问题说明3.1 在 Tensorrt 8.4.1.5 版本上使用 export.py 导出失败的问题3.2 把模型文件由 best.pt 更换成加速后的 best.engine 后&#xff0c;执行推理时标注的类别名不正确的问题3.3 导…...

SVR(支持向量机)用法介绍

一、SVR回归介绍 SVR(Support Vector Regression)是支持向量机(SVM)在回归问题中的应用。与SVM分类模型相似&#xff0c;SVR也是一种非概率性算法&#xff0c;通过使用核函数将数据映射到高维空间&#xff0c;并在该空间上寻找最优的超平面与训练数据之间的间隔最大化&#xf…...

是面试官放水,还是公司实在是太缺人?这都没挂,腾讯原来这么容易进···

本人211非科班&#xff0c;之前在字节和腾讯实习过&#xff0c;这次其实没抱着什么特别大的希望投递&#xff0c;没想到腾讯可以再给我一次机会&#xff0c;还是挺开心的。 本来以为有个机会就不错啦&#xff01;没想到能成功上岸&#xff0c;在这里要特别感谢帮我内推的同学&…...

算法模板(5):数学(1):数学知识(1)

数论 整数的整除性 [x]表示不超过x的最大整数&#xff0c;叫做取整函数或高斯函数。设整数a&#xff0c;b不同时为零&#xff0c;则存在一对整数m&#xff0c;n&#xff0c;使得 ( a , b ) a m b n (a, b) am bn (a,b)ambn。注&#xff1a;a和b的最大公因数会写成 (a, b)…...

电子行业 K 公司对接 Nexperia EDI 项目案例

项目背景 Nexperia 是一家全球领先的半导体制造商&#xff0c;专注于提供高性能、高可靠性和创新性的半导体解决方案。公司成立于2017年&#xff0c;是前飞思卡尔半导体业务的一部分&#xff0c;并在全球范围内拥有多个设计、研发和生产基地。 Nexperia 使用 EDI&#xff08;…...

chatgpt赋能python:Python如何将英文转化为中文的最佳方法

Python如何将英文转化为中文的最佳方法 介绍 在现代全球化社会中&#xff0c;国与国之间的交流越来越频繁&#xff0c;相应的语言翻译工具的需求也愈发迫切。Python是一种易于学习、快速上手的编程语言&#xff0c;适合初学者和经验丰富的程序员使用&#xff0c;在语言翻译方…...

知道这些英文文档翻译的方式吗

在工作中&#xff0c;大家有没有遇到领导交给你一份外语的文档&#xff0c;要你去观看和理解&#xff0c;但是我们看不太懂或者没啥时间去一点点翻译怎么办呢&#xff1f;我们就需要有工具来将文档翻译&#xff0c;它是一项非常实用和便捷的功能&#xff0c;它可以将文档中的文…...

供应链安全

供应链安全 目录 文章目录 供应链安全目录本节实战可信任软件供应链概述构建镜像Dockerfile文件优化镜像漏洞扫描工具&#xff1a;Trivy检查YAML文件安全配置&#xff1a;kubesec准入控制器&#xff1a; Admission Webhook准入控制器&#xff1a; ImagePolicyWebhook关于我最后…...

华硕天选4原装Windows11系统带ASUSRECOVERY恢复工厂模式安装

华硕工厂恢复系统 &#xff0c;安装结束后带隐藏分区以及机器所有驱动软件,奥创Myasus Recovery 文件地址https://pan.baidu.com/s/1Pq09oDzmFI6hXVdf8Vqjqw?pwd3fs8 提取码:3fs8 文件格式&#xff1a;5个底包(HDI KIT COM MCAFEE EDN) 1个引导工具TLK 支持ASUSRECOVERY型…...

数据库期末复习(8)并发控制

笔记 数据库DBMS并发控制(1)_旅僧的博客-CSDN博客 数据库 并发控制(2)死锁和意向锁_旅僧的博客-CSDN博客 同一个对象不能既有slock又有xlock; 冲突可串行化和锁 怎么判断是否可以进行冲突可串行化:简便的方法是优先图 只有不同对象和同一对象都是读才不能发生非串行化调…...

一文说透:低代码开发平台和零代码平台区别是什么?

低代码开发平台和零代码平台区别是什么&#xff1f; 一个简单的例子就可以解释清楚。 假设你想入住一套新房&#xff0c;回看住房变迁史&#xff1a; 最原始方式是&#xff1a;自己建造往后一点&#xff0c;交付“毛坯房”&#xff1a;开发商统一建小区&#xff0c;不需要自…...

4.将图神经网络应用于大规模图数据(Cluster-GCN)

到目前为止&#xff0c;我们已经为节点分类任务单独以全批方式训练了图神经网络。特别是&#xff0c;这意味着每个节点的隐藏表示都是并行计算的&#xff0c;并且可以在下一层中重复使用。 然而&#xff0c;一旦我们想在更大的图上操作&#xff0c;由于内存消耗爆炸&#xff0c…...

pymongo更新数据

使用 PyMongo&#xff0c;可以通过以下步骤将查询到的记录进行更新&#xff1a; 下面是一个简单的示例代码片段&#xff0c;展示如何向名为users的集合中的所有文档添加一个新字段age。 import pymongo # 连接 MongoDB client pymongo.MongoClient("mongodb://localh…...

手机软件测试规范(含具体用例)

菜单基本功能测试规范一、短消息功能测试规范测试选项操作方法观察与判断结果创建、编辑短消息并发送书写短消息1、分别使用菜单或快捷方式进入书写短消息是否有异常&#xff1b; 2、输入0个字符&#xff0c;选择、输入号码发送&#xff0c;应成功&#xff1b; 3、输入1个中文…...

mongodb源码分析session执行handleRequest命令find过程

mongo/transport/service_state_machine.cpp已经分析startSession创建ASIOSession过程&#xff0c;并且验证connection是否超过限制ASIOSession和connection是循环接受客户端命令&#xff0c;把数据流转换成Message&#xff0c;状态转变流程是&#xff1a;State::Created 》 St…...

Python爬虫实战:研究feedparser库相关技术

1. 引言 1.1 研究背景与意义 在当今信息爆炸的时代,互联网上存在着海量的信息资源。RSS(Really Simple Syndication)作为一种标准化的信息聚合技术,被广泛用于网站内容的发布和订阅。通过 RSS,用户可以方便地获取网站更新的内容,而无需频繁访问各个网站。 然而,互联网…...

测试markdown--肇兴

day1&#xff1a; 1、去程&#xff1a;7:04 --11:32高铁 高铁右转上售票大厅2楼&#xff0c;穿过候车厅下一楼&#xff0c;上大巴车 &#xffe5;10/人 **2、到达&#xff1a;**12点多到达寨子&#xff0c;买门票&#xff0c;美团/抖音&#xff1a;&#xffe5;78人 3、中饭&a…...

linux 错误码总结

1,错误码的概念与作用 在Linux系统中,错误码是系统调用或库函数在执行失败时返回的特定数值,用于指示具体的错误类型。这些错误码通过全局变量errno来存储和传递,errno由操作系统维护,保存最近一次发生的错误信息。值得注意的是,errno的值在每次系统调用或函数调用失败时…...

04-初识css

一、css样式引入 1.1.内部样式 <div style"width: 100px;"></div>1.2.外部样式 1.2.1.外部样式1 <style>.aa {width: 100px;} </style> <div class"aa"></div>1.2.2.外部样式2 <!-- rel内表面引入的是style样…...

Android 之 kotlin 语言学习笔记三(Kotlin-Java 互操作)

参考官方文档&#xff1a;https://developer.android.google.cn/kotlin/interop?hlzh-cn 一、Java&#xff08;供 Kotlin 使用&#xff09; 1、不得使用硬关键字 不要使用 Kotlin 的任何硬关键字作为方法的名称 或字段。允许使用 Kotlin 的软关键字、修饰符关键字和特殊标识…...

基于Java+MySQL实现(GUI)客户管理系统

客户资料管理系统的设计与实现 第一章 需求分析 1.1 需求总体介绍 本项目为了方便维护客户信息为了方便维护客户信息&#xff0c;对客户进行统一管理&#xff0c;可以把所有客户信息录入系统&#xff0c;进行维护和统计功能。可通过文件的方式保存相关录入数据&#xff0c;对…...

scikit-learn机器学习

# 同时添加如下代码, 这样每次环境(kernel)启动的时候只要运行下方代码即可: # Also add the following code, # so that every time the environment (kernel) starts, # just run the following code: import sys sys.path.append(/home/aistudio/external-libraries)机…...

Golang——6、指针和结构体

指针和结构体 1、指针1.1、指针地址和指针类型1.2、指针取值1.3、new和make 2、结构体2.1、type关键字的使用2.2、结构体的定义和初始化2.3、结构体方法和接收者2.4、给任意类型添加方法2.5、结构体的匿名字段2.6、嵌套结构体2.7、嵌套匿名结构体2.8、结构体的继承 3、结构体与…...

永磁同步电机无速度算法--基于卡尔曼滤波器的滑模观测器

一、原理介绍 传统滑模观测器采用如下结构&#xff1a; 传统SMO中LPF会带来相位延迟和幅值衰减&#xff0c;并且需要额外的相位补偿。 采用扩展卡尔曼滤波器代替常用低通滤波器(LPF)&#xff0c;可以去除高次谐波&#xff0c;并且不用相位补偿就可以获得一个误差较小的转子位…...