1.阅读源码准备工作
下载源码,以及mybatis-parent,对源码进行编译,详见 MyBatis如何给源码加中文注释 一文
2.测试代码
通过分析测试代码,带着几个疑惑开始阅读源码
package com.gupaoedu;
import com.gupaoedu.domain.associate.AuthorAndBlog;
import com.gupaoedu.domain.Blog;
import com.gupaoedu.domain.associate.BlogAndAuthor;
import com.gupaoedu.domain.associate.BlogAndComment;
import com.gupaoedu.mapper.BlogMapper;
import com.gupaoedu.mapper.BlogMapperExt;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.*;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class MyBatisTest {
private SqlSessionFactory sqlSessionFactory;
@Before
public void prepare() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 创建工厂类:解析配置文件,经过了什么样的过程,得到了什么样的结果
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}
/**
* 通过 SqlSession.getMapper(XXXMapper.class) 接口方式
* @throws IOException
*/
@Test
public void testSelect() throws IOException {
// 通过工厂类获取到SqlSession,这里有两个问题: 1. SqlSession是一个接口,这里返回的到底是什么样的一个实现 2、创建SqlSession实现类的时候,又创建了哪些对象
SqlSession session = sqlSessionFactory.openSession(); // ExecutorType.BATCH
try {
// BlogMapper到底是个什么样的对象,为什么传递进去一个BlogMapper,然后又返回一个BlogMapper
BlogMapper mapper = session.getMapper(BlogMapper.class);
// BlogMapper既然是一个接口,那么是无法去调用方法的,这里的mapper必然是BlogMapper的一个具体的实现类,那么它真正的执行方法是什么样的
Blog blog = mapper.selectBlogById(1);
System.out.println(blog);
} finally {
session.close();
}
}
}
从上面代码可以看出,mybatis主要工作流程包含4个步骤
- 解析配置文件
- 创建工厂类
- 创建会话
- 通过会话操作数据库
3.源码分析
1. 配置读取与解析
// 所有构造都调用这一个真正的build方法
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
// XMLConfigBuilder用来解析全局配置文件
// 为了解析不同的配置文件,还有其他的XML**Builder,比如XMLMapperBuilder是用来解析 mapper.xml映射器文件的
// XMLStatementBuilder是用来解析映射器文件中的增删改查标签的
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
// 先进入build方法,parser.parse()返回的是一个Configuration对象
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.
}
}
}
对于不同类型的内容,交给不同的builder去解析;
protected final Configuration configuration;
protected final TypeAliasRegistry typeAliasRegistry;
protected final TypeHandlerRegistry typeHandlerRegistry;
进入XMLConfigBuilder构造函数:
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
// 解析之前,先把Configuration对象创建好,用来保存xml配置文件根标签<configuration>的属性
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
返回org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.Reader, java.lang.String, java.util.Properties)
方法,重点看parser.parse() 方法,点击进入,看到一个parseConfiguration方法:
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// DMO SAX都有用到
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
parseConfiguration方法如下:
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
// 解析全局配置文件中的每个标签
propertiesElement(root.evalNode("properties"));
// 解析settings标签
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
// 设置日志实现类
loadCustomLogImpl(settings);
// 类型别名,之所以可以直接使用string这种类型别名,是因为TypeAliasRegistry类中已经 定义好了
typeAliasesElement(root.evalNode("typeAliases"));
// 插件
pluginElement(root.evalNode("plugins"));
// 对象工厂:把结果集封装成对象时,只能通过反射来创建对象并给对象属性赋值
objectFactoryElement(root.evalNode("objectFactory"));
// 用于对对象进行特殊的包装
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 反射工具箱
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// settings 子标签赋值,默认值就是在这里提供的;前面只是解析,并未赋值
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
// 创建数据源
environmentsElement(root.evalNode("environments"));
// 数据库厂商
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 类型转换器: JDBC类型与java类型之间的双向类型转换
typeHandlerElement(root.evalNode("typeHandlers"));
// mapper.xml映射文件的路径
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
其中的关键点在于以下几个方法
- settingsElement(settings) 设置mybatis的默认配置
- environmentsElement(root.evalNode(“environments”)); 创建数据源
- typeHandlerElement(root.evalNode(“typeHandlers”)); 类型转换器的配置,完成Java类型与数据库字段类型的双向转换,对于一个字段在两种数据类型做相互转换,mybatis用了一个 private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>(); 来保存这种类型映射关系
- mapperElement(root.evalNode(“mappers”)); 解析mapper.xml映射器文件
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)) {
// 事务工厂
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
// 数据源工厂
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
// 数据源
DataSource dataSource = dsFactory.getDataSource();
// 包含了事务工厂和数据源的Environment
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
// 全部设置到Configuration对象中
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
解析mapper:
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 不同的定义方式的扫描,最终都调用addMapper方法添加到mapperRegistry
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
// resource 相对路径
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 解析Mapper.xml
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
// 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) {
// class接口
Class<?> mapperInterface = Resources.classForName(mapperClass);
// 最终都会调者addMapper方法
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
关键方法在 mapperParser.parse() :
这里很关键
第一,解析出来的sql语句放到了哪个对象里面(sql的注册),涉及到后面使用sql时如何取
第二,注册namespace对应的接口,注册时做了哪些事情
public void parse() {
// 总体上做两件事情:对增删改查语句的注册 和namespace对应的接口的注册
if (!configuration.isResourceLoaded(resource)) {
// 具体增删改查标签解析
// 重点!!! 一个标签对应一个MappedStatement
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
// 把namespace接口类型和工厂类绑定,放到一个map中 一个namespace一个 MapperProxyFactory
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
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);
// 添加缓存对象
cacheRefElement(context.evalNode("cache-ref"));
// 解析cache标签,添加缓存对象,只有加了cache标签才会去解析,也就是说,namespace下默认是没有二级缓存的
cacheElement(context.evalNode("cache"));
// 创建ParameterMapping对象
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 创建List<ResultMapping>
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 解析可复用的sql
sqlElement(context.evalNodes("/mapper/sql"));
// 重点:解析增删改查标签,得到MappedStatement, 后续使用简写都为ms
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: 使用XMLStatementBuilder 解析增删改查sql标签
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
// 解析Statement
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
// 用来解析增删改查标签的XMLStatementBuilder
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
// 解析Statement,添加MappedStateMent对象
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
statementParser.parseStatementNode();
-----> org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode
-----> 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)
返回一个MappedStatement对象,这个对象保存了增删改查标签中的各种属性的值:
可以看到sql语句被解析到了MappedStatement对象中,那么 对于namespace中的接口,是如何解析的呢?回到org.apache.ibatis.builder.xml.XMLMapperBuilder#parse方法的 bindMapperForNamespace();中:
private void bindMapperForNamespace() {
// namespace就是接口类型(接口类的全路径名)
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);
// 添加到MapperRegistry 本质是个map,里面也有Configuration
configuration.addMapper(boundType);
}
}
}
}
addMapper是这样实现的:
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
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 {
// Map<Class<?>,MapperProxyFactory<?>>存放的是接口类型和该接口对应的工厂类的关系
//MapperProxyFactory.newInstance 返回接口的代理实现类!!!
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.
// 注册了接口之后,根据接口,开始解析所有方法上的注解如@Select
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
// 解析注解!
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
knownMappers : private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
上面的创建过程,可以总结如下:
回到org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.Reader, java.lang.String, java.util.Properties) 方法,
这个方法最终返回的是一个DefaultSqlSessionFactory对象
E:\resources-compile\mybatis-resources
E:\Baidudownload\2020期课程资料\02.架构师审美观\06.MyBatis原理篇\01.MyBatis应用分析与最佳实践\课堂源码\mybatis-standalone\