背景
- 按照项目的配置文件配置文件地址开始分析源码, 从SqlSessionFactoryBean开始分析。
- 如果配置了mapperScanner,会对每个mapper逐一调用SqlSessionFactoryBean对应的方法。
本篇文章分析时序图
分析SqlSessionFactoryBean
- SqlSessionFactoryBean实现了InitializingBean,这个接口的afterPropertiesSet()会在初始化时被调用,所以直接看SqlSessionFactoryBean的afterPropertiesSet()方法。
public void afterPropertiesSet() throws Exception {
//在配置文件里面设置的属性
notNull(dataSource, "Property 'dataSource' is required");
//这个是类的属性,sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();创建
notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
this.sqlSessionFactory = buildSqlSessionFactory();
}
- 调用自身的buildSqlSessionFactory()方法
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
Configuration configuration;
XMLConfigBuilder xmlConfigBuilder = null;
if (this.configLocation != null) {
/**
* configLocation对应了mybatis的configuration属性
* 以及底下的settings typeAliases的属性设置
* /
xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
configuration = xmlConfigBuilder.getConfiguration();
} else {
......
}
//......省略一些属性的配置
if (xmlConfigBuilder != null) {
try {
//重点步骤1 解析mybatis configuration配置的属性
xmlConfigBuilder.parse();
......
} catch (Exception ex) {
throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
} finally {
ErrorContext.instance().reset();
}
}
configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));
if (!isEmpty(this.mapperLocations)) {
for (Resource mapperLocation : this.mapperLocations) {
if (mapperLocation == null) {
continue;
}
try {
//重点步骤2 解析mapper.xml配置的属性
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
configuration, mapperLocation.toString(), configuration.getSqlFragments());
xmlMapperBuilder.parse();
} catch (Exception e) {
throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
} finally {
ErrorContext.instance().reset();
}
......
}
} else {
......
}
//通过SqlSessionFactoryBuilder构建SqlSessionFactory
return this.sqlSessionFactoryBuilder.build(configuration);
}
XmlConfigBuilder
- xmlConfigBuilder.parse()此方法是重点步骤1 解析mybatis configuration配置的属性 像什么settings,typeAliases, plugins, mappers,并设置到全局的configuration属性上去。
public Configuration parse() {
//被解析过了就抛出异常
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//解析configuration XPathParser已经做了些基础层的工作
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
- 继续往parseConfiguration()方法看,此方法是解析myabtis配置文件各个节点,代码贴出来,各个方法名称也比较容易知晓含义
private void parseConfiguration(XNode root) {
try {
Properties settings = settingsAsPropertiess(root.evalNode("settings"));
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
loadCustomVfs(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectionFactoryElement(root.evalNode("reflectionFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
/**
* 本项目没有在mybatis里面配置mappers属性,而是在
*sqlSessionFactoryBean里面配置,所以这里不生效
*/
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
XMLMapperBuilder
- xmlMapperBuilder.parse()方法是重点步骤3 这里负责解析映射配置文件
public void parse() {
//判断是否载入过resource
if (!configuration.isResourceLoaded(resource)) {
//主流程加载mapper
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
//注册mapper
bindMapperForNamespace();
}
//处理configurationElement解析失败的属性
parsePendingResultMaps();
//处理configurationElement解析失败的属性
parsePendingChacheRefs();
//处理configurationElement解析失败的sql语句
parsePendingStatements();
}
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
//记录当前命名空间
builderAssistant.setCurrentNamespace(namespace);
//以下均是接下对应节点,比如这个是解析cacha-ref
cacheRefElement(context.evalNode("cache-ref"));
//这个是解析cache mybats二级缓存
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
//解析select insert update delete重点关注 这个流程跟sql执行主流程相关
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
}
}
- 比较重要的分析下,解析cache mybats二级缓存,最后把cache通过builderAssistant放到Configuration的cache属性,cache属性是Map, key是Cache对应的属性id,id记录了二级缓存开启的sql。 Map caches = new StrictMap("Caches collection"); StrictMap是内部类检测到重复key会抛出异常
private void cacheElement(XNode context) throws Exception {
if (context != null) {
//获取节点的 type属性,默认位是 PERPETUAL
String type = context.getStringAttribute("type", "PERPETUAL");
Class extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
//获取节点的 eviction 属性,默认位是 LRU
String eviction = context.getStringAttribute("eviction", "LRU");
//解析 eviction 属,性指定的 Cache 装饰器类型
Class extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
Properties props = context.getChildrenAsProperties();
//通过 MapperBuilderAssistant 创建 Cache 对象,并添加到 Configuration . caches 集合中保存
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
- 还有一些resultMap的解析 这个后续再分析。
- 解析sql代码这里需要重点关注,涉及sql执行的流程,buildStatementFromContext(context.evalNodes("select|insert|update|delete"))
private void buildStatementFromContext(List list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
XMLStatementBuilder
- XMLStatementBuilder使用的数据结构
MappedStatement
//节点中的id属性 包括前缀
private String resource;
//SqlSource 对象,对应一条 SQL 语句
private SqlSource sqlSource;
// 枚举类型 UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
private SqlCommandType sqlCommandType;
SqlSourc(构造方法由Configuration和SqlNode来的 )
public interface SqlSource {
/**
* getBoundSql ()方法会根据映射文件或注解描述的 SQL 语句,
* 以及传入的参敛,返回可执行的 SQL
*/
BoundSql getBoundSql(Object parameterObject);
}
- 重要的流程分析,涉及sql执行的主流程 parseStatementNode()方法
public void parseStatementNode() {
/** 获取SQL节点的 id 以及 database Id 属性 ,若其 databaseid
* 属性值与当前使用的数据库不匹配,则不加载该 SQL 节点;
* 若存在相同 id 且databaseid不为空的 SQL 节点则不再加载该SQL节点(略)
*/
/** 获取SQL节,点的多种属性例如,fetchSize,timeout,parameterType,
* parameterMap ,resultMap等,具体实现比较简单,不在贴出来了。这些属性的具体含义
* 在 MyBatis 官方文档中已经有比较详细的介绍了,这里不再赘述
*/
//根据SQL节点的名称决定其 SqlCommandType
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
//在解析 SQL 语句 之前,先处理其中的include节点
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// 处理selectKey节点
processSelectKeyNodes(id, parameterTypeClass, langDriver);
//完成SQL节点的解析,该部分是parseStatementNode方法的核心后面单独分析
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))
? new Jdbc3KeyGenerator() : new NoKeyGenerator();
}
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
- 接下来是调用
LanguageDriver.createSqlSource()方法创建对应 的 SqISource 对象 , 最后创建 Mapped Statement 对
象,井添加到 Configuration .mappedStatements 集合中保存。
LanguageDriver接口
- 通过LanguageDriver.createSqlSource方法创建SqlSource对象
SqlSource createSqlSource(Configuration configuration, XNode script, Class> parameterType);
XMLLanguageDriver
//创建SqlSource
public SqlSource createSqlSource(Configuration configuration, XNode script, Class> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
XMLScriptBuilder
SqlNode
public interface SqlNode {
//最后通过传入的sql参数由动态sql解析出可被mysql执行的sql
boolean apply(DynamicContext context);
}
- MixedSqlNode, IfSqlNode, TextSqlNode, StaticTextSqlNode都是其实现类
- xmlScriptBuilder.parseScriptNode()方法创建SqlSource, 此方法运用了组合模式,是组合模式加入元素的部分
public SqlSource parseScriptNode() {
/**首先判断当前的节点是不是有动态 SQL ,动态 SQL 会包括占位符
*或是动态 SQL 的相关节点
*/
List contents = parseDynamicTags(context);
// SqlNode集合包装成一个MixedSqlNode
MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
SqlSource sqlSource = null;
//根据是否是动态 SQL,创建相应的SqlSource对象
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
- xmlScriptBuilder.parseDynamicTags()方法, 解析出组合模式的初始化属性, 这里对应含有各自if trim等元素的动态sql树
List parseDynamicTags(XNode node) {
List contents = new ArrayList();
//获取子节点, 比如select 底下有if什么的一层一层的
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
//创建 XNode ,该过程会将能解析掉的${}都解析掉
XNode child = node.newXNode(children.item(i));
//文本节点 TextSqlNode StaticTextSqlNode则直接添加
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
//如果是其他比如if trim这类节点一定是动态sql
//通过Handler处理其他动态节点
String nodeName = child.getNode().getNodeName();
//初始化内部类NodeHandler
NodeHandler handler = nodeHandlers(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
//处理动态 SQL ,并将解析得到 的 SqlNode 对象放入 contents 集合中保存
handler.handleNode(child, contents);
isDynamic = true;
}
}
return contents;
}
NodeHandler
NodeHandler nodeHandlers(String nodeName) {
Map map = new HashMap();
map.put("trim", new TrimHandler());
map.put("where", new WhereHandler());
map.put("set", new SetHandler());
map.put("foreach", new ForEachHandler());
map.put("if", new IfHandler());
map.put("choose", new ChooseHandler());
map.put("when", new IfHandler());
map.put("otherwise", new OtherwiseHandler());
map.put("bind", new BindHandler());
return map.get(nodeName);
}
- handleNode()将处理动态 SQL ,并将解析得到 的 SqlNode 对象放入 contents 集合中保存,使用IfHandlerNode举例
public void handleNode(XNode nodeToHandle, List targetContents) {
//递归调用parseDynamicTags,直到TextSqlNode为递归出口
List contents = parseDynamicTags(nodeToHandle);
MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
String test = nodeToHandle.getStringAttribute("test");
//创建 IfSqlNode其中也是多个SqlNode
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
targetContents.add(ifSqlNode);
}
- XMLMapperBuilder.parse()中调用bindMapperForNamespace(); 完成了映射配置文件与对应 Mapper 接
口的绑定
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class> boundType = null;
try {
//命名空间具体绑定的类
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);
//注册到configuration
configuration.addMapper(boundType);
}
}
}
}
IncompleteElementException
- XMLMapperBuilder.configurationElement()方法解析映射配置文件时,是按照从文件头到文件尾的顺序解析的,但是有时候在解析一个节点时, 会引用定义在该节点之后的、还未解析的节点,这就会导致解析失败井抛出IncompleteElementException 。
- 根据抛出异常的节点不同, MyBatis 会创建不同 *Resolver 对象, 井添加到 Configuration的不同 incomplete*集合中.
对异常部分感兴趣的可以参考mybatis技术内幕》第三章
- 参考自《mybatis技术内幕》第三章
- mybatis3中文文档