撸了几天Mybatis源码,将之前写的中间件重构了

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

前言

之前我写过一个工具,就是通过 “原生SQL去查Mongo数据”,具体可以查看我的这篇博文:听说你不会Mongo的API?我写个插件用SQL去查Mongo

写完这个工具后就没有再管它了,当时就是为了实现这样的功能,并没有考虑代码的优雅性,以及可扩展性。
后来我重新审视这个工具时,感觉这个代码乱糟糟的(也许以后看这个版本也是乱糟糟),所以我决定将这个工具重构一下。

最初的想法就是,增加这个工具的扩展性,因为我的这个工具有一些功能是没有支持的,可以交给使用者自己去扩展, 那这样的话,易扩展性就显得比较重要了。

说到扩展性,我觉得常用的就是使用模板方法设计模式,或者回调机制,把不变的部分做成模板,把灵活变动的地方交由用户自己实现,但是我在分析工具核心代码时,觉得这几种方式都不好。

注意:看这篇文章之前,请移步我写的另一篇文章《听说你不会Mongo的API?我写个插件用SQL去查Mongo

代码

代码托管在 Gitee 上,这是代码地址:gitee.com/listen_w/sq…
现在 master 分支还是第一版本的代码,新的代码在这个 restructure 分支上 image.png

哪些地方需要扩展

其实在重构之前我一直在考虑需要重构什么?哪些点可以改进?后来决定以扩展性为主。

既然要扩展,就要考虑哪些地方留给用户扩展的口子,我这次主要留了三个地方,一个是核心的SQL执行器(新增概念,Executor),第二个是SQL解析器(parser包下面),第三个是SQL分析器(新增概念,analyzer包下)

这次重构变化有哪些

这次重构,主要借鉴了 Mybatis 的思想,尤其是拦截器机制,这个版本主要有这些变化:

  1. SQL查询增加可配置缓存;
  2. 支持注解配置SQL语句;
  3. 增加配置核心类,Configuration
  4. 使用拦截器机制实现可扩展性;
  5. 使用设计模式:代理模式、装饰者模式、责任链模式、建造者模式、适配器模式、观察者模式;

整体概览

  1. 项目标注注解 EnableSqlToMongoMapper 的作用:

image.png 由上可以知道,这个注解主要是导入了 SqlToMongoRegistrar,而这个类的作用就是为标注 SqlToMongoMapper 的接口创建代理类,最终是在核心配置类 Configuration 中注册代理对象,具体后面专门分析。

  1. 项目启动后,根据springboot自动装配特性,会加载配置类 SqlToMongoAutoConfiguration,这个配置类会创建一些 Bean image.png 简单看一下各 Bean 的作用
  • MongoTemplateProxy :继承 MongoTemplate,主要是重写 doUpdate 方法,当Mongo文档更新后在此方法发送更新缓存的通知;
  • SaveMongoEventListener:继承 AbstractMongoEventListener,当 Mongo 文档保存时发送更新缓存的通知;
  • InterceptorConfigurer:拦截器配置,默认实现是拦截器适配器:InterceptorConfigurerAdapter,它会添加一个拦截器模板: InterceptorTemplate
  • Configuration:核心配置类,后面具体分析;
  • ClearCacheListener:继承 ApplicationListener,监听清除缓存事件通知;
  • SqlSession:发起 Mongo 查询请求,实际会通过 Executor 实现查询Mongo数据库 ;
  • SQLToMongoTemplate:方便 Mongo 查询的工具,通过 SqlSession 查询;
  1. 一条SQL转Mongo语法过程 image.png

工程目录结构

sqltomongo-spring-boot-starter
└── src
    └── main
        └── java
            ├──com.rrtv
            │  ├── adapter
            │  │   └── MatchExpressionVisitorAdapter.java  ------ 解析过滤匹配的 ExpressionVisitorAdapter
            │  ├── analyzer                                ------ SQL分析器,将SQL解析的元数据封装成 Mongo API 
            │  │   ├── AbstractAnalyzer.java               
            │  │   ├── Analyzer.java                       
            │  │   ├── GroupAnalyzer.java                 
            │  │   ├── HavingAnalyzer.java                 
            │  │   ├── JoinAnalyzer.java                  
            │  │   ├── LimitAnalyzer.java                 
            │  │   ├── MatchAnalyzer.java                  
            │  │   ├── ProjectAnalyzer.java              
            │  │   └── SortAnalyzer.java                   
            │  ├── annotation
            │  │   ├── EnableSqlToMongoMapper.java         ------ 启动类注解
            │  │   ├── Intercepts.java                     ------ 插件注解
            │  │   ├── Select.java                         ------ 查询注解
            │  │   ├── Signature.java                      ------ 插件注解
            │  │   └── SqlToMongoMapper.java               ------ Mapper 接口类注解
            │  ├── binding                                 ------ 绑定,Mapper接口代理注册Bean
            │  │   ├── MapperAnnotationBuilder.java        ------ Mapper注解解析,解析 Select 注解
            │  │   ├── MapperProxy.java                       
            │  │   ├── MapperProxyFactory.java         
            │  │   └── SqlToMongoMapperFactoryBean.java         
            │  ├── cache                                   ------ 缓存相关       
            │  │   ├── Cache.java         
            │  │   ├── CacheManager.java                   ------ 缓存管理器
            │  │   ├── ClearCacheEvent.java                ------ 清除缓存事件
            │  │   ├── ClearCacheListener.java             ------ 清除缓存监听器
            │  │   ├── ConcurrentHashMapCache.java         
            │  │   ├── DefaultCacheManager.java         
            │  │   ├── MongoTemplateProxy.java         
            │  │   └── SaveMongoEventListener.java         ------ Mongo 监听器
            │  ├── common
            │  │   ├── AggregationFunction.java            ------ 聚合函数枚举
            │  │   ├── ConversionFunction.java             ------ 转化函数枚举
            │  │   ├── ParserPartTypeEnum.java        
            │  │   └── MongoParserResult.java              ------ SQL解析后封装Mongo API 结果
            │  ├── configure
            │  │   ├── SqlToMongoAutoConfiguration.java    ------ 自动配置
            │  │   ├── SqlToMongoMapperFactoryBean.java    ------ SqlToMongoMapper 工厂Bean
            │  │   └── SqlToMongoRegistrar.java            ------ Mapper 接口 注册 
            │  ├── exception                               ------ 自定义异常
            │  │   ├── BindingException.java
            │  │   ├── NotSupportFunctionException.java
            │  │   ├── NotSupportSubSelectException.java
            │  │   ├── PluginException.java
            │  │   ├── SqlParameterException.java
            │  │   ├── SqlParserException.java
            │  │   ├── SqlTypeException.java
            │  │   └── TableAssociationException.java
            │  ├── executor                                ------ 具体执行器 
            │  │   ├── CachingExecutor.java
            │  │   ├── DefaultExecutor.java
            │  │   └── Executor.java
            │  ├── orm
            │  │   ├── Configuration.java                 ------ 核心配置类,重点
            │  │   ├── ConfigurationBuilder.java       
            │  │   ├── DefaultSqlSession.java             ------ SqlSession 实现类
            │  │   ├── DomParser.java                     ------ Dom 解析  
            │  │   ├── SqlSession.java
            │  │   ├── SqlSessionBuilder.java
            │  │   └── XNode.java                         ------ xml 解析结果封装 
            │  ├── parser                                 ------ SQL 解析
            │  │   ├── data                               ------ SQL 各个部分解析结果
            │  │   │   ├── GroupData.java               
            │  │   │   ├── LimitData.java
            │  │   │   ├── LookUpData.java
            │  │   │   ├── MatchData.java
            │  │   │   ├── PartSQLParserData.java
            │  │   │   ├── PartSQLParserResult.java
            │  │   │   ├── ProjectData.java
            │  │   │   └── SortData.java
            │  │   ├── GroupSQLParser.java               ------ 解析 SQL 分组
            │  │   ├── HavingSQLParser.java              ------ 解析 SQL Having   
            │  │   ├── JoinSQLParser.java                ------ 解析 SQL 表关联
            │  │   ├── LimitSQLParser.java               ------ 解析 SQL Limit
            │  │   ├── PartSQLParser.java                ------ 解析 SQL Limit
            │  │   ├── OrderSQLParser.java               ------ 解析 SQL 排序
            │  │   ├── ProjectSQLParser.java             ------ 解析 SQL 查询字段
            │  │   ├── SelectSQLTypeParser.java          ------ SQL 查询解析器,调用各个解析类解析SQL,并将元数据封装 Mongo 查询API
            │  │   └── WhereSQLParser.java               ------ 解析 SQL where 条件   
            │  ├── plugin                                ------ 插件相关,用于扩展
            │  │   ├── Interceptor.java                  ------ 拦截器接口
            │  │   ├── InterceptorChain.java             ------ 拦截器链,封装所有拦截器
            │  │   ├── InterceptorConfigurer.java        ------ 拦截器配置,用于自定义拦截器
            │  │   ├── InterceptorConfigurerAdapter.java ------ 拦截器配置适配器,默认添加拦截器模板
            │  │   ├── InterceptorTemplate.java          ------ 拦截器模板
            │  │   ├── Invocation.java                  
            │  │   └── Plugin.java                       ------ 插件具体逻辑
            │  ├── util
            │  │   ├── SqlCommonUtil.java                ------  SQL 公共 util
            │  │   ├── SqlParameterSetterUtil.java       ------  SQL 设置参数 util 
            │  │   ├── SqlSupportedSyntaxCheckUtil.java  ------  SQL 支持语法检查 util 
            │  │   └── StringUtils.java                
            │  └── SQLToMongoTemplate.java               ------  用于Mongo 查询的 bean,使用者直接注入该 Bean
            └── resources
                └── META-INF
                    └── spring.factories
复制代码

相对之前的最初版本,这里扩展了不少,有兴趣的可以去看看之前的那篇文章,这里主要加了 plugincacheexecutorbinding 包,而且把原来SQL解析和Mongo元数据分析部分都单独拆了出来。

模仿 Mybatis,新增 Configuration 配置类概念

ConfigurationMybatis中的Configuration思想一样,这里也是整个项目的核心配置类,上面我在介绍 "整体概览" 时讲到 SqlToMongoAutoConfiguration配置类,由那张图可以知道,Configuration 基本被所有的Bean依赖,所以说它是个粘合剂也不为过。 image.png

Configuration 封装了一些最核心的配置,比如:缓存配置、Mapper代理、SQL解析映射、拦截器、SQL解析器、分析器等等,以下是 Configuration 的结构: image.png
简单介绍几个核心方法:

  • getAnalyzerInstance:创建 SQL 分析器责任链 (被拦截器链代理的),使用单利设计模式;
  • getPartSQLParserInstance:获取SQL各个部分解析器,具备缓存作用;
  • newPartSQLParser:创建SQL解析器(被拦截器链代理的);
  • newExecutor:创建执行器(缓存执行器 和 默认执行器),并对执行器增加拦截器处理;
  • addInterceptor:添加拦截器;
  • getMapper:获取Mapper代理对象;
  • addMapper:添加Mapper代理对象,并解析出 @Select 注解

创建 Configuration 配置流程

Configuration Bean 会在springboot自动装配时创建(SqlToMongoAutoConfiguration配置类) image.png

通过 ConfigurationBuilder 建造者设计模式创建 Configuration 对象,由下图可见,创建 Configuration时,主要有以下工作:

  • 解析XML文件,目前只是解析 Select注解,获取SQL语句
  • 添加拦截器
  • 设置缓存
  • 设置缓存管理器

image.png

Configuration作为最核心的配置,后面结合各部分功能一起说明。

如何增加SQL注解配置功能

原先只支持 XML 解析SQL,类似MyBatis的Mapper.xml,原理就是根据Spring boot自动装配,创建Bean时扫描包路径,使用 Dom4j解析xml,解析出<select>标签,将结果保存在一个Map中,这一部分逻辑没啥变化,不过现在改为创建 Configuration 这个Bean时解析 Mapper.xml文件了。 image.png image.png

所以在创建 Configuration 对象时就完成了 XML的解析,类似下图这种 UserMapper.xml这种方式 image.png

但是我怎么同时支持用注解配置SQL呢?并且也希望注解解析的结果和XML解析的结果都在同一个配置里面,如下图:
image.png

我们知道这个 UserMapper 是个接口,一定会为它创建一个代理,那我们可以在创建代理时去解析注解配置的SQL。 image.png 具体代码分析创建过程: image.png 重点是这个Bean工厂,SqlToMongoMapperFactoryBean 有个属性 private Class<T> mapperInterface;,就是 Mapper 接口字节码,可以从这个字节码中解析出 @Select SQL配置注解,通过 afterPropertiesSet 方法,将Mapper接口加入 Configuration 中解析 image.png

knownMappers.put(type, new MapperProxyFactory<>(type));这行代码作用是为Mapper接口创建一个代理工厂,并缓存在一个Map中,MapperProxyFactory 是利用JDK动态代理的方式生产 MapperProxy 的。

这部分是效仿了MyBatis的做法

通过 MapperAnnotationBuilder 解析 Mapper 的SQL注解配置,并将结果保存在核心配置 Configuration 中。 image.png

注意:这里从 SqlToMongoMapperFactoryBean 开始分析,关于手动注册Bean等流程,之前那篇文章已经分析过了,重构版本也是这个地方做了修改

SQL查询增加可配置缓存

由于MyBatis有个一、二级缓存,所以出于性能考虑,是不是也需要有个查询缓存呢? 对于加缓存需要考虑这几个方面:

  • 查询缓存应该添加在哪里呢?
  • 是否应该支持可配置缓存?
  • 能否支持动态启停缓存?
  • 缓存如何更新?
  • 如何让使用者扩展缓存?

查询缓存应该添加在哪里呢?

查询缓存应该加在查询的地方,原来的设计是 SqlSession 提供查询的方法,如果在 SqlSession 中增加缓存查询的方法我觉得不够优雅,所以就弄了一个 Executor 的概念,SqlSession 持有 ExecutorSqlSession 的查询方法通过 Executor 完成,而 Executor 有两种实现,一种是 DefaultExecutor,它会去解析SQL,查询数据;另一种是 CachingExecutor ,它具备缓存功能,它持有 DefaultExecutor ,具体查询工作交由 DefaultExecutor 去做,这也是装饰者设计模式的体现。 image.png

配置是否需要缓存

MyBatis的一级缓存是默认开启的,但我觉得缓存是否开启还是交由使用者来决定比较好,所以在配置文件中留了口子,可配置是否启用缓存。 image.png

Configuration 有个 newExecutor 方法, 创建 Executor 时根据是否开启缓存来决定使用 CachingExecutor 还是 DefaultExecutorimage.png

缓存如何自定义配置

找到 CachingExecutor 执行器,它内部持有一个 Cache , 而 Cache 是通过 Configuration 获取的。 image.png

再回到创建 Configuration 的代码(ConfigurationBuilder.build 方法),如图,如果开启缓存,就配置一个默认的缓存管理器,而缓存管理器会根据配置的缓存类的全路劲名创建 Cache 对象。 image.png 缓存管理器创建缓存时,会利用反射创建Cache的子类对象,如果没有就使用 ConcurrentHashMapCache 对象。 image.png

由上可知,自定义缓存只要两步:

  1. 自己写一个类,实现 com.rrtv.cache.Cache 接口;
  2. 在spring配置中将自己实现的缓存类的全路径名配置上去,比如下图 image.png

Cache 接口 image.png

默认缓存实现

默认缓存的实现很简单,来看 CachingExecutor 类 (代码很简单,看注释即可) image.png 这里要注意一点就是,最后有个设置缓存索引,这个后面再说,和缓存更新有关系。

image.png 默认缓存使用 ConcurrentHashMap 来存储的,生成缓存的方式,就是参数拼接最后Md5。

缓存如何更新

缓存的更新这是非常重要的一点,要更新缓存,就要知道,缓存什么时候需要更新,修改了Mongo的实体就需要更新缓存了,这里有两个问题:

  • 怎么监听哪个实体被修改了
  • 被修改的实体影响了哪些查询语句

监听实体被修改的方式

jpa有审计的功能,Mongo也有文档修改监听功能,对这一块不了解的可以看我的这篇文章:一招解决 SpringDataMongodb 审计(统一维护公共字段)功能 实体修改后就需要清除缓存,清除缓存一般在缓存管理器中完成,那监听到修改后如何通知缓存管理器清除缓存呢?这里使用发布订阅设计模式,严格来说是使用 Spring的事件通知机制

事件发送 image.png image.png

事件监听 image.png 事件监听后,通过缓存管理器来清除缓存

清除与被修改的实体相关的缓存

现在有个问题就是,我怎么知道这个实体被哪个缓存引用着,其实这个决解方案也很简单,有点像倒排索引,比如我一条SQL语句:select * from user u inner join user_info ui on u.id = ui.uid,那如果 user 或者 user_info 修改了,就需要把这条SQL对应的缓存删除吧,所以就可以搞一个MAP,比如:key是 user,value是 这条语句的缓存Key,当修改user实体了,就可以找到这个Map对应的Vaule,删除这个Value对应的缓存就行。

由以上思路:我们在查询SQL时,获取SQL的表,将表和缓存Key维护在Map中 image.png image.png

当发现实体变化时,只要清除这个Map对应的Key,value就行 image.png

最核心变化,模仿Mybatis,通过拦截器来扩展本工具的核心模块

拦截器是本次重构的核心,通过拦截器来扩展核心功能。这里有几个注意的地方:

  • 拦截器作用在哪些地方
  • 拦截器工作原理
  • 如何添加自己的拦截器

拦截器作用在哪些地方

这次拦截器扩展的点,主要在这些地方 执行器(Executor)的查询方法、SQL解析器(PartSQLParser)的 proceedData 方法、Mongo分析器(Analyzer)的 proceed 方法。

拦截器工作原理

Configuration 里面维护了 拦截器链 image.png

Configuration 创建执行器、SQL解析器、Mongo 分析器代码: image.png image.png image.png 由上图可见,创建这些组件时都有这个 interceptorChain.pluginAll 代码。 image.png image.png

Plugin 代码比较长,直接粘贴了,可以看到每个拦截器都在目标对象上加了一层代理,通过代理去执行拦截器的方法。

/**
 * @Classname Plugin
 * @Description
 * @Date 2022/8/11 18:10
 * @Created by wangchangjiu
 */
public class Plugin implements InvocationHandler {

    private final Object target;
    private final Interceptor interceptor;
    private final Map<Class<?>, Set<Method>> signatureMap;

    private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
        this.target = target;
        this.interceptor = interceptor;
        this.signatureMap = signatureMap;
    }

    public static Object wrap(Object target, Interceptor interceptor) {
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        Class<?> type = target.getClass();
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
        if (interfaces.length > 0) {
            return Proxy.newProxyInstance(
                    type.getClassLoader(),
                    interfaces,
                    new Plugin(target, interceptor, signatureMap));
        }
        return target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            Set<Method> methods = signatureMap.get(method.getDeclaringClass());
            if (methods != null && methods.contains(method)) {
                return interceptor.intercept(new Invocation(target, method, args));
            }
            return method.invoke(target, args);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
        Intercepts interceptsAnnotation = Optional.ofNullable(interceptor.getClass().getAnnotation(Intercepts.class))
                .orElse(interceptor.getClass().getSuperclass().getAnnotation(Intercepts.class));
        if (interceptsAnnotation == null) {

            throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
        }

        Signature[] sigs = interceptsAnnotation.value();
        Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
        for (Signature sig : sigs) {
            Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
            try {
                Method method = sig.type().getMethod(sig.method(), sig.args());
                methods.add(method);
            } catch (NoSuchMethodException e) {
                throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
            }
        }
        return signatureMap;
    }

    private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
        Set<Class<?>> interfaces = new HashSet<>();
        while (type != null) {
            for (Class<?> c : type.getInterfaces()) {
                if (signatureMap.containsKey(c)) {
                    interfaces.add(c);
                }
            }
            type = type.getSuperclass();
        }
        return interfaces.toArray(new Class<?>[interfaces.size()]);
    }

}
复制代码

这里有个重点方法:Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor),这个方法就是获取注解中要拦截的类和方法,举个例子: image.png 这里是一个拦截器模板,getSignatureMap 方法就是解析 @Intercepts、@Signature 这两个注解,然后找到要拦截的类和方法,保存到Map中。由于创建执行器、SQL解析器、Mongo 分析器时返回的是代理对象,所以在执行这个组件方法时会调用代理对象的 invoke 方法,也就是 Plugin.invoke 方法,该方法就会判断要执行的类的方法是否需要拦截,需要的话就执行自己定义的拦截方法。

注意,拦截器的思想完全来自 Mybatis,我这里讲的比较简单,如果不理解,就去找资料看看MyBatis的拦截器原理是一样的。

用户如何自定义拦截器

首先拦截器维护在 Configuration 中,在创建 Configuration 时,有个参数 InterceptorConfigurer image.png

InterceptorConfigurer 是配置的 Bean image.png

所以我们只要自己写个类,实现 InterceptorConfigurer 接口,并把这个类加入Spring容器 image.png image.png

那知道怎么加入拦截器了,接下来就是怎么写一个拦截器了,为了使用者方便,我这里实现了一个拦截器模板,用户只需要继承这个模板,扩展自己需要的方法就行了

package com.rrtv.plugin;

import com.rrtv.analyzer.Analyzer;
import com.rrtv.annotation.Intercepts;
import com.rrtv.annotation.Signature;
import com.rrtv.executor.Executor;
import com.rrtv.parser.*;
import com.rrtv.parser.data.PartSQLParserData;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.statement.select.PlainSelect;
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;

import java.lang.reflect.InvocationTargetException;
import java.util.List;

/**
 * @Classname DefaultInterceptor
 * @Description 拦截器模板,自定义拦截器可以继承这个模板
 * @Date 2022/8/12 16:39
 * @Created by wangchangjiu
 */
@Slf4j
@Intercepts(
    {
      @Signature(type = PartSQLParser.class, method = "proceedData", args = {PlainSelect.class, PartSQLParserData.class}),
      @Signature(type = Executor.class, method = "selectOne", args = {String.class, Class.class, Object[].class}),
      @Signature(type = Executor.class, method = "selectList", args = {String.class, Class.class, Object[].class}),
      @Signature(type = Analyzer.class, method = "proceed", args = {List.class, PartSQLParserData.class})
    }
)
public class InterceptorTemplate implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        Object target = invocation.getTarget();
        Object[] args = invocation.getArgs();

        if(target instanceof Executor){
            return this.interceptExecutor(invocation);
        } else if(target instanceof Analyzer) {

            return this.interceptAnalyzer(invocation);

        } else {
            PlainSelect plain = (PlainSelect) args[0];
            PartSQLParserData data = (PartSQLParserData) args[1];
            if(target instanceof HavingSQLParser){
                return this.interceptHavingSQLParser(plain, data, invocation);
            } else if(target instanceof GroupSQLParser) {
                return this.interceptGroupSQLParser(plain, data, invocation);
            } else if(target instanceof JoinSQLParser) {
                return this.interceptJoinSQLParser(plain, data, invocation);
            } else if(target instanceof LimitSQLParser) {
                return this.interceptLimitSQLParser(plain, data, invocation);
            } else if(target instanceof OrderSQLParser) {
                return this.interceptOrderSQLParser(plain, data, invocation);
            } else if(target instanceof ProjectSQLParser) {
                return this.interceptProjectSQLParser(plain, data, invocation);
            } else if(target instanceof WhereSQLParser) {
                return this.interceptWhereSQLParser(plain, data, invocation);
            }
        }
        return invocation.proceed();
    }

    private Object interceptAnalyzer(Invocation invocation) throws Exception {
        return invocation.proceed();
    }

    private Object interceptExecutor(Invocation invocation) throws Exception {
        return invocation.proceed();
    }


    public Object interceptHavingSQLParser(PlainSelect plain, PartSQLParserData data, Invocation invocation) throws Exception {
        return invocation.proceed();
    }

    public Object interceptGroupSQLParser(PlainSelect plain, PartSQLParserData data, Invocation invocation) throws Exception {
        return invocation.proceed();
    }

    public Object interceptLimitSQLParser(PlainSelect plain, PartSQLParserData data, Invocation invocation) throws Exception {
        return invocation.proceed();
    }

    public Object interceptOrderSQLParser(PlainSelect plain, PartSQLParserData data, Invocation invocation) throws Exception {
        return invocation.proceed();
    }

    public Object interceptProjectSQLParser(PlainSelect plain, PartSQLParserData data, Invocation invocation) throws Exception {
        return invocation.proceed();
    }

    public Object interceptWhereSQLParser(PlainSelect plain, PartSQLParserData data, Invocation invocation) throws Exception {
        return invocation.proceed();
    }

    public Object interceptJoinSQLParser(PlainSelect plain, PartSQLParserData data, Invocation invocation) throws Exception {
        return invocation.proceed();
    }
}
复制代码

这个模板对SQL执行器、SQL解析器、SQL分析器的方法都有拦截,想要扩展那部分就实现对应的方法就行。 比如: image.png

SQL解析器(PartSQLParser)、Mongo分析器(Analyzer)设计

SQL解析器(PartSQLParser)设计

原来这块并没有任何设计,如下图:这种无法扩展,而且也得也不够优雅 image.png image.png

现在的做法,由于SQL解析器是对SQL各个部分单独解析的,而且没有先后顺序,所以完全可以抽象出一个解析器,并行的循环解析,每个解析器实现具体的解析过程,并设置解析结果。 image.png

解析器接口: image.png

以Where解析器为例 image.png 其他解析器就不列举了,和where解析器思想一样。

所有解析器,解析器在 parser 包下,一条查询语句各个部分有对应的解析
image.png

创建解析器时优先从缓存中获取,缓存没有,那么按照解析SQL的部位来创建解析器(实际是被拦截器包裹的解析器代理) image.png

Mongo分析器(Analyzer)设计

分析器也是按照SQL各个部分,分析组装Mongo语法的API,不同的是,组装Mongo API时是有顺序的,所以这里使用责任链设计模式

image.png

Configuration.getAnalyzerInstance()创建分析器组件,使用单例设计模式、建造者设计模式和责任链设计模式,使用抽象类 AbstractAnalyzer 按照顺序添加SQL分析器,同样每个分析器都被拦截器链包裹的,也就是说这里添加的都是SQL分析器代理对象。 image.png

分析器接口 image.png

分析器抽象类 image.png 由上图可知,分析器抽象类内部类 Builder 有个 addAnalyzer 方法,该方法就是设置责任链关系,维护下一个分析器。

SortAnalyzer 为例,具体分析器实现 image.png

下个版本

这个工具我会一直维护,功能也会一直去叠加,下个版本想支持动态SQL,类似 MyBatis 的动态标签,比如:"<if>"、"<where>" 等标签。

猜你喜欢

转载自juejin.im/post/7139444808468627463