概述
前几篇我们介绍了MyBatis的一些基本特性,对MyBatis有了个初步了解。接下来,我们将着手来分析一下MyBatis的源码,从源码层面复盘MyBatis的执行流程。
思维导图概括
配置文件解析过程分析
有了上述思维导图,我们对配置文件文件的解析过程就有了一个大概的认识,下面我们就来具体分析下解析过程。
配置文件解析入口
首先,我们来看看调用MyBatis的示例代码
String resource = "chapter1/mybatis-cfg.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
从上述示例代码中我们可以很清晰的看出,初始化过程是首先通过Resources 解析配置文件得到文件流。然后,将文件流传给SqlSessionFactoryBuilder的build方法,并最终得到sqlSessionFactory。
那么我们MyBatis的初始化入口就是SqlSessionFactoryBuilder的build 方法。
//* SqlSessionFactoryBuilder类
//以下3个方法都是调用下面第8种方法
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
//第8种方法和第4种方法差不多,Reader换成了InputStream
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
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.
}
}
}
//最后一个build方法使用了一个Configuration作为参数,并返回DefaultSqlSessionFactory
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
从上述源码,我们可以知道build 构建SqlSessionFactory 分为两步,首先 实例化一个XMLConfigBuilder,然后,调用XMLConfigBuilder的parse方法得到Configuration对象,最后将Configuration对象作为参数实例化一个DefaultSqlSessionFactory 即SqlSessionFactory对象。
接着往下看,下面我们来看看XMLConfigBuilder类。首先是实例化XMLConfigBuilder的过程。
//* XMLConfigBuilder
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
//上面6个构造函数最后都合流到这个函数,传入XPathParser
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
//首先调用父类初始化Configuration
super(new Configuration());
//错误上下文设置成SQL Mapper Configuration(XML文件配置),以便后面出错了报错用吧
ErrorContext.instance().resource("SQL Mapper Configuration");
//将Properties全部设置到Configuration里面去
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
//* XPathParser
public XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver) {
commonConstructor(validation, variables, entityResolver);
this.document = createDocument(new InputSource(reader));
}
private Document createDocument(InputSource inputSource) {
// important: this must only be called AFTER common constructor
try {
//这个是DOM解析方式
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setValidating(validation);
//名称空间
factory.setNamespaceAware(false);
//忽略注释
factory.setIgnoringComments(true);
//忽略空白
factory.setIgnoringElementContentWhitespace(false);
//把 CDATA 节点转换为 Text 节点
factory.setCoalescing(false);
//扩展实体引用
factory.setExpandEntityReferences(true);
DocumentBuilder builder = factory.newDocumentBuilder();
//需要注意的就是定义了EntityResolver(XMLMapperEntityResolver),这样不用联网去获取DTD,
//将DTD放在org\apache\ibatis\builder\xml\mybatis-3-config.dtd,来达到验证xml合法性的目的
builder.setEntityResolver(entityResolver);
builder.setErrorHandler(new ErrorHandler() {
@Override
public void error(SAXParseException exception) throws SAXException {
throw exception;
}
@Override
public void fatalError(SAXParseException exception) throws SAXException {
throw exception;
}
@Override
public void warning(SAXParseException exception) throws SAXException {
}
});
return builder.parse(inputSource);
} catch (Exception e) {
throw new BuilderException("Error creating document instance. Cause: " + e, e);
}
}
从上述源码中,我们可以看出在XMLConfigBuilder的实例化过程包括两个过程,1. 创建XPathParser的实例并初始化;2.创建Configuration的实例对象,然后将XPathParser的实例设置到XMLConfigBuilder中,而XPathParser 初始化主要做了两件事,初始化DocumentBuilder对象,并通过调用DocumentBuilder对象的parse方法得到Document对象,我们配置文件的配置就全部都转移到了Document对象中。我们下面通过调试看看Document 对象中的内容,测试用例是MyBatis 自身的单元测试XPathParserTest
测试的xml
<!--
nodelet_test.xml
-->
<employee id="${id_var}">
<blah something="that"/>
<first_name>Jim</first_name>
<last_name>Smith</last_name>
<birth_date>
<year>1970</year>
<month>6</month>
<day>15</day>
</birth_date>
<height units="ft">5.8</height>
<weight units="lbs">200</weight>
<active>true</active>
</employee>
测试用例:
//* XPathParserTest
@Test
public void shouldTestXPathParserMethods() throws Exception {
String resource = "resources/nodelet_test.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
XPathParser parser = new XPathParser(inputStream, false, null, null);
assertEquals((Long)1970l, parser.evalLong("/employee/birth_date/year"));
assertEquals((short) 6, (short) parser.evalShort("/employee/birth_date/month"));
assertEquals((Integer) 15, parser.evalInteger("/employee/birth_date/day"));
assertEquals((Float) 5.8f, parser.evalFloat("/employee/height"));
assertEquals((Double) 5.8d, parser.evalDouble("/employee/height"));
assertEquals("${id_var}", parser.evalString("/employee/@id"));
assertEquals(Boolean.TRUE, parser.evalBoolean("/employee/active"));
assertEquals("<id>${id_var}</id>", parser.evalNode("/employee/@id").toString().trim());
assertEquals(7, parser.evalNodes("/employee/*").size());
XNode node = parser.evalNode("/employee/height");
assertEquals("employee/height", node.getPath());
assertEquals("employee[${id_var}]_height", node.getValueBasedIdentifier());
}
调试结果:
介绍完XMLConfigBuilder的初始化过程之后,接着我们来看看XMLConfigBuilder中的parse()方法,由前面其初始化过程我们可以得知我们的配置信息已经保存到了XMLConfigBuilder的XPathParser对象的Document中了。解析来其实就是将XPathParser中的信息转移到Configuration对象中,不多说了,看看源码。
//* XMLConfigBuilder
//解析配置
public Configuration parse() {
//如果已经解析过了,报错
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//根节点是configuration
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
//解析配置
private void parseConfiguration(XNode root) {
try {
//分步骤解析
//issue #117 read properties first
//1.properties
propertiesElement(root.evalNode("properties"));
//2.类型别名
typeAliasesElement(root.evalNode("typeAliases"));
//3.插件
pluginElement(root.evalNode("plugins"));
//4.对象工厂
objectFactoryElement(root.evalNode("objectFactory"));
//5.对象包装工厂
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
//6.设置
settingsElement(root.evalNode("settings"));
// read it after objectFactory and objectWrapperFactory issue #631
//7.环境
environmentsElement(root.evalNode("environments"));
//8.databaseIdProvider
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//9.类型处理器
typeHandlerElement(root.evalNode("typeHandlers"));
//10.映射器
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
至此,一个MyBatis的解析过程就出来了,每个配置的解析逻辑封装在相应的方法中,接下来将重点介绍一些常用的配置,例如properties,settings,environments,typeAliases, typeHandler, mappers。闲话少叙,接下来我们首先来分析下properties的解析过程
解析properties配置
首先我们来看看一个普通的properties配置。
<properties resource="org/mybatis/example/config.properties">
<property name="username" value="dev_user"/>
<property name="password" value="F2Fa3!33TYyg"/>
</properties>
- 1
- 2
- 3
- 4
//* XMLConfigBuilder
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
//如果在这些地方,属性多于一个的话,MyBatis 按照如下的顺序加载它们:
//1.在 properties 元素体内指定的属性首先被读取。
//2.从类路径下资源或 properties 元素的 url 属性中加载的属性第二被读取,它会覆盖已经存在的完全一样的属性。
//3.作为方法参数传递的属性最后被读取, 它也会覆盖任一已经存在的完全一样的属性,这些属性可能是从 properties 元素体内和资源/url 属性中加载的。
//传入方式是调用构造函数时传入,public XMLConfigBuilder(Reader reader, String environment, Properties props)
//1.XNode.getChildrenAsProperties函数方便得到孩子所有Properties
Properties defaults = context.getChildrenAsProperties();
//2.然后查找resource或者url,加入前面的Properties
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));
}
//3.Variables也全部加入Properties
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
parser.setVariables(defaults);
//4. 将属性值设置到configuration中
configuration.setVariables(defaults);
}
}
/**
* //得到孩子,返回Properties,孩子的格式肯定都有name,value属性
* @return
*/
public Properties getChildrenAsProperties() {
Properties properties = new Properties();
for (XNode child : getChildren()) {
String name = child.getStringAttribute("name");
String value = child.getStringAttribute("value");
if (name != null && value != null) {
// 设置属性到属性对象中
properties.setProperty(name, value);
}
}
return properties;
}
代码中注释比较详实,代码结构不太复杂,读者们看下就会明白。不过需要特别说明:properties元素的解析顺序是:
1. 在Properties 元素体内指定的属性首先被读取。
2. 在类路径下资源或properties元素的url 属性中加载的属性第二个被读取,它会覆盖完全一样的属性
3. 作为方法参数传递的属性最后被读取,它也会覆盖任一已存在的完全一样的属性,这些属性可能是从properties 元素体内和资源 /url 属性中加载的。
//传入方式是调用构造函数时传入,public XMLConfigBuilder(Reader reader, String environment, Properties props)
解析settings配置
settings相关配置是MyBatis中非常重要的配置,这些配置用户调整MyBatis运行时的行为。settings配置繁多,在对这些配置不熟悉的情况下,保持默认的配置即可。详细的配置说明可以参考MyBatis官方文档setting
我们先看看一个settings 的简单配置
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="multipleResultSetsEnabled" value="true"/>
</settings>
接下来我们来看看setting的解析源码。
//*XMLConfigBuilder
private void settingsElement(XNode context) throws Exception {
if (context != null) {
// 获取settings子节点中的内容
Properties props = context.getChildrenAsProperties();
// 创建Configuration 类的"元信息"对象
MetaClass metaConfig = MetaClass.forClass(Configuration.class);
for (Object key : props.keySet()) {
// Check that all settings are known to the configuration class
//检查下是否在Configuration类里都有相应的setter方法(没有拼写错误)
if (!metaConfig.hasSetter(String.valueOf(key))) {
throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive).");
}
}
从上述源码中我们可以总结出setting 的解析主要分为如下几个步骤:
- 获取settings 子节点中的内容,这段代码在之前已经解释过,再次不在赘述。
- 然后就是创建Configuration类的“元信息”对象,在这一部分中出现了一个陌生的类MetaClass,我们一会在分析。
- 接着检查是否在Configuration类里都有相应的setter方法,不存在则抛出异常。
- 若通过MetaClass的检测,则将Properties中的信息设置到configuration对象中,逻辑结束
上述代码看似简单,实际上在第二步创建元信息对象还是蛮复杂的。接下来我们就来看看MetaClass类
//*MetaClass
public class MetaClass {
//有一个反射器
//可以看到方法基本都是再次委派给这个Reflector
private Reflector reflector;
private MetaClass(Class<?> type) {
// 根据类型创建Reflector
this.reflector = Reflector.forClass(type);
}
public static MetaClass forClass(Class<?> type) {
// 调用构造器方法
return new MetaClass(type);
}
/**
* 检查指定的属性是否有setter方法。
* @param name
* @return
*/
public boolean hasSetter(String name) {
// 属性分词器,用于解析属性名
PropertyTokenizer prop = new PropertyTokenizer(name);
// hasNext返回true,则表明是一个复合属性
if (prop.hasNext()) {
// 调用reflector的hasSetter方法
if (reflector.hasSetter(prop.getName())) {
// 为属性创建MetaClass
MetaClass metaProp = metaClassForProperty(prop.getName());
// 再次调用hasSetter
return metaProp.hasSetter(prop.getChildren());
} else {
return false;
}
} else {
// 非复合属性则直接调用hasSetter一次即可
return reflector.hasSetter(prop.getName());
}
}
public MetaClass metaClassForProperty(String name) {
Class<?> propType = reflector.getGetterType(name);
return MetaClass.forClass(propType);
}
从源码我们可以看出MetaClass 的forClass 方法最终委托给了这个Reflector的forClass方法。而hasSetter 方法中又调用了reflector的hasSetter方法,那么Reflector类内部实现如何呢?同时我们还注意到出现了一个新的类PropertyTokenizer,那么这个类内部实现如何呢?我们待会再来分析下。首先我们简单介绍下这几个类。
Reflector -----> 反射器,用于解析和存储目标类的元信息
PropertyTokenizer -----> 属性分词器,用于解析属性名。
接下来,我们来看看Reflector的相关实现。
Reflector 类的源码较多,在此处我们不做一一分析。我主要从以下三个方面:
- Reflector的构造方法和成员变量分析
- getter 方法解析过程分析
- setter 方法解析过程分析
//* Reflector
private static boolean classCacheEnabled = true;
private static final String[] EMPTY_STRING_ARRAY = new String[0];
//这里用ConcurrentHashMap,多线程支持,作为一个缓存
private static final Map<Class<?>, Reflector> REFLECTOR_MAP = new ConcurrentHashMap<Class<?>, Reflector>();
private Class<?> type;
//getter的属性列表
private String[] readablePropertyNames = EMPTY_STRING_ARRAY;
//setter的属性列表
private String[] writeablePropertyNames = EMPTY_STRING_ARRAY;
//setter的方法列表
private Map<String, Invoker> setMethods = new HashMap<String, Invoker>();
//getter的方法列表
private Map<String, Invoker> getMethods = new HashMap<String, Invoker>();
//setter的类型列表
private Map<String, Class<?>> setTypes = new HashMap<String, Class<?>>();
//getter的类型列表
private Map<String, Class<?>> getTypes = new HashMap<String, Class<?>>();
//构造函数
private Constructor<?> defaultConstructor;
private Map<String, String> caseInsensitivePropertyMap = new HashMap<String, String>();
/**
* 得到某个类的反射器,是静态方法,而且要缓存,
* 又要多线程,所以REFLECTOR_MAP是一个ConcurrentHashMap
*/
public static Reflector forClass(Class<?> clazz) {
if (classCacheEnabled) {
// synchronized (clazz) removed see issue #461
//对于每个类来说,我们假设它是不会变的,这样可以考虑将这个类的信息
// (构造函数,getter,setter,字段)加入缓存,以提高速度
Reflector cached = REFLECTOR_MAP.get(clazz);
if (cached == null) {
cached = new Reflector(clazz);
REFLECTOR_MAP.put(clazz, cached);
}
return cached;
} else {
return new Reflector(clazz);
}
}
private 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[getMethods.keySet().size()]);
// 从setMethods 映射中获取可写属性名数组
writeablePropertyNames = setMethods.keySet().toArray(new String[setMethods.keySet().size()]);
//将所有属性名的大写形式作为键,属性名作为值,存入到caseInsensitivePropertyMap中
for (String propName : readablePropertyNames) {
caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
}
for (String propName : writeablePropertyNames) {
caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
}
}
//省略其他方法
如上,Reflector 定义了一个ConcurrentHashMap 用于缓存每个类的反射器,以提高速度。我们知道ConcurrentHashMap是一个线程安全类,所以不存在线程安全问题。同时,其他的集合用于存储getter,setter 方法的相关信息。构造器里会讲元信息里里的构造方法,属性字段,setter方法,getter方法设置到相应的集合中。
接下来,我们来分析下getter方法。
//* Reflector
private void addGetMethods(Class<?> cls) {
Map<String, List<Method>> conflictingGetters = new HashMap<String, List<Method>>();
// 获取当前类,接口,以及父类中的方法。该方法逻辑不是很复杂
Method[] methods = getClassMethods(cls);
for (Method method : methods) {
// getter方法不应该有参数,若存在参数,则忽略当前方法
if (method.getParameterTypes().length > 0) {
continue;
}
String name = method.getName();
// 过滤出以get或is开头的方法
if (name.startsWith("get") && name.length() > 3) {
if (method.getParameterTypes().length == 0) {
// 将getXXX方法名转成相应的属性,比如 getName -> name
name = PropertyNamer.methodToProperty(name);
/* 将冲突的方法添加到conflictingGetters中,考虑这样一种情况
getTitle和isTitle两个方法经过methodToProperty处理,
均得到 name=title,这会导致冲突
对于冲突的方法,这里想统一存起来,后续在解决冲突
*/
addMethodConflict(conflictingGetters, name, method);
}
} else if (name.startsWith("is") && name.length() > 2) {
if (method.getParameterTypes().length == 0) {
name = PropertyNamer.methodToProperty(name);
addMethodConflict(conflictingGetters, name, method);
}
}
}
// 处理getter冲突
resolveGetterConflicts(conflictingGetters);
}
如上, addGetMethods 方法的的执行流程如下:
- 获取当前类,接口,以及父类中的方法
- 遍历上一步获取的方法数组,并过滤出以get和is开头方法
- 根据方法名截取出属性名
- 将冲突的属性名和方法对象添加到冲突集合中
- 处理getter冲突,筛选出合适的方法。
我们知道getter截取属性冲突主要是由于 getXXX() 和isXXX() 两种类型的方法,截取属性后会冲突。
比较核心的知识点就是处理getter 冲突,接下来,我们就来看看相应的源码
//* Reflector
/**
* // 添加属性名和方法对象到冲突集合中
* @param conflictingMethods
* @param name
* @param method
*/
private void addMethodConflict(Map<String, List<Method>> conflictingMethods, String name, Method method) {
List<Method> list = conflictingMethods.get(name);
if (list == null) {
list = new ArrayList<Method>();
conflictingMethods.put(name, list);
}
list.add(method);
}
/**
* 解决冲突
* @param conflictingGetters
*/
private void resolveGetterConflicts(Map<String, List<Method>> conflictingGetters) {
for (String propName : conflictingGetters.keySet()) {
List<Method> getters = conflictingGetters.get(propName);
Iterator<Method> iterator = getters.iterator();
Method firstMethod = iterator.next();
if (getters.size() == 1) {
addGetMethod(propName, firstMethod);
} else {
Method getter = firstMethod;
// 获取返回值类型
Class<?> getterType = firstMethod.getReturnType();
while (iterator.hasNext()) {
Method method = iterator.next();
Class<?> methodType = method.getReturnType();
/**
* 两个方法的返回值类型一致,若两个方法返回值类型均为boolean,则选取isXXX方法
* 为getterType,则无法决定哪个方法更为合适,只能抛出异常
*
* */
if (methodType.equals(getterType)) {
throw new ReflectionException("Illegal overloaded getter method with ambiguous type for property "
+ propName + " in class " + firstMethod.getDeclaringClass()
+ ". This breaks the JavaBeans " + "specification and can cause unpredicatble results.");
/**
* getterType是methodType的子类,类型上更为具体
* 则认为当前的getter 是合适的,无需做什么事情
*
* */
} else if (methodType.isAssignableFrom(getterType)) {
// OK getter type is descendant
/**
* methodType 是getterType的子类,此时认为method方法更为合适,
* 故将getter更新为method
*/
} else if (getterType.isAssignableFrom(methodType)) {
getter = method;
getterType = methodType;
} else {
throw new ReflectionException("Illegal overloaded getter method with ambiguous type for property "
+ propName + " in class " + firstMethod.getDeclaringClass()
+ ". This breaks the JavaBeans " + "specification and can cause unpredicatble results.");
}
}
// 将筛选出的方法添加到getMethods中,并将方法返回值添加到getType中
addGetMethod(propName, getter);
}
}
}
private void addGetMethod(String name, Method method) {
if (isValidPropertyName(name)) {
// 解析返回值类型
getMethods.put(name, new MethodInvoker(method));
// 将返回值类型由Type 转为Class,并将转换后的结果缓存到getTypes中
getTypes.put(name, method.getReturnType());
}
}
如上,该处理getter冲突的的过程,代码较长,在这里大家只要记住处理冲突的规则就能够理解上面的逻辑:
- 冲突方法返回值类型具有继承关系,则认为子类的方法更加合适。
- 冲突方法返回值类型相同,则无法确定有用哪个方法,直接抛出异常。
- 冲突方法返回值类型完全不相关,则无法确定有用哪个方法,抛出异常。
我们来看看MyBatis的测试用例理解下ReflectorTest
。。。。。。。。。。。。。。。。。
版权原因,完整文章,请参考如下: