java:Druid工具类解析sql获取表名

java:Druid工具类解析sql获取表名

1 前言

alibaba的druid连接池除了sql执行的功能外,还有sql语法解析的工具提供,参考依赖如下:

<dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.15</version>
</dependency>

2 使用

参考druid的工具类:com.alibaba.druid.sql.parser.SQLParserUtils#getTables(String sql, DbType dbType)方法,可以用于获取sql的表名:

比如针对mysql的select语句:

package com.xiaoxu.parser;import com.alibaba.druid.DbType;
import com.alibaba.druid.sql.parser.SQLParserUtils;import java.util.List;/*** @author xiaoxu* @date 2024-03-11* java_demo2:com.xiaoxu.parser.SQLParserTest*/
public class SQLParserTest {public static void main(String[] args) {List<String> tables = SQLParserUtils.getTables("select * from my_fuitrs_99 where id = ?", DbType.mysql);System.out.println(tables);String nihao_99 = "select * from my_fuitrs_99 where id = ?".replace(tables.get(0), "my_fuitrs");System.out.println(nihao_99);}}

执行结果如下:
在这里插入图片描述

该方法的本质是根据自定义的hash值,对于每个标识符,能唯一生成一个hashCode,将标识符与存入map中的标识符来比对。比如当lexer.token获取的是com.alibaba.druid.sql.parser.Token类中的FROM(“FROM”)时,先执行lexer.nextToken()方法调用,获取到用户自定义的from后续的标识符(from关键字后续跟着的空格等字符会跳过),判断在没有获取到,则将其置为:Token.IDENTIFIER,表示用户自定义的identifier标识符。如下 keywords.getKeyword(hashLCase) 如果获取的标识符不存在,则将判断为用户自定义的标识符。

参考com.alibaba.druid.sql.parser.Lexer的scanIdentifier()方法:

public void scanIdentifier() {this.hashLCase = 0;this.hash = 0;final char first = ch;if (ch == '`') {mark = pos;bufPos = 1;char ch;int startPos = pos + 1;int quoteIndex = text.indexOf('`', startPos);if (quoteIndex == -1) {throw new ParserException("illegal identifier. " + info());}hashLCase = 0xcbf29ce484222325L;hash = 0xcbf29ce484222325L;for (int i = startPos; i < quoteIndex; ++i) {ch = text.charAt(i);hashLCase ^= ((ch >= 'A' && ch <= 'Z') ? (ch + 32) : ch);hashLCase *= 0x100000001b3L;hash ^= ch;hash *= 0x100000001b3L;}stringVal = MySqlLexer.quoteTable.addSymbol(text, pos, quoteIndex + 1 - pos, hash);//stringVal = text.substring(mark, pos);pos = quoteIndex + 1;this.ch = charAt(pos);token = Token.IDENTIFIER;return;}final boolean firstFlag = isFirstIdentifierChar(first);if (!firstFlag) {throw new ParserException("illegal identifier. " + info());}hashLCase = 0xcbf29ce484222325L;hash = 0xcbf29ce484222325L;hashLCase ^= ((ch >= 'A' && ch <= 'Z') ? (ch + 32) : ch);hashLCase *= 0x100000001b3L;hash ^= ch;hash *= 0x100000001b3L;mark = pos;bufPos = 1;char ch = 0;for (; ; ) {char c0 = ch;ch = charAt(++pos);if (!isIdentifierChar(ch)) {if ((ch == '(' || ch == ')') && c0 > 256) {bufPos++;continue;}break;}hashLCase ^= ((ch >= 'A' && ch <= 'Z') ? (ch + 32) : ch);hashLCase *= 0x100000001b3L;hash ^= ch;hash *= 0x100000001b3L;bufPos++;continue;}this.ch = charAt(pos);if (bufPos == 1) {switch (first) {case '(':token = Token.LPAREN;return;case ')':token = Token.RPAREN;return;default:break;}token = Token.IDENTIFIER;stringVal = CharTypes.valueOf(first);if (stringVal == null) {stringVal = Character.toString(first);}return;}Token tok = keywords.getKeyword(hashLCase);if (tok != null) {token = tok;if (token == Token.IDENTIFIER) {stringVal = SymbolTable.global.addSymbol(text, mark, bufPos, hash);} else {stringVal = null;}} else {token = Token.IDENTIFIER;stringVal = SymbolTable.global.addSymbol(text, mark, bufPos, hash);}
}

获取表名的工具类方法:

public static List<String> getTables(String sql, DbType dbType) {Set<String> tables = new LinkedHashSet<>();boolean set = false;Lexer lexer = createLexer(sql, dbType);lexer.nextToken();SQLExprParser exprParser;switch (dbType) {case odps:exprParser = new OdpsExprParser(lexer);break;case mysql:exprParser = new MySqlExprParser(lexer);break;default:exprParser = new SQLExprParser(lexer);break;}for_:for (; lexer.token != Token.EOF; ) {switch (lexer.token) {case CREATE:case DROP:case ALTER:set = false;lexer.nextToken();if (lexer.token == Token.TABLE) {lexer.nextToken();if (lexer.token == Token.IF) {lexer.nextToken();if (lexer.token == Token.NOT) {lexer.nextToken();}if (lexer.token == Token.EXISTS) {lexer.nextToken();}}SQLName name = exprParser.name();tables.add(name.toString());if (lexer.token == Token.AS) {lexer.nextToken();}}continue for_;case FROM:case JOIN:lexer.nextToken();if (lexer.token != Token.LPAREN&& lexer.token != Token.VALUES) {SQLName name = exprParser.name();tables.add(name.toString());}continue for_;case SEMI:set = false;break;case SET:set = true;break;case EQ:if (set && dbType == DbType.odps) {lexer.nextTokenForSet();continue for_;}break;default:break;}lexer.nextToken();}return new ArrayList<>(tables);
}

比如上述的from关键字,当执行完lexer.nextToken()方法后,lexer.stringVal()方法即可以通过字符串的头尾下标切割字符串并返回该标识符,比如表名,也就是我们自定义的标识符。同时关注源码逻辑可知,数据库的表名,druid工具类会将大写字符、小写字符(大写字母A-Z的ASCII码值范围是65-90,而小写字母的ASCII码值范围是97-122,在大写字母的ASCII码值上+32即可转换成小写字母)判定为标识符,同时druid在处理时,除了大小写字母外,下划线(_)、美元符号($)、数字(0-9,ASCII码值范围是48-57)等等,均可判定为标识符。

修改表名含有大写字母,如下获取sql的表名:

List<String> tables = SQLParserUtils.getTables("select * from my_fuitrs_99 where id = ?", DbType.mysql);
System.out.println(tables);
String nihao_99 = "select * from my_fuitrs_99 where id = ?".replace(tables.get(0), "my_fuitrs");
System.out.println(nihao_99);
List<String> tables2 = SQLParserUtils.getTables("select * from MY_fuitrs_99 where id = ?", DbType.mysql);
System.out.println(tables2);

重新执行执行结果如下:

[my_fuitrs_99]
select * from my_fuitrs where id = ?
[MY_fuitrs_99]

3 举一反三

那么我们可以根据上面的工具,简单自定义实现一个替换sql表名的工具,工具类如下:

SQLParserUtil:

package com.xiaoxu.parser;import com.alibaba.druid.DbType;
import com.alibaba.druid.sql.ast.SQLName;
import com.alibaba.druid.sql.dialect.mysql.parser.MySqlExprParser;
import com.alibaba.druid.sql.dialect.mysql.parser.MySqlLexer;
import com.alibaba.druid.sql.parser.Lexer;
import com.alibaba.druid.sql.parser.SQLExprParser;
import com.alibaba.druid.sql.parser.SQLParserFeature;
import com.alibaba.druid.sql.parser.Token;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;/*** @author xiaoxu* @date 2024-03-12* java_demo2:com.xiaoxu.parser.SQLParserUtil*/
@SuppressWarnings("all")
public class SQLParserUtil {public static String getSqlFromReplaceNameIfNeccessary(String sql, DbType dbType, @Nullable String replaceTableName) {String table = null;String newSql = sql;Lexer lexer = createLexer(sql, dbType);lexer.nextToken();SQLExprParser exprParser;switch (dbType) {case mysql:exprParser = new MySqlExprParser(lexer);break;default:exprParser = new SQLExprParser(lexer);break;}for_:for (; lexer.token() != Token.EOF; ) {switch (lexer.token()) {case FROM:case INTO:lexer.nextToken();if (lexer.token() != Token.LPAREN && lexer.token() != Token.VALUES) {if (StringUtils.hasText(replaceTableName)) {int mark = ((MLexer) lexer).getMark();int bufPoss = ((MLexer) lexer).getBufPos();StringBuilder sbd = new StringBuilder();sbd.append(sql.substring(0, mark));sbd.append(replaceTableName);sbd.append(sql.substring(mark + bufPoss));newSql = sbd.toString();}SQLName name = exprParser.name();table = name.toString();}break for_;default:break;}lexer.nextToken();}System.out.println("原本的表名:" + table);System.out.println("替换表名为:" + replaceTableName + "后的sql:" + newSql);return newSql;}public static Lexer createLexer(String sql, DbType dbType) {return createLexer(sql, dbType, new SQLParserFeature[0]);}public static Lexer createLexer(String sql, DbType dbType, SQLParserFeature... features) {if (dbType == null) {dbType = DbType.mysql;}switch (dbType) {case mysql:return new MLexer(sql);default:return new Lexer(sql, null, dbType);}}private static class MLexer extends MySqlLexer {public MLexer(char[] input, int inputLength, boolean skipComment) {super(input, inputLength, skipComment);}public MLexer(String input) {super(input);}public MLexer(String input, SQLParserFeature... features) {super(input, features);}public MLexer(String input, boolean skipComment, boolean keepComments) {super(input, skipComment, keepComments);}public int getBufPos() {return this.bufPos;}public int getMark() {return this.mark;}}}

测试下我们自定义的SQLParserUtil工具类:

package com.xiaoxu.parser;import com.alibaba.druid.DbType;/*** @author xiaoxu* @date 2024-03-12* java_demo2:com.xiaoxu.parser.SQLParserTest2*/
public class SQLParserTest2 {public static void main(String[] args) {String sql = SQLParserUtil.getSqlFromReplaceNameIfNeccessary("select * from my_fruits_99 where id = ?",DbType.mysql, "xiaoxu_88");System.out.println(sql);System.out.println("\n");String sql2 = SQLParserUtil.getSqlFromReplaceNameIfNeccessary("insert into apple_$66 values()",DbType.mysql, "Pear$_88");System.out.println(sql2);}}

执行结果如下:

在这里插入图片描述

可以看到,上面工具针对扫描到标识符为FROM(比如select * from语句)或者INTO(比如insert into语句)时,可以实现替换sql的表名功能,其余类似功能参考druid的工具类自行实现即可。

再来举个栗子,新增方法getSqlInHoldCountIfNeccessary如下:

package com.xiaoxu.parser;import com.alibaba.druid.DbType;
import com.alibaba.druid.sql.ast.SQLName;
import com.alibaba.druid.sql.dialect.mysql.parser.MySqlExprParser;
import com.alibaba.druid.sql.dialect.mysql.parser.MySqlLexer;
import com.alibaba.druid.sql.parser.Lexer;
import com.alibaba.druid.sql.parser.SQLExprParser;
import com.alibaba.druid.sql.parser.SQLParserFeature;
import com.alibaba.druid.sql.parser.Token;
import com.google.common.collect.Lists;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;import java.util.List;/*** @author xiaoxu* @date 2024-03-12* java_demo2:com.xiaoxu.parser.SQLParserUtil*/
@SuppressWarnings("all")
public class SQLParserUtil {public static String getSqlInHoldCountIfNeccessary(String sql, DbType dbType, @Nullable Integer[] count) {StringBuilder tempSql = new StringBuilder();Lexer lexer = createLexer(sql, dbType);lexer.nextToken();int posInc = 0;int startPos = 0;int endPos = 0;int subStartPos = 0;for_:for (; lexer.token() != Token.EOF; ) {switch (lexer.token()) {case IN:lexer.nextToken();startPos = endPos;endPos = ((MLexer) lexer).getPos();if (lexer.token() == Token.LPAREN) {tempSql.append(sql, startPos, endPos);subStartPos = endPos;do {endPos = ((MLexer) lexer).getPos();lexer.nextToken();} while (lexer.token() != Token.RPAREN);String replaceMent = sql.substring(subStartPos, endPos);if (count != null && count.length > posInc && count[posInc] != null && count[posInc] > 0) {List<String> incStrs = Lists.newArrayList();for (int i = 0; i < count[posInc]; i++) {incStrs.add("?");}tempSql.append(String.join(",", incStrs));} else {tempSql.append(replaceMent);}posInc++;}continue for_;default:break;}lexer.nextToken();}tempSql.append(sql.substring(endPos));return tempSql.toString();}public static String getSqlFromReplaceNameIfNeccessary(String sql, DbType dbType, @Nullable String replaceTableName) {String table = null;String newSql = sql;Lexer lexer = createLexer(sql, dbType);lexer.nextToken();SQLExprParser exprParser;switch (dbType) {case mysql:exprParser = new MySqlExprParser(lexer);break;default:exprParser = new SQLExprParser(lexer);break;}for_:for (; lexer.token() != Token.EOF; ) {switch (lexer.token()) {case FROM:case INTO:lexer.nextToken();if (lexer.token() != Token.LPAREN && lexer.token() != Token.VALUES) {if (StringUtils.hasText(replaceTableName)) {int mark = ((MLexer) lexer).getMark();int bufPoss = ((MLexer) lexer).getBufPos();StringBuilder sbd = new StringBuilder();sbd.append(sql.substring(0, mark));sbd.append(replaceTableName);sbd.append(sql.substring(mark + bufPoss));newSql = sbd.toString();}SQLName name = exprParser.name();table = name.toString();}break for_;default:break;}lexer.nextToken();}System.out.println("原本的表名:" + table);System.out.println("替换表名为:" + replaceTableName + "后的sql:" + newSql);return newSql;}public static Lexer createLexer(String sql, DbType dbType) {return createLexer(sql, dbType, new SQLParserFeature[0]);}public static Lexer createLexer(String sql, DbType dbType, SQLParserFeature... features) {if (dbType == null) {dbType = DbType.mysql;}switch (dbType) {case mysql:return new MLexer(sql);default:return new Lexer(sql, null, dbType);}}private static class MLexer extends MySqlLexer {public MLexer(char[] input, int inputLength, boolean skipComment) {super(input, inputLength, skipComment);}public MLexer(String input) {super(input);}public MLexer(String input, SQLParserFeature... features) {super(input, features);}public MLexer(String input, boolean skipComment, boolean keepComments) {super(input, skipComment, keepComments);}public int getBufPos() {return this.bufPos;}public int getMark() {return this.mark;}public int getPos() {return this.pos;}}}

getSqlInHoldCountIfNeccessary方法的效果是,我们知道在mysql的子查询IN中,假设一个sql有多处具有子查询IN,假定为:in (?)。但是我们需要自定义IN子查询后续的参数个数,意即类似更新sql的子查询参数数目为in (?,?,?),同时不改变原有sql的语句,那么通过我们自定义的该方法可以达到效果,测试如下:

package com.xiaoxu.parser;import com.alibaba.druid.DbType;/*** @author xiaoxu* @date 2024-03-12* java_demo2:com.xiaoxu.parser.SQLParserTest2*/
public class SQLParserTest2 {public static void main(String[] args) {String sql = SQLParserUtil.getSqlInHoldCountIfNeccessary("select * from my where id in( ?,?) and status is not null and name in (?) and ot ='N' and pr in ()",DbType.mysql, new Integer[]{null, 4});System.out.println("最终结果是:");System.out.println(sql);String sql2 = SQLParserUtil.getSqlInHoldCountIfNeccessary("select * from my where id in (?)",DbType.mysql, new Integer[]{3});System.out.println(sql2);String sql3 = SQLParserUtil.getSqlInHoldCountIfNeccessary("select * from my where id = ?",DbType.mysql, new Integer[]{3});System.out.println(sql3);}}

执行结果如下:

在这里插入图片描述
其中参数new Integer[]{null, 4}的效果是,第一个IN子查询不变,第二个子查询更新为in (?,?,?,?)。该逻辑是按照顺序更新IN的后续参数数目,同时不改变原有的sql。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/2869351.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

使用paho.mqtt.client实现MQTT Client连接物联网平台(ThingsCloud)

目录 概述 1 ThingsCloud平台上创建项目 1.1 创建项目 1.2 配置App UI 2 认识paho.mqtt.client 3 实现MQTT Client 3.1 实现的接口介绍 3.2 paho.mqtt.client库函数介绍 3.3 MQTT Client类实现 3.3.1 创建项目 3.3.2 编写MQTT Client类代码 3.3.3 Log工具源码 4 实…

客户端:Vue3,服务端:Node,基于Socket.IO实现单聊的功能

目录 1.介绍 2.环境搭建 3.本功能实现的主要逻辑 4.客户端和服务端的主要代码 5.效果展示 6.socket.io的运作原理 1.介绍 本篇主要讲讲基于Socket.IO实现单聊功能的主要实现&#xff0c;包括了客户端和服务端Node。 在这个即时通讯无处不在的时代&#xff0c;实时聊天功能…

Java面试题总结18之springcloud四种分布式事务解决方案

XA规范&#xff1a;分布式事务规范&#xff0c;规定了分布式事务模型 四个角色&#xff1a;事务管理器&#xff08;协调者TM&#xff09;&#xff0c;资源管理器&#xff08;参与者RM&#xff09;&#xff0c;应用程序AP&#xff0c;通信资源管理器CRM 全局事务&#xff1a;一…

【Hadoop大数据技术】——MapReduce分布式计算框架(学习笔记)

&#x1f4d6; 前言&#xff1a;MapReduce是Hadoop系统核心组件之一&#xff0c;它是一种可用于大数据并行处理的计算模型、框架和平台&#xff0c;主要解决海量数据的计算问题&#xff0c;是目前分布式计算模型中应用较为广泛的一种。 目录 &#x1f552; 1. MapReduce概述&am…

JVM学习-JMM

目录 1.什么是JMM 2.JMM怎样保障数据的可见性、有序性、原子性 2.1保证原子性 2.2.保证可见性 2.3保证有序性 3.CAS 3.1乐观锁和悲观锁 3.2 CAS介绍 4.重量级锁的自旋优化 1.什么是JMM JMM即Java内存模型 &#xff0c;定义了一套在多线程读写共享数据&#xff08;如数组、成…

贪心算法(算法竞赛、蓝桥杯)--糖果传递

1、B站视频链接&#xff1a;A31 贪心算法 P2512 [HAOI2008] 糖果传递_哔哩哔哩_bilibili 题目链接&#xff1a;[HAOI2008] 糖果传递 - 洛谷 #include <bits/stdc.h> using namespace std; const int N1000005; int n,a[N],c[N]; long long b,ans;int main(){scanf(&quo…

鸿蒙Harmony应用开发—ArkTS声明式开发(容器组件:UIExtensionComponent (系统接口))

UIExtensionComponent用于支持在本页面内嵌入其他应用提供的UI。展示的内容在另外一个进程中运行&#xff0c;本应用并不参与其中的布局和渲染。 通常用于有进程隔离诉求的模块化开发场景。 说明&#xff1a; 该组件从API Version 10开始支持。后续版本如有新增内容&#xff0…

QT C++ QButtonGroup应用

//QT 中&#xff0c;按钮数量比较少&#xff0c;可以分别用各按钮的信号和槽处理。 //当按钮数量较多时&#xff0c;用QButtonGroup可以实现共用一个槽函数&#xff0c;批量处理&#xff0c;减少垃圾代码&#xff0c; //减少出错。 //开发平台&#xff1a;win10QT6.2.4 MSVC…

Xilinx FPGA模式配置

Xilinx FPGA模式配置 Xilinx UltraScale FPGA有7种配置模式&#xff0c;由模式输入引脚M[2:0]决定。七种模式如图1所示。 图1 7种配置模式 7种模式可分为3大类&#xff0c; 1、JTAG模式&#xff08;可归为从模式&#xff09;&#xff1b; 2、主模式&#xff1b; 3、从模式…

基于SpringBoot SSM vue办公自动化系统

基于SpringBoot SSM vue办公自动化系统 系统功能 登录 个人中心 请假信息管理 考勤信息管理 出差信息管理 行政领导管理 代办事项管理 文档管理 公告信息管理 企业信息管理 会议室信息管理 资产设备管理 员工信息管理 开发环境和技术 开发语言&#xff1a;Java 使用框架: S…

OpenCV4.9.0开源计算机视觉库在 Linux 中安装

返回目录&#xff1a;OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇&#xff1a;OpenCV 环境变量参考 下一篇&#xff1a;将OpenCV与gcc和CMake结合使用 引言&#xff1a; OpenCV是一个开源的计算机视觉库&#xff0c;由英特尔公司所赞助。它是一个跨…

【LeetCode热题100】146. LRU 缓存(链表)

一.题目要求 请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCache 类&#xff1a; LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存int get(int key) 如果关键字 key 存在于缓存中&#xff0c;则返回关键字的值&#xff0c…

wsl or 虚拟机 安装

1.wsl2安装 WSL全称Windows Subsystem for Linux&#xff0c;是微软开发的适用于Linux的Windows子系统 如今已经有两代了&#xff1a; 所以用的多的还是wsl2。 安装前需要先去设置启用或关闭Windows功能&#xff1a; 打开适用于linux的子系统和虚拟机平台。 Microsoft Store里…

matlab simulink 工业现场漆包机烘炉模糊PID控制算法

1、内容简介 略 70-可以交流、咨询、答疑 2、内容说明 matlab simulink 工业现场漆包机烘炉模糊PID控制算法 第五章 控制算法仿真与分析 在第二章系统分析结合工业现场漆包机烘炉数学模型详细分析模糊PID控制算法以及解耦控制算法的优缺点、实现原理&#xff0c;为本章算法…

Jmeter---分布式

分布式&#xff1a;多台机协作&#xff0c;以集群的方式完成测试任务&#xff0c;可以提高测试效率。 分布式架构&#xff1a;控制机&#xff08;分发任务&#xff09;与多台执行机&#xff08;执行任务&#xff09; 环境搭建&#xff1a; 不同的测试机上安装 Jmeter 配置基…

Acwing-基础算法课笔记之动态规划(线性DP)

Acwing-基础算法课笔记之动态规划&#xff08;线性DP&#xff09; 一、数字三角形1、概述2、闫氏dp分析法代码示例 二、最长上升子序列1、概述2、闫氏dp分析法3、过程模拟4、代码演示 三、最长上升子序列强化版1、概述2、代码示例 四、最长公共子序列&#xff08;LCS&#xff0…

FFmpeg查看所有支持的编码/解码器/封装/解封装/媒体格式/滤镜

查看所有支持的编码器与解码器 ffmpeg -codecs 只查看所有编码器: ffmpeg -encoders 只查看所有解码器: ffmpeg -decoders 只查看H264编码器: ffmpeg -h encoderh264 只查看H264解码器: ffmpeg -h decoderh264 查看所有支持的封装: ffmpeg -muxers 查看所有支持的解封装…

三.使用java的API文档

在Java中&#xff0c;API是指“应用程序接口”&#xff08;Application Programming Interface&#xff09;。Java API是Java编程语言中提供的类和接口的集合&#xff0c;用于开发各种类型的应用程序。类比C的STL&#xff08;标准模板库&#xff09;。 通俗理解就当做些封装好…

Spark-Scala语言实战(1)

在之前的文章中&#xff0c;我们学习了如何在Linux安装Spark以及Scala&#xff0c;想了解的朋友可以查看这篇文章。同时&#xff0c;希望我的文章能帮助到你&#xff0c;如果觉得我的文章写的不错&#xff0c;请留下你宝贵的点赞&#xff0c;谢谢。 Spark及Scala的安装https:/…

如何在Windows11上安装WSL和Linux子系统以及搭建Docker环境

今天给大家介绍一下如何在Windows11上安装Docker 打开控制面板&#xff1a; 打开程序&#xff1a; 打开启用或关闭Windows功能。 勾选Linux子系统&#xff1a; 此时&#xff0c;可能需要重启电脑。 以管理员身份打开PowerShell执行&#xff1a; bcdedit /set hyperv…