🙂🙂🙂关注微信公众号:【芋艿的后端小屋】有福利:
  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

  • 1. 概述
  • 2. SQLParsingEngine
  • 3. SQLParser SQL解析器
    • 3.2.1 #parseExpression() 和 SQLExpression
    • 3.2.2 #parseAlias()
    • 3.2.3 #parseSingleTable()
    • 3.2.4 #skipJoin()
    • 3.2.5 #parseWhere()
    • 3.1 AbstractParser
    • 3.2 SQLParser
  • 4. StatementParser SQL语句解析器
    • 4.1 StatementParser
    • 4.2 Statement
  • 5. 彩蛋

1. 概述

上篇文章《词法解析》分享了词法解析器Lexer是如何解析 SQL 里的词法。本文分享SQL解析引擎是如何解析与理解 SQL的。因为本文建立在《词法解析》之上,你需要阅读它后在开始这段旅程。🙂如果对词法解析不完全理解,请给我的公众号(芋艿的后端小屋)留言,我会逐条认真耐心回复!
区别于 Lexer,Parser 理解SQL
  • 提炼分片上下文
  • 标记需要SQL改写的部分
Parser 有三个组件:
  • SQLParsingEngine :SQL 解析引擎
  • SQLParser :SQL 解析器
  • StatementParser :SQL语句解析器
SQLParsingEngine 调用 StatementParser 解析 SQL。

StatementParser 调用 SQLParser 解析 SQL 表达式。

SQLParser 调用 Lexer 解析 SQL 词法。
😜 是不是觉得 SQLParser 和 StatementParser 看起来很接近?下文为你揭开这个答案。
Sharding-JDBC 正在收集使用公司名单:传送门。

🙂 你的登记,会让更多人参与和使用 Sharding-JDBC。传送门

Sharding-JDBC 也会因此,能够覆盖更多的业务场景。传送门

登记吧,骚年!传送门

2. SQLParsingEngine

SQLParsingEngine,SQL 解析引擎。其 #parse() 方法作为 SQL 解析入口,本身不带复杂逻辑,通过调用 SQL 对应的 StatementParser 进行 SQL 解析。
核心代码如下:
  1. // SQLParsingEngine.java
  2. publicSQLStatement parse(){
  3. // 获取 SQL解析器
  4. SQLParser sqlParser = getSQLParser();
  5. //
  6.   sqlParser.skipIfEqual(Symbol.SEMI);// 跳过 ";"
  7. if(sqlParser.equalAny(DefaultKeyword.WITH)){// WITH Syntax
  8.       skipWith(sqlParser);
  9. }
  10. // 获取对应 SQL语句解析器 解析SQL
  11. if(sqlParser.equalAny(DefaultKeyword.SELECT)){
  12. returnSelectParserFactory.newInstance(sqlParser).parse();
  13. }
  14. if(sqlParser.equalAny(DefaultKeyword.INSERT)){
  15. returnInsertParserFactory.newInstance(shardingRule, sqlParser).parse();
  16. }
  17. if(sqlParser.equalAny(DefaultKeyword.UPDATE)){
  18. returnUpdateParserFactory.newInstance(sqlParser).parse();
  19. }
  20. if(sqlParser.equalAny(DefaultKeyword.DELETE)){
  21. returnDeleteParserFactory.newInstance(sqlParser).parse();
  22. }
  23. thrownewSQLParsingUnsupportedException(sqlParser.getLexer().getCurrentToken().getType());
  24. }

3. SQLParser SQL解析器

SQLParser,SQL 解析器。和词法解析器 Lexer 一样,不同数据库有不同的实现。
类图如下(包含所有属性和方法)(放大图片):

3.1 AbstractParser

AbstractParser,SQLParser 的抽象父类,对 Lexer 简单封装。例如:
  • #skipIfEqual():判断当前词法标记类型是否与其中一个传入值相等
  • #equalAny():判断当前词法标记类型是否与其中一个传入值相等
这里有一点我们需要注意,SQLParser 并不是等 Lexer 解析完词法( Token ),再根据词法去理解 SQL。而是,在理解 SQL 的过程中,调用 Lexer 进行分词。
  1. // SQLParsingEngine.java#parse()片段
  2. if(sqlParser.equalAny(DefaultKeyword.SELECT)){
  3. returnSelectParserFactory.newInstance(sqlParser).parse();
  4. }
  5. // AbstractParser.java
  6. publicfinalboolean equalAny(finalTokenType... tokenTypes){
  7. for(TokenType each : tokenTypes){
  8. if(each == lexer.getCurrentToken().getType()){
  9. returntrue;
  10. }
  11. }
  12. returnfalse;
  13. }
  • ↑↑↑ 判断当前词法是否为 SELECT。实际 AbstractParser 只知道当前词法,并不知道后面还有哪些词法,也不知道之前有哪些词法。
我们来看 AbstractParser 里比较复杂的方法 #skipParentheses() 帮助大家再理解下。请认真看代码注释噢。
  1. // AbstractParser.java
  2. /**
  3. * 跳过小括号内所有的词法标记.
  4. *
  5. * @return 小括号内所有的词法标记
  6. */
  7. publicfinalString skipParentheses(){
  8. StringBuilder result =newStringBuilder("");
  9. int count =0;
  10. if(Symbol.LEFT_PAREN == getLexer().getCurrentToken().getType()){
  11. finalint beginPosition = getLexer().getCurrentToken().getEndPosition();
  12.       result.append(Symbol.LEFT_PAREN.getLiterals());
  13.       getLexer().nextToken();
  14. while(true){
  15. if(equalAny(Symbol.QUESTION)){
  16.               increaseParametersIndex();
  17. }
  18. // 到达结尾 或者 匹配合适数的)右括号
  19. if(Assist.END== getLexer().getCurrentToken().getType()||(Symbol.RIGHT_PAREN == getLexer().getCurrentToken().getType()&&0== count)){
  20. break;
  21. }
  22. // 处理里面有多个括号的情况,例如:SELECT COUNT(DISTINCT(order_id) FROM t_order
  23. if(Symbol.LEFT_PAREN == getLexer().getCurrentToken().getType()){
  24.               count++;
  25. }elseif(Symbol.RIGHT_PAREN == getLexer().getCurrentToken().getType()){
  26.               count--;
  27. }
  28. // 下一个词法
  29.           getLexer().nextToken();
  30. }
  31. // 获得括号内的内容
  32.       result.append(getLexer().getInput().substring(beginPosition, getLexer().getCurrentToken().getEndPosition()));
  33. // 下一个词法
  34.       getLexer().nextToken();
  35. }
  36. return result.toString();
  37. }
这个类其它方法很重要,逻辑相对简单,我们就不占用篇幅了。大家一定要看哟,后面调用非常非常多。AbstractParser.java 传送门。👼也可以关注我的公众号(芋艿的后端小屋)发送关键字【sjdbc】获取增加方法内注释的项目地址

3.2 SQLParser

SQLParser,SQL 解析器,主要提供只考虑 SQL 块的解析方法,不考虑 SQL 上下文。下文即将提到的 StatementParser 将 SQL 拆成对应的,调用 SQLParser 进行解析。🤓 这么说,可能会有些抽象,我们下面来一起看。
SQLParser 看起来方法特别多,合并下一共 5 种:
方法说明
#parseExpression()解析表达式
#parseAlias()解析别名
#parseSingleTable()解析单表
#skipJoin()跳过表关联词法
#parseWhere()解析查询条件
看了这 5 个方法是否有点理解了?SQLParser 不考虑 SQL 是 SELECT / INSERT / UPDATE / DELETE ,它考虑的是,给我的是 WHERE 处解析查询条件,或是 INSERT INTO 解析单表 等,提供 SELECT / INSERT / UPDATE / DELETE 需要的 SQL 块公用解析。

3.2.1 #parseExpression() 和 SQLExpression

SQLExpression,SQL表达式接口。目前 6 种实现:
说明对应Token
SQLIdentifierExpression标识表达式Literals.IDENTIFIER
SQLPropertyExpression属性表达式
SQLNumberExpression数字表达式Literals.INT, Literals.HEX
SQLPlaceholderExpression占位符表达式Symbol.QUESTION
SQLTextExpression字符表达式Literals.CHARS
SQLIgnoreExpression分片中无需关注的SQL表达式
  • SQLPropertyExpression 例如: SELECT*FROM t_order o ORDER BY o.order_id 中的 o.order_idSQLPropertyExpression 从 SQLIdentifierExpression 进一步判断解析而来。
  • SQLIgnoreExpression 例如: SELECT*FROM t_order o ORDER BY o.order_id%2 中的 o.order_id%2复合表达式都会解析成 SQLIgnoreExpression。
解析 SQLExpression 核心代码如下:
  1. // SQLParser.java
  2. /**
  3. * 解析表达式.
  4. *
  5. * @return 表达式
  6. */
  7. // TODO 完善Expression解析的各种场景
  8. publicfinalSQLExpression parseExpression(){
  9. // 解析表达式
  10. String literals = getLexer().getCurrentToken().getLiterals();
  11. finalSQLExpression expression = getExpression(literals);
  12. // SQLIdentifierExpression 需要特殊处理。考虑自定义函数,表名.属性情况。
  13. if(skipIfEqual(Literals.IDENTIFIER)){
  14. if(skipIfEqual(Symbol.DOT)){// 例如,ORDER BY o.uid 中的 "o.uid"
  15. Stringproperty= getLexer().getCurrentToken().getLiterals();
  16.           getLexer().nextToken();
  17. return skipIfCompositeExpression()?newSQLIgnoreExpression():newSQLPropertyExpression(newSQLIdentifierExpression(literals),property);
  18. }
  19. if(equalAny(Symbol.LEFT_PAREN)){// 例如,GROUP BY DATE(create_time) 中的 "DATE(create_time)"
  20.           skipParentheses();
  21.           skipRestCompositeExpression();
  22. returnnewSQLIgnoreExpression();
  23. }
  24. return skipIfCompositeExpression()?newSQLIgnoreExpression(): expression;
  25. }
  26.   getLexer().nextToken();
  27. return skipIfCompositeExpression()?newSQLIgnoreExpression(): expression;
  28. }
  29. /**
  30. * 获得 词法Token 对应的 SQLExpression
  31. *
  32. * @param literals 词法字面量标记
  33. * @return SQLExpression
  34. */
  35. privateSQLExpression getExpression(finalString literals){
  36. if(equalAny(Symbol.QUESTION)){
  37.       increaseParametersIndex();
  38. returnnewSQLPlaceholderExpression(getParametersIndex()-1);
  39. }
  40. if(equalAny(Literals.CHARS)){
  41. returnnewSQLTextExpression(literals);
  42. }
  43. // TODO 考虑long的情况
  44. if(equalAny(Literals.INT)){
  45. returnnewSQLNumberExpression(Integer.parseInt(literals));
  46. }
  47. if(equalAny(Literals.FLOAT)){
  48. returnnewSQLNumberExpression(Double.parseDouble(literals));
  49. }
  50. // TODO 考虑long的情况
  51. if(equalAny(Literals.HEX)){
  52. returnnewSQLNumberExpression(Integer.parseInt(literals,16));
  53. }
  54. if(equalAny(Literals.IDENTIFIER)){
  55. returnnewSQLIdentifierExpression(SQLUtil.getExactlyValue(literals));
  56. }
  57. returnnewSQLIgnoreExpression();
  58. }
  59. /**
  60. * 如果是 复合表达式,跳过。
  61. *
  62. * @return 是否跳过
  63. */
  64. privateboolean skipIfCompositeExpression(){
  65. if(equalAny(Symbol.PLUS,Symbol.SUB,Symbol.STAR,Symbol.SLASH,Symbol.PERCENT,Symbol.AMP,Symbol.BAR,Symbol.DOUBLE_AMP,Symbol.DOUBLE_BAR,Symbol.CARET,Symbol.DOT,Symbol.LEFT_PAREN)){
  66.       skipParentheses();
  67.       skipRestCompositeExpression();
  68. returntrue;
  69. }
  70. returnfalse;
  71. }
  72. /**
  73. * 跳过剩余复合表达式
  74. */
  75. privatevoid skipRestCompositeExpression(){
  76. while(skipIfEqual(Symbol.PLUS,Symbol.SUB,Symbol.STAR,Symbol.SLASH,Symbol.PERCENT,Symbol.AMP,Symbol.BAR,Symbol.DOUBLE_AMP,Symbol.DOUBLE_BAR,Symbol.CARET,Symbol.DOT)){
  77. if(equalAny(Symbol.QUESTION)){
  78.           increaseParametersIndex();
  79. }
  80.       getLexer().nextToken();
  81.       skipParentheses();
  82. }
  83. }
解析了 SQLExpression 有什么用呢?我们会在《查询SQL解析》、《插入SQL解析》、《更新SQL解析》、《删除SQL解析》。留个悬念😈,关注我的公众号(芋艿的后端小屋)实时收到新文更新通知

3.2.2 #parseAlias()

  1. /**
  2. * 解析别名.不仅仅是字段的别名,也可以是表的别名。
  3. *
  4. * @return 别名
  5. */
  6. publicOptional<String> parseAlias(){
  7. // 解析带 AS 情况
  8. if(skipIfEqual(DefaultKeyword.AS)){
  9. if(equalAny(Symbol.values())){
  10. returnOptional.absent();
  11. }
  12. String result =SQLUtil.getExactlyValue(getLexer().getCurrentToken().getLiterals());
  13.       getLexer().nextToken();
  14. returnOptional.of(result);
  15. }
  16. // 解析别名
  17. // TODO 增加哪些数据库识别哪些关键字作为别名的配置
  18. if(equalAny(Literals.IDENTIFIER,Literals.CHARS,DefaultKeyword.USER,DefaultKeyword.END,DefaultKeyword.CASE,DefaultKeyword.KEY,DefaultKeyword.INTERVAL,DefaultKeyword.CONSTRAINT)){
  19. String result =SQLUtil.getExactlyValue(getLexer().getCurrentToken().getLiterals());
  20.       getLexer().nextToken();
  21. returnOptional.of(result);
  22. }
  23. returnOptional.absent();
  24. }

3.2.3 #parseSingleTable()

  1. /**
  2. * 解析单表.
  3. *
  4. * @param sqlStatement SQL语句对象
  5. */
  6. publicfinalvoid parseSingleTable(finalSQLStatement sqlStatement){
  7. boolean hasParentheses =false;
  8. if(skipIfEqual(Symbol.LEFT_PAREN)){
  9. if(equalAny(DefaultKeyword.SELECT)){// multiple-update 或者 multiple-delete
  10. thrownewUnsupportedOperationException("Cannot support subquery");
  11. }
  12.       hasParentheses =true;
  13. }
  14. Table table;
  15. finalint beginPosition = getLexer().getCurrentToken().getEndPosition()- getLexer().getCurrentToken().getLiterals().length();
  16. String literals = getLexer().getCurrentToken().getLiterals();
  17.   getLexer().nextToken();
  18. if(skipIfEqual(Symbol.DOT)){
  19.       getLexer().nextToken();
  20. if(hasParentheses){
  21.           accept(Symbol.RIGHT_PAREN);
  22. }
  23.       table =newTable(SQLUtil.getExactlyValue(literals), parseAlias());
  24. }else{
  25. if(hasParentheses){
  26.           accept(Symbol.RIGHT_PAREN);
  27. }
  28.       table =newTable(SQLUtil.getExactlyValue(literals), parseAlias());
  29. }
  30. if(skipJoin()){// multiple-update 或者 multiple-delete
  31. thrownewUnsupportedOperationException("Cannot support Multiple-Table.");
  32. }
  33.   sqlStatement.getSqlTokens().add(newTableToken(beginPosition, literals));
  34.   sqlStatement.getTables().add(table);
  35. }

3.2.4 #skipJoin()

跳过表关联词法,支持 SELECT*FROM t_user,t_order WHERE..., SELECT*FROM t_user JOIN t_order ON...。下篇《查询SQL解析》解析表会用到这个方法。
  1. // SQLParser.java
  2. /**
  3. * 跳过表关联词法.
  4. *
  5. * @return 是否表关联.
  6. */
  7. publicfinalboolean skipJoin(){
  8. if(skipIfEqual(DefaultKeyword.LEFT,DefaultKeyword.RIGHT,DefaultKeyword.FULL)){
  9.       skipIfEqual(DefaultKeyword.OUTER);
  10.       accept(DefaultKeyword.JOIN);
  11. returntrue;
  12. }elseif(skipIfEqual(DefaultKeyword.INNER)){
  13.       accept(DefaultKeyword.JOIN);
  14. returntrue;
  15. }elseif(skipIfEqual(DefaultKeyword.JOIN,Symbol.COMMA,DefaultKeyword.STRAIGHT_JOIN)){
  16. returntrue;
  17. }elseif(skipIfEqual(DefaultKeyword.CROSS)){
  18. if(skipIfEqual(DefaultKeyword.JOIN,DefaultKeyword.APPLY)){
  19. returntrue;
  20. }
  21. }elseif(skipIfEqual(DefaultKeyword.OUTER)){
  22. if(skipIfEqual(DefaultKeyword.APPLY)){
  23. returntrue;
  24. }
  25. }
  26. returnfalse;
  27. }

3.2.5 #parseWhere()

解析 WHERE 查询条件。目前支持 AND 条件,不支持 OR 条件。近期 OR 条件支持的可能性比较低。另外条件这块对括号解析需要继续优化,实际使用请勿写冗余的括号。例如: SELECT*FROM tbl_name1 WHERE((val1=?)AND(val2=?))AND val3=?
根据不同的运算操作符,分成如下情况:
运算符附加条件方法
=#parseEqualCondition()
IN#parseInCondition()
BETWEEN#parseBetweenCondition()
<, <=, >, >=Oracle 或 SQLServer 分页#parseRowNumberCondition()
<, <=, >, >=#parseOtherCondition()
LIKEparseOtherCondition
代码如下:
  1. // SQLParser.java
  2. /**
  3. * 解析所有查询条件。
  4. * 目前不支持 OR 条件。
  5. *
  6. * @param sqlStatement SQL
  7. */
  8. privatevoid parseConditions(finalSQLStatement sqlStatement){
  9. // AND 查询
  10. do{
  11.       parseComparisonCondition(sqlStatement);
  12. }while(skipIfEqual(DefaultKeyword.AND));
  13. // 目前不支持 OR 条件
  14. if(equalAny(DefaultKeyword.OR)){
  15. thrownewSQLParsingUnsupportedException(getLexer().getCurrentToken().getType());
  16. }
  17. }
  18. // TODO 解析组合expr
  19. /**
  20. * 解析单个查询条件
  21. *
  22. * @param sqlStatement SQL
  23. */
  24. publicfinalvoid parseComparisonCondition(finalSQLStatement sqlStatement){
  25.   skipIfEqual(Symbol.LEFT_PAREN);
  26. SQLExpression left = parseExpression(sqlStatement);
  27. if(equalAny(Symbol.EQ)){
  28.       parseEqualCondition(sqlStatement, left);
  29.       skipIfEqual(Symbol.RIGHT_PAREN);
  30. return;
  31. }
  32. if(equalAny(DefaultKeyword.IN)){
  33.       parseInCondition(sqlStatement, left);
  34.       skipIfEqual(Symbol.RIGHT_PAREN);
  35. return;
  36. }
  37. if(equalAny(DefaultKeyword.BETWEEN)){
  38.       parseBetweenCondition(sqlStatement, left);
  39.       skipIfEqual(Symbol.RIGHT_PAREN);
  40. return;
  41. }
  42. if(equalAny(Symbol.LT,Symbol.GT,Symbol.LT_EQ,Symbol.GT_EQ)){
  43. if(left instanceofSQLIdentifierExpression&& sqlStatement instanceofSelectStatement
  44. && isRowNumberCondition((SelectStatement) sqlStatement,((SQLIdentifierExpression) left).getName())){
  45.           parseRowNumberCondition((SelectStatement) sqlStatement);
  46. }elseif(left instanceofSQLPropertyExpression&& sqlStatement instanceofSelectStatement
  47. && isRowNumberCondition((SelectStatement) sqlStatement,((SQLPropertyExpression) left).getName())){
  48.           parseRowNumberCondition((SelectStatement) sqlStatement);
  49. }else{
  50.           parseOtherCondition(sqlStatement);
  51. }
  52. }elseif(equalAny(DefaultKeyword.LIKE)){
  53.       parseOtherCondition(sqlStatement);
  54. }
  55.   skipIfEqual(Symbol.RIGHT_PAREN);
  56. }
#parseComparisonCondition() 解析到 SQL表达式(left) 和 运算符,调用相应方法进一步处理。我们选择 #parseEqualCondition() 看下,其他方法有兴趣跳转 SQLParser 查看。
  1. // SQLParser.java
  2. /**
  3. * 解析 = 条件
  4. *
  5. * @param sqlStatement SQL
  6. * @param left 左SQLExpression
  7. */
  8. privatevoid parseEqualCondition(finalSQLStatement sqlStatement,finalSQLExpression left){
  9.   getLexer().nextToken();
  10. SQLExpression right = parseExpression(sqlStatement);
  11. // 添加列
  12. // TODO 如果有多表,且找不到column是哪个表的,则不加入condition,以后需要解析binding table
  13. if((sqlStatement.getTables().isSingleTable()|| left instanceofSQLPropertyExpression)
  14. // 只有对路由结果有影响的才会添加到 conditions。SQLPropertyExpression 和 SQLIdentifierExpression 无法判断,所以未加入 conditions
  15. &&(right instanceofSQLNumberExpression|| right instanceofSQLTextExpression|| right instanceofSQLPlaceholderExpression)){
  16. Optional<Column> column = find(sqlStatement.getTables(), left);
  17. if(column.isPresent()){
  18.           sqlStatement.getConditions().add(newCondition(column.get(), right), shardingRule);
  19. }
  20. }
  21. }
#parseEqualCondition() 解析到 SQL表达式(right),并判断 左右SQL表达式 与路由逻辑是否有影响,如果有,则加入到 Condition。这个就是 #parseWhere() 的目的:解析 WHERE 查询条件对路由有影响的条件。《路由》相关的逻辑,会单独开文章介绍。这里,我们先留有映像。

4. StatementParser SQL语句解析器

4.1 StatementParser

StatementParser,SQL语句解析器。每种 SQL,都有相应的 SQL语句解析器实现。不同数据库,继承这些 SQL语句解析器,实现各自 SQL 上的差异。大体结构如下:
SQLParsingEngine 根据不同 SQL 调用对应工厂创建 StatementParser。核心代码如下:
  1. publicfinalclassSelectParserFactory{
  2. /**
  3.     * 创建Select语句解析器.
  4.     *
  5.     * @param sqlParser SQL解析器
  6.     * @return Select语句解析器
  7.     */
  8. publicstaticAbstractSelectParser newInstance(finalSQLParser sqlParser){
  9. if(sqlParser instanceofMySQLParser){
  10. returnnewMySQLSelectParser(sqlParser);
  11. }
  12. if(sqlParser instanceofOracleParser){
  13. returnnewOracleSelectParser(sqlParser);
  14. }
  15. if(sqlParser instanceofSQLServerParser){
  16. returnnewSQLServerSelectParser(sqlParser);
  17. }
  18. if(sqlParser instanceofPostgreSQLParser){
  19. returnnewPostgreSQLSelectParser(sqlParser);
  20. }
  21. thrownewUnsupportedOperationException(String.format("Cannot support sqlParser class [%s].", sqlParser.getClass()));
  22. }
  23. }
调用 StatementParser#parse() 实现方法,对 SQL 进行解析。具体解析过程,另开文章分享。

4.2 Statement

不同 SQL 解析后,返回对应的 SQL 结果,即 Statement。大体结构如下:
Statement 包含两部分信息:
  • 分片上下文:用于 SQL 路由。
  • SQL 标记对象:用于 SQL 改写。
我们会在后文增删改查SQL解析的过程中分享到它们。

4.3 预告

ParserStatement分享文章
SelectStatementParserSelectStatement + AbstractSQLStatement《查询SQL解析》
InsertStatementParserInsertStatement《插入SQL解析》
UpdateStatementParserUpdateStatement《更新SQL解析》
DeleteStatementParserDeleteStatement《删除SQL解析》

5. 彩蛋

老铁,是不是有丢丢长?

如果有地方错误,烦请指出🙂。

如果有地方不是很理解,可以加我的公众号
(芋艿的后端小屋)
留言,我会
逐条认真耐心
回复。

如果觉得还凑合,劳驾分享朋友圈或者基佬。
《查询SQL解析》已经写了一半,预计很快...
继续阅读
阅读原文