(高级)ShardingSphere源码解析-SQL 解析流程

对于分片引擎而言,第一个核心组件就是 SQL 解析引擎。下面是SQL解析设计到的核心类。
在这里插入图片描述

从 DataSource 到 SQL 解析引擎入口

//创建分片规则配置类 
ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration(); 
//创建分表规则配置类 
TableRuleConfiguration tableRuleConfig = new TableRuleConfiguration("user", "ds${0..1}.user${0..1}"); 
//创建分布式主键生成配置类 
Properties properties = new Properties(); 
result.setProperty("worker.id", "33"); 
KeyGeneratorConfiguration keyGeneratorConfig = new KeyGeneratorConfiguration("SNOWFLAKE", "id", properties);
result.setKeyGeneratorConfig(keyGeneratorConfig);
shardingRuleConfig.getTableRuleConfigs().add(tableRuleConfig); 
//根据年龄分库,一共分为2个库 
shardingRuleConfig.setDefaultDatabaseShardingStrategyConfig(new InlineShardingStrategyConfiguration("sex", "ds${sex % 2}")); 
//根据用户id分表,一共分为2张表 
shardingRuleConfig.setDefaultTableShardingStrategyConfig(new StandardShardingStrategyConfiguration("id", "user${id % 2}")); 
//通过工厂类创建具体的DataSource 
return ShardingDataSourceFactory.createDataSource(createDataSourceMap(), shardingRuleConfig, new Properties());

可以看到,上述代码构建了几个数据源,加上分库、分表策略以及分片规则,然后通过 ShardingDataSourceFactory 获取了目前数据源 DataSource 。显然,对于应用开发而言,DataSource 就是我们使用 ShardingSphere 框架的入口。事实上,对于 ShardingSphere 内部的运行机制而言,DataSource 同样是引导我们进入分片引擎的入口。围绕 DataSource,通过跟踪代码的调用链路,我们可以得到如下所示的类层结构图:
在这里插入图片描述
上图已经引出了 ShardingSphere 内核中的很多核心对象,但今天我们只关注位于整个链路的最底层对象,即图中的 SQLParseEngine。一方面,在 DataSource 的创建过程中,最终初始化了 SQLParseEngine;另一方面,负责执行路由功能的 ShardingRouter 也依赖于 SQLParseEngine。这个 SQLParseEngine 就是 ShardingSphere 中负责整个 SQL 解析过程的入口。

SQL解析的核心类SQLParseEngine

对于 SQL 解析引擎而言,SQLParseEngine 本身并不提供外观作用,而是把这部分功能委托给了另一个核心类 SQLParseKernel。从命名上看,这个类才是 SQL 解析的内核类,也是所谓的外观类。SQLParseKernel 屏蔽了后端服务中复杂的 SQL 抽象语法树对象 SQLAST、SQL 片段对象 SQLSegment ,以及最终的 SQL 语句 SQLStatement 对象的创建和管理过程。上述这些类之间的关系如下所示:
在这里插入图片描述

SQLParseEngine

从前面的类层结构图中可以看到,AbstractRuntimeContext 是 SQLParseEngine 的构建入口。顾名思义,RuntimeContext 在 ShardingSphere 中充当一种运行时上下文,保存着与运行时环境下相关的分片规则、分片属性、数据库类型、执行引擎以及 SQL 解析引擎。作为 RuntimeContext 接口的实现类,AbstractRuntimeContex 在其构造函数中完成了对 SQLParseEngine 的构建,构建过程如下所示:

protected AbstractRuntimeContext(final T rule, final Properties props, final DatabaseType databaseType) { 
       … 
       parseEngine = SQLParseEngineFactory.getSQLParseEngine(DatabaseTypes.getTrunkDatabaseTypeName(databaseType));}

显然,这里通过工厂类 SQLParseEngineFactory 完成了 SQLParseEngine 的创建过程。工厂类 SQLParseEngineFactory 的实现如下:

public final class SQLParseEngineFactory { 
    private static final Map<String, SQLParseEngine> ENGINES = new ConcurrentHashMap<>(); 
	public static SQLParseEngine getSQLParseEngine(final String databaseTypeName) {
        if (ENGINES.containsKey(databaseTypeName)) { 
            return ENGINES.get(databaseTypeName); 
        } 
        synchronized (ENGINES) { 
           //如果缓存中包含了指定数据库类型对应的SQLParseEngine,则直接返回 
            if (ENGINES.containsKey(databaseTypeName)) { 
                return ENGINES.get(databaseTypeName); 
            } 
           //创建SQLParseEngine 
            SQLParseEngine result = new SQLParseEngine(databaseTypeName); 
           //将新创建的SQLParseEngine放入缓存中 
            ENGINES.put(databaseTypeName, result); 
            return result; 
        } 
    } 
}

为了提高访问性能,ShardingSphere 大量使用这种方式来构建基于内容的缓存机制。

接下来,我们来看 SQLParseEngine 类本身,该类的完整代码如下所示:

public final class SQLParseEngine { 
    private final String databaseTypeName; 
    private final SQLParseResultCache cache = new SQLParseResultCache(); 
	public SQLStatement parse(final String sql, final boolean useCache) { 
	    //基于Hook机制进行监控和跟踪 
        ParsingHook parsingHook = new SPIParsingHook(); 
        parsingHook.start(sql); 
        try { 
           //完成SQL的解析,并返回一个SQLStatement对象 
            SQLStatement result = parse0(sql, useCache); 
            parsingHook.finishSuccess(result); 
            return result; 
        } catch (final Exception ex) { 
            parsingHook.finishFailure(ex); 
            throw ex; 
        } 
    } 
	private SQLStatement parse0(final String sql, final boolean useCache) { 
	    //如果使用缓存,先尝试从缓存中获取SQLStatement 
        if (useCache) { 
            Optional<SQLStatement> cachedSQLStatement = cache.getSQLStatement(sql); 
            if (cachedSQLStatement.isPresent()) { 
                return cachedSQLStatement.get(); 
            } 
        } 
        //委托SQLParseKernel创建SQLStatement 
	SQLStatement result = new SQLParseKernel(ParseRuleRegistry.getInstance(), databaseTypeName, sql).parse(); 
        if (useCache) { 
            cache.put(sql, result); 
        } 
        return result; 
    } 
}

关于 SQLParseEngine 有几点值得注意:

首先,这里使用了 ParsingHook 作为系统运行时的 Hook 管理,也就是我们常说的代码钩子。ShardingSphere 提供了一系列的 ParsingHook 实现,后续我们在讨论到 ShardingSphere 的链路跟踪时会对 Hook 机制进一步展开。

其次,我们发现用于解析 SQL 的 parse 方法返回了一个 SQLStatement 对象。也就是说,这个 SQLStatement 就是整个 SQL 解析引擎的最终输出对象。这里同样基于 Google Guava 框架中的 Cache 类构建了一个 SQLParseResultCache,对解析出来的 SQLStatement 进行缓存处理。

最后,我们发现 SQLParseEngine 把真正的解析工作委托给了 SQLParseKernel。接下来,我们就来看这个 SQLParseKernel 类。

SQLParseKernel

在 SQLParseKernel 类中,发现了如下所示的三个 Engine 类定义,包括 SQL 解析器引擎 SQLParserEngine(请注意该类名与 SQLParseEngine 类名的区别)、SQLSegment 提取器引擎 SQLSegmentsExtractor 以及 SQLStatement 填充器引擎 SQLStatementFiller。

//SQL解析器引擎 
private final SQLParserEngine parserEngine; 
//SQLSegment提取器引擎 
private final SQLSegmentsExtractorEngine extractorEngine; 
//SQLStatement填充器引擎 
private final SQLStatementFillerEngine fillerEngine;

作为外观类的 SQLParseKernel 提供了如下所示的 parse 方法,来完成 SQL 解析的整个过程,该方法中分别用到了上述三个引擎类,如下所示:

public SQLStatement parse() { 
	   //利用ANTLR4 解析SQL的抽象语法树 
	   SQLAST ast = parserEngine.parse(); 
	   //提取AST中的Token,封装成对应的TableSegment、IndexSegment 等各种Segment 
	   Collection<SQLSegment> sqlSegments = extractorEngine.extract(ast); 
	   Map<ParserRuleContext, Integer> parameterMarkerIndexes = ast.getParameterMarkerIndexes(); 
	    //填充SQLStatement并返回 
	    return fillerEngine.fill(sqlSegments, parameterMarkerIndexes.size(), ast.getSqlStatementRule()); 
  }

如何生成 SQLAST

主要包括三个步骤:
(1)通过 SQLParserEngine 生成 SQL 抽象语法树。
(2)通过 SQLSegmentsExtractorEngine 提取 SQLSegment。
(3)通过 SQLStatementFiller 填充 SQLStatement。

在这里插入图片描述
至此,我们看到由解析、提取和填充这三个阶段所构成的整体 SQL 解析流程已经完成。现在能够根据一条 SQL 语句解析出对应的 SQLStatement 对象,供后续的 ShardingRouter 等路由引擎进行使用。

SQL解析

本课时我们首先关注流程中的第一阶段,即如何生成一个 SQLAST(后两个阶段会在后续课时中讲解)。这部分的实现过程位于 SQLParserEngine 的 parse 方法,如下所示:

public SQLAST parse() { 
	    SQLParser sqlParser = SQLParserFactory.newInstance(databaseTypeName, sql); 
	     //利用ANTLR4获取解析树 
	     ParseTree parseTree; 
	     try { 
	         ((Parser) sqlParser).setErrorHandler(new BailErrorStrategy()); 
	         ((Parser) sqlParser).getInterpreter().setPredictionMode(PredictionMode.SLL); 
	         parseTree = sqlParser.execute().getChild(0); 
	        } catch (final ParseCancellationException ex) { 
	         ((Parser) sqlParser).reset(); 
	         ((Parser) sqlParser).setErrorHandler(new DefaultErrorStrategy()); 
	         ((Parser) sqlParser).getInterpreter().setPredictionMode(PredictionMode.LL); 
	         parseTree = sqlParser.execute().getChild(0); 
	        } 
	     if (parseTree instanceof ErrorNode) { 
	            throw new SQLParsingException(String.format("Unsupported SQL of `%s`", sql)); 
	        } 
	     //获取配置文件中的StatementRule 
	     SQLStatementRule rule = parseRuleRegistry.getSQLStatementRule(databaseTypeName, parseTree.getClass().getSimpleName()); 
	     if (null == rule) { 
	         throw new SQLParsingException(String.format("Unsupported SQL of `%s`", sql)); 
	        } 
	     //封装抽象语法树AST 
	     return new SQLAST((ParserRuleContext) parseTree, getParameterMarkerIndexes((ParserRuleContext) parseTree), rule); 
	}

上述代码中 SQLParser 接口负责具体的 SQL 到 AST(Abstract Syntax Tree,抽象语法树)的解析过程。而具体 SQLParser 实现类的生成由 SQLParserFactory 负责,SQLParserFactory 定义如下:

public final class SQLParserFactory { 
    public static SQLParser newInstance(final String databaseTypeName, final String sql) { 
     //通过SPI机制加载所有扩展 
     for (SQLParserEntry each : NewInstanceServiceLoader.newServiceInstances(SQLParserEntry.class)) { 
        //判断数据库类型 
        if (each.getDatabaseTypeName().equals(databaseTypeName)) { 
              return createSQLParser(sql, each); 
         } 
     } 
     throw new UnsupportedOperationException(String.format("Cannot support database type '%s'", databaseTypeName)); 
    }}

SQLParser 和 SQLParserEntry 这两个接口的定义和实现都与基于 ANTLR4 的 AST 生成机制有关。ANTLR 是 Another Tool for Language Recognition 的简写,是一款能够根据输入自动生成语法树的开源语法分析器。ANTLR 可以将用户编写的 ANTLR 语法规则直接生成 Java、Go 语言的解析器,在 ShardingSphere 中就使用了 ANTLR4 来生成 AST。
我们注意到 SQLParserEngine 的 parse 方法最终返回的是一个 SQLAST,该类的定义如下所示。

public final class SQLAST { 
    private final ParserRuleContext parserRuleContext; 
    private final Map<ParserRuleContext, Integer> parameterMarkerIndexes; 
    private final SQLStatementRule sqlStatementRule; 
}

这里的 ParserRuleContext 实际上就来自 ANTLR4,而 SQLStatementRule 则是一个规则对象,包含了对 SQLSegment 提取器的定义。

提取 SQL 片段

要理解 SQLStatementRule,就需要先介绍 ParseRuleRegistry 类。

	private final ExtractorRuleDefinitionEntityLoader extractorRuleLoader = new ExtractorRuleDefinitionEntityLoader(); 
     
    private final FillerRuleDefinitionEntityLoader fillerRuleLoader = new FillerRuleDefinitionEntityLoader(); 
     
    private final SQLStatementRuleDefinitionEntityLoader statementRuleLoader = new SQLStatementRuleDefinitionEntityLoader(); 

从命名上可以看到这三个 Loader 类分别处理对 SQLStatementRule、ExtractorRule 和 FillerRule 这三种规则定义的加载。

我们先来看 SQLStatementRule,它们的定义位于 sql-statement-rule-definition.xml 配置文件中。我们以 Mysql 为例,这个配置文件位于 shardingsphere-sql-parser-mysql 工程中的 META-INF/parsing-rule-definition/mysql 目录下。我们截取该配置文件中的部分配置信息作为演示,如下所示:

<sql-statement-rule-definition> 
    <sql-statement-rule context="select" sql-statement-class="org.apache.shardingsphere.sql.parser.sql.statement.dml.SelectStatement" extractor-rule-refs="tableReferences, columns, selectItems, where, predicate, groupBy, orderBy, limit, subqueryPredicate, lock" /> 
    <sql-statement-rule context="insert" sql-statement-class="org.apache.shardingsphere.sql.parser.sql.statement.dml.InsertStatement" extractor-rule-refs="table, columns, insertColumns, insertValues, setAssignments, onDuplicateKeyColumns" /> 
    <sql-statement-rule context="update" sql-statement-class="org.apache.shardingsphere.sql.parser.sql.statement.dml.UpdateStatement" extractor-rule-refs="tableReferences, columns, setAssignments, where, predicate" /> 
    <sql-statement-rule context="delete" sql-statement-class="org.apache.shardingsphere.sql.parser.sql.statement.dml.DeleteStatement" extractor-rule-refs="tables, columns, where, predicate" /></sql-statement-rule-definition> 

基于 ParseRuleRegistry 类进行规则获取和处理过程,涉及一大批实体对象以及用于解析 XML 配置文件的 JAXB 工具类的定义,内容虽多但并不复杂。核心类之间的关系如下图所示:
在这里插入图片描述
当获取规则之后,对于具体某种数据库类型的每条 SQL 而言,都会有一个 SQLStatementRule 对象。我们注意到每个 SQLStatementRule 都定义了一个“context”以及一个“sql-statement-class”。

这里的 context 实际上就是通过 SQL 解析所生成的抽象语法树 SQLAST 中的 ParserRuleContext,包括 CreateTableContext、SelectContext 等各种 StatementContext。

public interface SQLStatement { 
     
    //获取参数个数 
    int getParametersCount(); 
     
    //获取所有SQLSegment 
    Collection<SQLSegment> getAllSQLSegments(); 
     
    //根据类型获取一个SQLSegment 
    <T extends SQLSegment> Optional<T> findSQLSegment(Class<T> sqlSegmentType); 
     
    //根据类型获取一组SQLSegment 
    <T extends SQLSegment> Collection<T> findSQLSegments(Class<T> sqlSegmentType); 
} 

你可以看到,作为解析引擎最终产物的 SQLStatement ,实际上封装的是对 SQL 片段对象 SQLSegment 的获取操作。显然,对于每一个 ParserRuleContext 而言,我们最终就是构建了一个包含一组 SQLSegment 的 SQLStatement 对象,而这些 SQLSegment 的构建过程就是所谓的提取 SQLSegment 的过程。我们在配置文件中也明确看到了 SQLStatementRule 中对各种提取规则对象 ExtractorRule 的引用。

在 ShardingSphere 中内置了一大批通用的 SQLSegment,包括查询选择项(SelectItems)、表信息(Table)、排序信息(OrderBy)、分组信息(GroupBy)以及分页信息(Limit)等。这些通用 SQLSegment 都有对应的 SQLSegmentExtractor,我们可以直接在 SQLStatementRule 中进行使用。

另一方面,考虑到 SQL 方言的差异性,ShardingSphere 同样提供了针对各种数据库的 SQLSegment 的提取器定义。以 Mysql 为例,在其代码工程的 META-INF/parsing-rule-definition/mysql 目录下,存在一个 extractor-rule-definition.xml 配置文件,专门用来定义针对 Mysql 的各种 SQLSegmentExtractor,部分定义如下所示,作为一款适用于多数据库的中间件,这也是 ShardingSphere 应对 SQL 方言的实现机制之一。

<extractor-rule-definition> 
    <extractor-rule id="addColumnDefinition" extractor-class="org.apache.shardingsphere.sql.parser.core.extractor.ddl.MySQLAddColumnDefinitionExtractor" /> 
    <extractor-rule id="modifyColumnDefinition" extractor-class="org.apache.shardingsphere.sql.parser.core.extractor.ddl.MySQLModifyColumnDefinitionExtractor" /></extractor-rule-definition> 

假设有这样一句 SQL:

SELECT task_id, task_name FROM health_task WHERE user_id = 'user1' AND record_id = 2  

通过解析,我们获取了如下所示的抽象语法树:

在这里插入图片描述
我们发现,对于上述抽象语法树中的某些节点(如 SELECT、FROM 和 WHERE)没有子节点,而对于如 FIELDS、TABLES 和 CONDITIONS 节点而言,本身也是一个树状结构。显然,这两种节点的提取规则应该是不一样的。

因此,ShardingSphere 提供了两种 SQLSegmentExtractor,一种是针对单节点的 OptionalSQLSegmentExtractor;另一种是针对树状节点的 CollectionSQLSegmentExtractor。由于篇幅因素,这里以 TableExtractor 为例,展示如何提取 TableSegment 的过程,TableExtractor 的实现方法如下所示:

public final class TableExtractor implements OptionalSQLSegmentExtractor { 
     
    @Override 
    public Optional<TableSegment> extract(final ParserRuleContext ancestorNode, final Map<ParserRuleContext, Integer> parameterMarkerIndexes) { 
        //从Context中获取TableName节点 
     Optional<ParserRuleContext> tableNameNode = ExtractorUtils.findFirstChildNode(ancestorNode, RuleName.TABLE_NAME); 
        if (!tableNameNode.isPresent()) { 
            return Optional.absent(); 
        }         
        //根据TableName节点构建TableSegment 
        TableSegment result = getTableSegment(tableNameNode.get()); 
        //设置表的别名 
        setAlias(tableNameNode.get(), result); 
        return Optional.of(result); 
    } 
     
    private TableSegment getTableSegment(final ParserRuleContext tableNode) { 
     //从Context中获取Name节点       
        ParserRuleContext nameNode = ExtractorUtils.getFirstChildNode(tableNode, RuleName.NAME); 
        //根据Name节点获取节点的起止位置以及节点内容 
        TableSegment result = new TableSegment(nameNode.getStart().getStartIndex(), nameNode.getStop().getStopIndex(), nameNode.getText()); 
        //从Context中获取表的Owner节点,如果有的话就设置Owner 
        Optional<ParserRuleContext> ownerNode = ExtractorUtils.findFirstChildNodeNoneRecursive(tableNode, RuleName.OWNER); 
        if (ownerNode.isPresent()) { 
            result.setOwner(new SchemaSegment(ownerNode.get().getStart().getStartIndex(), ownerNode.get().getStop().getStopIndex(), ownerNode.get().getText())); 
        } 
        return result; 
    } 
     
    private void setAlias(final ParserRuleContext tableNameNode, final TableSegment tableSegment) { 
     //从Context中获取Alias节点,如果有的话就设置别名 
     Optional<ParserRuleContext> aliasNode = ExtractorUtils.findFirstChildNode(tableNameNode.getParent(), RuleName.ALIAS); 
        if (aliasNode.isPresent()) { 
            tableSegment.setAlias(aliasNode.get().getText()); 
        } 
    } 
} 

显然,语法树中的 Table 是一种单节点,所以 TableExtractor 继承自 OptionalSQLSegmentExtractor。对于 TableExtractor 而言,整个解析过程就是从 ParserRuleContext 中获取与表定义相关的各种节点,然后通过节点的起止位置以及节点内容来构建 TableSegment 对象。

public final class TableSegment implements SQLSegment, TableAvailable, OwnerAvailable<SchemaSegment>, AliasAvailable { 
     
    private final int startIndex;     
    private final int stopIndex;   
    private final String name;  
    private final QuoteCharacter quoteCharacter; 
    private SchemaSegment owner;  
	private String alias;} 

现在,基于以上关于提取器以及提取操作的相关概念的理解,我们来看一下 SQLSegment 提取引擎 SQLSegmentsExtractorEngine 的实现,如下所示:

public final class SQLSegmentsExtractorEngine { 
     
    //用来提取SQLAST语法树中的SQL片段 
    public Collection<SQLSegment> extract(final SQLAST ast) { 
        Collection<SQLSegment> result = new LinkedList<>(); 
         
        //遍历提取器,从Context中提取对应类型的SQLSegment,比如TableSegment         
        for (SQLSegmentExtractor each : ast.getSqlStatementRule().getExtractors()) {            
            //单节点的场景,直接提取单一节点下的内容 
            if (each instanceof OptionalSQLSegmentExtractor) { 
                Optional<? extends SQLSegment> sqlSegment = ((OptionalSQLSegmentExtractor) each).extract(ast.getParserRuleContext(), ast.getParameterMarkerIndexes()); 
                if (sqlSegment.isPresent()) { 
                    result.add(sqlSegment.get()); 
                } 
                 
            树状节点的场景,遍历提取节点下的所有子节点// 
            } else if (each instanceof CollectionSQLSegmentExtractor) { 
                result.addAll(((CollectionSQLSegmentExtractor) each).extract(ast.getParserRuleContext(), ast.getParameterMarkerIndexes())); 
            } 
        } 
        return result; 
    } 
} 

显然,SQLSegmentsExtractorEngine 的作用就是针对某一条 SQL,遍历 SQLStatementRule 中所配置的提取器,然后从 Context 中提取对应类型的 SQLSegment,并最终存放在一个集合对象中进行返回。

填充 SQL 语句

完成所有 SQLSegment 的提取之后,我们就来到了解析引擎的最后一个阶段,即填充 SQLStatement。所谓的填充过程,就是通过填充器 SQLSegmentFiller 为 SQLStatement 注入具体 SQLSegment 的过程。

public interface SQLSegmentFiller<T extends SQLSegment> { 
     
    void fill(T sqlSegment, SQLStatement sqlStatement); 
} 

那么问题就来了,我们如何正确把握 SQLSegmentFiller、SQLSegment 和 SQLStatement 这三者之间的处理关系呢?我们先根据某个 SQLSegment 找到对应的 SQLSegmentFiller,这部分关系在 ShardingSphere 中同样是维护在一个 filler-rule-definition.xml 配置文件中,截取部分配置项如下所示:

<filler-rule-definition> 
    <filler-rule sql-segment-class="org.apache.shardingsphere.sql.parser.sql.segment.generic.TableSegment" filler-class="org.apache.shardingsphere.sql.parser.core.filler.impl.TableFiller" /> 
    <filler-rule sql-segment-class="org.apache.shardingsphere.sql.parser.sql.segment.generic.SchemaSegment" filler-class="org.apache.shardingsphere.sql.parser.core.filler.impl.dal.SchemaFiller" /></filler-rule-definition> 

显然,这里保存着 SQLSegment 与 SQLSegmentFiller 之间的对应关系。当然,对于不同的 SQL 方言,也同样可以维护自身的 filler-rule-definition.xml 文件。

我们还是以与 TableSegment 对应的 TableFiller 为例,来分析一个 SQLSegmentFiller 的具体实现方法,TableFiller 类如下所示:

public final class TableFiller implements SQLSegmentFiller<TableSegment> { 
     
    @Override 
    public void fill(final TableSegment sqlSegment, final SQLStatement sqlStatement) { 
        if (sqlStatement instanceof TableSegmentAvailable) { 
            ((TableSegmentAvailable) sqlStatement).setTable(sqlSegment); 
        } else if (sqlStatement instanceof TableSegmentsAvailable) { 
            ((TableSegmentsAvailable) sqlStatement).getTables().add(sqlSegment); 
        } 
    } 
} 

这段代码在实现上采用了回调机制来完成对象的注入。在 ShardingSphere 中,基于回调的处理方式也非常普遍。本质上,回调解决了因为类与类之间的相互调用而造成的循环依赖问题,回调的实现策略通常采用了如下所示的类层结构:
在这里插入图片描述
TableFiller 中所依赖的 TableSegmentAvailable 和 TableSegmentsAvailable 接口就类似于上图中的 Callback 接口,具体的 SQLStatement 就是 Callback 的实现类,而 TableFiller 则是 Callback 的调用者。以 TableFiller 为例,我们注意到,如果对应的 SQLStatement 实现了这两个接口中的任意一个,那么就可以通过 TableFiller 注入对应的 TableSegment,从而完成 SQLSegment 的填充。

这里以 TableSegmentAvailable 接口为例,它有一组实现类,如下所示:
在这里插入图片描述

以上图中的 CreateTableStatement 为例,该类同时实现了 TableSegmentAvailable 和 IndexSegmentsAvailable 这两个回调接口,所以就可以同时操作 TableSegment 和 IndexSegment 这两个 SQLSegment。CreateTableStatement 类的实现如下所示:

public final class CreateTableStatement extends DDLStatement implements TableSegmentAvailable, IndexSegmentsAvailable { 
     
    private TableSegment table; 
     
    private final Collection<ColumnDefinitionSegment> columnDefinitions = new LinkedList<>(); 
     
    private final Collection<IndexSegment> indexes = new LinkedList<>(); 
} 

至此,我们通过一个示例解释了与填充操作相关的各个类之间的协作关系,如下所示的类图展示了这种协作关系的整体结构。
在这里插入图片描述
有了上图的基础,我们理解填充引擎 SQLStatementFillerEngine 就显得比较简单了,SQLStatementFillerEngine 类的实现如下所示:

public final class SQLStatementFillerEngine { 
     
    private final ParseRuleRegistry parseRuleRegistry;     
    private final String databaseTypeName; 
    
    @SuppressWarnings("unchecked") 
    @SneakyThrows 
    public SQLStatement fill(final Collection<SQLSegment> sqlSegments, final int parameterMarkerCount, final SQLStatementRule rule) { 
     //从SQLStatementRule中获取SQLStatement实例,如CreateTableStatement 
     SQLStatement result = rule.getSqlStatementClass().newInstance(); 
        //通过断言对SQLStatement的合法性进行校验 
     Preconditions.checkArgument(result instanceof AbstractSQLStatement, "%s must extends AbstractSQLStatement", result.getClass().getName()); 
         
        //设置参数个数 
        ((AbstractSQLStatement) result).setParametersCount(parameterMarkerCount); 
       
        //添加所有的SQLSegment到SQLStatement中 
        result.getAllSQLSegments().addAll(sqlSegments); 
         
        //遍历填充对应类型的SQLSegment 
        for (SQLSegment each : sqlSegments) { 
         //根据数据库类型和SQLSegment找到对应的SQLSegmentFiller,并为SQLStatement填充SQLSegment 
            //如通过TableSegment找到获取TableFiller,然后通过TableFiller为CreateTableStatement填充TableSegment 
         Optional<SQLSegmentFiller> filler = parseRuleRegistry.findSQLSegmentFiller(databaseTypeName, each.getClass()); 
            if (filler.isPresent()) {                
              //利用SQLSegmentFiller来填充SQLStatement中的SQLSegment 
                filler.get().fill(each, result); 
            } 
        } 
        return result; 
	}  
} 

我们对 SQLStatementFillerEngine 中的核心代码都添加了注释,注意到这里通过数据库类型以及 SQLSegment 的类型,从规则注册表 ParseRuleRegistry 中获取了对应的 SQLSegmentFiller 并完成对 SQLStatement 的填充操作。

至此,ShardingSphere 中 SQL 解析引擎的三大阶段介绍完毕。

总结了一组设计和实现上的技巧

设计模式的应用方式

在本文中,我们主要涉及了两种设计模式的应用场景,一种是工厂模式,另一种是外观模式。

工厂模式的应用比较简单,作用也比较直接。例如,SQLParseEngineFactory 工厂类用于创建 SQLParseEngine,而 SQLParserFactory 工厂类用于创建 SQLParser。

相比工厂模式,外观类通常比较难识别和把握,因此,我们也花了一定篇幅介绍了 SQL 解析引擎中的外观类 SQLParseKernel,以及与 SQLParseEngine 之间的委托关系。

缓存的实现方式

在本文中,我们就接触到了两种缓存的实现方式。

第一种是通过 ConcurrentHashMap 类来保存 SQLParseEngine 的实例,使用上比较简单。

另一种则基于 Guava 框架中的 Cache 类构建了一个 SQLParseResultCache 来保存 SQLStatement 对象。Guava 中的 Cache 类初始化方法如下所示,我们可以通过 put 和 getIfPresent 等方法对缓存进行操作:

Cache<String, SQLStatement> cache = CacheBuilder.newBuilder().softValues().initialCapacity(2000).maximumSize(65535).build();     

配置信息的两级管理机制

在 ShardingSphere 中,关于各种提取规则和填充规则的定义都放在了 XML 配置文件中,并采用了配置信息的两级管理机制。这种两级管理机制的设计思想在于,系统在提供了对各种通用规则默认实现的同时,也能够集成来自各种 SQL 方言的定制化规则,从而形成一套具有较高灵活性以及可扩展性的规则管理体系。

代码编写时,一些变动的参数建议换到参数表或者配置在xml中,这样变于以后的调整。

回调机制

所谓回调,本质上就是一种双向调用模式,也就是说,被调用方在被调用的同时也会调用对方。在实现上,我们可以提取一个用于业务接口作为一种 Callback 接口,然后让具体的业务对象去实现这个接口。这样,当外部对象依赖于这个业务场景时,只需要依赖这个 Callback 接口,而不需要关心这个接口的具体实现类。

这在软件设计和实现过程中是一种常见的消除业务对象和外部对象之间循环依赖的处理方式。ShardingSphere 中大量采用了这种实现方式来确保代码的可维护性,这非常值得我们学习。

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值