本文用到的Mybatis源码版本为3.5.6。
在使用 MyBatis 时,第一步要做的事情一般是根据配置文件构建 SqlSessionFactory对象。相关代码大致如下:
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory =
new SqlSessionFactoryBuilder().build(inputStream);
在上面代码中,首先会使用 MyBatis 提供的工具类 Resources 加载配置文件,得到一个输入流。然后再通过 SqlSessionFactoryBuilder 对象的 build 方法构建 SqlSessionFactory对象。这里的 build 方法是我们分析配置文件解析过程的入口方法,所以先看一下SqlSessionFactoryBuilder(位于org.apache.ibatis.session)类中的build方法
:
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 创建配置文件解析器
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 调用 parse 方法解析配置文件,生成 Configuration 对象
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
public SqlSessionFactory build(Configuration config) {
// 创建 DefaultSqlSessionFactory
return new DefaultSqlSessionFactory(config);
}
从上面的代码中,我们大致可以猜出 MyBatis 配置文件是通过 XMLConfigBuilder 进行解析的。继续看一下XMLConfigBuilder(位于org.apache.ibatis.builder.xml) 的 parse 方法
:
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// 解析配置
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
上述代码中有个xpath 表达式—— /configuration。这个表达式代表的是 MyBatis 配置文件的
节点,这里通过 xpath 选中这个节点,并传递给 parseConfiguration 方法
:
private void parseConfiguration(XNode root) {
try {
// issue #117 read properties first
// 解析 properties 配置
propertiesElement(root.evalNode("properties"));
// 解析 settings 配置,并将其转换为 Properties 对象
Properties settings = settingsAsProperties(root.evalNode("settings"));
// 加载 vfs
loadCustomVfs(settings);
loadCustomLogImpl(settings);
// 解析 typeAliases 配置
typeAliasesElement(root.evalNode("typeAliases"));
// 解析 plugins 配置
pluginElement(root.evalNode("plugins"));
// 解析 objectFactory 配置
objectFactoryElement(root.evalNode("objectFactory"));
// 解析 objectWrapperFactory 配置
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 解析 reflectorFactory 配置
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// settings 中的信息设置到 Configuration 对象中
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
// 解析 environments 配置
environmentsElement(root.evalNode("environments"));
// 解析 databaseIdProvider,获取并设置 databaseId 到 Configuration 对象
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 解析 typeHandlers 配置
typeHandlerElement(root.evalNode("typeHandlers"));
// 解析 mappers 配置
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
至此,一个完整的配置解析过程就呈现出来了,每个节点的的解析逻辑均封装在了相应的方法中。
节点的解析工作由XMLConfigBuilder类中的 propertiesElement 这个方法完成的,在分析方法的源码前,我们先来看一下
节点是如何配置的:
<properties resource="jdbc.properties">
<property name="jdbc.username" value="coolblog"/>
<property name="hello" value="world"/>
properties>
上面配置包含了一个 resource 属性,和两个子节点。接下面,我们参照上面的配置,来分析 propertiesElement方法的逻辑:
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
// 解析 propertis 的子节点,并将这些节点内容转换为属性对象 Properties
Properties defaults = context.getChildrenAsProperties();
// 获取 propertis 节点中的 resource 和 url 属性值
String resource = context.getStringAttribute("resource");
String url = context.getStringAttribute("url");
if (resource != null && url != null) {
throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other.");
}
if (resource != null) {
// 从文件系统中加载并解析属性文件
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
// 通过 url 加载并解析属性文件
defaults.putAll(Resources.getUrlAsProperties(url));
}
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
parser.setVariables(defaults);
// 将属性值设置到 configuration 中
configuration.setVariables(defaults);
}
}
上面的代码调用了XNode(位于org.apache.ibatis.parsing)中的getChildrenAsProperties和getChildren方法:
public Properties getChildrenAsProperties() {
Properties properties = new Properties();
// 获取并遍历子节点
for (XNode child : getChildren()) {
// 获取 property 节点的 name 和 value 属性
String name = child.getStringAttribute("name");
String value = child.getStringAttribute("value");
if (name != null && value != null) {
// 设置属性到属性对象中
properties.setProperty(name, value);
}
}
return properties;
}
public List<XNode> getChildren() {
List<XNode> children = new ArrayList<>();
// 获取子节点列表
NodeList nodeList = node.getChildNodes();
if (nodeList != null) {
for (int i = 0, n = nodeList.getLength(); i < n; i++) {
Node node = nodeList.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
// 将节点对象封装到 XNode 中,并将 XNode 对象放入 children 列表中
children.add(new XNode(xpathParser, node, variables));
}
}
}
return children;
}
节点解析过程主要包含三个步骤,一是解析
节点的子节点,并将解析结果设置到 Properties 对象中。二是从文件系统或通过网络读取属性配置,这取决于
节点的 resource 和 url 是否为空。第二步对应的代码比较简单,就不分析了。最后一步则是将包含属性信息的 Properties 对象设置到XPathParser 和 Configuration 中。
需要注意的是,propertiesElement 方法是先解析
节点的子节点内容,然后再从文件系统或者网络读取属性配置,并将所有的属性及属性值都放入到 defaults 属性对象中。这会导致同名属性覆盖的问题,也就是从文件系统,或者网络上读取到的属性和属性值会覆盖掉
子节点中同名的属性和及值。
相关配置是 MyBatis 中非常重要的配置,这些配置用于调整 MyBatis 运行时的行为。settings 配置繁多,在对这些配置不熟悉的情况下,保持默认配置即可。先来看一个比较简单的配置:
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="autoMappingBehavior" value="PARTIAL"/>
settings>
最开始是XMLConfigBuilder中的settingsAsProperties方法:
private Properties settingsAsProperties(XNode context) {
if (context == null) {
return new Properties();
}
// 获取 settings 子节点中的内容
Properties props = context.getChildrenAsProperties();
// Check that all settings are known to the configuration class
// 创建 Configuration 类的“元信息”对象
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
for (Object key : props.keySet()) {
// 检测 Configuration 中是否存在相关属性,不存在则抛出异常
if (!metaConfig.hasSetter(String.valueOf(key))) {
throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive).");
}
}
return props;
}
在上面的代码中出现了一个陌生的类 MetaClass,这个类是用来做什么的呢?答案是用来解析目标类的一些元信息,比如类的成员变量,getter/setter 方法等。简单总结一下上面代码的逻辑:
- 解析 settings 子节点的内容,并将解析结果转成 Properties 对象
- 为 Configuration 创建元信息对象
- 通过 MetaClass 检测 Configuration 中是否存在某个属性的 setter 方法,不存在则抛异常
- 若通过 MetaClass 的检测,则返回 Properties 对象,方法逻辑结束
我们需要重点关注一下第 2 步和第 3 步的流程。
元信息类 MetaClass(位于org.apache.ibatis.reflection) 的构造方法为私有类型,所以不能直接创建,必须使用其提供的forClass 方法进行创建:
public class MetaClass {
private final ReflectorFactory reflectorFactory;
private final Reflector reflector;
private MetaClass(Class<?> type, ReflectorFactory reflectorFactory) {
this.reflectorFactory = reflectorFactory;
this.reflector = reflectorFactory.findForClass(type);
}
public static MetaClass forClass(Class<?> type, ReflectorFactory reflectorFactory) {
return new MetaClass(type, reflectorFactory);
}
// 省略其他方法
}
上面代码出现了两个新的类ReflectorFactory 和 Reflector,MetaClass 通过引入这些新类帮助它完成功能。关于这些类,可以先看下hasSetter方法:
public boolean hasSetter(String name) {
// 属性分词器,用于解析属性名
PropertyTokenizer prop = new PropertyTokenizer(name);
// hasNext 返回 true,则表明 name 是一个复合属性
if (prop.hasNext()) {
if (reflector.hasSetter(prop.getName())) {
// 为属性创建创建 MetaClass
MetaClass metaProp = metaClassForProperty(prop.getName());
// 再次调用 hasSetter
return metaProp.hasSetter(prop.getChildren());
} else {
return false;
}
} else {
// 调用 reflector 的 hasSetter 方法
return reflector.hasSetter(prop.getName());
}
}
可以看出 MetaClass 中的 hasSetter 方法最终调用了 Reflector 的hasSetter 方法。下面来简单介绍一下上面代码中出现的几个类:
DefaultReflectorFactory(位于org.apache.ibatis.reflection) 用于创建 Reflector,同时兼有缓存的功能:
public class DefaultReflectorFactory implements ReflectorFactory {
private boolean classCacheEnabled = true;
/** 目标类和反射器映射缓存 */
private final ConcurrentMap<Class<?>, Reflector> reflectorMap = new ConcurrentHashMap<>();
// 省略部分代码
@Override
public Reflector findForClass(Class<?> type) {
if (classCacheEnabled) {
// synchronized (type) removed see issue #461
//如果type对应的Reflector不存在,就创建一个新的 Reflector 实例
return reflectorMap.computeIfAbsent(type, Reflector::new);
} else {
// 创建一个新的 Reflector 实例
return new Reflector(type);
}
}
}
Reflector(位于org.apache.ibatis.reflection) 这个类的用途主要是是通过反射获取目标类的 getter 方法及其返回值类型,setter 方法及其参数值类型等元信息。并将获取到的元信息缓存到相应的集合中,供后续使用。下面分析该类的部分逻辑:
- Reflector 构造方法及成员变量分析
- getter 方法解析过程
- setter 方法解析过程
public class Reflector {
private final Class<?> type;
private final String[] readablePropertyNames;
private final String[] writablePropertyNames;
private final Map<String, Invoker> setMethods = new HashMap<>();
private final Map<String, Invoker> getMethods = new HashMap<>();
private final Map<String, Class<?>> setTypes = new HashMap<>();
private final Map<String, Class<?>> getTypes = new HashMap<>();
private Constructor<?> defaultConstructor;
private Map<String, String> caseInsensitivePropertyMap = new HashMap<>();
public Reflector(Class<?> clazz) {
type = clazz;
// 解析目标类的默认构造方法,并赋值给 defaultConstructor 变量
addDefaultConstructor(clazz);
// 解析 getter 方法,并将解析结果放入 getMethods 中
addGetMethods(clazz);
// 解析 setter 方法,并将解析结果放入 setMethods 中
addSetMethods(clazz);
// 解析属性字段,并将解析结果添加到 setMethods 或 getMethods 中
addFields(clazz);
// 从 getMethods 映射中获取可读属性名数组
readablePropertyNames = getMethods.keySet().toArray(new String[0]);
// 从 setMethods 映射中获取可写属性名数组
writablePropertyNames = setMethods.keySet().toArray(new String[0]);
// 将所有属性名的大写形式作为键,属性名作为值,存入到 caseInsensitivePropertyMap 中
for (String propName : readablePropertyNames) {
caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
}
for (String propName : writablePropertyNames) {
caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
}
}
// 省略其他方法
}
Reflector 部分成员变量的用途:
变量名 | 用途 |
---|---|
readablePropertyNames | 可读属性名称数组,用于保存 getter 方法对应的属性名称 |
writeablePropertyNames | 可写属性名称数组,用于保存 setter 方法对应的属性名称 |
setMethods | 用于保存属性名称到 Invoke 的映射。setter 方法会被封装到 MethodInvoker 对象中 |
getMethods | 用于保存属性名称到 Invoke 的映射。同上,getter 方法也会被封装到 MethodInvoker 对象中 |
setTypes | 用于保存 setter 对应的属性名与参数类型的映射 |
getTypes | 用于保存 getter 对应的属性名与返回值类型的映射 |
caseInsensitivePropertyMap | 用于保存大写属性名与属性名之间的映射,比如 |
private void addGetMethods(Class<?> clazz) {
Map<String, List<Method>> conflictingGetters = new HashMap<>();
// 获取当前类,接口,以及父类中的方法
Method[] methods = getClassMethods(clazz);
// getter 方法不应该有参数,若存在参数,则忽略当前方法
Arrays.stream(methods).filter(m -> m.getParameterTypes().length == 0 && PropertyNamer.isGetter(m.getName()))
// 将 getXXX 或 isXXX 等方法名转成相应的属性,比如 getName -> name
//addMethodConflict方法用于解决冲突。 将冲突的方法添加到 conflictingGetters 中。考虑这样一种情况:getTitle 和 isTitle 两个方法经过 methodToProperty 处理,均得到 name = title,这会导致冲突。对于冲突的方法,这里先统一起存起来,后续再解决冲突
.forEach(m -> addMethodConflict(conflictingGetters, PropertyNamer.methodToProperty(m.getName()), m));
// 解决 getter 冲突
resolveGetterConflicts(conflictingGetters);
}
addGetMethods 方法的逻辑:
- 获取当前类,接口,以及父类中的方法
- 遍历上一步获取的方法数组,并过滤出以 get 和 is 开头的方法
- 将方法名转换成相应的属性名
- 将属性名和方法对象添加到冲突集合中
- 解决冲突
第4步和第5步的具体逻辑:
/** 添加属性名和方法对象到冲突集合中 */
private void addMethodConflict(Map<String, List<Method>> conflictingMethods, String name, Method method) {
if (isValidPropertyName(name)) {
List<Method> list = conflictingMethods.computeIfAbsent(name, k -> new ArrayList<>());
list.add(method);
}
}
/** 解决冲突 */
private void resolveGetterConflicts(Map<String, List<Method>> conflictingGetters) {
for (Entry<String, List<Method>> entry : conflictingGetters.entrySet()) {
Method winner = null;
String propName = entry.getKey();
boolean isAmbiguous = false;
for (Method candidate : entry.getValue()) {
if (winner == null) {
winner = candidate;
continue;
}
// 获取返回值类型
Class<?> winnerType = winner.getReturnType();
Class<?> candidateType = candidate.getReturnType();
/*
* 两个方法的返回值类型一致,若两个方法返回值类型均为 boolean,
* 则选取 isXXX 方法为 winner。否则无法决定哪个方法更为合适,
* 只能抛出异常
*/
if (candidateType.equals(winnerType)) {
if (!boolean.class.equals(candidateType)) {
isAmbiguous = true;
break;
/*
* 如果方法返回值类型为 boolean,且方法名以 "is" 开头,
* 则认为候选方法 candidate 更为合适
*/
} else if (candidate.getName().startsWith("is")) {
winner = candidate;
}
/*
* winnerType 是 candidateType 的子类,类型上更为具体,
* 则认为当前的 winner 仍是合适的,无需做什么事情
*/
} else if (candidateType.isAssignableFrom(winnerType)) {
// OK getter type is descendant
/*
* candidateType 是 winnerType 的子类,此时认为 candidate 方法
* 更为合适,故将 winner 更新为 candidate
*/
} else if (winnerType.isAssignableFrom(candidateType)) {
winner = candidate;
} else {
isAmbiguous = true;
break;
}
}
// 将筛选出的方法添加到 getMethods 中,并将方法返回值添加到 getTypes 中
addGetMethod(propName, winner, isAmbiguous);
}
}
private void addGetMethod(String name, Method method, boolean isAmbiguous) {
MethodInvoker invoker = isAmbiguous
? new AmbiguousMethodInvoker(method, MessageFormat.format(
"Illegal overloaded getter method with ambiguous type for property ''{0}'' in class ''{1}''. This breaks the JavaBeans specification and can cause unpredictable results.",
name, method.getDeclaringClass().getName()))
: new MethodInvoker(method);
getMethods.put(name, invoker);
// 解析返回值类型
Type returnType = TypeParameterResolver.resolveReturnType(method, type);
// 将返回值类型由 Type 转为 Class,并将转换后的结果缓存到 setTypes 中
getTypes.put(name, typeToClass(returnType));
}
以上就是解除冲突的过程,只要记住解决冲突的规则即可理解上面代码的逻辑。相关规则如下:
- 冲突方法返回值类型具有继承关系,子类返回值对应方法被认为是更合适的选择
- 冲突方法的返回值类型相同,如果返回值类型为 boolean,那么以 is 开头的方法则是更合适的选择
- 冲突方法的返回值类型相同,但类型非 boolean,此时出现歧义,抛出异常
- 冲突方法的返回值类型不相关,无法确定哪个是更好的选择,此时直接抛异常分析完 getter 方法的解析过程,下面继续分析 setter 方法的解析过程。
private void addSetMethods(Class<?> clazz) {
Map<String, List<Method>> conflictingSetters = new HashMap<>();
// 获取当前类,接口,以及父类中的方法
Method[] methods = getClassMethods(clazz);
// 过滤出 setter 方法
// setter 方法发生冲突原因是:可能存在重载情况,比如:void setSex(int sex);void setSex(SexEnum sex);
Arrays.stream(methods).filter(m -> m.getParameterTypes().length == 1 && PropertyNamer.isSetter(m.getName()))
.forEach(m -> addMethodConflict(conflictingSetters, PropertyNamer.methodToProperty(m.getName()), m));
// 解决 setter 冲突
resolveSetterConflicts(conflictingSetters);
}
setter 方法之间出现冲突的原因。即方法存在重载,方法重载导致 methodToProperty 方法解析出的属性名完全一致。而 getter 方法之间出现冲突的原因是 getXXX 和 isXXX 对应的属性名一致。解决冲突的具体逻辑:
private void resolveSetterConflicts(Map<String, List<Method>> conflictingSetters) {
for (Entry<String, List<Method>> entry : conflictingSetters.entrySet()) {
String propName = entry.getKey();
List<Method> setters = entry.getValue();
/*
* 获取 getter 方法的返回值类型,由于 getter 方法不存在重载的情况,
* 所以可以用它的返回值类型反推哪个 setter 的更为合适
*/
Class<?> getterType = getTypes.get(propName);
boolean isGetterAmbiguous = getMethods.get(propName) instanceof AmbiguousMethodInvoker;
boolean isSetterAmbiguous = false;
Method match = null;
for (Method setter : setters) {
if (!isGetterAmbiguous && setter.getParameterTypes()[0].equals(getterType)) {
// should be the best match
// 参数类型和返回类型一致,则认为是最好的选择,并结束循环
match = setter;
break;
}
if (!isSetterAmbiguous) {
// 选择一个更为合适的方法
match = pickBetterSetter(match, setter, propName);
isSetterAmbiguous = match == null;
}
}
// 将筛选出的方法放入 setMethods 中,并将方法参数值添加到 setTypes 中
if (match != null) {
addSetMethod(propName, match);
}
}
}
/** 从两个 setter 方法中选择一个更为合适方法 */
private Method pickBetterSetter(Method setter1, Method setter2, String property) {
if (setter1 == null) {
return setter2;
}
Class<?> paramType1 = setter1.getParameterTypes()[0];
Class<?> paramType2 = setter2.getParameterTypes()[0];
// 如果参数 2 可赋值给参数 1,即参数 2 是参数 1 的子类,
// 则认为参数 2 对应的 setter 方法更为合适
if (paramType1.isAssignableFrom(paramType2)) {
return setter2;
} else if (paramType2.isAssignableFrom(paramType1)) {
return setter1;
}
MethodInvoker invoker = new AmbiguousMethodInvoker(setter1,
MessageFormat.format(
"Ambiguous setters defined for property ''{0}'' in class ''{1}'' with types ''{2}'' and ''{3}''.",
property, setter2.getDeclaringClass().getName(), paramType1.getName(), paramType2.getName()));
setMethods.put(property, invoker);
Type[] paramTypes = TypeParameterResolver.resolveParamTypes(setter1, type);
setTypes.put(property, typeToClass(paramTypes[0]));
return null;
}
private void addSetMethod(String name, Method method) {
MethodInvoker invoker = new MethodInvoker(method);
setMethods.put(name, invoker);
// 解析参数类型列表
Type[] paramTypes = TypeParameterResolver.resolveParamTypes(method, type);
// 将参数类型由 Type 转为 Class,并将转换后的结果缓存到 setTypes
setTypes.put(name, typeToClass(paramTypes[0]));
}
setter 方法冲突的解析规则:
- 冲突方法的参数类型与 getter 的返回类型一致,则认为是最好的选择
- 冲突方法的参数类型具有继承关系,子类参数对应的方法被认为是更合适选择
- 冲突方法的参数类型不相关,无法确定哪个是更好的选择,此时直接抛异常
PropertyTokenizer(位于org.apache.ibatis.reflection.property) 对数组和复合属性均处理:
public class PropertyTokenizer implements Iterator<PropertyTokenizer> {
private String name;
private final String indexedName;
private String index;
private final String children;
public PropertyTokenizer(String fullname) {
// 检测传入的参数中是否包含字符 '.'
int delim = fullname.indexOf('.');
if (delim > -1) {
/*
* 以点位为界,进行分割。比如:
* fullname = www.coolblog.xyz
*
* 以第一个点为分界符:
* name = www
* children = coolblog.xyz
*/
name = fullname.substring(0, delim);
children = fullname.substring(delim + 1);
} else {
// fullname 中不存在字符 '.'
name = fullname;
children = null;
}
indexedName = name;
// 检测传入的参数中是否包含字符 '['
delim = name.indexOf('[');
if (delim > -1) {
/*
* 获取中括号里的内容,比如:
* 1. 对于数组或 List 集合:[] 中的内容为数组下标,
* 比如 fullname = articles[1],index = 1
* 2. 对于 Map:[] 中的内容为键,
* 比如 fullname = xxxMap[keyName],index = keyName
*/
index = name.substring(delim + 1, name.length() - 1);
// 获取分解符前面的内容,比如:fullname = articles[1],name = articles
name = name.substring(0, delim);
}
}
@Override
public boolean hasNext() {
return children != null;
}
@Override
public PropertyTokenizer next() {
// 对 children 进行再次切分,用于解析多重复合属性
return new PropertyTokenizer(children);
}
}
节点内容解析出来后,需要有一个存放的地方,以使其他代码可以找到这些配置。这个存放地方就是 Configuration 对象,本节来看一下将
节点内容设置到 Configuration 对象中的过程。以下的代码仍在XMLConfigBuilder类中:
private void settingsElement(Properties props) {
// 设置 autoMappingBehavior 属性,默认值为 PARTIAL
configuration.setAutoMappingBehavior(AutoMappingBehavior.
valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
// 设置 cacheEnabled 属性,默认值为 true
configuration.setCacheEnabled(
booleanValueOf(props.getProperty("cacheEnabled"), true));
configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), false));
configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null));
configuration.setDefaultResultSetType(resolveResultSetType(props.getProperty("defaultResultSetType")));
configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
// 设置默认枚举处理器
configuration.setDefaultEnumTypeHandler(
resolveClass(props.getProperty("defaultEnumTypeHandler")));
configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true));
configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false));
configuration.setLogPrefix(props.getProperty("logPrefix"));
configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
configuration.setShrinkWhitespacesInSql(booleanValueOf(props.getProperty("shrinkWhitespacesInSql"), false));
configuration.setDefaultSqlProviderType(resolveClass(props.getProperty("defaultSqlProviderType")));
}
在上面的代码中,调用了BaseBuilder(位于org.apache.ibatis.builder)类中的resolveClass方法:
protected <T> Class<? extends T> resolveClass(String alias) {
if (alias == null) {
return null;
}
try {
// 通过别名解析
return resolveAlias(alias);
} catch (Exception e) {
throw new BuilderException("Error resolving class. Cause: " + e, e);
}
}
protected final TypeAliasRegistry typeAliasRegistry;
protected <T> Class<? extends T> resolveAlias(String alias) {
// 通过别名注册器解析别名对于的类型 Class
return typeAliasRegistry.resolveAlias(alias);
}
TypeAliasRegistry 的用途就是将别名和类型进行映射,这样就可以用别名表示某个类了,方便使用。既然聊到了别名,那下面我们不妨看看别名的配置的解析过程。
MyBatis 中,我们可以为自己写的一些类定义一个别名。这样在使用的时候,只需要输入别名即可,无需再把全限定的类名写出来。在 MyBatis 中,我们有两种方式进行别名配置。第一种是仅配置包名,让 MyBatis 去扫描包中的类型,并根据类型得到相应的别名。这种方式可配合 Alias 注解使用,即通过注解为某个类配置别名,而不是让 MyBatis 按照默认规则生成别名:
<typeAliases>
<package name="xyz.coolblog.chapter2.model1"/>
<package name="xyz.coolblog.chapter2.model2"/>
typeAliases>
第二种方式是通过手动的方式,明确为某个类型配置别名。这种方式的配置如下:
<typeAliases>
<typeAlias alias="article" type="xyz.coolblog.chapter2.model.Article" />
<typeAlias alias="author" type="xyz.coolblog.chapter2.model.Author" />
typeAliases>
第一种自动扫描的方式配置起来比较简单,第二种方式比较繁琐,特别是配置项比较多时。
上面是使用,接下来就看下具体在代码中是怎么解析的,即XMLConfigBuilder中的typeAliasesElement方法:
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 从指定的包中解析别名和类型的映射
if ("package".equals(child.getName())) {
String typeAliasPackage = child.getStringAttribute("name");
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
// 从 typeAlias 节点中解析别名和类型的映射
} else {
// 获取 alias 和 type 属性值,alias 不是必填项,可为空
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
// 加载 type 对应的类型
Class<?> clazz = Resources.classForName(type);
// 注册别名到类型的映射
if (alias == null) {
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
}
上面的代码通过一个 if-else 条件分支来处理两种不同的配置。
public void registerAlias(Class<?> type) {
// 获取全路径类名的简称
String alias = type.getSimpleName();
Alias aliasAnnotation = type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
// 从注解中取出别名
alias = aliasAnnotation.value();
}
// 调用重载方法注册别名和类型映射
registerAlias(alias, type);
}
public void registerAlias(String alias, Class<?> value) {
if (alias == null) {
throw new TypeException("The parameter alias cannot be null");
}
// issue #748
// 将别名转成小写
String key = alias.toLowerCase(Locale.ENGLISH);
// 如果 TYPE_ALIASES 中存在了某个类型映射,这里判断当前类型与映射中的类型
// 是否一致,不一致则抛出异常,不允许一个别名对应两种类型
if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) {
throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'.");
}
// 缓存别名到类型映射
typeAliases.put(key, value);
}
若用户未明确配置 alias 属性,MyBatis 会使用类名的小写形式作为别名。比如,全限定类名 xyz.coolblog.model.Author 的别名为 author。若类中有@Alias 注解,则从注解中取值作为别名。
public void registerAliases(String packageName) {
// 调用重载方法注册别名
registerAliases(packageName, Object.class);
}
public void registerAliases(String packageName, Class<?> superType) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
// 查找某个包下的父类为 superType 的类。从调用栈来看,这里的
// superType = Object.class,所以 ResolverUtil 将查找所有的类。
// 查找完成后,查找结果将会被缓存到内部集合中。
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
// 获取查找结果
Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
for (Class<?> type : typeSet) {
// Ignore inner classes and interfaces (including package-info.java)
// Skip also inner classes. See issue #6
// 忽略匿名类,接口,内部类
if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
// 为类型注册别名
registerAlias(type);
}
}
}
public Configuration() {
// 注册事务工厂的别名
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
// 省略部分代码
// 注册数据源的别名
typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
// 注册缓存策略的别名
typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
typeAliasRegistry.registerAlias("LRU", LruCache.class);
// 注册日志类的别名
typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
// 注册动态代理工厂的别名
typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);
}
以下代码在TypeAliasRegistry类中:
public TypeAliasRegistry() {
// 注册 String 的别名
registerAlias("string", String.class);
// 注册基本类型包装类的别名
registerAlias("byte", Byte.class);
registerAlias("long", Long.class);
registerAlias("short", Short.class);
registerAlias("int", Integer.class);
registerAlias("integer", Integer.class);
registerAlias("double", Double.class);
registerAlias("float", Float.class);
registerAlias("boolean", Boolean.class);
// 注册基本类型包装类数组的别名
registerAlias("byte[]", Byte[].class);
registerAlias("long[]", Long[].class);
registerAlias("short[]", Short[].class);
registerAlias("int[]", Integer[].class);
registerAlias("integer[]", Integer[].class);
registerAlias("double[]", Double[].class);
registerAlias("float[]", Float[].class);
registerAlias("boolean[]", Boolean[].class);
// 注册基本类型的别名
registerAlias("_byte", byte.class);
registerAlias("_long", long.class);
registerAlias("_short", short.class);
registerAlias("_int", int.class);
registerAlias("_integer", int.class);
registerAlias("_double", double.class);
registerAlias("_float", float.class);
registerAlias("_boolean", boolean.class);
// 注册基本类型包装类的别名
registerAlias("_byte[]", byte[].class);
registerAlias("_long[]", long[].class);
registerAlias("_short[]", short[].class);
registerAlias("_int[]", int[].class);
registerAlias("_integer[]", int[].class);
registerAlias("_double[]", double[].class);
registerAlias("_float[]", float[].class);
registerAlias("_boolean[]", boolean[].class);
// 注册 Date, BigDecimal, Object 等类型的别名
registerAlias("date", Date.class);
registerAlias("decimal", BigDecimal.class);
registerAlias("bigdecimal", BigDecimal.class);
registerAlias("biginteger", BigInteger.class);
registerAlias("object", Object.class);
// 注册 Date, BigDecimal, Object 等数组类型的别名
registerAlias("date[]", Date[].class);
registerAlias("decimal[]", BigDecimal[].class);
registerAlias("bigdecimal[]", BigDecimal[].class);
registerAlias("biginteger[]", BigInteger[].class);
registerAlias("object[]", Object[].class);
// 注册集合类型的别名
registerAlias("map", Map.class);
registerAlias("hashmap", HashMap.class);
registerAlias("list", List.class);
registerAlias("arraylist", ArrayList.class);
registerAlias("collection", Collection.class);
registerAlias("iterator", Iterator.class);
// 注册 ResultSet 的别名
registerAlias("ResultSet", ResultSet.class);
}
以上就是别名解析的全部流程。
插件是 MyBatis 提供的一个拓展机制,通过插件机制我们可在 SQL 执行过程中的某些点上做一些自定义操作。实现一个插件需要比简单,首先需要让插件类实现 Interceptor接口。然后在插件类上添加@Intercepts 和@Signature 注解,用于指定想要拦截的目标方法。
比较常见的插件有分页插件、分表插件。在分析插件的配置的解析过程之前,我们先来了解一下插件的配置:
<plugins>
<plugin interceptor="xyz.coolblog.mybatis.ExamplePlugin">
<property name="key" value="value"/>
plugin>
plugins>
关于该配置的解析在XMLConfigBuilder中:
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
// 获取配置信息
Properties properties = child.getChildrenAsProperties();
// 解析拦截器的类型,并创建拦截器
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
// 设置属性
interceptorInstance.setProperties(properties);
// 添加拦截器到 Configuration 中
configuration.addInterceptor(interceptorInstance);
}
}
}
插件解析的过程还是比较简单的。首先是获取配置,然后再解析拦截器类型,并实例化拦截器。最后向拦截器中设置属性,并将拦截器添加到 Configuration 中。
事务管理器和数据源是配置在
节点中的。它们的配置大致如下:
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
dataSource>
environment>
environments>
对于上述配置的解析过程也在在XMLConfigBuilder中:
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
// 获取 default 属性
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
// 获取 id 属性
String id = child.getStringAttribute("id");
// 检测当前 environment 节点的 id 与其父节点 environments 的
// 属性 default 内容是否一致,一致则返回 true,否则返回 false
if (isSpecifiedEnvironment(id)) {
// 解析 transactionManager 节点
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
// 解析 dataSource 节点,逻辑和插件的解析逻辑很相似,不在赘述
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
// 创建 DataSource 对象
DataSource dataSource = dsFactory.getDataSource();
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
// 构建 Environment 对象,并设置到 configuration 中
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
在向数据库存储或读取数据时,我们需要将数据库字段类型和 Java 类型进行一个转换。比如数据库中有 CHAR 和 VARCHAR 等类型,但 Java 中没有这些类型,不过 Java 有 String类型。所以我们在从数据库中读取 CHAR 和 VARCHAR 类型的数据时,就可以把它们转成String。在 MyBatis 中,数据库类型和 Java 类型之间的转换任务是委托给类型处理器TypeHandler 去处理的。MyBatis 提供了一些常见类型的类型处理器,除此之外,我们还可以自定义类型处理器以非常见类型转换的需求。
下类型处理器的配置方法:
<typeHandlers>
<package name="xyz.coolblog.handlers"/>
typeHandlers>
<typeHandlers>
<typeHandler jdbcType="TINYINT"
javaType="xyz.coolblog.constant.ArticleTypeEnum"
handler="xyz.coolblog.mybatis.ArticleTypeHandler"/>
typeHandlers>
使用自动扫描的方式注册类型处理器时,应使用@MappedTypes 和@MappedJdbcTypes注解配置 javaType 和 jdbcType。其对应的解析代码也在XMLConfigBuilder中:
private void typeHandlerElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 从指定的包中注册 TypeHandler
if ("package".equals(child.getName())) {
String typeHandlerPackage = child.getStringAttribute("name");
// 注册方法 ①
typeHandlerRegistry.register(typeHandlerPackage);
} else {
// 从 typeHandler 节点中解析别名到类型的映射
String javaTypeName = child.getStringAttribute("javaType");
String jdbcTypeName = child.getStringAttribute("jdbcType");
String handlerTypeName = child.getStringAttribute("handler");
// 解析上面获取到的属性值
Class<?> javaTypeClass = resolveClass(javaTypeName);
JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
Class<?> typeHandlerClass = resolveClass(handlerTypeName);
// 根据 javaTypeClass 和 jdbcType 值的情况进行不同的注册策略
if (javaTypeClass != null) {
if (jdbcType == null) {
// 注册方法 ②
typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
} else {
// 注册方法 ③
typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
}
} else {
// 注册方法 ④
typeHandlerRegistry.register(typeHandlerClass);
}
}
}
}
}
在上面的代码中,用照①②③④标注了四个重载方法,这些代码均在TypeHandlerRegistry(位于org.apache.ibatis.type)类中。
// java type + jdbc type + handler type
public void register(Class<?> javaTypeClass, JdbcType jdbcType, Class<?> typeHandlerClass) {
// 调用终点方法
register(javaTypeClass, jdbcType, getInstance(javaTypeClass, typeHandlerClass));
}
/** 类型处理器注册过程的终点 */
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
if (javaType != null) {
// JdbcType 到 TypeHandler 的映射
Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType);
if (map == null || map == NULL_TYPE_HANDLER_MAP) {
map = new HashMap<>();
}
map.put(jdbcType, handler);
// 存储 javaType 到 Map 的映射
typeHandlerMap.put(javaType, map);
}
// 存储所有的 TypeHandler
allTypeHandlersMap.put(handler.getClass(), handler);
}
所谓的注册过程也就是把类型和处理器进行映射而已。
public void register(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
// 调用中间方法 register(Type, TypeHandler)
register(javaTypeClass, getInstance(javaTypeClass, typeHandlerClass));
}
private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
// 获取 @MappedJdbcTypes 注解
MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
if (mappedJdbcTypes != null) {
// 遍历 @MappedJdbcTypes 注解中配置的值
for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
// 调用终点方法,参考上一小节的分析
register(javaType, handledJdbcType, typeHandler);
}
if (mappedJdbcTypes.includeNullJdbcType()) {
// 调用终点方法,jdbcType = null
register(javaType, null, typeHandler);
}
} else {
// 调用终点方法,jdbcType = null
register(javaType, null, typeHandler);
}
}
上面的逻辑也比较简单,主要做的事情是尝试从注解中获取 JdbcType 的值。
public void register(Class<?> typeHandlerClass) {
boolean mappedTypeFound = false;
// 获取 @MappedTypes 注解
MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
if (mappedTypes != null) {
// 遍历 @MappedTypes 注解中配置的值
for (Class<?> javaTypeClass : mappedTypes.value()) {
// 调用注册方法 ②
register(javaTypeClass, typeHandlerClass);
mappedTypeFound = true;
}
}
if (!mappedTypeFound) {
// 调用中间方法 register(TypeHandler)
register(getInstance(null, typeHandlerClass));
}
}
public <T> void register(TypeHandler<T> typeHandler) {
boolean mappedTypeFound = false;
// 获取 @MappedTypes 注解
MappedTypes mappedTypes = typeHandler.getClass().getAnnotation(MappedTypes.class);
if (mappedTypes != null) {
for (Class<?> handledType : mappedTypes.value()) {
// 调用中间方法 register(Type, TypeHandler)
register(handledType, typeHandler);
mappedTypeFound = true;
}
}
// 自动发现映射类型
if (!mappedTypeFound && typeHandler instanceof TypeReference) {
try {
TypeReference<T> typeReference = (TypeReference<T>) typeHandler;
// 获取参数模板中的参数类型,并调用中间方法 register(Type, TypeHandler)
register(typeReference.getRawType(), typeHandler);
mappedTypeFound = true;
} catch (Throwable t) {
// maybe users define the TypeReference with a different type and are not assignable, so just ignore it
}
}
if (!mappedTypeFound) {
// 调用中间方法 register(Class, TypeHandler)
register((Class<T>) null, typeHandler);
}
}
public <T> void register(
Class<T> javaType, TypeHandler<? extends T> typeHandler) {
// 调用中间方法 register(Type, TypeHandler)
register((Type) javaType, typeHandler);
}
不管是通过注解的方式,还是通过反射的方式,它们最终目的是为了解析出 javaType 的值。解析完成后,这些方法会调用中间方法
register(Type,TypeHandler),这个方法负责解析 jdbcType,该方法上一节已经分析过。一个负责解析 javaType,另一个负责解析 jdbcType,逻辑比较清晰了。
public void register(String packageName) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
// 从指定包中查找 TypeHandler
resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses();
for (Class<?> type : handlerSet) {
// 忽略内部类,接口,抽象类等
if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
// 调用注册方法 ④
register(type);
}
}
}
与配置文件不同,映射文件用于配置 SQL语句,字段映射关系等。映射文件中包含
、
、
、
、
映射文件的解析过程是配置文件解析过程的一部分,MyBatis 会在解析配置文件的过程中对映射文件进行解析。解析逻辑封装在XMLConfigBuilder中的mapperElement 方法中:
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
// 获取 节点中的 name 属性
String mapperPackage = child.getStringAttribute("name");
// 从指定包中查找 mapper 接口,并根据 mapper 接口解析映射配置
configuration.addMappers(mapperPackage);
} else {
// 获取 resource/url/class 等属性
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
// resource 不为空,且其他两者为空,则从指定路径中加载配置
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 解析映射文件
mapperParser.parse();
// url 不为空,且其他两者为空,则通过 url 加载配置
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
// 解析映射文件
mapperParser.parse();
// mapperClass 不为空,且其他两者为空,则通过 mapperClass 解析映射配置
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
// 以上条件不满足,则抛出异常
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
代码的主要逻辑是遍历 mappers 的子节点,并根据节点属性值判断通过何种方式加载映射文件或映射信息。这里把配置在注解中的内容称为映射信息,以 XML 为载体的配置称为映射文件。在 MyBatis 中,共有四种加载映射文件或映射信息的方式。第一种是从文件系统中加载映射文件;第二种是通过 URL 的方式加载映射文件;第三种是通过 mapper 接口加载映射信息,映射信息可以配置在注解中,也可以配置在映射文件中。最后一种是通过包扫描的方式获取到某个包下的所有类,并使用第三种方式为每个类解析映射信息。
在 MyBatis中,通过注解配置映射信息的方式是有一定局限性的,这一点 MyBatis 官方文档中描述的比较清楚:
因为最初设计时,MyBatis 是一个 XML 驱动的框架。配置信息是基于 XML 的,而且映射语句也是定义在 XML 中的。而到了 MyBatis3,就有新选择了。MyBatis3 构建在全面且强大的基于 Java 语言的配置 API 之上。这个配置 API 是基于 XML 的 MyBatis 配置的基础,也是新的基于注解配置的基础。注解提供了一种简单的方式来实现简单映射语句,而不会引入大量的开销。
注意:不幸的是,Java 注解的表达力和灵活性十分有限。尽管很多时间都花在调查、设计和试验上,最强大的MyBatis映射并不能用注解来构建。
从上面内容可以看出:限于 Java 注解的表达力和灵活性,通过注解的方式并不能完全发挥 MyBatis 的能力。因此,对于一些较为复杂的配置信息,我们还是应该通过XML 的方式进行配置。
下面开始分析映射文件的解析过程,在分析之前,先来看一下映射文件解析入口。在上面的mapperElement方法中调用了mapperParser.parse()方法,这就是我们想要的入口,即XMLMapperBuilder(位于org.apache.ibatis.builder.xml)中的parse方法:
public void parse() {
// 检测映射文件是否已经被解析过
if (!configuration.isResourceLoaded(resource)) {
// 解析 mapper 节点
configurationElement(parser.evalNode("/mapper"));
// 添加资源路径到“已解析资源集合”中
configuration.addLoadedResource(resource);
// 通过命名空间绑定 Mapper 接口
bindMapperForNamespace();
}
// 处理未完成解析的节点
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
映射文件解析入口逻辑包含三个核心操作:
- 解析 mapper 节点
- 通过命名空间绑定 Mapper 接口
- 处理未完成解析的节点
映射文件 包 含 多 种 二 级 节 点 , 比 如
,
,
以 及
<mapper namespace="xyz.coolblog.dao.AuthorDao">
<cache/>
<resultMap id="authorResult" type="Author">
<id property="id" column="id"/>
<result property="name" column="name"/>
resultMap>
<sql id="table">
author
sql>
<select id="findOne" resultMap="authorResult">
SELECT
id, name, age, sex, email
FROM
<include refid="table"/>
WHERE
id = #{id}
select>
mapper>
以上配置中每种节点的解析逻辑都封装在了相应的方法中,这些方法由 XMLMapperBuilder(位于org.apache.ibatis.builder.xml)类 的configurationElement 方法统一调用:
private void configurationElement(XNode context) {
try {
// 获取 mapper 命名空间
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
// 设置命名空间到 builderAssistant 中
builderAssistant.setCurrentNamespace(namespace);
// 解析 节点
cacheRefElement(context.evalNode("cache-ref"));
// 解析 节点
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 解析 节点
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 解析 节点
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);
}
}
在阅读源码时,我们按部就班的分析每个方法调用即可。不过本章在叙述的过程中会对分析顺序进行一些调整,本章将会先分析
节点的解析过程,然后再分析
节点,之后会按照顺序分析其他节点的解析过程。
MyBatis 提供了一、二级缓存,其中一级缓存是 SqlSession 级别的,默认为开启状态。二级缓存配置在映射文件中,使用者需要显示配置才能开启。如果无特殊要求,二级缓存的配置很简单。
<cache/>
如果想修改缓存的一些属性,可以像下面这样配置:
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
上面配置的意思是:
- 按先进先出的策略淘汰缓存项
- 缓存的容量为 512 个对象引用
- 缓存每隔 60 秒刷新一次
- 缓存返回的对象是写安全的,即在外部修改对象不会影响到缓存内部存储对象
当然,除了使用Mybatis自带的缓存,也可以使用第三方缓存,比如Ehcache:
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
<property name="timeToIdleSeconds" value="3600"/>
<property name="timeToLiveSeconds" value="3600"/>
<property name="maxEntriesLocalHeap" value="1000"/>
<property name="maxEntriesLocalDisk" value="10000000"/>
<property name="memoryStoreEvictionPolicy" value="LRU"/>
cache>
缓存配置的解析逻辑在XMLMapperBuilder中实现:
private void cacheElement(XNode context) {
if (context != null) {
// 获取各种属性
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
// 获取子节点配置
Properties props = context.getChildrenAsProperties();
// 构建缓存对象
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
上面代码中,大段代码用来解析
节点的属性和子节点,缓存对象的构建逻辑封装在 MapperBuilderAssistant(位于org.apache.ibatis.builder)类的 useNewCache 方法中:
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
// 使用建造模式构建缓存实例
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
// 添加缓存到 Configuration 对象中
configuration.addCache(cache);
// 设置 currentCache 遍历,即当前使用的缓存
currentCache = cache;
return cache;
}
接下来看下Cache 实例构建过程,在CacheBuilder(位于org.apache.ibatis.mapping)中实现:
public Cache build() {
// 设置默认的缓存类型(PerpetualCache)和缓存装饰器(LruCache)
setDefaultImplementations();
// 通过反射创建缓存
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
// 仅对内置缓存 PerpetualCache 应用装饰器
if (PerpetualCache.class.equals(cache.getClass())) {
// 遍历装饰器集合,应用装饰器
for (Class<? extends Cache> decorator : decorators) {
// 通过反射创建装饰器实例
cache = newCacheDecoratorInstance(decorator, cache);
// 设置属性值到缓存实例中
setCacheProperties(cache);
}
// 应用标准的装饰器,比如 LoggingCache、SynchronizedCache
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
// 应用具有日志功能的缓存装饰器
cache = new LoggingCache(cache);
}
return cache;
}
上面的代码可以分为4步:
- 设置默认的缓存类型及装饰器
- 应用装饰器到 PerpetualCache 对象上
- 应用标准装饰器
- 对非 LoggingCache 类型的缓存应用 LoggingCache 装饰器
最后一步的逻辑很简单,面按顺序分析前 3 个步骤:
private void setDefaultImplementations() {
if (implementation == null) {
// 设置默认的缓存实现类
implementation = PerpetualCache.class;
if (decorators.isEmpty()) {
// 添加 LruCache 装饰器
decorators.add(LruCache.class);
}
}
}
以上代码主要做的事情是在 implementation 为空的情况下,为它设置一个默认值。其实在调用setDefaultImplementations方法之前已经进行了很多非空判断,implementation 不可能为空,setDefaultImplementations 方法似乎没有存在的必要了。其实不然,如果有人不按套路写代码。比如:
Cache cache = new CacheBuilder(currentNamespace)
// 忘记设置 implementation
.build();
忘记设置 implementation ,或人为的将 implementation 设为空。如果不对implementation 进行判空,会导致 build 方法在构建实例时触发空指针异常,对于框架来说,这是一个低级错误。这种情况一定要避免,以提高框架的健壮性。
接下来看下,如果使用第三方缓存时,其对应的配置是如何设置到缓存实例中的。
private void setCacheProperties(Cache cache) {
if (properties != null) {
// 为缓存实例生成一个“元信息”实例,forObject 方法调用层次比较深,
// 但最终调用了 MetaClass 的 forClass 方法。
MetaObject metaCache = SystemMetaObject.forObject(cache);
for (Map.Entry<Object, Object> entry : properties.entrySet()) {
String name = (String) entry.getKey();
String value = (String) entry.getValue();
if (metaCache.hasSetter(name)) {
// 获取 setter 方法的参数类型
Class<?> type = metaCache.getSetterType(name);
// 根据参数类型对属性值进行转换,并将转换后的值
// 通过 setter 方法设置到 Cache 实例中
if (String.class == type) {
metaCache.setValue(name, value);
} else if (int.class == type
|| Integer.class == type) {
/*
* 此处及以下分支包含两个步骤:
* 1.类型转换 → Integer.valueOf(value)
* 2.将转换后的值设置到缓存实例中 →
* metaCache.setValue(name, value)
*/
metaCache.setValue(name, Integer.valueOf(value));
} else if (long.class == type
|| Long.class == type) {
metaCache.setValue(name, Long.valueOf(value));
} else if (short.class == type
|| Short.class == type) {
metaCache.setValue(name, Short.valueOf(value));
} else if (byte.class == type
|| Byte.class == type) {
metaCache.setValue(name, Byte.valueOf(value));
} else if (float.class == type
|| Float.class == type) {
metaCache.setValue(name, Float.valueOf(value));
} else if (boolean.class == type
|| Boolean.class == type) {
metaCache.setValue(name, Boolean.valueOf(value));
} else if (double.class == type
|| Double.class == type) {
metaCache.setValue(name, Double.valueOf(value));
} else {
throw new CacheException("Unsupported property type for cache: '" + name + "' of type " + type);
}
}
}
}
// 如果缓存类实现了 InitializingObject 接口,
// 则调用 initialize 方法执行初始化逻辑
if (InitializingObject.class.isAssignableFrom(cache.getClass())) {
try {
((InitializingObject) cache).initialize();
} catch (Exception e) {
throw new CacheException("Failed cache initialization for '"
+ cache.getId() + "' on '" + cache.getClass().getName() + "'", e);
}
}
}
上面的大段代码用于对属性值进行类型转换,和设置转换后的值到 Cache 实例中。
最后,看一下设置标准装饰器的过程。
private Cache setStandardDecorators(Cache cache) {
try {
// 创建“元信息”对象
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
// 设置 size 属性
metaCache.setValue("size", size);
}
if (clearInterval != null) {
// clearInterval 不为空,应用 ScheduledCache 装饰器
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
if (readWrite) {
// readWrite 为 true,应用 SerializedCache 装饰器
cache = new SerializedCache(cache);
}
/*
* 应用 LoggingCache,SynchronizedCache 装饰器,
* 使原缓存具备打印日志和线程同步的能力
*/
cache = new LoggingCache(cache);
cache = new SynchronizedCache(cache);
if (blocking) {
// blocking 为 true,应用 BlockingCache 装饰器
cache = new BlockingCache(cache);
}
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
}
以上代码用于为缓存应用一些基本的装饰器,除了 LoggingCache 和 SynchronizedCache这两个是必要的装饰器,其他的装饰器应用与否,取决于用户的配置。
在 MyBatis 中,二级缓存是可以共用的。这需要通过
节点为命名空间配置参照缓存,示例:
<mapper namespace="xyz.coolblog.dao.Mapper1">
<cache-ref namespace="xyz.coolblog.dao.Mapper2"/>
mapper>
<mapper namespace="xyz.coolblog.dao.Mapper2">
<cache/>
mapper>
cache-ref 的解析过程还是在XMLMapperBuilder中实现的:
private void cacheRefElement(XNode context) {
if (context != null) {
configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
// 创建 CacheRefResolver 实例
CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
try {
// 解析参照缓存
cacheRefResolver.resolveCacheRef();
} catch (IncompleteElementException e) {
// 捕捉 IncompleteElementException 异常,并将 cacheRefResolver
// 存入到 Configuration 的 incompleteCacheRefs 集合中
configuration.addIncompleteCacheRef(cacheRefResolver);
}
}
}
节点的解析逻辑封装在了 CacheRefResolver(位于org.apache.ibatis.builder) 的 resolveCacheRef 方法中:
public Cache resolveCacheRef() {
// 调用 builderAssistant 的 useNewCache(namespace) 方法
return assistant.useCacheRef(cacheRefNamespace);
}
上述代码又调用了MapperBuilderAssistant(位于org.apache.ibatis.builder)中的useCacheRef方法:
public Cache useCacheRef(String namespace) {
if (namespace == null) {
throw new BuilderException("cache-ref element requires a namespace attribute.");
}
try {
unresolvedCacheRef = true;
// 根据命名空间从全局配置对象(Configuration)中查找相应的缓存实例
Cache cache = configuration.getCache(namespace);
/*
* 若未查找到缓存实例,此处抛出异常。这里存在两种情况导致未查找到
* cache 实例,分别如下:
* 1.使用者在 中配置了一个不存在的命名空间,
* 导致无法找到 cache 实例
* 2.使用者所引用的缓存实例还未创建
*/
if (cache == null) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
}
// 设置 cache 为当前使用缓存
currentCache = cache;
unresolvedCacheRef = false;
return cache;
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
}
}
resultMap 是 MyBatis 框架中常用的特性,主要用于映射结果。
resultMap 元素是 MyBatis 中最重要最强大的元素。它可以让你从 90% 的 JDBC ResultSets数据提取代码中解放出来,并在一些情形下允许你做一些 JDBC 不支持的事情。实际上,在对复杂语句进行联合映射的时候,它很可能可以代替数千行的同等功能的代码。resultMap 的设计思想是,简单的语句不需要明确的结果映射,而复杂一点的语句只需要描述它们的关系就行了。
resultMap 配置的解析过程也是在XMLMapperBuilder中实现的:
private void resultMapElements(List<XNode> list) {
// 遍历 节点列表
for (XNode resultMapNode : list) {
try {
// 解析 resultMap 节点
resultMapElement(resultMapNode);
} catch (IncompleteElementException e) {
// ignore, it will be retried
}
}
}
private ResultMap resultMapElement(XNode resultMapNode) {
// 调用重载方法
return resultMapElement(resultMapNode, Collections.emptyList(), null);
}
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) {
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
// 获取 type 属性
String type = resultMapNode.getStringAttribute("type",
resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType",
resultMapNode.getStringAttribute("javaType"))));
// 解析 type 属性对应的类型
Class<?> typeClass = resolveClass(type);
if (typeClass == null) {
typeClass = inheritEnclosingType(resultMapNode, enclosingType);
}
Discriminator discriminator = null;
List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings);
// 获取并遍历 的子节点列表
List<XNode> resultChildren = resultMapNode.getChildren();
for (XNode resultChild : resultChildren) {
if ("constructor".equals(resultChild.getName())) {
// 解析 constructor 节点,并生成相应的 ResultMapping
processConstructorElement(resultChild, typeClass, resultMappings);
} else if ("discriminator".equals(resultChild.getName())) {
// 解析 discriminator 节点
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
} else {
List<ResultFlag> flags = new ArrayList<>();
if ("id".equals(resultChild.getName())) {
// 添加 ID 到 flags 集合中
flags.add(ResultFlag.ID);
}
// 解析 id 和 property 节点,并生成相应的 ResultMapping
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
// 获取 id 属性
String id = resultMapNode.getStringAttribute("id",
resultMapNode.getValueBasedIdentifier());
// 获取 extends 和 autoMapping
String extend = resultMapNode.getStringAttribute("extends");
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
try {
// 根据前面获取到的信息构建 ResultMap 对象
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
/*
* 如果发生 IncompleteElementException 异常,
* 这里将 resultMapResolver 添加到 incompleteResultMaps 集合中
*/
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}
上面的代码做的事情:
- 获取
节点的各种属性
- 遍历
的子节点,并根据子节点名称执行相应的解析逻辑
- 构建 ResultMap 对象
- 若构建过程中发生异常,则将resultMapResolver添加到
incompleteResultMaps 集合中
第 2 步和第 3 步分别是
节点的子节点解析过程,以及 ResultMap 对象的构建过程,这两个过程比较重要。
节点中,子节点
和
都是常规配置,这两个节点的解析是在XMLMapperBuilder中进行的: private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) {
String property;
// 根据节点类型获取 name 或 property 属性
if (flags.contains(ResultFlag.CONSTRUCTOR)) {
property = context.getStringAttribute("name");
} else {
property = context.getStringAttribute("property");
}
// 获取其他各种属性
String column = context.getStringAttribute("column");
String javaType = context.getStringAttribute("javaType");
String jdbcType = context.getStringAttribute("jdbcType");
String nestedSelect = context.getStringAttribute("select");
// 解析 resultMap 属性,该属性出现在 和 节点中。
// 若这两个节点不包含 resultMap 属性,则调用 processNestedResultMappings 方法
// 解析嵌套 resultMap。
String nestedResultMap = context.getStringAttribute("resultMap", () ->
processNestedResultMappings(context, Collections.emptyList(), resultType));
String notNullColumn = context.getStringAttribute("notNullColumn");
String columnPrefix = context.getStringAttribute("columnPrefix");
String typeHandler = context.getStringAttribute("typeHandler");
String resultSet = context.getStringAttribute("resultSet");
String foreignColumn = context.getStringAttribute("foreignColumn");
boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
// 解析 javaType、typeHandler 的类型以及枚举类型 JdbcType
Class<?> javaTypeClass = resolveClass(javaType);
Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
// 构建 ResultMapping 对象
return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
}
上面的代码主要用于获取
和
节点的属性。resultMap 属性的解析过程要相对复杂一些。该属性存在于
和
节点中。下面以
节点为例,演示该节点的两种配置方式。
第一种配置方式是通过 resultMap 属性引用其他的
节点:
<resultMap id="articleResult" type="Article">
<id property="id" column="id"/>
<result property="title" column="article_title"/>
<association property="article_author" column="article_author_id"
resultMap="authorResult"/>
resultMap>
<resultMap id="authorResult" type="Author">
<id property="id" column="author_id"/>
<result property="name" column="author_name"/>
resultMap>
第二种配置方式是采取 resultMap 嵌套的方式进行配置:
<resultMap id="articleResult" type="Article">
<id property="id" column="id"/>
<result property="title" column="article_title"/>
<association property="article_author" javaType="Author">
<id property="id" column="author_id"/>
<result property="name" column="author_name"/>
association>
resultMap>
的子节点是一些结果映射配置,这些结果配置最终也会被解析成 ResultMap。解析过程仍在XMLMapperBuilder中:
private String processNestedResultMappings(XNode context, List<ResultMapping> resultMappings, Class<?> enclosingType) {
// 判断节点名称
if (Arrays.asList("association", "collection", "case").contains(context.getName())
&& context.getStringAttribute("select") == null) {
validateCollection(context, enclosingType);
// resultMapElement 是解析 ResultMap 入口方法
ResultMap resultMap = resultMapElement(context, resultMappings, enclosingType);
// 返回 resultMap id
return resultMap.getId();
}
return null;
}
的子节点由 resultMapElement 方法解析成 ResultMap,并在最后返回resultMap.id。对于
节点,id 的值配置在该节点的 id 属性中。但
节点无法配置 id 属性,那么该 id 如何产生的呢?答案在 XNode 类的 getValueBasedIdentifier 方法中。
接下来分析 ResultMapping 的构建过程,该过程是在MapperBuilderAssistant中实现的:
public ResultMapping buildResultMapping(
Class<?> resultType,
String property,
String column,
Class<?> javaType,
JdbcType jdbcType,
String nestedSelect,
String nestedResultMap,
String notNullColumn,
String columnPrefix,
Class<? extends TypeHandler<?>> typeHandler,
List<ResultFlag> flags,
String resultSet,
String foreignColumn,
boolean lazy) {
// 若 javaType 为空,这里根据 property 的属性进行解析。关于下面方法中的参数,这里说明一下:
// - resultType:即 中的 type 属性
// - property:即 中的 property 属性
Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType);
// 解析 TypeHandler
TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
// 解析 column = {property1=column1, property2=column2} 的情况,
// 这里会将 column 拆分成多个 ResultMapping
List<ResultMapping> composites;
if ((nestedSelect == null || nestedSelect.isEmpty()) && (foreignColumn == null || foreignColumn.isEmpty())) {
composites = Collections.emptyList();
} else {
composites = parseCompositeColumnName(column);
}
// 通过建造模式构建 ResultMapping
return new ResultMapping.Builder(configuration, property, column, javaTypeClass)
.jdbcType(jdbcType)
.nestedQueryId(applyCurrentNamespace(nestedSelect, true))
.nestedResultMapId(applyCurrentNamespace(nestedResultMap, true))
.resultSet(resultSet)
.typeHandler(typeHandlerInstance)
.flags(flags == null ? new ArrayList<>() : flags)
.composites(composites)
.notNullColumns(parseMultipleColumnNames(notNullColumn))
.columnPrefix(columnPrefix)
.foreignColumn(foreignColumn)
.lazy(lazy)
.build();
}
接着调用了ResultMapping(位于org.apache.ibatis.mapping)中的build方法:
public ResultMapping build() {
// 将 flags 和 composites 两个集合变为不可修改集合
resultMapping.flags = Collections.unmodifiableList(resultMapping.flags);
resultMapping.composites = Collections.unmodifiableList(resultMapping.composites);
// 从 TypeHandlerRegistry 中获取相应 TypeHandler
resolveTypeHandler();
validate();
return resultMapping;
}
ResultMapping 的构建过程不是很复杂,首先是解析 javaType 类型,并创建 typeHandler实例。然后处理复合 column。最后通过建造器构建 ResultMapping 实例。
节点,示例:<constructor>
<idArg column="id" name="id"/>
<arg column="title" name="title"/>
<arg column="content" name="content"/>
constructor>
该节点的解析是在XMLMapperBuilder中的:
private void processConstructorElement(XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) {
// 获取子节点列表
List<XNode> argChildren = resultChild.getChildren();
for (XNode argChild : argChildren) {
List<ResultFlag> flags = new ArrayList<>();
// 向 flags 中添加 CONSTRUCTOR 标志
flags.add(ResultFlag.CONSTRUCTOR);
if ("idArg".equals(argChild.getName())) {
// 向 flags 中添加 ID 标志
flags.add(ResultFlag.ID);
}
// 构建 ResultMapping
resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags));
}
}
首先是获取并遍历子节点列表,然后为每个子节点创建 flags集合,并添加 CONSTRUCTOR 标志。对于 idArg 节点,额外添加 ID 标志。最后一步则是构建 ResultMapping。
,
等节点最终都被解析成了 ResultMapping。在得到这些 ResultMapping 后,紧接着要做的事情是构建 ResultMap。private ResultMap resultMapElement(XNode resultMapNode,
List<ResultMapping> additionalResultMappings) throws Exception {
// 获取 resultMap 节点中的属性
// ...
// 解析 resultMap 对应的类型
// ...
// 遍历 resultMap 节点的子节点,构建 ResultMapping 对象
// ...
// 创建 ResultMap 解析器
ResultMapResolver resultMapResolver = new ResultMapResolver(
builderAssistant, id, typeClass, extend, discriminator,
resultMappings, autoMapping);
try {
// 根据前面获取到的信息构建 ResultMap 对象
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}
ResultMap 的构建逻辑分装在 ResultMapResolver(位于org.apache.ibatis.builder) 的 resolve 方法中:
public ResultMap resolve() {
return assistant.addResultMap(this.id, this.type, this.extend,
this.discriminator, this.resultMappings, this.autoMapping);
}
上面的方法将构建 ResultMap 实例的任务委托给了 MapperBuilderAssistant 的addResultMap:
public ResultMap addResultMap(
String id,
Class<?> type,
String extend,
Discriminator discriminator,
List<ResultMapping> resultMappings,
Boolean autoMapping) {
// 为 ResultMap 的 id 和 extend 属性值拼接命名空间
id = applyCurrentNamespace(id, false);
extend = applyCurrentNamespace(extend, true);
if (extend != null) {
if (!configuration.hasResultMap(extend)) {
throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'");
}
ResultMap resultMap = configuration.getResultMap(extend);
List<ResultMapping> extendedResultMappings = new ArrayList<>(resultMap.getResultMappings());
// 为拓展 ResultMappings 取出重复项
extendedResultMappings.removeAll(resultMappings);
// Remove parent constructor if this resultMap declares a constructor.
boolean declaresConstructor = false;
// 检测当前 resultMappings 集合中是否包含 CONSTRUCTOR 标志的元素
for (ResultMapping resultMapping : resultMappings) {
if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
declaresConstructor = true;
break;
}
}
// 如果当前 节点中包含 子节点,
// 则将拓展 ResultMapping 集合中的包含 CONSTRUCTOR 标志的元素移除
if (declaresConstructor) {
extendedResultMappings.removeIf(resultMapping -> resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR));
}
// 将扩展 resultMappings 集合合并到当前 resultMappings 集合中
resultMappings.addAll(extendedResultMappings);
}
// 构建 ResultMap
ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
.discriminator(discriminator)
.build();
configuration.addResultMap(resultMap);
return resultMap;
}
上面的方法主要用于处理 resultMap 节点的 extend 属性,extend 不为空的话,这里将当前 resultMappings 集合和扩展 resultMappings 集合合二为一。随后,通过建造模式,在ResultMap(位于org.apache.ibatis.mapping)中构建ResultMap 实例。
public ResultMap build() {
if (resultMap.id == null) {
throw new IllegalArgumentException("ResultMaps must have an id");
}
resultMap.mappedColumns = new HashSet<>();
resultMap.mappedProperties = new HashSet<>();
resultMap.idResultMappings = new ArrayList<>();
resultMap.constructorResultMappings = new ArrayList<>();
resultMap.propertyResultMappings = new ArrayList<>();
final List<String> constructorArgNames = new ArrayList<>();
for (ResultMapping resultMapping : resultMap.resultMappings) {
// 检测 或 节点
// 是否包含 select 和 resultMap 属性
resultMap.hasNestedQueries = resultMap.hasNestedQueries || resultMapping.getNestedQueryId() != null;
resultMap.hasNestedResultMaps = resultMap.hasNestedResultMaps || (resultMapping.getNestedResultMapId() != null && resultMapping.getResultSet() == null);
final String column = resultMapping.getColumn();
if (column != null) {
// 将 colum 转换成大写,并添加到 mappedColumns 集合中
resultMap.mappedColumns.add(column.toUpperCase(Locale.ENGLISH));
} else if (resultMapping.isCompositeResult()) {
for (ResultMapping compositeResultMapping : resultMapping.getComposites()) {
final String compositeColumn = compositeResultMapping.getColumn();
if (compositeColumn != null) {
resultMap.mappedColumns.add(compositeColumn.toUpperCase(Locale.ENGLISH));
}
}
}
// 添加属性 property 到 mappedProperties 集合中
final String property = resultMapping.getProperty();
if (property != null) {
resultMap.mappedProperties.add(property);
}
// 检测当前 resultMapping 是否包含 CONSTRUCTOR 标志
if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
// 添加 resultMapping 到 constructorResultMappings 中
resultMap.constructorResultMappings.add(resultMapping);
// 添加属性(constructor 节点的 name 属性)到 constructorArgNames 中
if (resultMapping.getProperty() != null) {
constructorArgNames.add(resultMapping.getProperty());
}
} else {
// 添加 resultMapping 到 propertyResultMappings 中
resultMap.propertyResultMappings.add(resultMapping);
}
if (resultMapping.getFlags().contains(ResultFlag.ID)) {
// 添加 resultMapping 到 idResultMappings 中
resultMap.idResultMappings.add(resultMapping);
}
}
if (resultMap.idResultMappings.isEmpty()) {
resultMap.idResultMappings.addAll(resultMap.resultMappings);
}
if (!constructorArgNames.isEmpty()) {
// 获取构造方法参数列表
final List<String> actualArgNames = argNamesOfMatchingConstructor(constructorArgNames);
if (actualArgNames == null) {
throw new BuilderException("Error in result map '" + resultMap.id
+ "'. Failed to find a constructor in '"
+ resultMap.getType().getName() + "' by arg names " + constructorArgNames
+ ". There might be more info in debug log.");
}
resultMap.constructorResultMappings.sort((o1, o2) -> {
int paramIdx1 = actualArgNames.indexOf(o1.getProperty());
int paramIdx2 = actualArgNames.indexOf(o2.getProperty());
return paramIdx1 - paramIdx2;
});
}
// 将以下这些集合变为不可修改集合
resultMap.resultMappings = Collections.unmodifiableList(resultMap.resultMappings);
resultMap.idResultMappings = Collections.unmodifiableList(resultMap.idResultMappings);
resultMap.constructorResultMappings = Collections.unmodifiableList(resultMap.constructorResultMappings);
resultMap.propertyResultMappings = Collections.unmodifiableList(resultMap.propertyResultMappings);
resultMap.mappedColumns = Collections.unmodifiableSet(resultMap.mappedColumns);
return resultMap;
}
以上代码主要做的事情就是将 ResultMapping实例及属性分别存储到不同的集合中,仅此而已。ResultMap 中定义了五种不同的集合:
集合名称 | 用途 |
---|---|
mappedColumns | 用于存储 、 、 、 节点 column 属性 |
mappedProperties | 用于存储 和 节点的 property 属性,或 和 节点的 name 属性 |
idResultMappings | 用于存储 和 节点对应的 ResultMapping 对象 |
propertyResultMappings | 用于存储 和 节点对应的 ResultMapping 对象 |
constructorResultMappings | 用于存储 和 节点对应的 ResultMapping 对象 |
节点用来定义一些可重用的 SQL 语句片段,比如表名,或表的列名等。在映射文件中,我们可以通过
节点引用
节点定义的内容。
<sql id="table">
article
sql>
<select id="findOne" resultType="Article">
SELECT id, title FROM <include refid="table"/> WHERE id = #{id}
select>
解析的过程还是从XMLMapperBuilder中开始:
private void sqlElement(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
// 调用 sqlElement 解析 节点
sqlElement(list, configuration.getDatabaseId());
}
// 再次调用 sqlElement,不同的是,这次调用,该方法的第二个参数为 null
sqlElement(list, null);
}
第一次传入具体的 databaseId,用于解析带有 databaseId 属性,且属性值与此相等的
节点。第二次传入的 databaseId 为空,用于解析未配置 databaseId 属性的
节点。
private void sqlElement(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
// 获取 id 和 databaseId 属性
String databaseId = context.getStringAttribute("databaseId");
String id = context.getStringAttribute("id");
// id = currentNamespace + "." + id
id = builderAssistant.applyCurrentNamespace(id, false);
// 检测当前 databaseId 和 requiredDatabaseId 是否一致
if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
// 将 键值对缓存到 sqlFragments 中
sqlFragments.put(id, context);
}
}
}
上面的逻辑:首先是获取
节点的 id 和 databaseId 属性,然后为 id 属性值拼接命名空间。最后,通过检测当前 databaseId 和requiredDatabaseId 是否一致,来决定保存还是忽略当前的
节点。接下来看databaseId 的匹配逻辑:
private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
if (requiredDatabaseId != null) {
// 当前 databaseId 和目标 databaseId 不一致时,返回 false
return requiredDatabaseId.equals(databaseId);
}
// 如果目标 databaseId 为空,但当前 databaseId 不为空。两者不一致,返回 false
if (databaseId != null) {
return false;
}
// 如果当前 节点的 id 与之前的 节点重复,且先前节点
// databaseId 不为空。则忽略当前节点,并返回 false
if (!this.sqlFragments.containsKey(id)) {
return true;
}
// skip this fragment if there is a previous one with a not null databaseId
XNode context = this.sqlFragments.get(id);
return context.getStringAttribute("databaseId") == null;
}
databaseId 的匹配规则:
- databaseId 与 requiredDatabaseId 不一致,即失配,返回 false
- 当前节点与之前的节点出现 id 重复的情况,若之前的
节点 databaseId 属性不为空,返回 false
- 若以上两条规则均匹配失败,此时返回 true
。databaseId用于标明数据库厂商的身份,不同厂商有自己的 SQL 方言,MyBatis 可以根据 databaseId 执行不同 SQL 语句。databaseId 在
节点中有什么用呢?这个问题也不难回答。
节点用于保存 SQL 语句片段,如果 SQL 语句片段中包含方言的话,那么该
节点只能被同一databaseId 的查询语句或更新语句引用。
前面分析了
、
、
以及
节点,从这一节开始,我们来分析映射文件中剩余的几个节点,分别是、
、
以及
等。这几个节点中存储的是相同的内容,都是 SQL 语句,所以这几个节点的解析过程也是相同的。
在进行代码分析之前,这里需要特别说明一下:为了避免和
节点混淆,同时也为了描述方便,这里把
、
、
以及
等节点统称为 SQL 语句节点。
解析过程依旧从XMLMapperBuilder开始:
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
// 调用重载方法构建 Statement
buildStatementFromContext(list, configuration.getDatabaseId());
}
// 调用重载方法构建 Statement,requiredDatabaseId 参数为空
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
// 创建 Statement 建造类
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
// 解析 Statement 节点,并将解析结果存储到
// configuration 的 mappedStatements 集合中
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
// 解析失败,将解析器放入 Configuration 的 incompleteStatements 集合中
configuration.addIncompleteStatement(statementParser);
}
}
}
顺着代码继续看XMLStatementBuilder(位于org.apache.ibatis.builder.xml):
public void parseStatementNode() {
// 获取 id 和 databaseId 属性
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
// 根据 databaseId 进行检测
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
// 获取节点的名称,比如
String nodeName = context.getNode().getNodeName();
// 根据节点名称解析 SqlCommandType
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// 解析 节点
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// 解析 节点
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: and were parsed and removed)
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
// 获取 KeyGenerator 实例
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
// 创建 KeyGenerator 实例
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
// 解析 SQL 语句
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");
// 通过别名解析 resultType 对应的类型
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
// 解析 ResultSetType
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 对象,并将该对象存储到
// Configuration 的 mappedStatements 集合中
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
上面的代码中起码有一半的代码是用来获取节点属性,以及解析部分属性等。抛去这部分代码,以上代码做的事情:
- 解析
节点
- 解析
节点
- 解析 SQL,获取 SqlSource
- 构建 MappedStatement 实例
节点的解析逻辑封装在 XMLIncludeTransformer(位于org.apache.ibatis.builder.xml)的applyIncludes方法 中: public void applyIncludes(Node source) {
Properties variablesContext = new Properties();
Properties configurationVariables = configuration.getVariables();
// 将 configurationVariables 中的数据添加到 variablesContext 中
Optional.ofNullable(configurationVariables)
.ifPresent(variablesContext::putAll);
// 调用重载方法处理 节点
applyIncludes(source, variablesContext, false);
}
创建了一个新的 Properties 对象,并将全局 Properties 添加到其中。这样做的原因是 applyIncludes 的重载方法会向 Properties 中添加新的元素,如果直接将全局 Properties 传给重载方法,会造成全局 Properties 被污染。
private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
if ("include".equals(source.getNodeName())) {
// 获取 节点。若 refid 中包含属性占位符 ${},
// 则需先将属性占位符替换为对应的属性值
Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
// 解析的子节点,并将解析结果与 variablesContext 融合,
// 然后返回融合后的 Properties。若 节点的 value 属性中存在
// 占位符 ${},则将占位符替换为对应的属性值
Properties toIncludeContext = getVariablesContext(source, variablesContext);
/*
* 这里是一个递归调用,用于将 节点内容中出现的属性占位符 ${}
* 替换为对应的属性值。这里要注意一下递归调用的参数:
* - toInclude: 节点对象
* - toIncludeContext: 子节点 的解析结果与
* 全局变量融合后的结果
*/
applyIncludes(toInclude, toIncludeContext, true);
// 如果 和 节点不在一个文档中,
// 则从其他文档中将 节点引入到 所在文档中
if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
toInclude = source.getOwnerDocument().importNode(toInclude, true);
}
// 将 节点替换为 节点
source.getParentNode().replaceChild(toInclude, source);
while (toInclude.hasChildNodes()) {
// 将 中的内容插入到 节点之前
toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
}
// 前面已经将 节点的内容插入到 dom 中了,
// 现在不需要 节点了,这里将该节点从 dom 中移除
toInclude.getParentNode().removeChild(toInclude);
} else if (source.getNodeType() == Node.ELEMENT_NODE) {
if (included && !variablesContext.isEmpty()) {
// replace variables in attribute values
NamedNodeMap attributes = source.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
Node attr = attributes.item(i);
// 将 source 节点属性中的占位符 ${} 替换成具体的属性值
attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
}
}
NodeList children = source.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
// 递归调用
applyIncludes(children.item(i), variablesContext, included);
}
} else if (included && (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE)
&& !variablesContext.isEmpty()) {
// 将文本(text)节点中的属性占位符 ${} 替换成具体的属性值
source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
}
}
节点中同时配置两个 select 语句,这会导致 SQL 语句出错。对于这个问题,MyBatis 提供的
可以很好的解决。<insert id="saveAuthor">
<selectKey keyProperty="id" resultType="int" order="BEFORE">
select author_seq.nextval from dual
selectKey>
insert into Author
(id, name, password)
values
(#{id}, #{username}, #{password})
insert>
在上面的配置中,查询语句会先于插入语句执行,这样我们就可以在插入时获取到主键的值。该节点的解析是从XMLStatementBuilder中开始的:
private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {
List<XNode> selectKeyNodes = context.evalNodes("selectKey");
if (configuration.getDatabaseId() != null) {
// 解析 节点,databaseId 不为空
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
}
// 解析 节点,databaseId 为空
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);
// 将 节点从 dom 树中移除
removeSelectKeyNodes(selectKeyNodes);
}
节点在解析完成后,会被从 dom 树中移除。这样后续可以更专注的解析
或
节点中的 SQL,无需再额外处理
节点。继续跟踪:
private void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) {
for (XNode nodeToHandle : list) {
// id = parentId + !selectKey,比如 saveUser!selectKey
String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
// 获取 节点的 databaseId 属性
String databaseId = nodeToHandle.getStringAttribute("databaseId");
// 匹配 databaseId
if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {
// 解析 节点
parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);
}
}
}
private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {
// 获取各种属性
String resultType = nodeToHandle.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));
// 设置默认值
boolean useCache = false;
boolean resultOrdered = false;
KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
Integer fetchSize = null;
Integer timeout = null;
boolean flushCache = false;
String parameterMap = null;
String resultMap = null;
ResultSetType resultSetTypeEnum = null;
// 创建 SqlSource
SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
// 节点中只能配置 SELECT 查询语句,
// 因此 sqlCommandType 为 SqlCommandType.SELECT
SqlCommandType sqlCommandType = SqlCommandType.SELECT;
// 构建 MappedStatement,并将 MappedStatement
// 添加到 Configuration 的 mappedStatements map 中
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);
// id = namespace + "." + id
id = builderAssistant.applyCurrentNamespace(id, false);
MappedStatement keyStatement = configuration.getMappedStatement(id, false);
// 创建 SelectKeyGenerator,并添加到 keyGenerators map 中
configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
}
以上代码比较重要的一些步骤:
- 创建 SqlSource 实例
- 构建并缓存 MappedStatement 实例
- 构建并缓存 SelectKeyGenerator 实例
和
节点的解析过程,这两个节点解析完成后,都会以不同的方式从 dom 树中消失。所以目前的 SQL 语句节点由一些文本节点和普通节点组成,比如
、
等。下面我们来看一下移除掉
和
节点后的 SQL 语句节点是如何解析的。 public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
接着到了XMLScriptBuilder(位于org.apache.ibatis.scripting.xmltags):
public SqlSource parseScriptNode() {
// 解析 SQL 语句节点
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
// 根据 isDynamic 状态创建不同的 SqlSource
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
SQL 语句的解析逻辑被封装在了 XMLScriptBuilder 类的 parseScriptNode 方法中。该方法首先会调用 parseDynamicTags 解析 SQL 语句节点,在解析过程中,会判断节点是是否包含一些动态标记,比如${}占位符以及动态 SQL 节点等。若包含动态标记,则会将isDynamic 设为 true。后续可根据 isDynamic 创建不同的 SqlSource。
在XMLScriptBuilder构造方法中,会调用initNodeHandlerMap:
/** 该方法用于初始化 nodeHandlerMap 集合,该集合后面会用到 */
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());
}
接下来继续跟踪parseDynamicTags:
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 = new TextSqlNode(data);
// 若文本中包含 ${} 占位符,也被认为是动态节点
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
// 创建 StaticTextSqlNode
contents.add(new StaticTextSqlNode(data));
}
// child 节点是 ELEMENT_NODE 类型,比如 、 等
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
// issue #628
// 获取节点名称,比如 if、where、trim 等
String nodeName = child.getNode().getNodeName();
// 根据节点名称获取 NodeHandler
NodeHandler handler = nodeHandlerMap.get(nodeName);
// 如果 handler 为空,表明当前节点对与 MyBatis 来说,是未知节点。
// MyBatis 无法处理这种节点,故抛出异常
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
// 处理 child 节点,生成相应的 SqlNode
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
以上方法主要是用来判断节点是否包含一些动态标记,比如${}占位符以及动态 SQL 节点等。这里,不管是动态 SQL 节点还是静态 SQL 节点,我们都可以把它们看成是 SQL 片段,一个 SQL 语句由多个 SQL 片段组成。在解析过程中,这些 SQL片段被存储在 contents 集合中。最后,该集合会被传给 MixedSqlNode 构造方法,用于创建MixedSqlNode 实例。从 MixedSqlNode 类名上可知,它会存储多种类型的 SqlNode。
SqlNode的不同实现类用于处理不同的动态 SQL 逻辑,这些 SqlNode是由各种 NodeHandler 生成。之前代码中的handler.handleNode(child, contents);
就是用于处理动态 SQL 节点,并生成相应的 SqlNode。
看一个XMLScriptBuilder中的WhereHandler例子:
private class WhereHandler implements NodeHandler {
public WhereHandler() {
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
// 调用 parseDynamicTags 解析 节点
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
// 创建 WhereSqlNode
WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
// 添加到 targetContents
targetContents.add(where);
}
}
handleNode 方法内部会再次调用 parseDynamicTags 解析节点中的内容,这样又会生成一个 MixedSqlNode 对象。最终,整个 SQL 语句节点会生成一个具有树状结构的 MixedSqlNode。
到此,SQL 语句的解析过程就分析完了。现在,我们已经将 XML 配置解析了 SqlSource,但这还没有结束。SqlSource 中只能记录 SQL 语句信息,除此之外,这里还有一些额外的信息需要记录。因此,我们需要一个类能够同时存储 SqlSource 和其他的信息。这个类就是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;
// 创建建造器,设置各种属性
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
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
// 构建 MappedStatement
MappedStatement statement = statementBuilder.build();
// 添加 MappedStatement 到 configuration 的 mappedStatements 集合中
configuration.addMappedStatement(statement);
return statement;
}
上面就是 MappedStatement 的构建过程。
映射文件解析完成后,并不意味着整个解析过程就结束了。此时还需要通过命名空间绑定 mapper 接口,这样才能将映射文件中的 SQL 语句和 mapper 接口中的方法绑定在一起,后续可直接通过调用 mapper 接口方法执行与之对应的 SQL 语句。
mapper 接口的绑定过程从XMLMapperBuilder开始:
private void bindMapperForNamespace() {
// 获取映射文件的命名空间
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
// 根据命名空间解析 mapper 类型
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
// ignore, bound type is not required
}
// 检测当前 mapper 类是否被绑定过
if (boundType != null && !configuration.hasMapper(boundType)) {
// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
configuration.addLoadedResource("namespace:" + namespace);
// 绑定 mapper 类
configuration.addMapper(boundType);
}
}
}
接着继续追踪到Configuration(位于org.apache.ibatis.session):
public <T> void addMapper(Class<T> type) {
// 通过 MapperRegistry 绑定 mapper 类
mapperRegistry.addMapper(type);
}
接着继续追踪到MapperRegistry(位于org.apache.ibatis.binding):
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 将 type 和 MapperProxyFactory 进行绑定,
// MapperProxyFactory 可为 mapper 接口生成代理类
knownMappers.put(type, new MapperProxyFactory<>(type));
// 创建注解解析器。在 MyBatis 中,有 XML 和 注解两种配置方式可选
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
// 解析注解中的信息
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
Mapper 接口的绑定过程,这里简单总结一下:
- 获取命名空间,并根据命名空间解析 mapper 类型
- 将 type 和 MapperProxyFactory 实例存入 knownMappers 中
- 解析注解中的信息
在解析某些节点的过程中,如果这些节点引用了其他一些未被解析的配置,会导致当前节点解析工作无法进行下去。对于这种情况,MyBatis 的做法是抛出 IncompleteElementException异常。外部逻辑会捕捉这个异常,并将节点对应的解析器放入 incomplet*集合中。先看XMLMapperBuilder:
public void parse() {
// 省略部分代码
// 解析 mapper 节点
configurationElement(parser.evalNode("/mapper"));
// 处理未完成解析的节点
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
parse 方法是映射文件的解析入口。上面三个以 parsePending 开头的方法逻辑一致,所以下面我只会分析其中一个方法的源码。简单起见,选择分析 parsePendingCacheRefs 的源码。先看个
节点无法完成解析的例子:
<mapper namespace="xyz.coolblog.dao.Mapper1">
<cache-ref namespace="xyz.coolblog.dao.Mapper2"/>
mapper>
<mapper namespace="xyz.coolblog.dao.Mapper2">
<cache/>
mapper>
假设 MyBatis 先解析映射文件 1,然后再解析映射文件 2。按照这样的解析顺序,映射文件 1 中的
节点就无法完成解析,因为它所引用的缓存还未被解析。当映射文件 2 解析完成后,MyBatis 会调用parsePendingCacheRefs 方法处理在此之前未完成解析的
节点。
private void parsePendingCacheRefs() {
// 获取 CacheRefResolver 列表
Collection<CacheRefResolver> incompleteCacheRefs = configuration.getIncompleteCacheRefs();
synchronized (incompleteCacheRefs) {
Iterator<CacheRefResolver> iter = incompleteCacheRefs.iterator();
// 通过迭代器遍历列表
while (iter.hasNext()) {
try {
// 尝试解析 节点,若解析失败,则抛出
// IncompleteElementException,此时下面的删除操作不会被执行
iter.next().resolveCacheRef();
// 移除 CacheRefResolver 对象。如果代码能执行到此处,
// 表明已成功解析了 节点
iter.remove();
} catch (IncompleteElementException e) {
// 如果再次发生 IncompleteElementException 异常,表明当前
// 映射文件中并没有所引用的缓存。有可能所引用的缓存
// 在后面的映射文件中,所以这里不能将解析失败的 CacheRefResolver
// 从集合中删除
}
}
}
}
上面的逻辑比较简单,这里简单总结一下:
- 获取获取 CacheRefResolver 列表,并进行遍历
- 尝试解析
节点,若解析失败再次抛出异常
- 若解析成功则列表中移除相关节点