mybatis专栏 https://blog.csdn.net/worn_xiao/category_6530299.html?spm=1001.2014.3001.5482
第一步 先看一个初始化代码
public static void setup() throws Exception {
createBlogDataSource();
final String resource = "org/apache/ibatis/builder/MapperConfig.xml";
final Reader reader = Resources.getResourceAsReader(resource);
sqlMapper = new SqlSessionFactoryBuilder().build(reader);
}
如上图所示是构建SqlSessionFactory的全过程,那么具体是怎么执行的呢,首先我们大致可以看出代码中的意思无非就是读取配置文件生成SqlSessionFactory工厂,这里先给出一个比较全的配置文件格式代码。
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="org/apache/ibatis/databases/blog/blog-derby.properties"/>
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="false"/>
<setting name="multipleResultSetsEnabled" value="true"/>
<setting name="useColumnLabel" value="true"/>
<setting name="useGeneratedKeys" value="false"/>
<setting name="defaultExecutorType" value="SIMPLE"/>
<setting name="defaultStatementTimeout" value="25"/>
</settings>
<typeAliases>
<typeAlias alias="Author" type="org.apache.ibatis.domain.blog.Author"/>
<typeAlias alias="Blog" type="org.apache.ibatis.domain.blog.Blog"/>
<typeAlias alias="Comment" type="org.apache.ibatis.domain.blog.Comment"/>
<typeAlias alias="Post" type="org.apache.ibatis.domain.blog.Post"/>
<typeAlias alias="Section" type="org.apache.ibatis.domain.blog.Section"/>
<typeAlias alias="Tag" type="org.apache.ibatis.domain.blog.Tag"/>
</typeAliases>
<typeHandlers>
<typeHandler javaType="String" jdbcType="VARCHAR" handler="org.apache.ibatis.builder.CustomStringTypeHandler"/>
</typeHandlers>
<objectFactory type="org.apache.ibatis.builder.ExampleObjectFactory">
<property name="objectFactoryProperty" value="100"/>
</objectFactory>
<plugins>
<plugin interceptor="org.apache.ibatis.builder.ExamplePlugin">
<property name="pluginProperty" value="100"/>
</plugin>
</plugins>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<property name="" value=""/>
</transactionManager>
<dataSource type="UNPOOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="org/apache/ibatis/builder/AuthorMapper.xml"/>
<mapper resource="org/apache/ibatis/builder/BlogMapper.xml"/>
<mapper resource="org/apache/ibatis/builder/CachedAuthorMapper.xml"/>
<mapper resource="org/apache/ibatis/builder/PostMapper.xml"/>
<mapper resource="org/apache/ibatis/builder/NestedBlogMapper.xml"/>
</mappers>
</configuration>
如下图所示是xml文件加载成为字符流的过程
首先是Resource组合了ClassLoaderWrapper通过获取各种类加载器,把配置文件加载成为字节流,然后再通过流转换,把字节流转换成为字符流返回,配置文件的流文件
具体过程
public static Reader getResourceAsReader(String resource) throws IOException {
Reader reader;
if (charset == null) {
reader = new InputStreamReader(getResourceAsStream(resource));
} else {
reader = new InputStreamReader(getResourceAsStream(resource), charset);
}
return reader;
}
//Resource中获取Reader的核心代码
public class Resources {
private static ClassLoaderWrapper classLoaderWrapper = new ClassLoaderWrapper();
//可以看到Resource中组合了classloaderWrapper的代码
ClassLoader[] getClassLoaders(ClassLoader classLoader) {
return new ClassLoader[]{
classLoader,
defaultClassLoader,
Thread.currentThread().getContextClassLoader(),
getClass().getClassLoader(),
systemClassLoader};
}
可以看到这是ClassLoaderWrapper类获取各种类加载器的代码,还可以传入参数指定类的加载器。
接下来可以看到就是jdk读取资源路径下的资源文件转化成为字节流的过程了。再回到入库出就是把字节流转化成为字符流的初始化过程了。这就是整个流程件的加载过程。接下来我们分析一下SqlSessionFactory的构建过程。有了字符流文件,那么自然我们想到的就是吧字符流文件转化成对象做成一个配置类,提供后面自行调用。好的那我们接下来梳理一下流程
如上图所示,大致的流程就是解析字符流,转化为Configuration然后构建成一个工厂类
InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
for (ClassLoader cl : classLoader) {
if (null != cl) {
InputStream returnValue = cl.getResourceAsStream(resource);
if (null == returnValue) {
returnValue = cl.getResourceAsStream("/" + resource);
}
if (null != returnValue) {
return returnValue;
}
}
}
return null;
}
如上所示一般会给三个参数一个是流文件
<properties resource="org/apache/ibatis/databases/blog/blog-derby.properties"/>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<property name="" value=""/>
</transactionManager>
<dataSource type="UNPOOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
所以有另外两个参数,主要是方便我们自定是数据源与数据源连接地址,当然在源码中也是做了控制的,如果有自定义的就会装配自定义的。
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 {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
2接下来我们看XMLConfigBuilder中的核心代码
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
从上面的代码很容易看出实际上是通过XPathParser中的parser把xml的字符流文件解析成Configuration类的过程那么XPathParser是如何工作的呢。
3 XPathParser核心代码如下。
public XPathParser(Reader reader) {
commonConstructor(false, null, null);
this.document = createDocument(new InputSource(reader));
}
private Document createDocument(InputSource inputSource) {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setValidating(validation);
factory.setNamespaceAware(false);
factory.setIgnoringComments(true);
factory.setIgnoringElementContentWhitespace(false);
factory.setCoalescing(false);
factory.setExpandEntityReferences(true);
DocumentBuilder builder = factory.newDocumentBuilder();
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);
}
}
把流作为输入源通过jdk的DocumentBuilderFactory解析输入源生成Document的文档对象。private final Document document;
实际上不难看出还可以把字节流和字符串作为输入源进行解析。通过条用parser.evalNode("/configuration") 把xml的文档对象解析返回Configuration类实际上是调用jdk的Xpath类按路径进行解析的。
3 XMLConfigBuilder把xml转化成Configuration的核心代码
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
如上代码可以看到就是把配置文件中的各个xml节点转化成Configuration的过程。那么我们来看四个properties, environments, mappers,plugins 这四个配置项直接指导了我们的配置。分别是我们的数据源配置文件,mapper.xml配置文件,还有插件的配置文件。
正如我们前面代码所预料的那样,在我们配置了自定义的数据源与配置文件的时候,就不走默认的配置.接下来是插件的代码
如上图所示遍历上下文的插件注册到Configuration当中。接下来我们看看我们比较关心的mapper.xml文件的读取过程
如上图所示通过读取pachage包中的映射,通过XMLMapperBuilder读取xml文件中的内容放入Configuration中就可以了,但是我们也可以看到这里有三个判断一个是有resource路径,一个是有url,另一个是有class.那么mapper这里到底怎么加载的呢。实际上你可以看到url,和resource的都是通过xmlMapperBuilder进行parse的,然而mapperClass则是通过mapperInterface进行加载mapper文件的,那么问题就在这里了,有什么区别呢.接下来我们来看看XmlMapperBuider做了什么
一目了然就是解析了mapper文件然后把想入的节点映射再组装到一个类中。最后把相应的类组装进Configuration中。给到DefaultSqlSessionFactory进行保存。解析/mapper就是我们.xml文件的根节点。
再往里面走就是解析各个mapper.xml文件里面的片段了,这里包括sql,parameterMap,cahe,chache-refd,dcl语句等等的,那么我们可以来看看做了什么
cache-ref实际是往configutation中添加cacheRef.其他的也是类似的都是往Configuration中注册相应的字段映射。
或者类是select|insert等注解对应对象的构造器都是生成特定的对象,然后在那个在公共的配置类中生成映射表
如上代码片段是Configuration中的类的映射信息,xml文件中类似insert|delete等的每个操作片段映射成一个mappedStatements包括parameterMap和resultMap等等的 。
好了说完了url和resource映射接下来改说一下class映射了吧。这里是重点,我先给大家画一下,为什么呢,因为在不久的将来,随着springboot的新起,你会发现xml的配置方式,将会被注解彻底的取代,那么Mybaties的注解,自然就来了,让我们看一下通过class获取mapper的接口干了什么吧
如上代码所示,大家可以看到一个是给出包名的package,一个是单纯的一个类接口,那么好无疑问给包的干了什么呢,一眼就可以
看出来就是获取到包路径然后addMappers
再来看addMappers的源码,可以看到就是获取包西面接口的整个集合,遍历添加到mapper中,那么Configuration.addMapper()都做了哪些事情呢,好吧让我们继续看源代码
其实你一看就可以发现无非就是把mapper的类型做为key,映射一个mapper的代理,这个代理具体有什么用呢,请大家关注我一片Mybaties的博客,讲解Mybatis中注解式的mapper的执行过程。我们可以看到MapperAnnotationBuilder类那么我们自然关注的就是parse()方法了。
可以看到先是加载xml,然后在parseStatement(method),问题来了,集成可以通过类型对应的xml加载配置信息,那么不就完了么,parseStatement还要来干什么呢,让我们一步步的走进去
void parseStatement(Method method) {
Class<?> parameterTypeClass = getParameterType(method);
LanguageDriver languageDriver = getLanguageDriver(method);
SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
if (sqlSource != null) {//要以注解的形式加载的前提是 mapper的方法上要有相应的注解 所以如果用了xml文件自然mapper上也不会有注解 sqlSource自然也就为null 也就不会装配注解
Options options = method.getAnnotation(Options.class);
final String mappedStatementId = type.getName() + "." + method.getName();
Integer fetchSize = null;
Integer timeout = null;
StatementType statementType = StatementType.PREPARED;
ResultSetType resultSetType = ResultSetType.FORWARD_ONLY;
SqlCommandType sqlCommandType = getSqlCommandType(method);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = !isSelect;
boolean useCache = isSelect;
KeyGenerator keyGenerator;
String keyProperty = "id";
String keyColumn = null;
if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
// first check for SelectKey annotation - that overrides everything else
SelectKey selectKey = method.getAnnotation(SelectKey.class);
if (selectKey != null) {
keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
keyProperty = selectKey.keyProperty();
} else if (options == null) {
keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
} else {
keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
keyProperty = options.keyProperty();
keyColumn = options.keyColumn();
}
} else {
keyGenerator = NoKeyGenerator.INSTANCE;
}
if (options != null) {
if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
flushCache = true;
} else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
flushCache = false;
}
useCache = options.useCache();
fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
timeout = options.timeout() > -1 ? options.timeout() : null;
statementType = options.statementType();
resultSetType = options.resultSetType();
}
String resultMapId = null;
ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
if (resultMapAnnotation != null) {
String[] resultMaps = resultMapAnnotation.value();
StringBuilder sb = new StringBuilder();
for (String resultMap : resultMaps) {
if (sb.length() > 0) {
sb.append(",");
}
sb.append(resultMap);
}
resultMapId = sb.toString();
} else if (isSelect) {
resultMapId = parseResultMap(method);
}
assistant.addMappedStatement(//所以 一个mapperStatementId 实际是一个mapper的具体方法
mappedStatementId,
sqlSource,
statementType,
sqlCommandType,
fetchSize,
timeout,
// ParameterMapID
null,
parameterTypeClass,
resultMapId,
getReturnType(method),
resultSetType,
flushCache,
useCache,
// TODO gcode issue #577
false,
keyGenerator,
keyProperty,
keyColumn,
// DatabaseID
null,
languageDriver,
// ResultSets
options != null ? nullOrEmpty(options.resultSets()) : null);
}
}
这段代码似乎有点长可以看到sqlSource那里有个判断,那么我们看看SqlSource做了什么
实际上这个方法就干了一件事,解析mapper方法上面的注解,可以看到为什么平时我们可以在Mapper接口上写@Select或者MapperProvider的注解,实际上这里就是根据注解,获取里面的sql语句生成SQL数据源。再回到原来的代码与问题分析,为什么Mybaties的配置通常人们要么配置XML要么用注解呢,这里就说明了 如果用注解那么SqlSource 就不为空就会执行注解,如果用xml注解非空就会通过注解解析各种parameterMap,或者resultMap放入到放入到Configuration中,所以不会重复。好的,整个Mybaties的初始化过程我已经讲解完成了。实际上我们通过Spring配置package的包路径以后,为什么能够生效。实际上你应该明白了,无非就是扫描包下面所有的class信息,加载xml或者mapper上的注解,把相关的配置,集成映射到Configuration中 请看下回分解!