在这里记录一下自己学习mybatis源码过程中的一些学习体会,文章内容基于mybatis3.5.3-SNAPSHOT:
下面是mybatis一个测试用例中配置文件的截图,配置文件详情参考mybatis中文官网:
1.事例
下面是mybatis测试用例中加载配置文件,并且运行的过程,这篇文章主要记录一下mybatis加载配置文件的过程
@BeforeAll
static void setUp() throws Exception {
// create a SqlSessionFactory
try (Reader reader = Resources.getResourceAsReader("org/apache/ibatis/submitted/permissions/mybatis-config.xml")) {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
}
// populate in-memory database
BaseDataTest.runScript(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(),
"org/apache/ibatis/submitted/permissions/CreateDB.sql");
}
从以上的实例代码可以看到关于mybatis读取默认配置文件的过程,接下来就是详细的看看整体的过程。
2.源码分析
2.1创建SqlSessionFactory
SqlSession是mybatis的关键,这个接口包含了sql执行,事务,缓存等许多的方法。要获取SqlSession就要先得到SqlSessionFactory。为了得到SqlSessionFactory就需要使用SqlSessionFactoryBuilder来解析配置文件,SqlSessionFactoryBuilder有多个build方法,基本一致,挑一个来看看。
public SqlSessionFactorybuild(Reader reader, String environment, Properties properties) {
try {
// 创建 XMLConfigBuilder 对象,底层使用的是jdk的XPath解析xml文件
XMLConfigBuilder parser =new XMLConfigBuilder(reader, environment, properties);
// 执行 XML 解析
// 创建 DefaultSqlSessionFactory 对象
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.2 解析配置文件
下面我们来看看parser.parse():
public Configurationparse() {
// 判断是否已经加载过
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed =true;
// 解析configuration节点
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
最重要的是parseConfiguration方法:
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
// 属性
propertiesElement(root.evalNode("properties"));
// 设置,这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为
Properties settings = settingsAsProperties(root.evalNode("settings"));
// 加载自定义 VFS 实现类
loadCustomVfs(settings);
// 指定 MyBatis 所用日志的具体实现,未指定时将自动查找
loadCustomLogImpl(settings);
// 类型别名,为 Java 类型设置一个短的名字
typeAliasesElement(root.evalNode("typeAliases"));
// 插件,在已映射语句执行过程中的某一点进行拦截调用
pluginElement(root.evalNode("plugins"));
// 对象工厂,MyBatis 每次创建结果对象的新实例时,它都会使用一个对象工厂实例来完成
objectFactoryElement(root.evalNode("objectFactory"));
// 对象包装工厂
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 反射工厂
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// 设置settings属性到configuration中,没有时设置默认配置
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
// 环境配置
environmentsElement(root.evalNode("environments"));
// 数据库厂商标识
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 类型处理器,MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,
// 还是从结果集中取出一个值时, 都会用类型处理器将获取的值以合适的方式转换成 Java 类型
typeHandlerElement(root.evalNode("typeHandlers"));
// SQL 映射语句
mapperElement(root.evalNode("mappers"));
}catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
大多数都是属性的设置,最终所有的设置都会配置到XMLConfigBuilder以及父类BaseBuilder的属性对象中,其中mapperElement方法是解析mapper.xml,即我们的mapper.xml文件或者*mapper.java接口(针对在java文件中通过注解创建sql和加上一些配置等)。
2.3 xml文件以及接口解析
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 如果是配置的package那就扫描包,针对已经在方法上使用注解实现功能
<1>
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");
// 解析本地的xml文件
<2>
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
}
// 解析远程地址上的xml文件
<3>
else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
}
// 单个文件解析,也是针对已经在方法上使用注解实现功能
<4>
else if (resource == null && url == null && mapperClass != null) {
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.");
}
}
}
}
}
<1>,<2>,<3>,<4>处的代码,最终的解析方式都是解析解析xml的同时解析对应的接口内的方法,或者是先解析接口内的方法再解析接口对应的xml文件
configuration.addMappers,先来看下MapperRegistry.addMapper方法:
public void addMapper(Class type) {
// 判断必须是接口
if (type.isInterface()) {
// 判断是否解析过
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 用于判断是否解析过
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);
<1>
parser.parse();
loadCompleted = true;
} finally {
// 解析错误,留到后面解析
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
<1>处代码最关键
public void parse() {
String resource = type.toString();
// 判断是否加载过
if (!configuration.isResourceLoaded(resource)) {
// 加载对应的*mapper.xml文件
<1>
loadXmlResource();
// 用于判断是否加载
configuration.addLoadedResource(resource);
// 设置当前命名空间,如果与当前命名空间不一致,抛出错误
// 我理解可能是防止多线程下同时解析不同文件
assistant.setCurrentNamespace(type.getName());
// 解析@CacheNamespace,二级缓存相关
parseCache();
// 解析@CacheNamespaceRef,二级缓存相关
parseCacheRef();
Method[] methods = type.getMethods();
// 遍历每个方法,解析其上的注解
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
<2>
parseStatement(method);
}
} catch (IncompleteElementException e) {
// 解析失败,添加到 configuration 中
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
// 解析上面for循环解析失败的方法
parsePendingMethods();
}
其中<1>处代码是解析xml文件的,<2>处代码是解析对应的java接口
先来看看<1>处代码是怎么找到并且解析xml文件的
private void loadXmlResource() {
// Spring may not know the real resource name so we check a flag
// to prevent loading again a resource twice
// this flag is set at XMLMapperBuilder#bindMapperForNamespace
// 判断是否加载过
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
// 获取当前对应的xml的路径
String xmlResource = type.getName().replace('.', '/') + ".xml";
// #1347
// 获取当前模块中的xml文件流对象
InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
if (inputStream == null) {
// Search XML mapper that is not in the module but in the classpath.
try {
// 获取不在当前模块,但是在对应路径下的xml文件
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e2) {
// ignore, resource is not required
}
}
if (inputStream != null) {
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
xmlParser.parse();
}
}
}
继续来看看XMLMapperBuilder.parse()是如何解析xml文件的。
2.3.1解析xml
public void parse() {
// 如果没有加载过
if (!configuration.isResourceLoaded(resource)) {
// 解析xml文件中的所有标签
<1>
configurationElement(parser.evalNode("/mapper"));
// 标记该 Mapper 已经加载过
configuration.addLoadedResource(resource);
// 解析对应的*mapper.java文件,
// 解析xml或者java文件的时候都会去解析对应的另外一个文件
// 在解析对应的文件时都要判断是否已经解析过
bindMapperForNamespace();
}
// 解析待定的 节点
parsePendingResultMaps();
// 解析待定的 节点
parsePendingCacheRefs();
// 解析待定的 SQL 语句的节点
parsePendingStatements();
}
重点看看<1>处的代码,是如何解析整个xml文件中的所有节点的,最后面的3个方法是继续尝试解析前面解析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");
}
// 设置 namespace 属性
builderAssistant.setCurrentNamespace(namespace);
// 解析 节点
cacheRefElement(context.evalNode("cache-ref"));
// 解析 节点
cacheElement(context.evalNode("cache"));
// 已废弃!老式风格的参数映射。内联参数是首选,这个元素可能在将来被移除,这里不会记录。
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// <1> 解析 节点们,解析成resultMap对象保存在 configuration 中
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 解析 节点们,保存id和node对应关系到 sqlFragments 中
sqlElement(context.evalNodes("/mapper/sql"));
// <2> 解析
这里比较复杂的是<1>和<2>处的解析,别的比较简单,可自行看一下。
2.3.1.1resultMapElements(context.evalNodes("/mapper/resultMap"))
private ResultMap resultMapElement(XNode resultMapNode, List additionalResultMappings, Class> enclosingType) throws Exception {
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
// 获取 type
String type = resultMapNode.getStringAttribute("type",
resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType",
resultMapNode.getStringAttribute("javaType"))));
// 替换别名,获取 class 对象
Class> typeClass = resolveClass(type);
if (typeClass == null) {
typeClass = inheritEnclosingType(resultMapNode, enclosingType);
}
Discriminator discriminator = null;
List resultMappings = new ArrayList<>();
resultMappings.addAll(additionalResultMappings);
// 解析子节点
List resultChildren = resultMapNode.getChildren();
// 解析所有的节点到 resultMappings 中
for (XNode resultChild : resultChildren) {
if ("constructor".equals(resultChild.getName())) {
processConstructorElement(resultChild, typeClass, resultMappings);
} else if ("discriminator".equals(resultChild.getName())) {
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
} else {
List flags = new ArrayList<>();
if ("id".equals(resultChild.getName())) {
flags.add(ResultFlag.ID);
}
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
String id = resultMapNode.getStringAttribute("id",
resultMapNode.getValueBasedIdentifier());
String extend = resultMapNode.getStringAttribute("extends");
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
try {
// 保存 resultMap 到 configuration 中
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}
此处主要是解析
2.3.1.2buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
private void buildStatementFromContext(List list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
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);
}
}
}
public void parseStatementNode() {
// 例如
最终新增的MappedStatement对象代表的是xml一个sql标签,包含这个sql的所有配置以及sql语句等属性。
2.3.2 解析java接口
代码入口在MapperAnnotationBuilder.parse()中的parseStatement(method)方法,这就是解析接口中的每个方法以及方法上的注解的。
void parseStatement(Method method) {
// 获取非分页的形参类型,多个参数用 ParamMap 表示
Class> parameterTypeClass = getParameterType(method);
// 注解式的动态sql
LanguageDriver languageDriver = getLanguageDriver(method);
// 获取 sqlsourse 对象
SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
if (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 = configuration.getDefaultResultSetType();
SqlCommandType sqlCommandType = getSqlCommandType(method);
// 是否是 select sql 语句
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
// 如果不是 select 默认开始二级缓存
boolean flushCache = !isSelect;
// select 默认开启一级缓存
boolean useCache = isSelect;
KeyGenerator keyGenerator;
String keyProperty = null;
String keyColumn = null;
// 获取生成自动主键的 keyGenerator
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();
if (options.resultSetType() != ResultSetType.DEFAULT) {
resultSetType = options.resultSetType();
}
}
String resultMapId = null;
ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
if (resultMapAnnotation != null) {
resultMapId = String.join(",", resultMapAnnotation.value());
} else if (isSelect) {
resultMapId = parseResultMap(method);
}
// 构造 mappedstatement configuration 中
assistant.addMappedStatement(
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);
}
}
大致的解析解析过程就是这样的,总的来说就是解析所有的配置文件组成一个configuration对象,然后会调用SqlSessionFactory的build方法new 一个DefaultSqlSessionFactory对象,并且设置configuration,configuration几乎包含了mybatis所有的属性,贯穿几乎所有的mybatis流程。