前面一篇文章我们对Mybatis整体的执行流程做了一个详细的总结,可进入专栏查看;
本篇文章我们将分析一下配置信息是如何解析的以及SqlSessionFactory创建过程。
下面我们通过Debug方式点查看Mybatis如何获取配置文件:
//1、读取配置文件
String resource = "mybatis-config.xml";
InputStream inputStream;
try {
//主要是通过ClassLoader.getResourceAsStream()方法获取指定的classpath路径下的Resource
inputStream = Resources.getResourceAsStream(resource);
//2、初始化mybatis,创建SqlSessionFactory类实例
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
System.out.println(sqlSessionFactory);
} catch (IOException e) {
e.printStackTrace();
}
首先读取mybatis-config.xml全局配置文件,转换成文件流。然后第二步是构造SqlSessionFactory。接着查看build方法,我们来到SqlSessionFactoryBuilder的build()方法,利用XMLConfigBuilder方法中的parseConfiguration方法解析相关的配置,inputStream流是mybatis-config.xml配置对应的字节流,XMLConfigBuilder的parse()方法解析mybatis-config.xml节点得到Configuration配置类。
//org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.InputStream)
//传入我们classpath路径下mybatis-config.xml的文件流
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
//org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.InputStream, java.lang.String, java.util.Properties)
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
//构造一个XMLConfigBuilder对象,这里用到了建造者设计模式
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
//其中parser.parse()负责解析xml,然后返回configuration对象,接着通过build(configuration)创建SqlSessionFactory
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.
}
}
}
//parse()方法:负责解析xml,并将解析之后的配置封装到configuration中,返回给build方法,用于创建SqlSessionFactory
//org.apache.ibatis.builder.xml.XMLConfigBuilder#parse
public Configuration parse() {
//判断是否重复解析
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//解析mybatis-config.xml中<configuration>标签的内容,封装成XNode对象,然后传入parseConfiguration方法进行具体的配置解析
//parseConfiguration方法负责具体的配置解析
parseConfiguration(parser.evalNode("/configuration"));
//返回configuration对象
return configuration;
}
我们debug看一下,parser.evalNode("/configuration")返回的结果如下图所示:
parse()方法里面实际上是调用的parseConfiguration方法,负责解析XML配置。
根据上图configuration标签内我们声明的标签,遍历,挨个进行解析:
//org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration
private void parseConfiguration(XNode root) {
try {
//先解析properties标签
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
//typeAliases别名配置解析, 别名配置有两种方式: a.指定单个实体; b.指定包路径
//类型别名是为Java类型设置一个短的名字, 用来减少类完全限定名的冗余
typeAliasesElement(root.evalNode("typeAliases"));
//插件解析
pluginElement(root.evalNode("plugins"));
//对象工厂解析
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
//解析全局配置settings相关配置
settingsElement(settings);
// environments里面其实就包含我们的数据库连接信息等的配置,重点,下面详细介绍.
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//类型转换器相关配置解析,用于数据库类型和Java数据类型的转换
typeHandlerElement(root.evalNode("typeHandlers"));
//mapper接口配置解析,重点,下面详细介绍
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
总结:parseConfiguration完成的是解析configuration下的所有标签,主要包括environment、mapper接口、别名等的配置。
接下来我们先来看看Mybatis如何解析environments标签内容的。
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/user_mybatis?serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
查看environmentsElement()方法的源码:
//解析environments
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
//development
environment = context.getStringAttribute("default");
}
//循环遍历XML各个节点
for (XNode child : context.getChildren()) {
//获取ID属性,对应<environment id="development">的ID属性, 即id=development
String id = child.getStringAttribute("id");
if (isSpecifiedEnvironment(id)) {
//获取transactionManager事务管理器相关配置
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
//解析数据源配置,具体见下面分析
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
//将解析出来的数据源配置等信息赋值给configuration对象
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
我们看到,数据源的解析其实是在dataSourceElement()方法中进行的:
//org.apache.ibatis.builder.xml.XMLConfigBuilder#dataSourceElement
//解析dataSource数据源配置
private DataSourceFactory dataSourceElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type");
//将解析出来的属性内容封装成Properties对象
Properties props = context.getChildrenAsProperties();
DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();
factory.setProperties(props);
return factory;
}
throw new BuilderException("Environment declaration requires a DataSourceFactory.");
}
如下图,解析出标签内配置的四个properties属性,并封装成Properties对象,赋值给DataSourceFactory,返回DataSourceFactory对象。
小总结:dataSource数据源解析大体过程为:
this.configuration.setEnvironment(environmentBuilder.build());
以上就是关于数据源配置的解析。接下来我们再看一下mapper接口是如何解析的,其他配置的解析感兴趣的小伙伴可以自行Debug分析。
对mapper接口的解析,对应的解析方法是XMLConfigBuilder的mapperElement(),具体代码如下:
//mapper接口配置解析
//org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
//实际上就是获取到:<mapper resource="mapper/UserMapper.xml"/>
for (XNode child : parent.getChildren()) {
//判断是否是指定mapper包扫描配置,我们这是使用的resource配置
//解析<package name=""/>
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
//mapper接口所在的包扫描路径,实际上底层调用mapperRegistry.addMappers(packageName);
configuration.addMappers(mapperPackage);
} else {
//本示例中使用的就是resources方式: resource = mapper/UserMapper.xml
String resource = child.getStringAttribute("resource");
//因为我们没有指定url,所以url = null
String url = child.getStringAttribute("url");
//因为我们没有指定class,所以mapperClass = null
String mapperClass = child.getStringAttribute("class");
//接下来其实就是判断,mybatis-config.xml配置中我们指定的是哪一种方式配置mapper接口
//主要有四种方式:a.指定包扫描方式 b.指定resource方式 c.指定url方式 d.指定class方式
if (resource != null && url == null && mapperClass == null) {
//解析<mapper resource=""></mapper>
//b.指定resource方式
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
//封装成XMLMapperBuilder对象
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
//解析<mapper url=""></mapper>
//c.指定url方式
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
//解析<mapper class=""></mapper>
//d.指定class方式
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.");
}
}
}
}
}
这里我们以resource方式为例来看一下Mybatis对 Mapper 映射器的解析过程,主要是调用XMLMapperBuilder的parse()方法进行,这里也是使用了构建者模式。
if (resource != null && url == null && mapperClass == null) {
//拿到mybatis-config.xml中配置的mapper路径:resource="mapper/UserMapper.xml"
ErrorContext.instance().resource(resource);
//通过Resources读取转换成资源文件流
InputStream inputStream = Resources.getResourceAsStream(resource);
//使用构建者模式,调用XMLMapperBuilder类的parse()方法进行解析
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
}
//拿到mapper.xml文件之后的具体解析方法
//org.apache.ibatis.builder.xml.XMLMapperBuilder#parse
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
//解析所有的mapper子标签
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
//绑定mapper接口
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
我们看到,parse()方法先调用了configurationElement()来对具体的mapper子标签内容进行解析,然后调用bindMapperForNamespace()方法进行mapper接口与xml的绑定。
详细代码如下:
//org.apache.ibatis.builder.xml.XMLMapperBuilder#configurationElement
//configurationElement():解析的是Mapper.xml的标签
private void configurationElement(XNode context) {
try {
//获取到namespace命名空间
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
//缓存配置的引用
cacheRefElement(context.evalNode("cache-ref"));
//缓存配置
cacheElement(context.evalNode("cache"));
//解析parameterMap标签
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
//解析resultmap标签, 用来描述如何从数据库结果集中来加载对象
resultMapElements(context.evalNodes("/mapper/resultMap"));
//解析sql标签,可被其他语句引用的可重用sql语句块
sqlElement(context.evalNodes("/mapper/sql"));
//获得MappedStatement对象(增删改查语句)
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);
}
}
上面的代码中,buildStatementFromContext()方法很重要,实际上MappedStatement对象就是在这里面创建的。我们进入buildStatementFromContext方法:
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
//org.apache.ibatis.builder.xml.XMLMapperBuilder#buildStatementFromContext(java.util.List<org.apache.ibatis.parsing.XNode>, java.lang.String)
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
//list其实就是拿到我们mapper.xml中定义的一个select语句,具体如下图:
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
//解析insert/update/select/delete中的标签
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
statementParser.parseStatementNode()才是具体解析增删改查标签的方法,详细代码如下:
//org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode
//解析insert/update/select/delete中的标签
public void parseStatementNode() {
//对应我们select标签的id属性,即getById
//id对应于mapper接口的方法,单个mapper.xml中必须唯一,全路径名加+id作为mappedStatement的ID值,所以必须唯一
String id = context.getStringAttribute("id");
//数据库厂商标识
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
//参数类型
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
//高级结果映射,对应外部resultMap的命名引用
String resultMap = context.getStringAttribute("resultMap");
//对应我们select标签的resultType返回类型属性,即com.wsh.mybatis.mybatisdemo.entity.User
String resultType = context.getStringAttribute("resultType");
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
//通过ClassLoader装载我们的User类,即com.wsh.mybatis.mybatisdemo.entity.User
Class<?> resultTypeClass = resolveClass(resultType);
String resultSetType = context.getStringAttribute("resultSetType");
//默认Statement的类型为PREPARED:预编译类型的statement
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
//nodeName=select
String nodeName = context.getNode().getNodeName();
//拿到当前sql命令类型是哪一种, 主要有UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH这几种
//这里我们是SELECT查询
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);
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
//创建SqlSource
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
String resultSets = context.getStringAttribute("resultSets");
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
//创建MappedStatement对象,并存入configuration中
//MappedStatement其实就是每一个增删改查的标签的里的数据
//最终存放到mappedStatements中,mappedStatements存放的是一个个的增删改查
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
我们看到,parseStatementNode()方法前面都是对select标签体能设置的一些属性的解析,在方法的最后面,调用了addMappedStatement(),看名字,我们也能够猜到MappedStatement就是在这方法里面创建的。
//org.apache.ibatis.builder.MapperBuilderAssistant#addMappedStatement(java.lang.String, org.apache.ibatis.mapping.SqlSource, org.apache.ibatis.mapping.StatementType, org.apache.ibatis.mapping.SqlCommandType, java.lang.Integer, java.lang.Integer, java.lang.String, java.lang.Class<?>, java.lang.String, java.lang.Class<?>, org.apache.ibatis.mapping.ResultSetType, boolean, boolean, boolean, org.apache.ibatis.executor.keygen.KeyGenerator, java.lang.String, java.lang.String, java.lang.String, org.apache.ibatis.scripting.LanguageDriver, java.lang.String)
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对象中
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 statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
//使用构建者模式构建MappedStatement
MappedStatement statement = statementBuilder.build();
//将MappedStatement添加到configuration对象中
configuration.addMappedStatement(statement);
return statement;
}
继续查看configuration.addMappedStatement(statement)方法的实现:
//org.apache.ibatis.session.Configuration#addMappedStatement
public void addMappedStatement(MappedStatement ms) {
mappedStatements.put(ms.getId(), ms);
}
Debug图如下所示:
其中Map<String, MappedStatement> mappedStatements是configuration类中的一个成员属性,里面存放着我们所有mapper接口以及对应的SQL信息的绑定信息。为什么使用map来存放,其实就是为了后面执行具体的mapper方法的时候,从mappedStatements根据【namespace+方法名称】作为key,从mappedStatements中进行获取到MappedStatement对象,进而就能获取到对应的SQL已经参数信息,自然就能执行SQL了,以上就是MappedStatement的生成过程。
前面我们分析了XMLMapperBuilder的parse()方法对mapper接口的解析过程,在执行完configurationElement()方法后还调用了bindMapperForNamespace()方法将mapper接口与MapperProxyFactory绑定绑定的过程。
下面我们看一下bindMapperForNamespace()的源码:
//org.apache.ibatis.builder.xml.XMLMapperBuilder#bindMapperForNamespace
private void bindMapperForNamespace() {
//获取到当前mapper接口的命名空间:com.wsh.mybatis.mybatisdemo.mapper.UserMapper
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
//通过classloader生成对应的Class对象
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
//ignore, bound type is not required
}
if (boundType != null) {
if (!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接口,将mapper接口添加到configuration全局配置中
configuration.addMapper(boundType);
}
}
}
}
可以看到,真正调用的是configuration.addMapper()方法:
//org.apache.ibatis.session.Configuration#addMapper
public <T> void addMapper(Class<T> type) {
//mapperRegistry是Configuration类的一个成员属性
//protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
mapperRegistry.addMapper(type);
}
//org.apache.ibatis.binding.MapperRegistry#addMapper
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 {
//将mapper接口作为key,存入knownMappers中
//knownMappers是一个map, Map<Class<?>, MapperProxyFactory<?>> knownMappers
//接口类型(key)->工厂类的映射关系
knownMappers.put(type, new MapperProxyFactory<>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
Debug观察一下:
前面我们分析了数据源怎么解析的,还有mapper接口是如何解析的,并且如何生成MappedStatement的,分析完后,我们回到最开始解析XML那里,执行完parseConfiguration(),已经把解析出来的配置都封装进Configuration 对象了,所以XMLConfigBuilder的parse()方法执行完成之后,会返回configuration对象,然后传入SqlSessionFactoryBuilder#build(org.apache.ibatis.session.Configuration)中构建SqlSessionFactory对象,具体代码如下:
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
如下图,可以看到,所有相关配置解析出来之后都保存在了SqlSessionFactory 的成员变量configuration中,然后通过configuration创建了DefaultSqlSessionFactory对象。至此,我们的SqlSessionFactory对象就算构建成功了。
小总结:
XMLMapperBuilder.parse()方法,是对 Mapper接口的解析,里面有两个方法: