前言
每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的Configuration实例来构建出 SqlSessionFactory 实例。
MyBatis工作流程简介
MyBatis的运行流程可以分为三大阶段
- 初始化阶段:读取xml配置文件和注解中的配置信息,创建Configuration配置对象,并完成各个模块的初始化工作
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
- 代理封装阶段:封装iBatis的编程模型,使用mapper接口开发的初始化工作
try (SqlSession session = sqlSessionFactory.openSession()) {
CityMapper cityMapper = session.getMapper(CityMapper.class);
}
- 数据访问阶段:通过sqlSession完成SQL的解析,参数的映射、SQL的执行、结果的解析过程。
List<City> cityList = cityMapper.findAll();
MyBatis初始化
通过SqlSessionFactoryBuilder创建SqlSessionFactory的过程就是读取配置文件并初始化Configuration对象的过程。为什么这么说呢?我们可以看一下源码
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
1. 由于只会build一次 所以XMLConfigBuilder全局只有一个
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
2. 根据解析器解析的Configuration对象构造DefaultSqlSessionFactory对象 重点全部都在parse里面
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) {
return new DefaultSqlSessionFactory(config);
}
在build方法中,parser.parse()返回一个Configuration对象,然后作为DefaultSqlSessionFactory的构造参数构造一个SqlSessionFactory对象。如下所示
不难看出,这个构造过程相当的简单,只是一个简单的赋值过程。所以整个初始化的阶段的重心是构造一个全局唯一的Configuration对象。整个过程涉及两个大步骤:
- 根据读取的mybatis配置文件流(比如mybatis-config.xml)构造XMLConfigBuilder对象
从 XML 文件中构建 SqlSessionFactory 的实例非常简单,建议使用类路径下的资源文件进行配置。 但也可以使用任意的输入流(InputStream)实例,比如用文件路径字符串或 file:// URL 构造的输入流。MyBatis 包含一个名叫 Resources 的工具类,它包含一些实用方法,使得从类路径或其它位置加载资源文件更加容易。
- 通过XMLConfigBuilder解析配置文件
1. 构造XMLConfigBuilder对象
XMLConfigBuilder是专门用于解析MyBatis配置文件的工具类,这个类继承自BaseBuilder。同时继承这个类的还有很多其他的Builder,对应的关系图如下所示
从这个图首先可以看出,通过XMLConfigBuilder会创建XMLMapperBuilder,而XMLMapperBuilder又会创建XMLStatementBuilder和MapperBuilderAssistant,可谓环环相扣。其中XMLConfigBuilder解析配置文件直到mappers节点的时候,就需要解析MyBatis对应的mapper配置文件了,这个时候将工作交给XMLMapperBuilder来处理,同样当XMLMapperBuilder解析到复杂的SQL节点时又会将解析工作交给XMLStatementBuilder和MapperBuilderAssistant来解析,当然其中还会使用到其他的Builder。XMLConfigBuilder的主要构造参数包括配置文件对应的输入流,另外两个参数默认为空。
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
在内部构造一个XPathParser对象,这个对象主要是用于处理xml的,这样将繁杂的xml各种处理都统一到一个工具类中。
public XPathParser(InputStream inputStream, boolean validation, Properties variables,
EntityResolver entityResolver) {
// 配置一些用于读取xml文件的工具类
commonConstructor(validation, variables, entityResolver);
// 读取配置文件创建Document对象
this.document = createDocument(new InputSource(inputStream));
}
通过一系列的操作构造org.w3c.dom.Document对象,后续通过这个对象就可以读取xml配置文件中的各个节点的信息了。
这个类的核心方法如下所示
public List<XNode> evalNodes(String expression) {
return evalNodes(document, expression);
}
public List<XNode> evalNodes(Object root, String expression) {
List<XNode> xnodes = new ArrayList<XNode>();
NodeList nodes = (NodeList) evaluate(expression, root, XPathConstants.NODESET);
for (int i = 0; i < nodes.getLength(); i++) {
xnodes.add(new XNode(this, nodes.item(i), variables));
}
return xnodes;
}
public XNode evalNode(String expression) {
return evalNode(document, expression);
}
public XNode evalNode(Object root, String expression) {
Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
if (node == null) {
return null;
}
return new XNode(this, node, variables);
}
可以看到这里读取xml文件的Node 节点之后都是要转为XNode对象。
private final Node node;
private final String name;
private final String body;
private final Properties attributes;
private final Properties variables;
private final XPathParser xpathParser;
public XNode(XPathParser xpathParser, Node node, Properties variables) {
this.xpathParser = xpathParser;
this.node = node;
this.name = node.getNodeName();
// 用于解析占位符
this.variables = variables;
// 构造Node节点获取属性时 需要根据variables进行替换
this.attributes = parseAttributes(node);
// 获取主题信息时 也会根据variables进行替换
this.body = parseBody(node);
}
为什么要这么麻烦呢?我们知道在MyBatis的配置文件当中可以使用占位符模式来设置各种值。比如
<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=""/>
</dataSource>
</environment>
</environments>
这些包含${}的占位符需要在解析的过程中替换为真实的值。而MyBatis把这种共性事件直接放到构造XNode中来了,因为每个Node对象够要先转为XNode,转为XNode之后再进行其他的解析操作,这样一方面可以让后续的解析操作无需关心是否对应的值包含有占位符,同时保证占位符的解析操作在其他操作之前。在这个构造方法当中parseAttributes和parseBody分别为解析节点上的属性以及主体内容,在解析的过程中都会根据传入的variables参数替换占位符,在这里都是通过工具类org.apache.ibatis.parsing.PropertyParser来完成的。 源码如下
private Properties parseAttributes(Node n) {
Properties attributes = new Properties();
NamedNodeMap attributeNodes = n.getAttributes();
if (attributeNodes != null) {
for (int i = 0; i < attributeNodes.getLength(); i++) {
Node attribute = attributeNodes.item(i);
1. 替换占位符
String value = PropertyParser.parse(attribute.getNodeValue(), variables);
attributes.put(attribute.getNodeName(), value);
}
}
return attributes;
}
private String parseBody(Node node) {
String data = getBodyData(node);
if (data == null) {
NodeList children = node.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
data = getBodyData(child);
if (data != null) {
break;
}
}
}
return data;
}
private String getBodyData(Node child) {
if (child.getNodeType() == Node.CDATA_SECTION_NODE || child.getNodeType() == Node.TEXT_NODE) {
String data = ((CharacterData) child).getData();
1. 替换占位符
data = PropertyParser.parse(data, variables);
return data;
}
return null;
}
XNode除了以上的功能之外,还封装了处理xml的一些公共方法,比如
public Properties getChildrenAsProperties() {
Properties properties = new Properties();
// 此处getChildren获取XNode 构造过程中都会进行替换占位符的操作
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;
}
这个方法会让读取以下这些格式(包含name和value属性)的子节点变得容器。转变为Properties对象之后,就屏蔽了与xml文件交互的复杂性,为后面的解析工作简单化。还有一系列的evalShort、evalInteger、evalLong等等,读取节点值并转为对应的类型。以上这些无疑都是符合单一职责原则的,所有读取值并解析为目标值(占位符解析、类型转化)都是由XNode来完成的。
<properties resource="config.properties">
<property name="logPrefix" value="org.apache.ibatis."/>
</properties>
<settings>
<setting name="logPrefix" value="${logPrefix}"/>
<setting name="cacheEnabled" value="false"/>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
构造完成XPathParser之后,继续构造XMLConfigBuilder对象。
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
//XMLConfigBuilder全局唯一 唯一构造Configration 全局唯一实例 此时会初始化Configuration的各种默认属性配置
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
// 设置全局配置属性
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
另外在这里构造了全局唯一的Configuration对象。Configuration对象的构造会涉及到很多的初始化工作以及默认属性设置,比如注册了很多的别名,初始化了默认的反射工厂reflectorFactory、对象构造工厂objectFactory和对象包装工厂objectWrapperFactory。后续的大部分工作无非就是修改默认的配置以及填充各种属性。
protected Properties variables = new Properties();
// 反射工厂 根据对象类型创建反射器对象
protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
// 对象构造工厂
protected ObjectFactory objectFactory = new DefaultObjectFactory();
// 对象包装工厂
protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
public Configuration() {
// 注册事务工厂别名
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
// 注册数据源工厂别名
typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
....此处省略
}
2. 解析配置文件
解析MyBatis系统配置文件就是按照配置文件的格式一个一个的解析,首先最外层的就是configuration节点,而且这个节点只有一个。
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// 解析config配置文件开始 标签configuration 后面有很多这种evalNode方法 其实都是MyBatis针对原生java中Node的封装 封装时会做一个很重要的操作:parseAttributes
// 此时会针对属性进行占位符替换的操作
// 关键是在parseConfiguration方法
// 获取root节点 并解析config文件 mapper文件 mapper接口信息填充到Configuration中
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
针对configuration节点的解析根据子元素的不同分为以下十几个步骤。
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
1. 首先解析properties节点 其实就是读取配置文件 并设置configuration的variables属性 同时parser也会保存一份
propertiesElement(root.evalNode("properties"));
2. 读取settings配置 并验证Configuration包含对应属性 并不设置 在settingsElement中才会设置
Properties settings = settingsAsProperties(root.evalNode("settings"));
3. 读取vfsImpl并设置到configuration VFS文件系统 MyBatis包含默认的文件系统实现类 为啥需要文件系统呢?比如设置别名时整个路径下的所有类都会生成别名 此时就需要遍历文件夹了
4. MyBatis通过ResolverUtil来实现加载特定目标下满足设置条件的所有类
loadCustomVfs(settings);
5. 解析别名并设置到configuration的typeAliasRegistry属性当中 有批量注册和单个注册两种方式 而批量注册就涉及到VFS文件系统
typeAliasesElement(root.evalNode("typeAliases"));
6. 解析插件 支持别名 所以必须放在解析别名之后 添加插件到configuration到interceptorChain的interceptors列表当中
pluginElement(root.evalNode("plugins"));
7. 自定义objectFactory 支持type配置别名
objectFactoryElement(root.evalNode("objectFactory")); // 解析ObjectFactory
8. 自定义ObjectWrapperFactory 支持type配置别名
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); // 解析WarpperFactory
9. 自定义reflectorFactory 支持type配置别名
reflectorFactoryElement(root.evalNode("reflectorFactory"));
10. 根据settings配置初始化configuration各种属性 如果不存在 可能会使用默认值
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
11. 解析环境配置 包括事务、数据源信息 创建数据源会使用到反射模块
environmentsElement(root.evalNode("environments"));
12. 根据type配置(支持别名)获取databaseIdProvider 并解析对应子元素为properties配置
并根据上一步骤中解析的数据源读取数据库的产品名称 并根据properties配置映射databaseId
设置databaseId到configuration当中
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
13. 注册TypeHandler 主要是javaType、jdbcType、TypeHandler的各种映射关系 参考TypeHandlerRegistry类
typeHandlerElement(root.evalNode("typeHandlers"));
14. 解析mappers节点 同样支持多文件扫描和单文件查找
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
但是并不是按照子元素的顺序从上而下的。比如typeHandlers本来在xml中排在很前面而在解析时是放在倒数第二的。但是这并不说明解析的顺序有问题,相反,这里的解析可谓是层层递进的,甚至是后面需要依赖前面的解析结果。
比如说解析properties节点必须放在第一位,这是为什么?首先我们分析解析这个节点干了什么
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
// 1、 读取所有的子节点作为Properties对象
Properties defaults = context.getChildrenAsProperties();
// 2、 读取配置的文件作为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) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
// 3. 将读取的配置信息设置到所需的地方
parser.setVariables(defaults);
configuration.setVariables(defaults);
}
}
从这里可以看到,就是解析这个节点下面的子元素和在节点属性配置的文件最后构造一个Properties属性并设置到parser和configuration。设置到了parser之后,这样之后通过XPathParser#evalNodes和XPathParser#evalNode将Node节点构造为XNode的节点都会带上这些配置信息,也就是说如果存在占位符,这些配置信息就是用于替换占位符中的信息的。之所以把解析properties这个节点放在第一位,就是保证所有的其他节点都支持通过占位符配置信息。
然后第二步为啥需要是settings这个节点呢?其实这里可能要结合第三步要一起讲。我们首先看一下第三步要干什么?
private void loadCustomVfs(Properties props) throws ClassNotFoundException {
String value = props.getProperty("vfsImpl");
if (value != null) {
String[] clazzes = value.split(",");
for (String clazz : clazzes) {
if (!clazz.isEmpty()) {
@SuppressWarnings("unchecked")
Class<? extends VFS> vfsImpl = (Class<? extends VFS>) Resources.classForName(clazz);
configuration.setVfsImpl(vfsImpl);
}
}
}
}
这里是从setting配置信息中获取vfsImpl的配置值并设置到configuration中。其实在上一部中,会从setting节点中读取配置信息并通过反射验证这些配置信息的有效性,对于MyBatis反射模块不了解的话,可以阅读上一章内容。
private Properties settingsAsProperties(XNode context) {
if (context == null) {
return new Properties();
}
Properties props = context.getChildrenAsProperties();
// Check that all settings are known to the configuration class
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
for (Object key : props.keySet()) {
1. 判断Configuration是不是包含有setting配置中的属性
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;
}
也就是说这里只验证了有效性,并没有设置到configuration属性当中,而接下来的第三步只是把其中一个vfsImpl进行设置。那这个vfsImpl究竟有何重要之处呢?其实这里配置的是org.apache.ibatis.io.VFS的实现类,用于文件系统处理,啥意思呢?我们看一下接下来的typeAliases节点的解析。这个节点用于配置MyBatis的别名,何为别名?其实在上面我们介绍Configuration构造的时候其实已经说到过,如下
不但是Configuration构造时会注册别名,在TypeAliasRegistry构造的时候也会注册一堆的别名。如下
为啥需要别名呢?我们不妨也可以理解为另一个占位符。比如在如下的配置中JDBC就是别名,在真正解析的时候会根据别名查找到JdbcTransactionFactory这个类,而POOLED会查找到PooledDataSourceFactory这个类,也就是通过别名系统,我们可以让配置信息看起来简洁一点。
再比如如下的parameterType可以简写为string,而不需要写完全限定名java.lang.String。
MyBatis已经为我们自动设置了很多的别名,但用户自己自定义的类MyBatis是没办法的,所以只能由用户自己来定义了。定义别名其实有两种方式,一种是单个注册
<typeAliases>
<typeAlias type="sample.mybatis.domain.City" alias="City"/>
</typeAliases>
如果这样的实体类非常多的话,这样配置就麻烦了,所以MyBatis也支持批量配置的方式,也就是配置文件夹的方式。
<typeAliases>
<package name="sample.mybatis.domain"/>
</typeAliases>
如果按照这样的配置,如果是你,该如何解析并注册呢?当然要扫描这个目录,还有子目录,子目录的子目录…,其实这也就是上面VFS的作用了。以下是MyBatis解析typeAliases节点的源码
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
1. 批量注册别名
if ("package".equals(child.getName())) {
String typeAliasPackage = child.getStringAttribute("name");
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
2. 单个注册别名
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
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);
}
}
}
}
}
对于单个注册别名的方式没啥好说的,我们就看批量的。通过getStringAttribute获取到包名,然后按照包名进行注册org.apache.ibatis.type.TypeAliasRegistry#registerAliases(java.lang.String)
。
/**
* 注册指定包路径下所有的类为别名
*
* @param packageName 指定包路径
*/
public void registerAliases(String packageName) {
registerAliases(packageName, Object.class);
}
/**
* 注册别名 指定包路径下的所有类 只要满足是superType的子类
*
* @param packageName 指定的包路径
* @param superType 父类
*/
public void registerAliases(String packageName, Class<?> superType) {
1. 创建一个资源工具类
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
2. 通过这个资源工具类扫描特定的包 如果满足IsA的条件 在这里就是只要是Object的子类即可 就会添加到一个列表中
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
3. 获取在上面查找过程中满足条件的类
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()) {
4. 注册这些符合条件的类
registerAlias(type);
}
}
}
这里用到了ResolverUtil这个工具类。这个类里面就会用到VFS
/** The set of matches being accumulated. */
private Set<Class<? extends T>> matches = new HashSet<Class<? extends T>>();
/**
* Scans for classes starting at the package provided and descending into subpackages.
* Each class is offered up to the Test as it is discovered, and if the Test returns
* true the class is retained. Accumulated classes can be fetched by calling
* {@link #getClasses()}.
*
* @param test an instance of {@link Test} that will be used to filter classes
* @param packageName the name of the package from which to start scanning for
* classes, e.g. {@code net.sourceforge.stripes}
*/
public ResolverUtil<T> find(Test test, String packageName) {
String path = getPackagePath(packageName);
try {
1. 这里通过单例模式获取一个VFS的一个实现类 并调用它的list方法获取到指定路径下所有的文件
List<String> children = VFS.getInstance().list(path);
2. 针对文件进行遍历 如果文件是字节码文件 而且满足对应的条件 就会添加到matches列表当中
for (String child : children) {
// 如果文件是字节码文件 则匹配并添加
if (child.endsWith(".class")) {
addIfMatching(test, child);
}
}
} catch (IOException ioe) {
log.error("Could not read package: " + packageName, ioe);
}
return this;
}
对于addIfMatching代码比较简单,无非调用上面说的IsA判断是不是某个类的子类而已(其实这里还有一个AnnotatedWith的类用于判断是不是一个类包含有特定的注解)。对应的VFS获取单例的getInstance方法如下
/** The built-in implementations. */
public static final Class<?>[] IMPLEMENTATIONS = {JBoss6VFS.class, DefaultVFS.class};
/** The list to which implementations are added by {@link #addImplClass(Class)}. */
public static final List<Class<? extends VFS>> USER_IMPLEMENTATIONS = new ArrayList<Class<? extends VFS>>();
private static final Log log = LogFactory.getLog(VFS.class);
/**
* Get the singleton {@link VFS} instance. If no {@link VFS} implementation can be found for the
* current environment, then this method returns null.
*/
public static VFS getInstance() {
return VFSHolder.INSTANCE;
}
/** Singleton instance holder. */
private static class VFSHolder {
static final VFS INSTANCE = createVFS();
@SuppressWarnings("unchecked")
static VFS createVFS() {
// Try the user implementations first, then the built-ins
List<Class<? extends VFS>> impls = new ArrayList<Class<? extends VFS>>();
impls.addAll(USER_IMPLEMENTATIONS);
impls.addAll(Arrays.asList((Class<? extends VFS>[]) IMPLEMENTATIONS));
// Try each implementation class until a valid one is found
VFS vfs = null;
for (int i = 0; vfs == null || !vfs.isValid(); i++) {
Class<? extends VFS> impl = impls.get(i);
try {
vfs = impl.newInstance();
if (vfs == null || !vfs.isValid()) {
if (log.isDebugEnabled()) {
log.debug("VFS implementation " + impl.getName() +
" is not valid in this environment.");
}
}
} catch (InstantiationException e) {
log.error("Failed to instantiate " + impl, e);
return null;
} catch (IllegalAccessException e) {
log.error("Failed to instantiate " + impl, e);
return null;
}
}
if (log.isDebugEnabled()) {
log.debug("Using VFS adapter " + vfs.getClass().getName());
}
return vfs;
}
}
这是一个使用静态内部类来包装单例的单例方式。我们再回过头来看一下org.apache.ibatis.session.Configuration#setVfsImpl方法,是不是有点明白了。其实这里在VFS默认有一些VFS的实现类,但是有时候可能不能满足用户的要求,所以还允许自己定义读取文件的方式。
public void setVfsImpl(Class<? extends VFS> vfsImpl) {
if (vfsImpl != null) {
this.vfsImpl = vfsImpl;
VFS.addImplClass(this.vfsImpl);
}
}
而这些读取特定目录下文件对于查找特定目录下的类并注册别名又是息息相关的,所以必须在注册别名之前就设置VFS的实现类。而设置VFS的实现类,就必须先解析settings子节点了。所以以上的顺序一个也不能乱,当然VFS不是在注册别名的时候,后面还会有用。在这里注册别名如果扫描到了特定的类,最后会将什么作为别名与这个类相对应呢?我们再回过来看一下
/**
* 注册一个类为别名 类限定名简称或者类上上面Alias注解值:类全限定名
* @param type 类型名称
*/
public void registerAlias(Class<?> type) {
String alias = type.getSimpleName();
Alias aliasAnnotation = type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
alias = aliasAnnotation.value();
}
registerAlias(alias, type);
}
可以看到,这里有两个策略,如果类上面有Alias注解,就采用Alias注解中的值作为别名,如果不包含这个注解,则将类名称简称作为别名。假如sample.mybatis.domain.Hotel就是Hotel作为别名。
接下来就是解析plugins节点了,对应源码如下
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
1. 解析插件类 resolveClass支持别名 interceptor可以配置为别名
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
2. 设置插件对应的属性
interceptorInstance.setProperties(properties);
3. 添加插件到configuration到interceptorChain的interceptors列表当中
configuration.addInterceptor(interceptorInstance);
}
}
}
对于读取interceptor属性和getChildrenAsProperties已经不是难事了,前面讲解了好几次了,这里有个resolveClass是怎么回事呢?
/**
* 解析别名对应的类
* @param alias
* @return 如果别名对应的类不存在 则直接将别名作为类初始化
*/
protected Class<?> resolveClass(String alias) {
if (alias == null) {
return null;
}
try {
return resolveAlias(alias);
} catch (Exception e) {
throw new BuilderException("Error resolving class. Cause: " + e, e);
}
}
看到resolveAlias,有没有发现什么?我们不妨继续看
protected Class<?> resolveAlias(String alias) {
return typeAliasRegistry.resolveAlias(alias);
}
这下该明白了,这里resolveClass其实会调用解析别名的方式,也就是说通过前面注册了别名,后续凡是设置类信息的时候都可以使用别名了。如果没有别名呢?继续看org.apache.ibatis.type.TypeAliasRegistry#resolveAlias方法
public <T> Class<T> resolveAlias(String string) {
try {
if (string == null) {
return null;
}
// issue #748
String key = string.toLowerCase(Locale.ENGLISH);
Class<T> value;
if (TYPE_ALIASES.containsKey(key)) {
1. 已经注册了别名 则读取别名对应的类信息
value = (Class<T>) TYPE_ALIASES.get(key);
} else {
2. 不存在别名 则尝试加载这个类 加载不到 则抛出异常
value = (Class<T>) Resources.classForName(string);
}
return value;
} catch (ClassNotFoundException e) {
throw new TypeException("Could not resolve type alias '" + string + "'. Cause: " + e, e);
}
}
可以看到,如果读取不到别名,就会原来的值作为类来加载,因此一个parameterType类型是String的,我们既可以配置为别名string,也可以直接配置为类的全限定名java.lang.String,这都没什么关系,MyBatis中都支持。接下来的objectFactory、objectWrapperFactory、reflectorFactory都是相当简单的,看一下源码一下就明白
private void objectFactoryElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type");
Properties properties = context.getChildrenAsProperties();
ObjectFactory factory = (ObjectFactory) resolveClass(type).newInstance();
factory.setProperties(properties);
configuration.setObjectFactory(factory);
}
}
private void objectWrapperFactoryElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type");
ObjectWrapperFactory factory = (ObjectWrapperFactory) resolveClass(type).newInstance();
configuration.setObjectWrapperFactory(factory);
}
}
private void reflectorFactoryElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type");
ReflectorFactory factory = (ReflectorFactory) resolveClass(type).newInstance();
configuration.setReflectorFactory(factory);
}
}
一看到resolveClass,就该知道这里的type都可以配置为别名或者类的全限定名。
接下来才开始设置settings中配置的属性到configuration而不是第二步应该也明了了,因为这里涉及到大量的别名,必须在别名解析完才可以。
private void settingsElement(Properties props) throws Exception {
configuration.setAutoMappingBehavior(
AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior
.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
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.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")));
@SuppressWarnings("unchecked")
Class<? extends TypeHandler> typeHandler = (Class<? extends TypeHandler>) resolveClass(
props.getProperty("defaultEnumTypeHandler"));
configuration.setDefaultEnumTypeHandler(typeHandler);
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"));
@SuppressWarnings("unchecked")
Class<? extends Log> logImpl = (Class<? extends Log>) resolveClass(props.getProperty("logImpl"));
configuration.setLogImpl(logImpl);
configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
}
接下来终于轮到environments节点了,通过这个节点主要是配置环境使用的事务工厂、数据源工厂以及具体数据源的参数。源码如下
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
String id = child.getStringAttribute("id");
if (isSpecifiedEnvironment(id)) {
1. 根据transactionManager配置实例化事务工厂对象 支持别名
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
2. 根据dataSource配置实例化数据源工厂对象
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
3. 获取数据源
DataSource dataSource = dsFactory.getDataSource();
4. 将id 事务工厂 数据源等信息设置到Environment当中 这里使用了构造者模式
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
关于解析txFactory和dsFactory没啥好讲的,就是读取别名,根据别名找到对应的类, 并实例化类的过程。只是这里涉及到读取setProperties的方法。 对应的方法如下
@Override
public void setProperties(Properties properties) {
Properties driverProperties = new Properties();
MetaObject metaDataSource = SystemMetaObject.forObject(dataSource);
for (Object key : properties.keySet()) {
String propertyName = (String) key;
if (propertyName.startsWith(DRIVER_PROPERTY_PREFIX)) {
String value = properties.getProperty(propertyName);
driverProperties.setProperty(propertyName.substring(DRIVER_PROPERTY_PREFIX_LENGTH), value);
} else if (metaDataSource.hasSetter(propertyName)) {
String value = (String) properties.get(propertyName);
Object convertedValue = convertValue(metaDataSource, propertyName, value);
metaDataSource.setValue(propertyName, convertedValue);
} else {
throw new DataSourceException("Unknown DataSource property: " + propertyName);
}
}
if (driverProperties.size() > 0) {
metaDataSource.setValue("driverProperties", driverProperties);
}
}
这里用到了SystemMetaObject,如果有看过上一章关于反射模块的就会知道,这个类是反射模块中的,当然上面的objectFactory、objectWrapperFactory、reflectorFactory也都是反射模块的,本来反射应该使用用户设置的objectFactory、objectWrapperFactory、reflectorFactory,这样这些工厂类在environments节点之前就要解析是说得通的,只不过如果我们真的了解这个SystemMetaObject的话,其实它里面都有默认的实现类的,我想当初设计的时候是要使用外面传入的,只不过后来那些默认的已经够用了。
接下来就是解析databaseIdProvider标签了,这个节点是用来干啥的呢?为啥这个标签需要在environments节点之后呢?这个节点其实做多数据源配置的。可以参考博客:兼容Oracle和MySQL的那些事。至于为啥要放到environments之后,是因为需要依赖数据源,通过数据源获取数据库真实连接查询数据库的厂商名称,然后映射databaseId。具体解析
private void databaseIdProviderElement(XNode context) throws Exception {
DatabaseIdProvider databaseIdProvider = null;
if (context != null) {
String type = context.getStringAttribute("type");
// awful patch to keep backward compatibility
if ("VENDOR".equals(type)) {
type = "DB_VENDOR";
}
1. 读取配置
Properties properties = context.getChildrenAsProperties();
2. 默认的实现类org.apache.ibatis.mapping.VendorDatabaseIdProvider,支持别名
databaseIdProvider = (DatabaseIdProvider) resolveClass(type).newInstance();
3. 设置配置信息
databaseIdProvider.setProperties(properties);
}
Environment environment = configuration.getEnvironment();
if (environment != null && databaseIdProvider != null) {
4. 获取数据库产品名称并解析对应的databaseId 这里必须有数据源存在
String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
5. 将databaseId设置到configuration中
configuration.setDatabaseId(databaseId);
}
}
接下来就是关于typeHandlers,看一下源码
private void typeHandlerElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
1. 批量注册模式 会通过TypeReference或者MappedTypes注解两种方式查找javaType 通过MappedJdbcTypes查找jdbcType
if ("package".equals(child.getName())) {
String typeHandlerPackage = child.getStringAttribute("name");
typeHandlerRegistry.register(typeHandlerPackage);
} else {
2. 单一注册模式
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);
if (javaTypeClass != null) {
if (jdbcType == null) {
typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
} else {
typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
}
} else {
typeHandlerRegistry.register(typeHandlerClass);
}
}
}
}
}
首先一看package,我们立刻就要反应这是VFS要用的地方。其实与typeAliases一样,同样支持批量注册、单一注册,就连下面的mapper节点也都一样,它们都依赖VFS。
<mappers>
<!--package与mapper只能二选一-->
<package name="sample.mybatis.mapper"/>
<!-- <mapper resource="sample/mybatis/mapper/CityMapper.xml"/>-->
<!-- <mapper resource="sample/mybatis/mapper/HotelMapper.xml"/>-->
</mappers>
另外还有resolveClass,这又依赖别名解析了。所以也必须是typeAliases节点之后解析的。关于TypeHandler的使用和注册相关的内容也是一个重要而且篇幅够长的话题了,此处先不详述了。接下来就是解析mappers标签了,此处也先不继续,下一章再单独谈这一块。
总结
关于MyBatis的配置文件的元素看起来各个节点元素之间没啥关系,其实都是环环相扣的。一开始的别名系统、后面的别名系统可以说是最重要的,包括文件系统,都为后面的各种解析操作打好了基础而又互不影响。