前面在MyBatis中动态SQL执行过程一文中介绍了MyBatis在运行时大体的运行过程。其中有介绍到通过MappedStatement来获取SQL语句,不过没有展开分析。本文就接着来剖析一下,一段带有动态XML标签的文本是如何被MyBatis解析成SQL字符串的。
MappedStatement
在服务(与Spring Boot整合的情况,本文也只会分析这种情况)启动时,会构建SqlSessionFactory,而在这个构建过程中,会解析主配置configLocation指向路径下的mapper文件并为其中的四种SQL语句创建MappedStatement对象。所以本文先来介绍一下SqlSessionFactory的构建。
构建SqlSessionFactory
/*
* 创建sqlSessionFactory
*/
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
// 创建FactoryBean
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
// 设置数据源
factory.setDataSource(dataSource);
// 设置MyBatis使用的虚拟文件系统类型
factory.setVfs(SpringBootVFS.class);
// 如果配置了主配置文件的位置
if (StringUtils.hasText(this.properties.getConfigLocation())) {
// 设置主配置文件的位置
factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
}
// 将Configuration对象设置到factory的属性中
applyConfiguration(factory);
if (this.properties.getConfigurationProperties() != null) {
factory.setConfigurationProperties(this.properties.getConfigurationProperties());
}
// 将拦截器设置到工厂bean中,最后会设置到Configuration对象中,如果设置了的话
if (!ObjectUtils.isEmpty(this.interceptors)) {
factory.setPlugins(this.interceptors);
}
if (this.databaseIdProvider != null) {
factory.setDatabaseIdProvider(this.databaseIdProvider);
}
if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
}
if (this.properties.getTypeAliasesSuperType() != null) {
factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
}
// 设置类型处理器的包名,如果设置了的话
if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
}
// 设置类型处理器,如果设置了的话
if (!ObjectUtils.isEmpty(this.typeHandlers)) {
factory.setTypeHandlers(this.typeHandlers);
}
// 解析mapper文件的位置
Resource[] mapperLocations = this.properties.resolveMapperLocations();
if (!ObjectUtils.isEmpty(mapperLocations)) {
// 设置mapper文件的位置
factory.setMapperLocations(mapperLocations);
}
// 获取SqlSessionFactoryBean类中的字段名称
Set<String> factoryPropertyNames = Stream
.of(new BeanWrapperImpl(SqlSessionFactoryBean.class).getPropertyDescriptors()).map(PropertyDescriptor::getName)
.collect(Collectors.toSet());
// 获取默认的脚本语言驱动,默认是null
Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
// 默认情况下this.languageDrivers是null,所以不满足这里的if条件
if (factoryPropertyNames.contains("scriptingLanguageDrivers") && !ObjectUtils.isEmpty(this.languageDrivers)) {
// Need to mybatis-spring 2.0.2+
factory.setScriptingLanguageDrivers(this.languageDrivers);
if (defaultLanguageDriver == null && this.languageDrivers.length == 1) {
defaultLanguageDriver = this.languageDrivers[0].getClass();
}
}
if (factoryPropertyNames.contains("defaultScriptingLanguageDriver")) {
// Need to mybatis-spring 2.0.2+
factory.setDefaultScriptingLanguageDriver(defaultLanguageDriver);
}
// 默认没有操作
applySqlSessionFactoryBeanCustomizers(factory);
// 创建SqlSessionFactory
return factory.getObject();
}@Override
public SqlSessionFactory getObject() throws Exception {
// 如果还没创建对象,则创建
if (this.sqlSessionFactory == null) {
afterPropertiesSet();
}
return this.sqlSessionFactory;
}@Override
public void afterPropertiesSet() throws Exception {
notNull(dataSource, "Property 'dataSource' is required");
notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
// configuration和configLocation不能同时为空,也不能同时指定
state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
"Property 'configuration' and 'configLocation' can not specified with together");
// 构建SqlSessionFactory
this.sqlSessionFactory = buildSqlSessionFactory();
}/*
* 这个方法绝大部分都在构建和设置Configuration对象,这是整个MyBatis的顶层核心类,贯穿启动时和运行时。
*/
protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
// 记录MyBatis的配置
final Configuration targetConfiguration;
// 用于解析xml配置文件
XMLConfigBuilder xmlConfigBuilder = null;
if (this.configuration != null) { // 如果指定了configuration对象,则直接使用;对应了代码配置;
targetConfiguration = this.configuration;
if (targetConfiguration.getVariables() == null) {
targetConfiguration.setVariables(this.configurationProperties);
} else if (this.configurationProperties != null) {
targetConfiguration.getVariables().putAll(this.configurationProperties);
}
} else if (this.configLocation != null) { // 如果指定了配置文件的路径,则解析配置文件;对应了XML文件配置;
xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
/*
* 注意,这里获取到的是一个新创建的配置对象,还没有执行配置文件的解析,下面才会调用builder对象的parse方法来解析
* 但实际上,从这里到下面调用parse方法之间没有对xmlConfigBuilder的额外操作,那为什么不在这里就解析?
* 应该是因为避免下面以及在parse方法之前的一些操作覆盖了配置文件中的内容。
*/
targetConfiguration = xmlConfigBuilder.getConfiguration();
} else { // 如果两种配置方式都没使用,则创建空的配置对象
LOGGER.debug(
() -> "Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
targetConfiguration = new Configuration();
Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables);
}
Optional.ofNullable(this.objectFactory).ifPresent(targetConfiguration::setObjectFactory);
Optional.ofNullable(this.objectWrapperFactory).ifPresent(targetConfiguration::setObjectWrapperFactory);
Optional.ofNullable(this.vfs).ifPresent(targetConfiguration::setVfsImpl);
// 扫描类型别名
if (hasLength(this.typeAliasesPackage)) {
scanClasses(this.typeAliasesPackage, this.typeAliasesSuperType).stream()
.filter(clazz -> !clazz.isAnonymousClass()).filter(clazz -> !clazz.isInterface())
.filter(clazz -> !clazz.isMemberClass()).forEach(targetConfiguration.getTypeAliasRegistry()::registerAlias);
}
if (!isEmpty(this.typeAliases)) {
Stream.of(this.typeAliases).forEach(typeAlias -> {
targetConfiguration.getTypeAliasRegistry().registerAlias(typeAlias);
LOGGER.debug(() -> "Registered type alias: '" + typeAlias + "'");
});
}
// 设置插件
if (!isEmpty(this.plugins)) {
Stream.of(this.plugins).forEach(plugin -> {
// 添加插件到Configuration对象中
targetConfiguration.addInterceptor(plugin);
LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");
});
}
// 设置类型处理器
if (hasLength(this.typeHandlersPackage)) {
scanClasses(this.typeHandlersPackage, TypeHandler.class).stream().filter(clazz -> !clazz.isAnonymousClass())
.filter(clazz -> !clazz.isInterface()).filter(clazz -> !Modifier.isAbstract(clazz.getModifiers()))
.forEach(targetConfiguration.getTypeHandlerRegistry()::register);
}
if (!isEmpty(this.typeHandlers)) {
Stream.of(this.typeHandlers).forEach(typeHandler -> {
targetConfiguration.getTypeHandlerRegistry().register(typeHandler);
LOGGER.debug(() -> "Registered type handler: '" + typeHandler + "'");
});
}
// 设置默认的枚举类型处理器
targetConfiguration.setDefaultEnumTypeHandler(defaultEnumTypeHandler);
// 设置脚本语言驱动
if (!isEmpty(this.scriptingLanguageDrivers)) {
Stream.of(this.scriptingLanguageDrivers).forEach(languageDriver -> {
targetConfiguration.getLanguageRegistry().register(languageDriver);
LOGGER.debug(() -> "Registered scripting language driver: '" + languageDriver + "'");
});
}
/*
* 这里一般不会设置,而是在XMLConfigBuilder中调用的Configuration的setDefaultScriptingLanguage
* 如果这里有设置,会覆盖掉上面设置的
*/
Optional.ofNullable(this.defaultScriptingLanguageDriver)
.ifPresent(targetConfiguration::setDefaultScriptingLanguage);
// 设置数据库ID(有何作用?)
if (this.databaseIdProvider != null) {// fix #64 set databaseId before parse mapper xmls
try {
targetConfiguration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
} catch (SQLException e) {
throw new IOException("Failed getting a databaseId", e);
}
}
Optional.ofNullable(this.cache).ifPresent(targetConfiguration::addCache);
if (xmlConfigBuilder != null) {
try {
// 解析MyBatis的主配置(xml)文件
xmlConfigBuilder.parse();
LOGGER.debug(() -> "Parsed configuration file: '" + this.configLocation + "'");
} catch (Exception ex) {
throw new IOException("Failed to parse config resource: " + this.configLocation, ex);
} finally {
ErrorContext.instance().reset();
}
}
// 创建并设置环境对象,注意不是Spring中的Environment,而是MyBatis自己定义的类
targetConfiguration.setEnvironment(new Environment(this.environment,
// 创建并设置为Spring的事务工厂类
this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory,
this.dataSource));
/*
* 处理mapper的xml文件,注意别和configLocation搞混了。
*/
if (this.mapperLocations != null) {
if (this.mapperLocations.length == 0) { // 找不到指定的mapper配置
LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found.");
} else {
// 遍历mapper文件
for (Resource mapperLocation : this.mapperLocations) {
if (mapperLocation == null) {
continue;
}
try {
// 创建解析器(每个mapper文件对应一个解析器),注意不是上面解析主配置文件的XMLConfigBuilder,别搞混了。
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
// 解析mapper文件
xmlMapperBuilder.parse();
} catch (Exception e) {
throw new IOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
} finally {
ErrorContext.instance().reset();
}
LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");
}
}
} else {
LOGGER.debug(() -> "Property 'mapperLocations' was not specified.");
}
// 构建SqlSessionFactory,configuration对象会被设置到factory对象中
return this.sqlSessionFactoryBuilder.build(targetConfiguration);
}MyBatis使用了经典的FactoryBean来构建SqlSessionFactory,在其afterPropertiesSet阶段,执行了真正的构建过程。在该过程中涉及了两种文件的解析:
- 通过XMLConfigBuilder来解析主配置文件;
- 通过XMLMapperBuilder来解析mapper文件; 与本文主题相关的是后者。
解析四种SQL语句
public void parse() {
// 如果没有解析过才解析
if (!configuration.isResourceLoaded(resource)) {
/*
* 核心步骤:
* 解析mapper节点及其子节点,
* 主要的解析操作都在该方法中
*/
configurationElement(parser.evalNode("/mapper"));
// 添加被解析的mapper文件
configuration.addLoadedResource(resource);
// 解析命名空间并和当前mapper绑定,在运行时会通过这种映射关系找到mapper实现。
bindMapperForNamespace();
}
/*
* 解析一些pending状态(未完成解析)的标签,
* 至于为什么会产生这种状态的配置,就要参考上面的配置解析过程了。(感觉大概是有因为这几种配置中可以存在跨文件引用)
*/
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}/*
* 解析mapper文件内容
*/
private void configurationElement(XNode context) {
try {
// 获取namespace属性,没有校验配置有效性(比如配置的mapper接口是否真的存在?),而是仅判断有无
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
// 设置命名空间到帮助对象中
builderAssistant.setCurrentNamespace(namespace);
// 解析cache-ref节点
cacheRefElement(context.evalNode("cache-ref"));
// 解析cache节点
cacheElement(context.evalNode("cache"));
// 已废弃节点,可忽略
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 解析resultMap节点
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 解析sql节点
sqlElement(context.evalNodes("/mapper/sql"));
// 解析增删查改四种语句节点
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
// 解析语句节点
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
// 遍历节点
for (XNode context : list) {
// 创建builder对象
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
// 解析语句节点
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}整个解析过程涉及非常多的内容,而我们现在只关心四种SQL语句的解析。在XMLMapperBuilder中,这部分操作是委托给XmlStatementBuilder来执行的。
public void parseStatementNode() {
// 获取两个属性
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
// 判断databaseId是否匹配,不匹配则跳过不会解析
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
// 获取节点名称,如select、insert等
String nodeName = context.getNode().getNodeName();
// SQL语句类型,即标签语句名称的大写形式
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
// 下面三个属性只有对于select语句才有效
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); // 默认值为false:表示在执行语句前清空一级和二级缓存
boolean useCache = context.getBooleanAttribute("useCache", isSelect); // 默认值为true:表示使用二级缓存;但要主配置(Configuration)中开启了才有效。
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
// 解析include节点
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// 解析参数类型
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
/*
* 获取语言驱动,大部分情况下都是默认情况,但支持自定义语言驱动来支持特定的SQL生成需求。
*/
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
/*
* 创建sqlSource对象,用来处理SQL,在解析阶段会被封装在MappedStatement对象中(参考下面),
* 后续运行时会被Executor用来创建BoundSQL对象,而该对象又会被用来获取实际执行的SQL。
* 这里传入context(XML上下文)后会解析,然后判断创建哪种(动态还是静态)SqlSource,并把解析到的各种SqlNode保存到SqlSource对象中。
*/
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
// 获取Statement的类型,默认是PREPARED
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
// 解析结果类型,会涉及别名
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
// 构建MappedStatement对象,并将其存储到配置对象中
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}在上面的方法中,现在重点关注两个地方:
- 构建SqlSource,SqlSource与SQL解析关系非常密切,是一个非常重要的接口,MappedStatement实际上也是将SQL解析委托给它来处理的。
- 构建MappedStatement;
构建SqlSource
MyBatis作为一个框架,提供了不错的扩展性。比如,除了使用MyBatis提供的默认动态SQL节点外,还可以自定义其他使用方式,只需实现对应的LanguageDriver即可。不过,在实际工作中,很少场景需要自定义的,大部分还是使用的默认提供的能力。
获取LanguageDriver
SqlSource是被LanguageDriver来构建的。
private LanguageDriver getLanguageDriver(String lang) {
Class<? extends LanguageDriver> langClass = null;
// mapper文件指定了languageDriver
if (lang != null) {
langClass = resolveClass(lang);
}
// 默认情况
return configuration.getLanguageDriver(langClass);
}public void setDefaultScriptingLanguage(Class<? extends LanguageDriver> driver) {
if (driver == null) {
// 设置默认的driver类型
driver = XMLLanguageDriver.class;
}
getLanguageRegistry().setDefaultDriverClass(driver);
}public LanguageDriver getLanguageDriver(Class<? extends LanguageDriver> langClass) {
if (langClass == null) {
// 返回默认的语言驱动
return languageRegistry.getDefaultDriver();
}
languageRegistry.register(langClass);
return languageRegistry.getDriver(langClass);
}public class LanguageDriverRegistry {
private final Map<Class<? extends LanguageDriver>, LanguageDriver> LANGUAGE_DRIVER_MAP = new HashMap<>();
private Class<? extends LanguageDriver> defaultDriverClass;
public void register(Class<? extends LanguageDriver> cls) {
if (cls == null) {
throw new IllegalArgumentException("null is not a valid Language Driver");
}
MapUtil.computeIfAbsent(LANGUAGE_DRIVER_MAP, cls, k -> {
try {
return k.getDeclaredConstructor().newInstance();
} catch (Exception ex) {
throw new ScriptingException("Failed to load language driver for " + cls.getName(), ex);
}
});
}
public void register(LanguageDriver instance) {
if (instance == null) {
throw new IllegalArgumentException("null is not a valid Language Driver");
}
Class<? extends LanguageDriver> cls = instance.getClass();
if (!LANGUAGE_DRIVER_MAP.containsKey(cls)) {
LANGUAGE_DRIVER_MAP.put(cls, instance);
}
}
public LanguageDriver getDriver(Class<? extends LanguageDriver> cls) {
return LANGUAGE_DRIVER_MAP.get(cls);
}
public LanguageDriver getDefaultDriver() {
return getDriver(getDefaultDriverClass());
}
public Class<? extends LanguageDriver> getDefaultDriverClass() {
return defaultDriverClass;
}
public void setDefaultDriverClass(Class<? extends LanguageDriver> defaultDriverClass) {
register(defaultDriverClass);
this.defaultDriverClass = defaultDriverClass;
}
}一般情况下,项目中不会特地设置langDriver,而默认的设置会由XMLConfigBuilder在解析主配置过程中来触发,即调用Configuration中的setDefaultScriptingLanguage方法,而入参又是null,所以实际注册XMLLanguageDriver类型的driver,而该类型则是主角。
XMLScriptBuilder执行构建SqlSource
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
/*
* 创建builder对象,一条语句对应一个builder
* 实际的创建工作是委托给XMLScriptBuilder来执行的。
*/
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
// 执行解析
return builder.parseScriptNode();
}没想到,实际的主角应该是XMLScriptBuilder(上面标题已埋下伏笔)。
public SqlSource parseScriptNode() {
// 解析动态SQL标签,获取sqlNode根节点
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
/*
* 根据是否是动态SQL,创建不同的对象
* 注意,如果语句内容中只有文本,即不包含其他动态SQL的XML标签,而且文本中不包含“${}”,则会认为是静态的,即使包含“#{}”,也不是动态的。
*/
if (isDynamic) { // 动态SQL
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else { // 静态SQL
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}protected MixedSqlNode parseDynamicTags(XNode node) {
// 会把解析到的多个SqlNode封装为MixedSqlNode返回
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));
/*
* 如果节点类型是TEXT_NODE类型
* CDATA_SECTION_NODE是指XML中的一些特殊符号的转义表示,比如'>'被转义为'>',就会被解析为这种类型的节点。
*/
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
// 获取文本内容
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
/*
* 若文本中包含${}占位符,则认为是动态节点
* 注意,如果只有#{},则会创建StaticTextSqlNode,而不是TextSqlNode。
*/
if (textSqlNode.isDynamic()) {
// 可见如果是动态SQL,则添加的是TextSqlNode
contents.add(textSqlNode);
// 标记是动态语句
isDynamic = true;
} else {
// 如果是非动态SQL,则添加的是StaticTextSqlNode
contents.add(new StaticTextSqlNode(data));
}
}
// 如果是节点类型,如if、when等,共8种语句中的
else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
// 获取节点名称
String nodeName = child.getNode().getNodeName();
// 获取节点对应的处理器,并判断是不是所支持的节点类型
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
// 不是支持的节点,则抛出异常
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
// 处理子节点
handler.handleNode(child, contents);
// 只要是动态SQL节点,则直接标记为是动态SQL
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}这里根据SQL是否是动态的创建了不同的SqlSource实现类类型的对象。那这里的动态是否是指SQL中使用了变量?让我们来一探究竟! 答案在parseDynamicTags方法中,如果包含动态SQL的XML节点(如<if>),那么确实算作是动态SQL;但没有这些节点的话,则要由TextSqlNode来判断。
public boolean isDynamic() {
DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
/*
* 创建占位符为“${}”的解析器
* 只要文本中存在占位符就能说明是动态SQL,但是这里只考虑了”${}“,而没有考虑”#{}“,为什么会这样?
* 这就是MyBatis的专门设计吧,对#{}的处理应该在其他地方。
*/
GenericTokenParser parser = createParser(checker);
parser.parse(text);
return checker.isDynamic();
}private GenericTokenParser createParser(TokenHandler handler) {
return new GenericTokenParser("${", "}", handler);
}private static class DynamicCheckerTokenParser implements TokenHandler {
private boolean isDynamic;
public DynamicCheckerTokenParser() {
// Prevent Synthetic Access
}
public boolean isDynamic() {
return isDynamic;
}
@Override
public String handleToken(String content) {
// 只要存在token,就说明是动态SQL。
this.isDynamic = true;
/* 这里怎么返回null?岂不是会导致SQL字符串中被拼接“null”?
* 该TokenHandler只有在TextSqlNode的isDynamic方法中会被使用,而且仅用来判断是否是动态SQL,不涉及SQL字符串的拼接。
* 在拼接SQL的时候调的是BindingTokenParser这个处理器,而不是当前处理器。
*/
return null;
}
}public class GenericTokenParser {
private final String openToken;
private final String closeToken;
// 处理openToken和closeToken中间内容的处理器
private final TokenHandler handler;
public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
this.openToken = openToken;
this.closeToken = closeToken;
this.handler = handler;
}
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,便于处理下一个占位符
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中
expression.append(src, offset, end - offset);
break;
}
}
/*
* 如果没有找到结束占位符,则将剩下的内容直接全部拷贝,并退出循环,结束寻找
* 所以说,如果只定义了开始占位符,没有定义结束占位符,并不会在MyBatis层面报错,而是让SQL本身就变成有错误的SQL。
*/
if (end == -1) {
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
/*
* 取得参数对应的值,并拼接在sql中,占位符中可能并不是简单的参数名(比如还会设置jdbcType,typeHandler等),所以需要handler来处理
*/
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
// 从offset处开始往后找下一个开始占位符
start = text.indexOf(openToken, offset);
} while (start > -1);
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
}可以发现,如果文本中包含${},则会被认为是动态的。特别要注意的是,如果只包含#{},则不会被认为是动态的,而会被创建RawSqlSource,而不是DynamicSqlSource,关于两者有什么区别,在后续运行时阶段会介绍到。
在XMLScriptBuilder的parseDynamicTags方法中,动态SQL的各个部分会被解析为各种SqlNode,然后被封装在MixedSqlNode中,最后设置到SqlSource中。下面会单独介绍SqlNode。
构建MappedStatement
铺垫了这么久,终于到了开头提到的MappedStatement。
// 该方法其实就是在创建一个MappedStatement对象,然后把各种属性设置在其中
public MappedStatement addMappedStatement(
String id,
SqlSource sqlSource,
StatementType statementType,
SqlCommandType sqlCommandType,
Integer fetchSize,
Integer timeout,
String parameterMap,
Class<?> parameterType,
String resultMap,
Class<?> resultType,
ResultSetType resultSetType,
boolean flushCache,
boolean useCache,
boolean resultOrdered,
KeyGenerator keyGenerator,
String keyProperty,
String keyColumn,
String databaseId,
LanguageDriver lang,
String resultSets) {
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
// 创建用于构建statement的builder对象,其实这里往builder上设置的属性都会设置到MappedStatement对象中。
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);
// 获取语句的参数映射
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
// 执行构建,这里其实仅是返回内部封装的MappedStatement对象
MappedStatement statement = statementBuilder.build();
/*
* 添加statement
* 其实在启动时的解析阶段,就是为每个语句创建MappedStatement对象;
* 在运行时,Executor从Configuration中获取MappedStatement对象,然后通过其内部的SqlSource对象来创建BoundSql对象。
*/
configuration.addMappedStatement(statement);
return statement;
}这里其实没有做什么复杂的操作,就是创建MappedStatement实例,然后设置一些属性(尤其是SqlSource),最后注册到Configuration中就完了。
认识SqlNode
在介绍SQL构建之前,需要认识一下重要的接口SqlNode及其实现类。 在上面提到,一条(动态)SQL中的各个部分会被解析为各种SqlNode。
/*
* 该接口表示了MyBatis中动态SQL中的XML标签节点,比如where,set等;
*/
public interface SqlNode {
boolean apply(DynamicContext context);
}public class DynamicContext {
public static final String PARAMETER_OBJECT_KEY = "_parameter";
public static final String DATABASE_ID_KEY = "_databaseId";
static {
OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
}
// 动态SQL的参数映射
private final ContextMap bindings;
private final StringJoiner sqlBuilder = new StringJoiner(" ");
private int uniqueNumber = 0;
public DynamicContext(Configuration configuration, Object parameterObject) {
if (parameterObject != null && !(parameterObject instanceof Map)) {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
boolean existsTypeHandler = configuration.getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass());
bindings = new ContextMap(metaObject, existsTypeHandler);
} else {
bindings = new ContextMap(null, false);
}
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}
public Map<String, Object> getBindings() {
return bindings;
}
public void bind(String name, Object value) {
bindings.put(name, value);
}
public void appendSql(String sql) {
sqlBuilder.add(sql);
}
public String getSql() {
return sqlBuilder.toString().trim();
}
public int getUniqueNumber() {
return uniqueNumber++;
}
static class ContextMap extends HashMap<String, Object> {
private static final long serialVersionUID = 2977601501966151582L;
private final MetaObject parameterMetaObject;
private final boolean fallbackParameterObject;
public ContextMap(MetaObject parameterMetaObject, boolean fallbackParameterObject) {
this.parameterMetaObject = parameterMetaObject;
this.fallbackParameterObject = fallbackParameterObject;
}
@Override
public Object get(Object key) {
String strKey = (String) key;
if (super.containsKey(strKey)) {
return super.get(strKey);
}
if (parameterMetaObject == null) {
return null;
}
if (fallbackParameterObject && !parameterMetaObject.hasGetter(strKey)) {
return parameterMetaObject.getOriginalObject();
} else {
// issue #61 do not modify the context when reading
return parameterMetaObject.getValue(strKey);
}
}
}
static class ContextAccessor implements PropertyAccessor {
@Override
public Object getProperty(Map context, Object target, Object name) {
Map map = (Map) target;
Object result = map.get(name);
if (map.containsKey(name) || result != null) {
return result;
}
Object parameterObject = map.get(PARAMETER_OBJECT_KEY);
if (parameterObject instanceof Map) {
return ((Map)parameterObject).get(name);
}
return null;
}
@Override
public void setProperty(Map context, Object target, Object name, Object value) {
Map<Object, Object> map = (Map<Object, Object>) target;
map.put(name, value);
}
@Override
public String getSourceAccessor(OgnlContext arg0, Object arg1, Object arg2) {
return null;
}
@Override
public String getSourceSetter(OgnlContext arg0, Object arg1, Object arg2) {
return null;
}
}
}该接口只定义了一个方法,该方法在构建SQL时会被调用,用来提供各个部分对应的实际SQL的内容片段。整个解析过程中的中间数据会被保存在DynamicContext中,包括SQL字符串的拼接过程。
- TextNode:表示包含的${}纯文本SQL片段;
- StaticTextSqlNode:表示不包含的${}的纯文本SQL片段;
- IfSqlNode:用于处理<if>标签;
- ForEachSqlNode:用于处理<foreach>标签;
- TrimSqlNode:用于处理<trim>标签;
- SetSqlNode:用于处理<set>标签;
- WhereSqlNode:用于处理<where>标签;
- ChooseSqlNode:用于处理<choose>标签;
- VarDeclSqlNode:用于处理<bind/>标签,该标签可以用来定义一些变量,在需要多处引用复杂的表达式时很有用;
- MixedSqlNode:用于封装多个或多种SqlNode对象;
下面分别是各种SqlNode实现类的源码,逻辑比较简单,不再介绍。
/*
* TextSqlNode的设计目标是为了处理占位符${},这也是和StaticTextSqlNode不同的地方。
* 只要包含${},就会被认为是动态SQL,而只包含#{}(且没有其他动态SQL XML节点)则不会。
*/
public class TextSqlNode implements SqlNode {
private final String text;
private final Pattern injectionFilter;
public TextSqlNode(String text) {
this(text, null);
}
public TextSqlNode(String text, Pattern injectionFilter) {
this.text = text;
this.injectionFilter = injectionFilter;
}
public boolean isDynamic() {
DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
/*
* 创建占位符为“${}”的解析器
* 只要文本中存在占位符就能说明是动态SQL,但是这里只考虑了”${}“,而没有考虑”#{}“,为什么会这样?
* 这就是MyBatis的专门设计吧,对#{}的处理应该在其他地方。
*/
GenericTokenParser parser = createParser(checker);
parser.parse(text);
return checker.isDynamic();
}
@Override
public boolean apply(DynamicContext context) {
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
context.appendSql(parser.parse(text));
return true;
}
private GenericTokenParser createParser(TokenHandler handler) {
return new GenericTokenParser("${", "}", handler);
}
private static class BindingTokenParser implements TokenHandler {
private DynamicContext context;
private Pattern injectionFilter;
public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {
this.context = context;
this.injectionFilter = injectionFilter;
}
@Override
public String handleToken(String content) {
// 获取待绑定的参数
Object parameter = context.getBindings().get("_parameter");
if (parameter == null) {
context.getBindings().put("value", null);
} else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) { // 如果参数是简单类型
context.getBindings().put("value", parameter);
}
// 执行参数绑定
Object value = OgnlCache.getValue(content, context.getBindings());
String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
checkInjection(srtValue);
return srtValue;
}
private void checkInjection(String value) {
if (injectionFilter != null && !injectionFilter.matcher(value).matches()) {
throw new ScriptingException("Invalid input. Please conform to regex" + injectionFilter.pattern());
}
}
}
private static class DynamicCheckerTokenParser implements TokenHandler {
private boolean isDynamic;
public DynamicCheckerTokenParser() {
// Prevent Synthetic Access
}
public boolean isDynamic() {
return isDynamic;
}
@Override
public String handleToken(String content) {
// 只要存在token,就说明是动态SQL。
this.isDynamic = true;
/* 这里怎么返回null?岂不是会导致SQL字符串中被拼接“null”?
* 该TokenHandler只有在TextSqlNode的isDynamic方法中会被使用,而且仅用来判断是否是动态SQL,不涉及SQL字符串的拼接。
* 在拼接SQL的时候调的是BindingTokenParser这个处理器,而不是当前处理器。
*/
return null;
}
}
}public class StaticTextSqlNode implements SqlNode {
private final String text;
public StaticTextSqlNode(String text) {
this.text = text;
}
@Override
public boolean apply(DynamicContext context) {
context.appendSql(text);
return true;
}
}public class IfSqlNode implements SqlNode {
private final ExpressionEvaluator evaluator;
private final String test;
private final SqlNode contents;
public IfSqlNode(SqlNode contents, String test) {
this.test = test;
this.contents = contents;
this.evaluator = new ExpressionEvaluator();
}
@Override
public boolean apply(DynamicContext context) {
// 表达式为true,才应用子SqlNode
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
}
}public class ForEachSqlNode implements SqlNode {
public static final String ITEM_PREFIX = "__frch_";
private final ExpressionEvaluator evaluator;
private final String collectionExpression;
private final Boolean nullable;
private final SqlNode contents;
private final String open;
private final String close;
private final String separator;
private final String item;
private final String index;
private final Configuration configuration;
/**
* @deprecated Since 3.5.9, use the {@link #ForEachSqlNode(Configuration, SqlNode, String, Boolean, String, String, String, String, String)}.
*/
@Deprecated
public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, String index, String item, String open, String close, String separator) {
this(configuration, contents, collectionExpression, null, index, item, open, close, separator);
}
/**
* @since 3.5.9
*/
public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, Boolean nullable, String index, String item, String open, String close, String separator) {
this.evaluator = new ExpressionEvaluator();
this.collectionExpression = collectionExpression;
this.nullable = nullable;
this.contents = contents;
this.open = open;
this.close = close;
this.separator = separator;
this.index = index;
this.item = item;
this.configuration = configuration;
}
@Override
public boolean apply(DynamicContext context) {
Map<String, Object> bindings = context.getBindings();
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings,
Optional.ofNullable(nullable).orElseGet(configuration::isNullableOnForEach));
if (iterable == null || !iterable.iterator().hasNext()) {
return true;
}
boolean first = true;
applyOpen(context);
int i = 0;
for (Object o : iterable) {
DynamicContext oldContext = context;
if (first || separator == null) {
context = new PrefixedContext(context, "");
} else {
context = new PrefixedContext(context, separator);
}
int uniqueNumber = context.getUniqueNumber();
// Issue #709
if (o instanceof Map.Entry) {
@SuppressWarnings("unchecked")
Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
applyIndex(context, mapEntry.getKey(), uniqueNumber);
applyItem(context, mapEntry.getValue(), uniqueNumber);
} else {
applyIndex(context, i, uniqueNumber);
applyItem(context, o, uniqueNumber);
}
contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
if (first) {
first = !((PrefixedContext) context).isPrefixApplied();
}
context = oldContext;
i++;
}
applyClose(context);
context.getBindings().remove(item);
context.getBindings().remove(index);
return true;
}
private void applyIndex(DynamicContext context, Object o, int i) {
if (index != null) {
context.bind(index, o);
context.bind(itemizeItem(index, i), o);
}
}
private void applyItem(DynamicContext context, Object o, int i) {
if (item != null) {
context.bind(item, o);
context.bind(itemizeItem(item, i), o);
}
}
private void applyOpen(DynamicContext context) {
if (open != null) {
context.appendSql(open);
}
}
private void applyClose(DynamicContext context) {
if (close != null) {
context.appendSql(close);
}
}
private static String itemizeItem(String item, int i) {
return ITEM_PREFIX + item + "_" + i;
}
private static class FilteredDynamicContext extends DynamicContext {
private final DynamicContext delegate;
private final int index;
private final String itemIndex;
private final String item;
public FilteredDynamicContext(Configuration configuration,DynamicContext delegate, String itemIndex, String item, int i) {
super(configuration, null);
this.delegate = delegate;
this.index = i;
this.itemIndex = itemIndex;
this.item = item;
}
@Override
public Map<String, Object> getBindings() {
return delegate.getBindings();
}
@Override
public void bind(String name, Object value) {
delegate.bind(name, value);
}
@Override
public String getSql() {
return delegate.getSql();
}
@Override
public void appendSql(String sql) {
GenericTokenParser parser = new GenericTokenParser("#{", "}", content -> {
String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index));
if (itemIndex != null && newContent.equals(content)) {
newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index));
}
return "#{" + newContent + "}";
});
delegate.appendSql(parser.parse(sql));
}
@Override
public int getUniqueNumber() {
return delegate.getUniqueNumber();
}
}
private class PrefixedContext extends DynamicContext {
private final DynamicContext delegate;
private final String prefix;
private boolean prefixApplied;
public PrefixedContext(DynamicContext delegate, String prefix) {
super(configuration, null);
this.delegate = delegate;
this.prefix = prefix;
this.prefixApplied = false;
}
public boolean isPrefixApplied() {
return prefixApplied;
}
@Override
public Map<String, Object> getBindings() {
return delegate.getBindings();
}
@Override
public void bind(String name, Object value) {
delegate.bind(name, value);
}
@Override
public void appendSql(String sql) {
if (!prefixApplied && sql != null && sql.trim().length() > 0) {
delegate.appendSql(prefix);
prefixApplied = true;
}
delegate.appendSql(sql);
}
@Override
public String getSql() {
return delegate.getSql();
}
@Override
public int getUniqueNumber() {
return delegate.getUniqueNumber();
}
}
}public class TrimSqlNode implements SqlNode {
private final SqlNode contents;
private final String prefix;
private final String suffix;
private final List<String> prefixesToOverride;
private final List<String> suffixesToOverride;
private final Configuration configuration;
public TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, String prefixesToOverride, String suffix, String suffixesToOverride) {
this(configuration, contents, prefix, parseOverrides(prefixesToOverride), suffix, parseOverrides(suffixesToOverride));
}
protected TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, List<String> prefixesToOverride, String suffix, List<String> suffixesToOverride) {
this.contents = contents;
this.prefix = prefix;
this.prefixesToOverride = prefixesToOverride;
this.suffix = suffix;
this.suffixesToOverride = suffixesToOverride;
this.configuration = configuration;
}
@Override
public boolean apply(DynamicContext context) {
FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
boolean result = contents.apply(filteredDynamicContext);
filteredDynamicContext.applyAll();
return result;
}
private static List<String> parseOverrides(String overrides) {
if (overrides != null) {
// 多个override用竖线分隔
final StringTokenizer parser = new StringTokenizer(overrides, "|", false);
final List<String> list = new ArrayList<>(parser.countTokens());
while (parser.hasMoreTokens()) {
list.add(parser.nextToken().toUpperCase(Locale.ENGLISH));
}
return list;
}
return Collections.emptyList();
}
private class FilteredDynamicContext extends DynamicContext {
// 委托目标
private DynamicContext delegate;
private boolean prefixApplied;
private boolean suffixApplied;
private StringBuilder sqlBuffer;
public FilteredDynamicContext(DynamicContext delegate) {
super(configuration, null);
this.delegate = delegate;
this.prefixApplied = false;
this.suffixApplied = false;
this.sqlBuffer = new StringBuilder();
}
public void applyAll() {
sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
// 存在内容才处理
if (trimmedUppercaseSql.length() > 0) {
// 处理前缀
applyPrefix(sqlBuffer, trimmedUppercaseSql);
// 处理后缀
applySuffix(sqlBuffer, trimmedUppercaseSql);
}
// 处理trim标签的内部内容
delegate.appendSql(sqlBuffer.toString());
}
@Override
public Map<String, Object> getBindings() {
return delegate.getBindings();
}
@Override
public void bind(String name, Object value) {
delegate.bind(name, value);
}
@Override
public int getUniqueNumber() {
return delegate.getUniqueNumber();
}
@Override
public void appendSql(String sql) {
sqlBuffer.append(sql);
}
@Override
public String getSql() {
return delegate.getSql();
}
private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
if (!prefixApplied) {
prefixApplied = true;
if (prefixesToOverride != null) {
// 依次匹配定义的各个前缀并去除
for (String toRemove : prefixesToOverride) {
if (trimmedUppercaseSql.startsWith(toRemove)) {
// 移除前缀
sql.delete(0, toRemove.trim().length());
break;
}
}
}
if (prefix != null) {
sql.insert(0, " ");
sql.insert(0, prefix);
}
}
}
private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {
if (!suffixApplied) {
suffixApplied = true;
if (suffixesToOverride != null) {
for (String toRemove : suffixesToOverride) {
if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) {
int start = sql.length() - toRemove.trim().length();
int end = sql.length();
sql.delete(start, end);
break;
}
}
}
if (suffix != null) {
sql.append(" ");
sql.append(suffix);
}
}
}
}
}public class SetSqlNode extends TrimSqlNode {
private static final List<String> COMMA = Collections.singletonList(",");
public SetSqlNode(Configuration configuration,SqlNode contents) {
// 注意只会覆盖掉前缀,后缀是不会覆盖的,毕竟suffixesToOverride传的是null
super(configuration, contents, "SET", COMMA, null, COMMA);
}
}public class WhereSqlNode extends TrimSqlNode {
// 默认会去掉这些前缀
private static List<String> prefixList = Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");
public WhereSqlNode(Configuration configuration, SqlNode contents) {
// 注意只会覆盖掉前缀,后缀是不会覆盖的,毕竟suffixesToOverride传的是null
super(configuration, contents, "WHERE", prefixList, null, null);
}
}public class ChooseSqlNode implements SqlNode {
private final SqlNode defaultSqlNode;
private final List<SqlNode> ifSqlNodes;
public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) {
this.ifSqlNodes = ifSqlNodes;
this.defaultSqlNode = defaultSqlNode;
}
@Override
public boolean apply(DynamicContext context) {
for (SqlNode sqlNode : ifSqlNodes) {
if (sqlNode.apply(context)) {
return true;
}
}
if (defaultSqlNode != null) {
defaultSqlNode.apply(context);
return true;
}
return false;
}
}public class VarDeclSqlNode implements SqlNode {
private final String name;
private final String expression;
public VarDeclSqlNode(String name, String exp) {
this.name = name;
this.expression = exp;
}
@Override
public boolean apply(DynamicContext context) {
// 执行表达式的值
final Object value = OgnlCache.getValue(expression, context.getBindings());
// 将值绑定到上下文的参数映射中
context.bind(name, value);
return true;
}
}/*
* 这是一种抽象的特殊的SqlNode,用来封装其他多种具有实际意义的SqlNode。
*/
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;
}
}其中,SetSqlNode和WhereSqlNode是基于TrimSqlNode实现的,毕竟其逻辑就是在子节点开头设置和覆盖掉一些内容。
构建SQL
上面一开始就说到,SQL是通过MappedStatement来构建的。
public BoundSql getBoundSql(Object parameterObject) {
// 通过SqlSource来创建BoundSql对象,传入实际参数,处理动态SQL
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// 获取SQL中的#{}占位符对应的参数信息,被封装为ParameterMapping对象
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
/*
* 什么情况下为空? (如果XML内容中没有#{}占位符,就为empty,倒不会为null,因为有初始化)
* 这里parameterMap返回的ParameterMapping是空列表,具体参考上面Builder内部类的初始化操作。
*
* MyBatis已经不再推荐在sql标签上设置parameterMap属性,实际工作中也基本不会使用该属性
* 所以这里基本上可以理解为无效操作,可以忽略掉!
*/
if (parameterMappings == null || parameterMappings.isEmpty()) {
boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
}
// check for nested result maps in parameter mappings (issue #30)
/*
* 获取设置resultMap
* 为什么这里resultMap信息封装在ParameterMapping中的?
* 参考SqlSourceBuilder中的ParameterMappingTokenHandler内部类中的buildParameterMapping方法
*
* 另外可以忽略这里,暂时不知道什么场景会在#{}中指定resultMap。
*/
for (ParameterMapping pm : boundSql.getParameterMappings()) {
String rmId = pm.getResultMapId();
if (rmId != null) {
ResultMap rm = configuration.getResultMap(rmId);
if (rm != null) {
hasNestedResultMaps |= rm.hasNestedResultMaps();
}
}
}
return boundSql;
}这里第一行代码印证了前言所述,真正的SQL解析是由SqlSource来处理的。
/*
* 用于描述XML中的动态SQL,比如在SQL中用了变量
*/
public class DynamicSqlSource implements SqlSource {
private final Configuration configuration;
private final SqlNode rootSqlNode;
/*
* 在解析mapper时,在XMLScriptBuilder中的parseScriptNode方法中会调用该构造器。
*/
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
// 创建一个上下文对象,用来解析SQL
DynamicContext context = new DynamicContext(configuration, parameterObject);
// 调用各SqlNode来处理动态SQL的各个部分
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
/*
* 判断参数对象的类型,在上层MapperMethod中利用了参数解析器来将mapper方法的实参进行处理,
* 因为参数有各种情况,所以是以Object一路传下来的。
*
* 这里从实参来获取参数类型,而不是配置中的parameterType属性,是和RawSqlSource不同的地方。
* 原因是DynamicSqlSource每次运行时解析,而RawSqlSource是解析时运行一次解析后不再重复解析。
*/
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
/*
* 创建SqlSource对象,用于创建BoundSql对象
* 当前类就是一个SqlSource,为什么还要创建SqlSource?
* 别忘了,所有的SqlNode中都是没有处理#{}的,所以这里创建的SqlSource是在处理占位符#{}。
* 这里返回的SqlSource是StaticSqlSource类型的!
*/
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
// 正式创建BoundSql对象,再次强调,这里的sqlSource是StaticSqlSource
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// 将参数映射也设置到boundSql对象中
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
}/*
* 表示静态SQL,但并不是指不包含变量信息,可以包含#{},但不包含${}及其他动态SQL节点
*/
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);
// 如果没有指定parameterType,那么各参数的javaType会被设置为Object.class
Class<?> clazz = parameterType == null ? Object.class : parameterType;
/*
* 返回的是StaticSqlSource
* 这里是在构造方法中解析,整个运行过程中只需解析一次,而不像DynamicSqlSource一样每次运行时解析,这是两者的最大区别。
*/
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
}
private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
// 创建一个上下文用于各SqlNode执行
DynamicContext context = new DynamicContext(configuration, null);
rootSqlNode.apply(context);
return context.getSql();
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
return sqlSource.getBoundSql(parameterObject);
}
}两种SqlSource都是基于SqlSourceBuilder来进行解析的。但两者有本质区别,即RawSqlSource是在构造时进行解析,整个运行过程中只会执行一次解析;而DynamicSqlSource是每次运行时解析。因为这种运行机制,所以RawSqlSource会使用XML节点中配置的parameterType属性,而DynamicSqlSource是根据实参来获取类型。毕竟RawSqlSource被创建时还没实参,也不能根据参数来获取类型。
构建动态SQL
不管是哪种SqlSource,首先都会调用各种SqlNode的apply方法来将XML片段的SQL描述,转为纯文本的SQL。 上面已经给出了各种SqlNode和解析时用到的上下文DynamicContext的源码,所以这里不再赘述。 经过此阶段的处理后,DynamicContext的getSql方法返回的字符串中只可能包含#{}占位符,接下来会进行动态变量的处理。
这里将#{}引入的变量称为动态变量,而${}引入的变量称为静态变量。两者是有本质区别的,后者是在TextSqlNode中被处理的,是进行字符串的替换操作;而前者是转为PrepareStatement的参数占位符并进行参数设置。
处理SQL中的动态变量
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
// 创建参数映射处理器
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
/*
* 这里只有处理#{},那又在哪里处理${}呢? 答案就是在TextSqlNode中。
* 而且是这里之前就处理过了,originalSql就是各SqlNode处理之后的结果。
*/
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
/*
* 重点:
* 解析SQL语句,替换动态SQL中的占位符“#{}”,得到SQL的字符串形式;
* 既然这里都已经得到了SQL的字符串形式,为什么下面还需要封装为SqlSource,以及后续为什么还要转为BoundSql对象呢?
*/
String sql;
if (configuration.isShrinkWhitespacesInSql()) { // 是否移除SQL中的空格
sql = parser.parse(removeExtraWhitespaces(originalSql));
} else {
sql = parser.parse(originalSql);
}
// 封装解析结果
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
public static String removeExtraWhitespaces(String original) {
// 将SQL字符串根据空格拆分成多个有效内容部分
StringTokenizer tokenizer = new StringTokenizer(original);
StringBuilder builder = new StringBuilder();
boolean hasMoreTokens = tokenizer.hasMoreTokens();
while (hasMoreTokens) {
builder.append(tokenizer.nextToken());
hasMoreTokens = tokenizer.hasMoreTokens();
if (hasMoreTokens) {
// 每部分用单个空格拼接
builder.append(' ');
}
}
return builder.toString();
}
private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
private final List<ParameterMapping> parameterMappings = new ArrayList<>();
private final Class<?> parameterType;
private final MetaObject metaParameters;
public ParameterMappingTokenHandler(Configuration configuration, Class<?> parameterType, Map<String, Object> additionalParameters) {
super(configuration);
this.parameterType = parameterType;
this.metaParameters = configuration.newMetaObject(additionalParameters);
}
public List<ParameterMapping> getParameterMappings() {
return parameterMappings;
}
@Override
public String handleToken(String content) {
// 解析占位符内容
parameterMappings.add(buildParameterMapping(content));
// jdbc的参数占位符
return "?";
}
private ParameterMapping buildParameterMapping(String content) {
Map<String, String> propertiesMap = parseParameterMapping(content);
// 获取参数名称
String property = propertiesMap.get("property");
/*
* 判断参数的类型
* 这里会根据实际参数类型来判断javaType,所以编码时没有指定javaType,MyBatis也能正确判断参数类型。
*/
Class<?> propertyType;
// 引用<bind/>标签定义的参数,参考DynamicSqlSource中调用SqlSourceBuilder的parse方法的第三个参数
if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params
propertyType = metaParameters.getGetterType(property);
} else if (typeHandlerRegistry.hasTypeHandler(parameterType)) { // 如果全局注册的类型处理器支持当前参数类型,最常见的情况
propertyType = parameterType;
} else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
propertyType = java.sql.ResultSet.class;
} else if (property == null || Map.class.isAssignableFrom(parameterType)) { // 如果参数是null或者参数类型被封装为Map(具体要参考上层MapperMethod参数处理器中的逻辑)
propertyType = Object.class;
} else {
MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
// 引用对象中的字段,如常使用的透传dto存controller层到dao层
if (metaClass.hasGetter(property)) { // 如果有getter方法,则通过getter的返回值类型来判断参数类型
propertyType = metaClass.getGetterType(property);
} else { // 直接引用实参
propertyType = Object.class;
}
}
/*
* 这里传入了propertyType,会被设置为javaType,如果没有显示指定,那么就是使用的这个自动推断出来的javaType。
* 如果显式指定了,那么下面解析时会覆盖这里设置的。
*/
ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
Class<?> javaType = propertyType;
String typeHandlerAlias = null;
/*
* 遍历占位符中的每部分
* 常见和常用的就是javaType,jdbcType以及typeHandler
* 另外mode主要用于存储过程,numericScale用来调整小数的精度;
* resultMap不清楚什么场景会被用到!
*/
for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
if ("javaType".equals(name)) {
javaType = resolveClass(value);
builder.javaType(javaType);
} else if ("jdbcType".equals(name)) {
builder.jdbcType(resolveJdbcType(value));
} else if ("mode".equals(name)) {
builder.mode(resolveParameterMode(value));
} else if ("numericScale".equals(name)) {
builder.numericScale(Integer.valueOf(value));
} else if ("resultMap".equals(name)) {
builder.resultMapId(value);
} else if ("typeHandler".equals(name)) {
typeHandlerAlias = value;
} else if ("jdbcTypeName".equals(name)) {
builder.jdbcTypeName(value);
} else if ("property".equals(name)) {
// Do Nothing
} else if ("expression".equals(name)) {
throw new BuilderException("Expression based parameters are not supported yet");
} else {
throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}. Valid properties are " + PARAMETER_PROPERTIES);
}
}
if (typeHandlerAlias != null) {
/*
* 解析并设置指定的typeHandler,
* 如果没有指定,那么在下面的build过程中会尝试查找默认的typeHandler,一般常见的类型都是默认支持的,具体参考TypeHandlerRegistry。
* 如果最后没有找到typeHandler,则会抛出异常。
*/
builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
}
return builder.build();
}
private Map<String, String> parseParameterMapping(String content) {
try {
return new ParameterExpression(content);
} catch (BuilderException ex) {
throw ex;
} catch (Exception ex) {
throw new BuilderException("Parsing error was found in mapping #{" + content + "}. Check syntax #{property|(expression), var1=value1, var2=value2, ...} ", ex);
}
}
}
}public class GenericTokenParser {
private final String openToken;
private final String closeToken;
// 处理openToken和closeToken中间内容的处理器
private final TokenHandler handler;
public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
this.openToken = openToken;
this.closeToken = closeToken;
this.handler = handler;
}
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,便于处理下一个占位符
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中
expression.append(src, offset, end - offset);
break;
}
}
/*
* 如果没有找到结束占位符,则将剩下的内容直接全部拷贝,并退出循环,结束寻找
* 所以说,如果只定义了开始占位符,没有定义结束占位符,并不会在MyBatis层面报错,而是让SQL本身就变成有错误的SQL。
*/
if (end == -1) {
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
/*
* 取得参数对应的值,并拼接在sql中,占位符中可能并不是简单的参数名(比如还会设置jdbcType,typeHandler等),所以需要handler来处理
*/
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
// 从offset处开始往后找下一个开始占位符
start = text.indexOf(openToken, offset);
} while (start > -1);
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
}在该类的parse方法中,会处理#{},用?来替换参数,并构建参数的ParameterMapping对象。后续在Executor中执行设置SQL参数时会被用到。 一般在#{}中除了指定参数名称外,还会指定其他属性,这些属性的处理也是在这里进行的,会被一起封装在ParameterMapping中。一般常用的是javaType,jdbcType和typeHandler。 值得一提的是,typeHandler是必须的,否则报错找不到类型处理器。但开发中一般不会显示指定这三个参数(除了真实需要),那为什么没有报错?原因是MyBatis中针对常用数据类型预定义好了一些typeHandler。 另外如果没有指定javaType,那么MyBatis会根据实参类型来推断,即buildParameterMapping方法中根据多种情况来推断了propertyType,默认会以该类型作为javaType;当然如果显式指定了,会覆盖该默认机制。 如果是RawSqlSource,是在启动解析时构建的SQL,此时没有实参,那么是怎么推断的propertyType呢?分为两种情况:
- 如果指定了parameterType,而且该类型注册有typeHandler,那么则推断为指定类型;否则使用兜底的Object.class;
- 如果没有指定parameterType,那么RawSqlSource会将parameterType指定为Object.class;
处理typeHandler
解析显式配置的typeHandler
如果显示指定了typeHandler属性,那么在这一步会加载和解析。
protected TypeHandler<?> resolveTypeHandler(Class<?> javaType, String typeHandlerAlias) {
if (typeHandlerAlias == null) {
return null;
}
// 获取指定的类型处理器的Class
Class<?> type = resolveClass(typeHandlerAlias);
if (type != null && !TypeHandler.class.isAssignableFrom(type)) {
throw new BuilderException("Type " + type.getName() + " is not a valid TypeHandler because it does not implement TypeHandler interface");
}
@SuppressWarnings("unchecked") // already verified it is a TypeHandler
Class<? extends TypeHandler<?>> typeHandlerType = (Class<? extends TypeHandler<?>>) type;
return resolveTypeHandler(javaType, typeHandlerType);
}
protected TypeHandler<?> resolveTypeHandler(Class<?> javaType, Class<? extends TypeHandler<?>> typeHandlerType) {
if (typeHandlerType == null) {
return null;
}
// javaType ignored for injected handlers see issue #746 for full detail
// 判断该类型的类型处理器是否已经被处理过了
TypeHandler<?> handler = typeHandlerRegistry.getMappingTypeHandler(typeHandlerType);
if (handler == null) {
// not in registry, create a new one
// 还没注册过,则创建并注册一个新的类型处理器实例
handler = typeHandlerRegistry.getInstance(javaType, typeHandlerType);
}
return handler;
}
// jdbcType和typeHandler的映射
private final Map<JdbcType, TypeHandler<?>> jdbcTypeHandlerMap = new EnumMap<>(JdbcType.class);
// 两级映射,一级key是javaType,二级key是jdbcType
private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>();
private final TypeHandler<Object> unknownTypeHandler;
// key是typeHandler的类型,value是typeHandler实例
private final Map<Class<?>, TypeHandler<?>> allTypeHandlersMap = new HashMap<>();
private static final Map<JdbcType, TypeHandler<?>> NULL_TYPE_HANDLER_MAP = Collections.emptyMap();
private Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class;
public TypeHandler<?> getMappingTypeHandler(Class<? extends TypeHandler<?>> handlerType) {
return allTypeHandlersMap.get(handlerType);
}@SuppressWarnings("unchecked")
public <T> TypeHandler<T> getInstance(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
if (javaTypeClass != null) {
try {
Constructor<?> c = typeHandlerClass.getConstructor(Class.class);
return (TypeHandler<T>) c.newInstance(javaTypeClass);
} catch (NoSuchMethodException ignored) {
// ignored
} catch (Exception e) {
throw new TypeException("Failed invoking constructor for handler " + typeHandlerClass, e);
}
}
try {
Constructor<?> c = typeHandlerClass.getConstructor();
return (TypeHandler<T>) c.newInstance();
} catch (Exception e) {
throw new TypeException("Unable to find a usable constructor for " + typeHandlerClass, e);
}
}TypeHandlerRegistry是管理typeHandler的工具类,这里也是通过工具类来创建typeHandler实例,稍后会注册;如果是有注册过的,那么则不会重复创建;否则每次解析时新建对象,因为这种情况不会在新建后进行注册。
获取默认的typeHandler
如果没有显示指定typeHandler,那么会尝试获取默认的typeHandler,如果找不到则会报错。
public ParameterMapping build() {
// 处理typeHandler
resolveTypeHandler();
validate();
return parameterMapping;
}private void resolveTypeHandler() {
// 如果没有设置typeHandler,这里javaType应该是能保证不为null的
if (parameterMapping.typeHandler == null && parameterMapping.javaType != null) {
Configuration configuration = parameterMapping.configuration;
// 获取registry对象
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
// 根据javaType和jdbcType来查找类型处理器
parameterMapping.typeHandler = typeHandlerRegistry.getTypeHandler(parameterMapping.javaType, parameterMapping.jdbcType);
}
}private void validate() {
if (ResultSet.class.equals(parameterMapping.javaType)) {
if (parameterMapping.resultMapId == null) {
throw new IllegalStateException("Missing resultmap in property '"
+ parameterMapping.property + "'. "
+ "Parameters of type java.sql.ResultSet require a resultmap.");
}
} else {
/*
* 不能找到或没设置参数处理器,抛出异常
* 一般常见大部分类型都有默认的类型处理器,但是自定义的类型则需要设置自定义的参数处理器
*/
if (parameterMapping.typeHandler == null) {
throw new IllegalStateException("Type handler was null on parameter mapping for property '"
+ parameterMapping.property + "'. It was either not specified and/or could not be found for the javaType ("
+ parameterMapping.javaType.getName() + ") : jdbcType (" + parameterMapping.jdbcType + ") combination.");
}
}
}
// jdbcType和typeHandler的映射
private final Map<JdbcType, TypeHandler<?>> jdbcTypeHandlerMap = new EnumMap<>(JdbcType.class);
// 两级映射,一级key是javaType,二级key是jdbcType
private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>();
private final TypeHandler<Object> unknownTypeHandler;
// key是typeHandler的类型,value是typeHandler实例
private final Map<Class<?>, TypeHandler<?>> allTypeHandlersMap = new HashMap<>();
private static final Map<JdbcType, TypeHandler<?>> NULL_TYPE_HANDLER_MAP = Collections.emptyMap();
private Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class;
public <T> TypeHandler<T> getTypeHandler(Class<T> type) {
return getTypeHandler((Type) type, null);
}
public <T> TypeHandler<T> getTypeHandler(TypeReference<T> javaTypeReference) {
return getTypeHandler(javaTypeReference, null);
}
public TypeHandler<?> getTypeHandler(JdbcType jdbcType) {
return jdbcTypeHandlerMap.get(jdbcType);
}
public <T> TypeHandler<T> getTypeHandler(Class<T> type, JdbcType jdbcType) {
return getTypeHandler((Type) type, jdbcType);
}
public <T> TypeHandler<T> getTypeHandler(TypeReference<T> javaTypeReference, JdbcType jdbcType) {
return getTypeHandler(javaTypeReference.getRawType(), jdbcType);
}
@SuppressWarnings("unchecked")
private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
if (ParamMap.class.equals(type)) {
return null;
}
// 先根据JavaType找到映射关系
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type);
TypeHandler<?> handler = null;
// 再根据jdbcType查找typeHandler
if (jdbcHandlerMap != null) {
handler = jdbcHandlerMap.get(jdbcType);
if (handler == null) {
// 没找到对应jdbcType的typeHandler,则查找这一javaType默认的typeHandler
handler = jdbcHandlerMap.get(null);
}
if (handler == null) {
// 还是没找到,如果这一javaType只注册了一个typeHandler,则返回它,否则还是返回null。
// #591
handler = pickSoleHandler(jdbcHandlerMap);
}
}
// type drives generics here
return (TypeHandler<T>) handler;
} private Map<JdbcType, TypeHandler<?>> getJdbcHandlerMap(Type type) {
// 根据java类型来存两级映射中获取jdbcType和typeHandler的映射
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = typeHandlerMap.get(type);
if (jdbcHandlerMap != null) {
return NULL_TYPE_HANDLER_MAP.equals(jdbcHandlerMap) ? null : jdbcHandlerMap;
}
if (type instanceof Class) {
Class<?> clazz = (Class<?>) type;
if (Enum.class.isAssignableFrom(clazz)) {
Class<?> enumClass = clazz.isAnonymousClass() ? clazz.getSuperclass() : clazz;
jdbcHandlerMap = getJdbcHandlerMapForEnumInterfaces(enumClass, enumClass);
if (jdbcHandlerMap == null) {
register(enumClass, getInstance(enumClass, defaultEnumTypeHandler));
return typeHandlerMap.get(enumClass);
}
} else {
jdbcHandlerMap = getJdbcHandlerMapForSuperclass(clazz);
}
}
typeHandlerMap.put(type, jdbcHandlerMap == null ? NULL_TYPE_HANDLER_MAP : jdbcHandlerMap);
return jdbcHandlerMap;
} private TypeHandler<?> pickSoleHandler(Map<JdbcType, TypeHandler<?>> jdbcHandlerMap) {
TypeHandler<?> soleHandler = null;
for (TypeHandler<?> handler : jdbcHandlerMap.values()) {
if (soleHandler == null) { // 如果只存在一个typeHandler,则返回
soleHandler = handler;
} else if (!handler.getClass().equals(soleHandler.getClass())) { // 如果存在多个,则返回null
// More than one type handlers registered.
return null;
}
}
return soleHandler;
}自动注册的typeHandler
MyBatis会默认注册一些typeHandler,极大地方便了开发。
/**
* The default constructor.
*/
public TypeHandlerRegistry() {
this(new Configuration());
}
/**
* The constructor that pass the MyBatis configuration.
*
* @param configuration a MyBatis configuration
* @since 3.5.4
*/
public TypeHandlerRegistry(Configuration configuration) {
this.unknownTypeHandler = new UnknownTypeHandler(configuration);
register(Boolean.class, new BooleanTypeHandler());
register(boolean.class, new BooleanTypeHandler());
register(JdbcType.BOOLEAN, new BooleanTypeHandler());
register(JdbcType.BIT, new BooleanTypeHandler());
register(Byte.class, new ByteTypeHandler());
register(byte.class, new ByteTypeHandler());
register(JdbcType.TINYINT, new ByteTypeHandler());
register(Short.class, new ShortTypeHandler());
register(short.class, new ShortTypeHandler());
register(JdbcType.SMALLINT, new ShortTypeHandler());
register(Integer.class, new IntegerTypeHandler());
register(int.class, new IntegerTypeHandler());
register(JdbcType.INTEGER, new IntegerTypeHandler());
register(Long.class, new LongTypeHandler());
register(long.class, new LongTypeHandler());
register(Float.class, new FloatTypeHandler());
register(float.class, new FloatTypeHandler());
register(JdbcType.FLOAT, new FloatTypeHandler());
register(Double.class, new DoubleTypeHandler());
register(double.class, new DoubleTypeHandler());
register(JdbcType.DOUBLE, new DoubleTypeHandler());
register(Reader.class, new ClobReaderTypeHandler());
register(String.class, new StringTypeHandler());
register(String.class, JdbcType.CHAR, new StringTypeHandler());
register(String.class, JdbcType.CLOB, new ClobTypeHandler());
register(String.class, JdbcType.VARCHAR, new StringTypeHandler());
register(String.class, JdbcType.LONGVARCHAR, new StringTypeHandler());
register(String.class, JdbcType.NVARCHAR, new NStringTypeHandler());
register(String.class, JdbcType.NCHAR, new NStringTypeHandler());
register(String.class, JdbcType.NCLOB, new NClobTypeHandler());
register(JdbcType.CHAR, new StringTypeHandler());
register(JdbcType.VARCHAR, new StringTypeHandler());
register(JdbcType.CLOB, new ClobTypeHandler());
register(JdbcType.LONGVARCHAR, new StringTypeHandler());
register(JdbcType.NVARCHAR, new NStringTypeHandler());
register(JdbcType.NCHAR, new NStringTypeHandler());
register(JdbcType.NCLOB, new NClobTypeHandler());
register(Object.class, JdbcType.ARRAY, new ArrayTypeHandler());
register(JdbcType.ARRAY, new ArrayTypeHandler());
register(BigInteger.class, new BigIntegerTypeHandler());
register(JdbcType.BIGINT, new LongTypeHandler());
register(BigDecimal.class, new BigDecimalTypeHandler());
register(JdbcType.REAL, new BigDecimalTypeHandler());
register(JdbcType.DECIMAL, new BigDecimalTypeHandler());
register(JdbcType.NUMERIC, new BigDecimalTypeHandler());
register(InputStream.class, new BlobInputStreamTypeHandler());
register(Byte[].class, new ByteObjectArrayTypeHandler());
register(Byte[].class, JdbcType.BLOB, new BlobByteObjectArrayTypeHandler());
register(Byte[].class, JdbcType.LONGVARBINARY, new BlobByteObjectArrayTypeHandler());
register(byte[].class, new ByteArrayTypeHandler());
register(byte[].class, JdbcType.BLOB, new BlobTypeHandler());
register(byte[].class, JdbcType.LONGVARBINARY, new BlobTypeHandler());
register(JdbcType.LONGVARBINARY, new BlobTypeHandler());
register(JdbcType.BLOB, new BlobTypeHandler());
register(Object.class, unknownTypeHandler);
register(Object.class, JdbcType.OTHER, unknownTypeHandler);
register(JdbcType.OTHER, unknownTypeHandler);
register(Date.class, new DateTypeHandler());
register(Date.class, JdbcType.DATE, new DateOnlyTypeHandler());
register(Date.class, JdbcType.TIME, new TimeOnlyTypeHandler());
register(JdbcType.TIMESTAMP, new DateTypeHandler());
register(JdbcType.DATE, new DateOnlyTypeHandler());
register(JdbcType.TIME, new TimeOnlyTypeHandler());
register(java.sql.Date.class, new SqlDateTypeHandler());
register(java.sql.Time.class, new SqlTimeTypeHandler());
register(java.sql.Timestamp.class, new SqlTimestampTypeHandler());
register(String.class, JdbcType.SQLXML, new SqlxmlTypeHandler());
register(Instant.class, new InstantTypeHandler());
register(LocalDateTime.class, new LocalDateTimeTypeHandler());
register(LocalDate.class, new LocalDateTypeHandler());
register(LocalTime.class, new LocalTimeTypeHandler());
register(OffsetDateTime.class, new OffsetDateTimeTypeHandler());
register(OffsetTime.class, new OffsetTimeTypeHandler());
register(ZonedDateTime.class, new ZonedDateTimeTypeHandler());
register(Month.class, new MonthTypeHandler());
register(Year.class, new YearTypeHandler());
register(YearMonth.class, new YearMonthTypeHandler());
register(JapaneseDate.class, new JapaneseDateTypeHandler());
// issue #273
register(Character.class, new CharacterTypeHandler());
register(char.class, new CharacterTypeHandler());
}封装SQL构建结果
SqlSourceBuilder的parse方法会把处理结果封装为StaticSqlSource,然后会调用其getBoundSql对象,将解析的结果封装为BoundSql,供后续Executor使用。
/*
* 用于描述ProviderSqlSource、DynamicSqlSource及RawSqlSource解析后得到的静态SQL资源。
*/
public class StaticSqlSource implements SqlSource {
private final String sql;
private final List<ParameterMapping> parameterMappings;
private final Configuration configuration;
public StaticSqlSource(Configuration configuration, String sql) {
this(configuration, sql, null);
}
public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {
this.sql = sql;
this.parameterMappings = parameterMappings;
this.configuration = configuration;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
// 不管哪种情况,executor得到的BoundSql对象都是这里创建的。
return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}
}设置SQL参数
最后在Executor中执行时,会通过typeHandler替换SQL中的参数占位符。
@Override
public void parameterize(Statement statement) throws SQLException {
// 通过参数处理器来设置参数
parameterHandler.setParameters((PreparedStatement) statement);
}@Override
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
// 获取SQL中的参数占位符
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
/*
* 这个mode很少用,一般与存储过程有关,当然没用时getMode返回null,所以这里也是满足条件的
*/
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
// 获取TypeHandler
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
// 通过typeHandler来设置参数
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException | SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}这里在遍历前面解析到的ParameterMapping,并通过其内部封装的typeHandler来设置参数。
总结
本文全面分析了MyBatis中SQL语句的处理过程和执行原理。从SqlSessionFactory的构建,到触发mapper文件的解析;从SqlSource和MappedStatement的构建,到将XML的各标签和文本解析成各种SqlNode;从#{}占位符到ParameterMapping的转变。 在容易上手开发的背后,是MyBatis为我们铺垫了太多太多。