优化与扩展Mybatis的SqlMapper解析
接上一篇博文,这一篇来讲述怎么实现SchemaSqlMapperParserDelegate——解析SqlMapper配置文件。
要想实现SqlMapper文件的解析,还需要仔细分析一下mybatis的源码,我画了一个图来协助理解,也可以帮助形成一个整体概念:
当然,这幅图不止是原生的解析,也包括了XSD模式下的解析,下面对着这幅图来说明一下。
一、Mybatis全局配置
Mybatis的全局配置,对应内存对象为Configuration,是重量级对象,和数据源DataSource、会话工厂SqlSessionFactory属于同一级别,一般来说(单数据源系统)是全局单例。从SqlSessionFactoryBean的doGetConfigurationWrapper()方法可以看到,有三种方式构建,优先级依次为:
1.spring容器中注入,由用户直接注入一个Configuration对象
2.根据mybatis-config.xml中加载,而mybatis-config.xml的路径由configLocation指定,配置文件使用组件XMLConfigBuilder来解析
3.采用mybatis内部默认的方式,直接new一个配置对象Configuration
这里为了简单,偷一个懒,不具体分析XMLConfigBuilder了,而直接采用spring中注入的方式,这种方式也给了扩展Configuration一个极大的自由。
二、读取所有SqlMapper.xml配置文件
也有两种方式,一种是手工配置,一种是使用自动扫描。推荐的自然是自动扫描,就不多说了。
加载所有SqlMapper.xml配置文件之后就是循环处理每一个文件了。
三、解析单个SqlMapper.xml配置文件
单个SqlMapper.xml文件的解析入口是SqlSessionFactoryBean的doParseSqlMapperResource()方法,在这个方法中,自动侦测是DTD还是XSD,然后分两条并行路线分别解析:
1、DTD模式:创建XMLMapperBuilder对象进行解析
2、XSD模式:根据ini配置文件,找到sqlmapper命名空间的处理器SchemaSqlMapperNamespaceParser,该解析器将具体的解析工作委托给SchemaSqlMapperParserDelegate类。
四、解析Statement级元素
Statement级元素指的是根元素<mapper>的一级子元素,这些元素有cache|cache-ref|resultMap|parameterMap|sql|insert|update|delete|select,其中insert|update|delete|select就是通常所说的增删改查,用于构建mybatis一次执行单元,也就是说,每一次mybatis方法调用都是对 insert|update|delete|select 元素的一次访问,而不能说只访问select的某个下级子元素;其它的一级子元素则是用于帮助构建执行单元(resultMap|parameterMap|sql)或者影响执行单元的行为的(cache|cache-ref)。
所以一级子元素可以总结如下:
- 执行单元元素:insert | update | delete | select
- 单元辅助元素:resultMap | parameterMap | sql
- 执行行为元素:cache | cache-ref
这些元素是按如下方式解析的:
1、DTD模式:使用XMLMapperBuilder对象内的方法分别解析
上面负责解析的每行代码都是一个内部方法,比如解析select|insert|update|delete元素的方法:
可以看到,具体解析又转给XMLStatementBuilder了,而最终每一个select|insert|update|delete元素在内存中表现为一个MappedStatement对象。
2、XSD模式:这里引入一个Statement级元素解析接口IStatementHandler
public interface IStatementHandler { void handleStatementNode(Configuration configuration, SchemaSqlMapperParserDelegate delegate, XNode node); }
每个实现类负责解析一种子元素,原生元素对应实现类有:
然后创建一个注册器类SchemaHandlers来管理这些实现类。
这个过程主要有两步:
(1)应用启动时,将IStatementHandler的实现类和对应命名空间的相应元素事先注册好
//静态代码块,注册默认命名空间的StatementHandler register("cache-ref", new CacheRefStatementHandler()); register("cache", new CacheStatementHandler()); register("parameterMap", new ParameterMapStatementHandler()); register("resultMap", new ResultMapStatementHandler()); register("sql", new SqlStatementHandler()); register("select|insert|update|delete", new CRUDStatementHandler());
(2)在解析时,根据XML中元素的命名空间和元素名,找到IStatementHandler的实现类,并调用接口方法
/** * 执行解析 */ public void parse() { if (!configuration.isResourceLoaded(location)) { try { Element root = document.getDocumentElement(); String namespace = root.getAttribute("namespace"); if (Tool.CHECK.isBlank(namespace)) { throw new BuilderException("Mapper's namespace cannot be empty"); } builderAssistant.setCurrentNamespace(namespace); doParseStatements(root); } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML["+location+"]. Cause: " + e, e); } configuration.addLoadedResource(location); bindMapperForNamespace(); } doParsePendings(); } /** * 解析包含statements及其相同级别的元素[cache|cache-ref|parameterMap|resultMap|sql|select|insert|update|delete]等 * @param parent */ public void doParseStatements(Node parent) { NodeList nl = parent.getChildNodes(); for (int i = 0, l = nl.getLength(); i < l; i++) { Node node = nl.item(i); if (!(node instanceof Element)) { continue; } doParseStatement(node); } } /** * 解析一个和statement同级别的元素 * @param node */ public void doParseStatement(Node node) { IStatementHandler handler = SchemaHandlers.getStatementHandler(node); if (null == handler) { throw new BuilderException("Unknown statement element <" + getDescription(node) + "> in SqlMapper ["+location+"]."); } else { SchemaXNode context = new SchemaXNode(parser, node, configuration.getVariables()); handler.handleStatementNode(configuration, this, context); } }
这样,只要事先编写好IStatementHandler的实现类,并调用SchemaHandlers的注册方法,解析就能顺利进行,而不管是原生的元素,还是自定义命名空间的扩展元素。
举个例子,和select|insert|update|delete对应的实现类如下:
public class CRUDStatementHandler extends StatementHandlerSupport{ @Override public void handleStatementNode(Configuration configuration, SchemaSqlMapperParserDelegate delegate, XNode node) { String databaseId = configuration.getDatabaseId(); if(databaseId != null){ buildStatementFromContext(configuration, delegate, node, databaseId); } buildStatementFromContext(configuration, delegate, node, null); } private void buildStatementFromContext(Configuration configuration, SchemaSqlMapperParserDelegate delegate, XNode node, String requiredDatabaseId) { XMLStatementBuilder statementParser = SqlSessionComponetFactorys.newXMLStatementBuilder(configuration, delegate.getBuilderAssistant(), node, requiredDatabaseId); try { statementParser.parseStatementNode(); } catch (IncompleteElementException e) { configuration.addIncompleteStatement(statementParser); } } }
这里,也将具体解析转给XMLStatementBuilder了,只不过这里不是直接new对象,而是通过工厂类创建而已。
五、LanguageDriver
从上面知道DTD和XSD又汇集到XMLStatementBuilder了,而在这个类里面,间接的创建了LanguageDriver的实现类,用来解析脚本级的SQL文本和元素,以及处理SQL脚本中的参数。LanguageDriver的作用实际上就是组件工厂,和我们的ISqlSessionComponentFactory类似:
public interface LanguageDriver { /** * 创建参数处理器*/ ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql); /** * 根据XML节点创建SqlSource对象 */ SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType); /** * 根据注解创建SQLSource对象 */ SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType); }
这里因为要再次区分DTD和XSD,需要使用我们自己的实现类,并在Configuration里面配置,又因为是使用XML配置,所以第三个方法就不管了:
public class SchemaXMLLanguageDriver extends XMLLanguageDriver {
// 返回ExpressionParameterHandler,可以处理表达式的参数处理器 @Override public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { return SqlSessionComponetFactorys.newParameterHandler(mappedStatement, parameterObject, boundSql); }
// 如果是DTD,则使用XMLScriptBuilder,否则使用SchemaXMLScriptBuilder,从而再次分开处理 @Override public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) { XMLScriptBuilder builder = SqlSessionComponetFactorys.newXMLScriptBuilder(configuration, script, parameterType); return builder.parseScriptNode(); } }
六、解析Script级元素
Script级元素指的是除根元素和一级子元素之外的元素(当然也不包括注释元素了。。。),是用来构建Statement级元素的,包括SQL文本和动态配置元素(include|trim|where|set|foreach|choose|if),这些元素按如下方式解析:
1、DTD模式:使用XMLScriptBuilder解析,这里mybatis倒是使用了一个解析接口,可惜的是内部的私有接口,并且在根据元素名称获取接口实现类时也是莫名其妙(竟然每次获取都先创建所有的实现类,然后返回其中的一个,这真是莫名其妙的一塌糊涂!):
另外,SQL文本则是使用TextSqlNode解析。
2、XSD模式:和Statement级元素类似,这里引入一个Script级元素解析接口IScriptHandler
public interface IScriptHandler { void handleScriptNode(Configuration configuration, XNode node, List<SqlNode> targetContents); }
每个实现类负责解析一种子元素,也使用SchemaHanders来管理这些实现类。具体也是两个步骤:
(1)静态方法中注册
//注册默认命名空间的ScriptHandler register("trim", new TrimScriptHandler()); register("where", new WhereScriptHandler()); register("set", new SetScriptHandler()); register("foreach", new ForEachScriptHandler()); register("if|when", new IfScriptHandler()); register("choose", new ChooseScriptHandler()); //register("when", new IfScriptHandler()); register("otherwise", new OtherwiseScriptHandler()); register("bind", new BindScriptHandler());
(2)在使用SchemaXMLScriptBuilder解析时根据元素命名空间和名称获取解析器
public static List<SqlNode> parseDynamicTags(Configuration configuration, XNode node) { List<SqlNode> contents = new ArrayList<SqlNode>(); NodeList children = node.getNode().getChildNodes(); for (int i = 0; i < children.getLength(); i++) { XNode child = node.newXNode(children.item(i)); short nodeType = child.getNode().getNodeType(); if (nodeType == Node.CDATA_SECTION_NODE || nodeType == Node.TEXT_NODE) { String data = child.getStringBody(""); data = decorate(configuration.getDatabaseId(), data);//对SQL文本进行装饰,从而嵌入SQL配置函数的处理 ExpressionTextSqlNode expressionTextSqlNode = new ExpressionTextSqlNode(data);//使用表达式SQL文本,从而具有处理表达式的能力 if (expressionTextSqlNode.isDynamic()) { contents.add(expressionTextSqlNode); setDynamic(true); } else { contents.add(new StaticTextSqlNode(data)); } } else if (nodeType == Node.ELEMENT_NODE) { // issue // #628 IScriptHandler handler = SchemaHandlers.getScriptHandler(child.getNode());//使用处理器机制,从而可以方便、自由地扩展 if (handler == null) { throw new BuilderException("Unknown element <" + child.getNode().getNodeName() + "> in SQL statement."); } handler.handleScriptNode(configuration, child, contents); setDynamic(true); } } return contents; }
七、处理$fn_name{args}、${(exp)}和#{(exp)}
这里引进了两个概念来扩展mybatis的配置:
1、SQL配置函数
(1)SQL配置函数,只用于配置SQL文本,和SQL函数不同,SQL函数是在数据库中执行的,而SQL配置函数只是JAVA中生成SQL脚本时候解析
(2)SQL配置函数形如 $fn_name{args},其中函数名是字母或下划线开头的字母数字下划线组合,不能为空(为空则是mybatis原生的字符串替换语法)
(3)SQL配置函数在mybatis加载时解析一次,并将解析结果存储至SqlNode对象中,不需要每次运行都解析
(4)SQL配置函数的定义和解析接口ISqlConfigFunction如下:
public interface ISqlConfigFunction { /** * 优先级,如果有多个同名函数,使用order值小的 * @return */ public int getOrder(); /** * 函数名称 * @return */ public String getName(); /** * 执行SQL配置函数 * @param databaseId 数据库ID * @param args 字符串参数 * @return */ public String eval(String databaseId, String[] args); }
(5)SQL配置函数的设别表达式如下(匆匆写就,尚未测试充分)
(6)ISqlConfigFunction也使用SchemaHandlers统一注册和管理。
(7)SQL配置函数名不区分大小写,但参数区分大小写。
2、扩展表达式
(1)作用是扩展mybatis原生的${}和#{}
(2)在原生用法中属性的外面包一对小括号,就成为扩展表达式,形如${(exp)}、#{(exp)}
(3)扩展表达式每次执行都需要解析,其中${()}表达式解析后直接替换SQL字符串,而#{(exp)}则将解析后的结果作为参数调用JDBC的set族方法设置进数据库
(4)扩展表达式的定义和解析接口IExpressionHandler如下:
public interface IExpressionHandler { public boolean isSupport(String expression, String databaseId); public Object eval(String expression, Object parameter, String databaseId); }
第一个方法用于判断是否支持需要解析的表达式,第二个方法用于根据传入参数和数据库ID来解析表达式。
如果有多个处理器可以支持需要解析的表达式,将取第一个,这是典型的责任链模式,也是Spring MVC中大量使用的模式。
(5)扩展表达式的设别很简单,就是在mybatis已经识别的基础上,判断是否以小括号开头,并以小括号结尾。
(6)IExpressionHandler也使用SchemaHandlers统一注册和管理 。
(7)扩展表达式区分大小写。
上面就是整个解析过程的一个概述了,总结一下引进的几个接口:
- 语句级元素解析处理器IStatementHandler
- 脚本级元素解析处理器IScriptHandler
- SQL配置函数ISqlConfigFunction
- 扩展表达式处理器IExpressionHandler
今天到此为止,下一篇博客就描述怎么应用这些扩展。
郴江幸自绕郴山,为谁流下潇湘去?
欲将心事付瑶琴,知音少,弦断有谁听?
倩何人,唤取红巾翠袖,揾英雄泪!
零落成泥碾作尘,只有香如故!