MyBatis原理剖析(三)--加载配置文件
下面我们正式进入mybatis的源码学习,之前我们已经了解过mybatis中通过配置文件来保证与数据库的交互。配置文件分为核心配置文件和映射配置文件,核心配置文件的主要作用就是加载数据库的一些配置信息而映射配置文件则是执行对应的sql语句。同时核心配置文件中也会集成映射配置文件的路径信息。
核心配置文件的加载
那么如果mybatis想要执行第一步就需要加载这些配置文件,保证后续与数据库的操作能够顺利执行。下面我们看看加载的源码。
//解析配置文件通过类加载器加载为stream流
InputStream resourceAsStream = Resources.getResourceAsStream("/configMapper.xml");//进行加载文件根据文件信息解析出SqlSessionFactory
SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);
最外层的方法加载配置文件只用到了两个方法,分别是通过Resources类的方法得到InputStream 和SqlSessionFactoryBuilder的build来根据InputStream加载出SqlSessionFactoryBuilder。
下面我们进入getResourceAsStream方法来看看这个方法的作用
/*** Returns a resource on the classpath as a Stream object** @param resource* The resource to find** @return The resource** @throws java.io.IOException* If the resource cannot be found or read*/public static InputStream getResourceAsStream(String resource) throws IOException {return getResourceAsStream(null, resource);}/*** Returns a resource on the classpath as a Stream object** @param loader* The classloader used to fetch the resource* @param resource* The resource to find** @return The resource** @throws java.io.IOException* If the resource cannot be found or read*/public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);if (in == null) {throw new IOException("Could not find resource " + resource);}return in;}
在这个方法的内部又调用了重载方法而第一个参数loader则是类加载器,也就是说可以指定类加载器来进行加载这个文件,而在这个getResourceAsStream方法的内部则是调用了一个classLoaderWrapper来进行真正的加载。
/*** Get a resource from the classpath, starting with a specific class loader** @param resource* - the resource to find* @param classLoader* - the first class loader to try** @return the stream or null*/public InputStream getResourceAsStream(String resource, ClassLoader classLoader) {return getResourceAsStream(resource, getClassLoaders(classLoader));}/*** Try to get a resource from a group of classloaders** @param resource* - the resource to get* @param classLoader* - the classloaders to examine** @return the resource or null*/InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {for (ClassLoader cl : classLoader) {if (null != cl) {// try to find the resource as passedInputStream returnValue = cl.getResourceAsStream(resource);// now, some class loaders want this leading "/", so we'll add it and try again if we didn't find the resourceif (null == returnValue) {returnValue = cl.getResourceAsStream("/" + resource);}if (null != returnValue) {return returnValue;}}}return null;}
点开classLoaderWrapper的这个方法可以发现依旧是调用了一个重载方法,并且在调用重载方法之前则是调用了getClassLoaders方法,那么我们打开方法进行查看
ClassLoader[] getClassLoaders(ClassLoader classLoader) {return new ClassLoader[] { classLoader, defaultClassLoader, Thread.currentThread().getContextClassLoader(),getClass().getClassLoader(), systemClassLoader };}
可以看到整个方法实际上就是生成了一个类加载器数组,而参数的作用就是在这个数组中再多添加一个类加载器,并且这个数组中的类加载器的顺序也是有讲究的,优先级高的类加载器则是放在前面,目的则是为了后续策略模式的调用。
InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {for (ClassLoader cl : classLoader) {if (null != cl) {// try to find the resource as passedInputStream returnValue = cl.getResourceAsStream(resource);// now, some class loaders want this leading "/", so we'll add it and try again if we didn't find the resourceif (null == returnValue) {returnValue = cl.getResourceAsStream("/" + resource);}if (null != returnValue) {return returnValue;}}}return null;}
我们继续回去看接下来的方法可以看到当进入重载方法的时候则是遍历了作为参数传递进来的classLoader数组,然后通过遍历每个类加载器来选择正确的类加载器进行加载。这种方式实际上就是设计模式的策略模式,根据不同的参数动态的选择不同的策略进行执行。
那么这个classLoaderWrapper的作用是什么呢
public class ClassLoaderWrapper {ClassLoader defaultClassLoader;ClassLoader systemClassLoader;ClassLoaderWrapper() {try {systemClassLoader = ClassLoader.getSystemClassLoader();} catch (SecurityException ignored) {// AccessControlException on Google App Engine}}}
点开这个类我们发现这个类内部含有多个类加载器,并且结合其他方法我们发现其实这个类是将多个类加载器进行集成,同时采用了策略模式的方式将不同的类加载器进行加载不同的文件将其转化为InputStream。也就是说整个方法的核心就是在于这个ClassLoaderWrapper类,它内部集成了多个类加载器,并且根据传递的文件路径通过此类加载器进行加载,最终返回成一个InputStream来表示整个方法的执行完毕。
至此getResourceAsStream方法解析完毕,那么我们从这个方法的源码中其实可以学习到策略模式的使用以及在同一个类的不同重载方法并不是说是毫不相干的就像mybatis源码中那样,基本每个重载类最后都会调用另一个重载类,而最后真正执行业务逻辑的则只有一个重载类。其他类则是更多对于参数的封装和校验等操作最后调用真正执行业务逻辑的类。
之后我们再看看下一个方法
//进行加载文件根据文件信息解析出SqlSessionFactory
SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);
我们看到这个方法是通过一个SqlSessionFactory的构造器来根据InputStream进行构造返回一个SqlSessionFactory对象,这里有涉及到了两种设计模式生成器模式和工厂模式
下面我们进入方法内部看看如何解析配置文件的
public class SqlSessionFactoryBuilder {public SqlSessionFactory build(Reader reader, String environment, Properties properties) {try {XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);return build(parser.parse());} catch (Exception e) {throw ExceptionFactory.wrapException("Error building SqlSession.", e);} finally {ErrorContext.instance().reset();try {if (reader != null) {reader.close();}} catch (IOException e) {// Intentionally ignore. Prefer previous error.}}}public SqlSessionFactory build(InputStream inputStream) {return build(inputStream, null, null);}public SqlSessionFactory build(InputStream inputStream, String environment) {return build(inputStream, environment, null);}public SqlSessionFactory build(InputStream inputStream, Properties properties) {return build(inputStream, null, properties);}
}
上述代码可以看出依旧是一个方法内部调用了重载方法,而真正执行业务逻辑的只有一个重载方法
可以看出最终是调用了这个方法
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 {if (inputStream != null) {inputStream.close();}} catch (IOException e) {// Intentionally ignore. Prefer previous error.}}}
而除了inputStream之外则是含有另外两个参数,这两个参数的主要作用就是选择执行配置文件中的哪些配置,因为一个核心配置文件是可以有多个配置的数据源和运行环境的,而另外两个参数则是决定优先使用哪个数据源和运行环境。
代码第一行就new了一个XMLConfigBuilder对象,而这个XMLConfigBuilder对象用的很明显也是生成器模式。下面我们进入这个对象内部来进行看看。
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {this(Configuration.class, inputStream, environment, props);}public XMLConfigBuilder(Class<? extends Configuration> configClass, InputStream inputStream, String environment,Properties props) {this(configClass, new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);}private XMLConfigBuilder(Class<? extends Configuration> configClass, XPathParser parser, String environment,Properties props) {super(newConfig(configClass));ErrorContext.instance().resource("SQL Mapper Configuration");this.configuration.setVariables(props);this.parsed = false;this.environment = environment;this.parser = parser;}
进入方法中依旧是调用了多个构造函数来进行参数封装真正执行的业务逻辑的构造函数只有一个,其中我们看到Configuration这个类,这个类是整个mybatis加载文件的核心类,它的主要作用就是集成配置文件中所解析出来的所有信息于这个类中。并且后续的sql方法执行也是根据这个类内部的属性进行数据源等其他信息的配置来完成执行。因此这个Configuration类则是整个mybatis加载配置文件中的核心中的核心。而后续的XPathParser类则是将inputStream流直接解析为document文件。XMLConfigBuilder内部会生成一个初始化的Configuration类,并且在后续的parse方法中进行Configuration类对象的赋值。可能会产生一个疑问,直接操作成员变量不会造成线程安全的问他码,其实这里则是采用了一个非常巧妙的设计。线程隔离机制,我们发现所有直接操作成员变量的对象都是在方法体中直接new出来的对象,也就是说每个线程都有自己的独有的对象,这样进行访问的时候就是单线程访问了从而达成了线程隔离的效果。
public SqlSessionFactory build(Configuration config) {return new DefaultSqlSessionFactory(config);}
最终我们发现是通过XMLConfigBuilder来将xml对象进行解析随后注入到Configuration对象中最后再将Configuration对象放入注入到SqlSessionFactory对象中完成真正的构建sql工厂。
至此配置文件算是完成了解析并且注入。从读源码的过程中我们发现源码用到了多种设计模式:生成器模式--这个模式的主要目的就是讲对象生成过程中的复杂逻辑进行封装,外部使用者直接调用生成器的方法就可以返回成品对象,降低了代码的耦合度。以及工厂模式等设计模式。通过理解这些源码的代码风格以及设计模式将会学到很多知识。