前言
在 Mybatis初始化的流程 中,有一个关键的方法叫做createSqlSource(),它在XMLStatementBuilder类中的parseStatementNode()方法中被调用,上一层的调用链为XMLMapperBuilder类中的buildStatementFromContext()方法。
顾名思义,buildStatementFromContext 就是从上下文中去构建Statement,来支撑后面jdbc对数据库的操作。
而createSqlSource()这个方法的作用:就是在jdbc操作数据库之前,进行了sql语句占位符的替换,将Mybatis中的占位符#{}替换成了jdbc能够解析的?号,同时将实际的参数值也保存了下来,同时 对于动态sql标签 也进行了解析处理。可以说 没有这一个步骤 后面的逻辑都没有办法执行。
最近学习源码的过程中刚好学到了这个地方,就在这里记录一下,个人学习过程难免有错误或不合理的地方,还希望各路大佬指正,谢谢!下面正文开始。
正文
LanguageDriver语言驱动器
方法的调用位置在XMLStatementBuilder.parseStatementNode()中,部分源码如下
java
public class XMLStatementBuilder extends BaseBuilder {
//省略成员变量及构造器...
public void parseStatementNode() {
//省略其他代码...
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
//省略其他代码...
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
//省略其他代码...
}
private LanguageDriver getLanguageDriver(String lang) {
Class<? extends LanguageDriver> langClass = null;
if (lang != null) {
langClass = resolveClass(lang);
}
return configuration.getLanguageDriver(langClass);
}
}
可以看出createSqlSource方法 是由一个langDriver对象来调用的,这里其实就是一个语言驱动器,LanguageDriver是一个接口,下面还有多个实现类
而langDriver对象,是调用getLanguageDriver方法获取的,可以看出,最后是从configuration这个全局配置类中获取的,是调用的configuration的getLanguageDriver方法
这里可以再看一下getLanguageDriver方法,下面代码中可以看出 这里是通过调用一个getDefaultDriver()方法,返回了一个默认的driver
java
public LanguageDriver getLanguageDriver(Class<? extends LanguageDriver> langClass) {
if (langClass == null) {
return languageRegistry.getDefaultDriver();
}
languageRegistry.register(langClass);
return languageRegistry.getDriver(langClass);
}
这里再继续往下看一下,看看在哪里配置的默认值
java
public class LanguageDriverRegistry {
private Class<? extends LanguageDriver> defaultDriverClass;
public LanguageDriver getDefaultDriver() {
return getDriver(getDefaultDriverClass());
}
public Class<? extends LanguageDriver> getDefaultDriverClass() {
return defaultDriverClass;
}
}
可以看出是LanguageDriverRegistry类中的成员变量defaultDriverClass中,配置了默认的语言驱动器的类对象,那这个defaultDriverClass是在哪里配置的呢?
实际上,是在全局配置对象Configuration的无参构造中,被赋值的,源码如下:
java
public class Configuration {
public Configuration() {
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
typeAliasRegistry.registerAlias("LRU", LruCache.class);
typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);
typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);
typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);
typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);
languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
languageRegistry.register(RawLanguageDriver.class);
}
//省略类中其他代码...
}
上面的源码中可以看出,在全局配置对象Configuration的无参构造中,进行一系列别名的注册、以及一些默认值的赋值
例如4-5行,进行了事务工厂别名的注册,这里决定了在xml配置文件中配置事务工厂的时候,可以直接使用别名JDBC或者MANAGED,来指定事务工厂了。根本原因 就是因为在上面的源码中,框架已经将别名配置好了。
在上面的代码中,可以看到在第33行,调用了languageRegistry.setDefaultDriverClass()方法 为defaultDriverClass赋了值,默认值为XMLLanguageDriver.class ,可以理解为,框架默认配置了一个xml语言驱动器,后续正是调用了这个驱动器中的createSqlSource方法 完成了sql占位符替换,以及动态sql标签的解析等操作。
createSqlSource()方法逻辑
由上面的源码可以看出,LanguageDriver.createSqlSource,实际就是调用的 XMLLanguageDriver 这个子类中的方法,源码如下:
java
public class XMLLanguageDriver implements LanguageDriver {
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
}
createSqlSource方法分为两步,
- 构建了XMLScriptBuilder对象
- 调用XMLScriptBuilder对象的parseScriptNode()完成占位符替换以及动态sql标签解析工作。
XMLScriptBuilder构造器
XMLScriptBuilder构造器中,除了进行了全局配置对象以及一些参数的传递(类似XMLConfigBuilder和XMLMapperBuilder,可以参考另一篇文章Mybatis源码 - 初始化流程 加载解析配置文件)之外,还调用了一个重要的方法:initNodeHandlerMap,这个方法中 是将一些可能会遇到的动态标签处理器,都设置了别名,并保存到了Map集合中。
java
public class XMLScriptBuilder extends BaseBuilder {
private final XNode context;
private boolean isDynamic;
private final Class<?> parameterType;
private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();
public XMLScriptBuilder(Configuration configuration, XNode context) {
this(configuration, context, null);
}
public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
super(configuration);
this.context = context;
this.parameterType = parameterType;
initNodeHandlerMap();
}
private void initNodeHandlerMap() {
nodeHandlerMap.put("trim", new TrimHandler());
nodeHandlerMap.put("where", new WhereHandler());
nodeHandlerMap.put("set", new SetHandler());
nodeHandlerMap.put("foreach", new ForEachHandler());
nodeHandlerMap.put("if", new IfHandler());
nodeHandlerMap.put("choose", new ChooseHandler());
nodeHandlerMap.put("when", new IfHandler());
nodeHandlerMap.put("otherwise", new OtherwiseHandler());
nodeHandlerMap.put("bind", new BindHandler());
}
parseScriptNode()方法
该方法源码如下:
java
public SqlSource parseScriptNode() {
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
表面上能看到的逻辑
- 通过调用parseDynamicTags方法,传入context,返回了一个MixedSqlNode
- 根据一个标识isDynamic,创建了不同类型的SqlSource对象,进行了返回
下面就对这个过程 进行一个具体的分析
parseDynamicTags()方法
这个方法的名称 顾名思义 就是"解析动态标签",在Mybatis的sql语句规则中,分为两种节点:文本节点、元素节点。
- 文本节点:就是纯文本的sql语句,包含${}和#{}的也算
- 元素节点:带有动态sql标签的部分
下面这张图更直观一点:
当前方法的返回值是一个MixedSqlNode类型,顾名思义这个一个混合sql节点对象,里面有一个属性contents
java
public class MixedSqlNode implements SqlNode {
private final List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
@Override
public boolean apply(DynamicContext context) {
contents.forEach(node -> node.apply(context));
return true;
}
}
在当前方法中,就是根据节点的不同,分别进行了处理,然后将多个节点,封装成不同的形式,保存到了contents这个属性中,然后将MixedSqlNode对象返回。
源码如下:
java
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
//如果当前节点为文本节点:创建了TextSqlNode对象
TextSqlNode textSqlNode = new TextSqlNode(data);
//sql是否为动态Sql,即sql中是否包含${}
if (textSqlNode.isDynamic()) {
//是:直接将TextSqlNode添加到集合中,同时设置一个标识isDynamic,方便后续的处理
contents.add(textSqlNode);
isDynamic = true;
} else {
//否:直接将sql封装到一个StaticTextSqlNode,添加到集合中
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
String nodeName = child.getNode().getNodeName();
//如果当前节点为元素节点
//去nodeHandlerMap集合中查找,看是否存在和节点匹配的处理器,如果没有 直接抛出异常
//nodeHandlerMap就是在XMLScriptBuilder的构造器中,封装了一系列动态标签的处理器
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
//由匹配的处理器处理节点,并设置动态标识isDynamic
handler.handleNode(child, contents);
isDynamic = true;
}
}
//最后将处理好的contents 封装到MixedSqlNode中,返回MixedSqlNode对象。
return new MixedSqlNode(contents);
}
上述代码的逻辑总结如下,代码中也有注释
- 判断当前循环到的sql节点类型,如果为文本节点,且不包含{}:返回StaticTextSqlNode对象;如果包含{}:返回TextSqlNode对象,并设置isDynamic标识。
- 如果当前为元素节点,先去判断当前节点名称,是否存在匹配的解析器,不存在:抛出异常;存在:获取到匹配的解析器进行处理,并设置isDynamic标识。
- 最后将处理好的contents 封装到MixedSqlNode中,返回MixedSqlNode对象。
封装SqlSource
在上一步中,已经封装好了所有的Sql节点,下面就要封装SqlSource对象并返回了,相关源码如下
java
public SqlSource parseScriptNode() {
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
if (isDynamic) {
//如果sql为动态sql,封装DynamicSqlSource对象返回
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
//如果sql不是动态sql,封装RawSqlSource对象返回
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
可以看出,封装的过程中 根据isDynamic这个动态标识的值,会封装成不同类型的SqlSource子类,下面简单说明一下SqlSource接口的结构,方便后续的阅读。
SqlSource介绍
SqlSource是一个接口,里面提供了一个getBoundSql方法,可以通过这个方法 来获取到解析后的sql,解析后的sql信息封装到了BoundSql对象中。
java
public interface SqlSource {
BoundSql getBoundSql(Object parameterObject);
}
BoundSql介绍
java
public class BoundSql {
private final String sql;
private final List<ParameterMapping> parameterMappings;
private final Object parameterObject;
private final Map<String, Object> additionalParameters;
private final MetaObject metaParameters;
}
BoundSql重要属性的含义如下:
-
sql
jdbc可以直接识别的sql语句,即使用?作为占位符。例如:select * from user where id = ?
-
parameterMappings
封装了原来sql中 每个#{}里面属性的值,例如下面这个sql中,存在两个参数:id、name
sqlselect * from user where id = #{id} and name = #{name}
这两个参数的值 会按顺序封装到parameterMappings这个集合中,这里还做了一层封装,封装为了ParameterMapping对象。
RawSqlSource
RawSqlSource类 是当sql不包含${}以及动态sql标签,仅作为一个纯文本sql时,封装而成的SqlSource对象,这里重点看一下它的解析过程
java
public class RawSqlSource implements SqlSource {
private final SqlSource sqlSource;
public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
this(configuration, getSql(configuration, rootSqlNode), parameterType);
}
public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> clazz = parameterType == null ? Object.class : parameterType;
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
}
private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
DynamicContext context = new DynamicContext(configuration, null);
rootSqlNode.apply(context);
return context.getSql();
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
return sqlSource.getBoundSql(parameterObject);
}
}
构造器中,显示通过getSql方法中的context.getSql()这一句 获取到了原本的Sql语句,然后调用了同名的构造方法,然后调用了sqlSourceParser.parse()方法
java
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql;
if (configuration.isShrinkWhitespacesInSql()) {
sql = parser.parse(removeExtraWhitespaces(originalSql));
} else {
sql = parser.parse(originalSql);
}
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
这个方法中使用了一个标签处理器ParameterMappingTokenHandler和标签解析器GenericTokenParser,去sql语句中解析以#{开头、以}结尾的部分,很明显这个部分就是查询参数的名称
parser.parse()方法中,进行了以#{开头、以}结尾的部分的解析
java
public String parse(String text) {
if (text == null || text.isEmpty()) {
return "";
}
// search open token
int start = text.indexOf(openToken);
if (start == -1) {
return text;
}
char[] src = text.toCharArray();
int offset = 0;
final StringBuilder builder = new StringBuilder();
StringBuilder expression = null;
do {
if (start > 0 && src[start - 1] == '\\') {
// this open token is escaped. remove the backslash and continue.
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
// found open token. let's search close token.
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
builder.append(src, offset, start - offset);
offset = start + openToken.length();
int end = text.indexOf(closeToken, offset);
while (end > -1) {
if (end > offset && src[end - 1] == '\\') {
// this close token is escaped. remove the backslash and continue.
expression.append(src, offset, end - offset - 1).append(closeToken);
offset = end + closeToken.length();
end = text.indexOf(closeToken, offset);
} else {
expression.append(src, offset, end - offset);
break;
}
}
if (end == -1) {
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset);
} while (start > -1);
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
在45行,有一处关键的代码:handler.handleToken(expression.toString());点开看一下
java
@Override
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
可以看出 这里就是将参数的值 封装成了一个ParameterMapping对象,然后添加到了parameterMappings这个集合中,而这个集合 就会传递到BoundSql中的parameterMappings属性。同时方法返回了? 号,完成了字符串的替换。
至此 就是RawSqlSource大概的处理过程了。
DynamicSqlSource
DynamicSqlSource类 是当sql为动态sql时,封装成的类型,它的结构如下:
java
public class DynamicSqlSource implements SqlSource {
private final Configuration configuration;
private final SqlNode rootSqlNode;
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
}
它和RawSqlSource类的区别在于,它没有解析的过程,而是在每次调用getBoundSql方法获取sql的时候,都重新进行解析,这是因为动态sql标签有不确定性,例如传入标签内的变量值都不可控,所以每次获取 都要重新解析。
总结
综上所述,介绍了一下SqlSource对占位符的替换、对参数值的保存、对动态Sql标签的处理。个人认为还是很有学习的必要的,感谢您的阅读,有不对的地方还请指正,谢谢大佬~