一、mybatis配置文件详情
在Spring中,是使用xml的配置文件或使用java代码对mybatis的连接属性,环境等进行配置的,我们先看一下mybatis中对配置文件中节点的要求。
通过mybatis官方网站中XML配置一栏,可以得出相应需求:
- properties(属性)
- settings(设置)
- typeAliases(类型别名)
- typeHandlers(类型处理器)
- objectFactory(对象工厂)
- plugins(插件)
- environments(环境配置)
- environment(环境变量)
- transactionManager(事务管理器)
- dataSource(数据源)
- environment(环境变量)
- databaseIdProvider(数据库厂商标识)
- mappers(映射器)
我们只有在知道mybatis中的配置中有哪些节点数据,才能够理解mybatis中的解析xml配置的相关代码。
SQL映射文件中只有很少的几个顶级元素:
- cache - 该命名空间的缓存配置
- cache-ref - 引用其他命名空间的缓存配置
- resultMap - 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素
- sql - 可被其他语句引用的可重用的语句块
- insert - 映射插入语句
- update - 映射更新语句
- delete - 映射删除语句
- select - 映射查询语句
二、mybatis配置文件解析
在了解了mybatis·1对配置文件的要求之后,以及配置文件中的相应xml节点及配置之后,我们便可以从源码的对XML配置文件的解析,开始了解mybatis的源码内部详情。
我们都知道,在Spring中使用mybatis的时候,需要为Spring提供两个XML文件,一个是beans-mybatis.xml,另外一个是mybatis-config.xml。其中beans-mybatis.xml文件中配置的是Mybatis和Spring结合使用时委托给Spring管理的bean。而mybatis-config.xml中是mybatis的自身的配置。
所以在Spring启动的时候,会通过解析beans-mybatis.xml文件来启动mybatis。我们看一下beans-mybatis.xml的配置文件例子:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean" p:dataSource-ref="dataSource" p:configLocation="classpath:mybatis-config.xml"
p:mapperLocations="classpath:mapper/**/*.xml" />
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<tx:annotation-driven transaction-manager="transactionManager" />
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer" p:basePackage="com.cn.kvn.usage.dao" />
其中bean标签指定的Spring容器启动的时候,需要实例化的bean。我们可以看到,启动mybatis的时候,首先启动的是SqlSessionFactoryBean。所以我们看一下在mybatis中SqlSessionFactory。
在mybatis中,SqlSessionFactory是一个接口,使用SqlSessionFactoryBuilder创建SqlSessionFactory实例。我们学习一个SqlSessionFactoryBuilder创建SqlSessionFactory的过程代码:
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.
}
}
}
我们通过build中的方法可以看出该方法有三个参数,一个是Reader,一个是字符串类型的环境参数,一个是属性参数。也就是我们在调用该方法的时候,可以传递相关参数信息,同时通过XMLConfigBuilder进行xml格式的解析,创建mybatis运行的基本环境。我们看一下XMLConfigBuilder类。在该类中,通过 parse 方法解析XM配置文件并生成Configuration对象。
我们先看一下mybatis-config.xml的配置文件整体的例子:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 1.properties属性引入外部配置文件 -->
<properties resource="config/db.properties">
<!-- property里面的属性全局均可使用 -->
<property name="jdbc.username" value="root"/>
<property name="jdbc.password" value="123456"/>
<!-- 启用默认值特性,这样${}拼接符才可以设置默认值 -->
<property name="org.apache.ibatis.parsing.PropertyParser.enable-default-value" value="true"/>
</properties>
<!-- 2.全局配置参数 -->
<settings>
<!-- 指定 MyBatis 所用日志的具体实现,未指定时将自动查找 -->
<setting name="logImpl" value="STDOUT_LOGGING"/>
<!-- 开启自动驼峰命名规则(camel case)映射 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 开启延迟加载开关 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 将积极加载改为消极加载(即按需加载),默认值就是false -->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 打开全局缓存开关(二级环境),默认值就是true -->
<setting name="cacheEnabled" value="true"/>
</settings>
<!-- 3.别名设置 -->
<typeAliases>
<typeAlias alias="user" type="com.pjb.mybatis.po.User"/>
<typeAlias alias="teacher" type="com.pjb.mybatis.po.Teacher"/>
<typeAlias alias="integer" type="java.lang.Integer"/>
</typeAliases>
<!-- 4.类型转换器 -->
<typeHandlers>
<!-- 一个简单的类型转换器 -->
<typeHandler handler="com.pjb.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>
<!-- 5.对象工厂 -->
<objectFactory type="com.pjb.mybatis.example.ExampleObjecFactory">
<!-- 对象工厂注入参数 -->
<property name="someProperty" value="100"/>
</objectFactory>
<!-- 6.插件 -->
<plugins>
<plugin interceptor="com.pjb.mybatis.example.ExamplePlugin">
<property name="someProperty" value="100"/>
</plugin>
</plugins>
<!-- 7.environments数据库环境配置 -->
<!-- 和Spring整合后environments配置将被废除 -->
<environments default="development">
<environment id="development">
<!-- 使用JDBC事务管理 -->
<transactionManager type="JDBC"/>
<!-- 数据库连接池 -->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username:root}"/>
<property name="password" value="${jdbc.password:123456}"/>
</dataSource>
</environment>
</environments>
<!-- 8.加载映射文件 -->
<mappers>
<mapper resource="com.pjb.mybatis.sqlmap.UserMapper.xml"/>
<mapper resource="com.pjb.mybatis.sqlmap.OtherMapper.xml"/>
</mappers>
</configuration>
我们看一下parse函数的实现:
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
为了防止配置文件被重新解析,我们定义了一个全局的parsed属性,当解析之前,会通过parsed属性判断是否已经对config文件进行判断。如果已经解析,则抛出配置文件只能解析一次的异常;如果还没有被解析,则对配置文件进行解析。
同时我们可以看到,在进行配置文件解析的时候,是以configuration节点为起点的,而我们配置文件中,configuration也是根节点。在这里获取根节点,然后进行其他节点属性的获取。
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);
// 加载并配置typeAliases属性
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"));
// 加载并配置数据表mapper
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
通过parseConfiguration方法获取其相应的属性并进行相应的配置。我们看一下mapper的映射。
在mybatis配置文件xml中,对mapper的配置描述有四种写法:相对于类路径的资源引用,使用完全限定资源定位符,使用映射器接口实现类的完全限定类名和将包内的映射器接口实现全部注册为映射器。
<mappers>
<!-- 使用相对于类路径的资源引用 -->
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
<!-- 使用完全限定资源定位符(URL) -->
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
<!-- 使用映射器接口实现类的完全限定类名 -->
<mapper class="org.mybatis.builder.AuthorMapper"/>
</mappers>
<!-- 将包内的映射器接口实现全部注册为映射器 -->
<mappers>
<package name="org.mybatis.builder"/>
</mappers>
我们看一下mybatis中是如何对这些mapper配置进行解析的,mapper属性配置参数页面中是使用XMLMappperBilder来进行工作的。
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
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) {
ErrorContext.instance().resource(resource);
try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
}
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
try(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<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
}
}
}
}
}
这段代码比较明显,无论是哪一种指定mapper对应关系的方法,该解析都可以将其对应的数据进行解析并保存到配置中。
接着我们看一下,在对mappers中mapper解析后,如何对xml映射器进行解析的。在上述代码中,我们可以看出,该程序是通过mapperParse.parse()对XML映射文件进行解析的。在查看代码之前,我们先了解一下XML映射文件的几个顶级元素:
- cache - 该命名空间的缓存配置
- cache-ref - 引用其他命名空间的缓存配置
- resultMap - 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素
parameterMap - 已被弃用- sql - 可被其他语句引用的可重用语句块
- insert - 映射插入语句
- update - 映射更新语句
- delete - 映射删除语句
- select - 映射查询语句
接着我们看一下解析代码:
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
// 加载所有的子标签
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
// 绑定该命名空间下的mapper
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
// 对mapper.xml文件的解析
private void configurationElement(XNode context) {
// 首先判断根节点是否有namespace属性,该属性是必须要有的
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
// 获取对其他命名空间缓存配置的引用
cacheRefElement(context.evalNode("cache-ref"));
// 对本地命名空间缓存的配置
cacheElement(context.evalNode("cache"));
// 对mapper映射中参数的配置
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 描述从数据库查询后的结果集转换为java对象
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 获取其他语句引用的可重用语句块
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);
}
}
下面看一下如何解析增删改查标签并绑定statement。
// 获取MappedStatement对象(增删改查标签)
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
// 获取MappedStatement对象(增删改查标签)
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
// 解析insert/delete/update/select标案
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
通过上述代码,可以看到,在进行增删改查解析的时候,mybatis使用XMLStatementBuilder进行解析,然后调用parseStatementNode()进行解析并绑定。
public void parseStatementNode() {
// 获取唯一ID
String id = context.getStringAttribute("id");
// 获取不同数据源的daaseId
String databaseId = context.getStringAttribute("databaseId");
// 如果获取的dataBaseId 与 需要的databasId 设置的一致
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); // flushCache select下默认设置为false
boolean useCache = context.getBooleanAttribute("useCache", isSelect); // useCache在select默认情况下,设置为true
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false); // 返回结果的顺序
// Include Fragments before parsing
// 在解析之前引入组件
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// 获取parameterType参数
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
// 获取语言参数
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// Parse selectKey after includes and remove them.
// 在解析之前解析selectKey并移除
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
// 这里用于处理ID是如何处理,是自增还是怎么
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;
}
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
在这里可以看出,mybatis首先获取xml中相应的数据参数,然后判断是否是select查询语句,并设置默认的useCache和flushCache。在这里,mybatis在默认情况下,如果是select语句,useCache设置为true,flushCache设置为false;对于其他修改,新增,删除操作,则反过来。
获取完成数据相应参数之后,在将一些引用的语句加入其中,然后对sql语句的参数进行处理,还有包括语言参数等。在进行statement解析之前,对SelectedKey进行处理,其中包括设置为自增的还是其他策略的。
最后,通过addMappedStatement方法对statement对解析整合。
我们首先来看一下该数据解析:
public final class MappedStatement {
private String resource;
private Configuration configuration;
private String id;
private Integer fetchSize;
private Integer timeout;
private StatementType statementType;
private ResultSetType resultSetType;
private SqlSource sqlSource;
private Cache cache;
private ParameterMap parameterMap;
private List<ResultMap> resultMaps;
private boolean flushCacheRequired;
private boolean useCache;
private boolean resultOrdered;
private SqlCommandType sqlCommandType;
private KeyGenerator keyGenerator;
private String[] keyProperties;
private String[] keyColumns;
private boolean hasNestedResultMaps;
private String databaseId;
private Log statementLog;
private LanguageDriver lang;
private String[] resultSets;
MappedStatement() {
// constructor disabled
}
public static class Builder {
private MappedStatement mappedStatement = new MappedStatement();
public Builder(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType) {
mappedStatement.configuration = configuration;
mappedStatement.id = id;
mappedStatement.sqlSource = sqlSource;
mappedStatement.statementType = StatementType.PREPARED;
mappedStatement.resultSetType = ResultSetType.DEFAULT;
mappedStatement.parameterMap = new ParameterMap.Builder(configuration, "defaultParameterMap", null, new ArrayList<>()).build();
mappedStatement.resultMaps = new ArrayList<>();
mappedStatement.sqlCommandType = sqlCommandType;
mappedStatement.keyGenerator = configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
String logId = id;
if (configuration.getLogPrefix() != null) {
logId = configuration.getLogPrefix() + id;
}
mappedStatement.statementLog = LogFactory.getLog(logId);
mappedStatement.lang = configuration.getDefaultScriptingLanguageInstance();
}
public Builder resource(String resource) {
mappedStatement.resource = resource;
return this;
}
public String id() {
return mappedStatement.id;
}
public Builder parameterMap(ParameterMap parameterMap) {
mappedStatement.parameterMap = parameterMap;
return this;
}
public Builder resultMaps(List<ResultMap> resultMaps) {
mappedStatement.resultMaps = resultMaps;
for (ResultMap resultMap : resultMaps) {
mappedStatement.hasNestedResultMaps = mappedStatement.hasNestedResultMaps || resultMap.hasNestedResultMaps();
}
return this;
}
public Builder fetchSize(Integer fetchSize) {
mappedStatement.fetchSize = fetchSize;
return this;
}
public Builder timeout(Integer timeout) {
mappedStatement.timeout = timeout;
return this;
}
public Builder statementType(StatementType statementType) {
mappedStatement.statementType = statementType;
return this;
}
public Builder resultSetType(ResultSetType resultSetType) {
mappedStatement.resultSetType = resultSetType == null ? ResultSetType.DEFAULT : resultSetType;
return this;
}
public Builder cache(Cache cache) {
mappedStatement.cache = cache;
return this;
}
public Builder flushCacheRequired(boolean flushCacheRequired) {
mappedStatement.flushCacheRequired = flushCacheRequired;
return this;
}
public Builder useCache(boolean useCache) {
mappedStatement.useCache = useCache;
return this;
}
public Builder resultOrdered(boolean resultOrdered) {
mappedStatement.resultOrdered = resultOrdered;
return this;
}
public Builder keyGenerator(KeyGenerator keyGenerator) {
mappedStatement.keyGenerator = keyGenerator;
return this;
}
public Builder keyProperty(String keyProperty) {
mappedStatement.keyProperties = delimitedStringToArray(keyProperty);
return this;
}
public Builder keyColumn(String keyColumn) {
mappedStatement.keyColumns = delimitedStringToArray(keyColumn);
return this;
}
public Builder databaseId(String databaseId) {
mappedStatement.databaseId = databaseId;
return this;
}
public Builder lang(LanguageDriver driver) {
mappedStatement.lang = driver;
return this;
}
public Builder resultSets(String resultSet) {
mappedStatement.resultSets = delimitedStringToArray(resultSet);
return this;
}
/**
* Resul sets.
*
* @param resultSet
* the result set
* @return the builder
* @deprecated Use {@link #resultSets}
*/
@Deprecated
public Builder resulSets(String resultSet) {
mappedStatement.resultSets = delimitedStringToArray(resultSet);
return this;
}
public MappedStatement build() {
assert mappedStatement.configuration != null;
assert mappedStatement.id != null;
assert mappedStatement.sqlSource != null;
assert mappedStatement.lang != null;
mappedStatement.resultMaps = Collections.unmodifiableList(mappedStatement.resultMaps);
return mappedStatement;
}
}
public KeyGenerator getKeyGenerator() {
return keyGenerator;
}
public SqlCommandType getSqlCommandType() {
return sqlCommandType;
}
public String getResource() {
return resource;
}
public Configuration getConfiguration() {
return configuration;
}
public String getId() {
return id;
}
public boolean hasNestedResultMaps() {
return hasNestedResultMaps;
}
public Integer getFetchSize() {
return fetchSize;
}
public Integer getTimeout() {
return timeout;
}
public StatementType getStatementType() {
return statementType;
}
public ResultSetType getResultSetType() {
return resultSetType;
}
public SqlSource getSqlSource() {
return sqlSource;
}
public ParameterMap getParameterMap() {
return parameterMap;
}
public List<ResultMap> getResultMaps() {
return resultMaps;
}
public Cache getCache() {
return cache;
}
public boolean isFlushCacheRequired() {
return flushCacheRequired;
}
public boolean isUseCache() {
return useCache;
}
public boolean isResultOrdered() {
return resultOrdered;
}
public String getDatabaseId() {
return databaseId;
}
public String[] getKeyProperties() {
return keyProperties;
}
public String[] getKeyColumns() {
return keyColumns;
}
public Log getStatementLog() {
return statementLog;
}
public LanguageDriver getLang() {
return lang;
}
public String[] getResultSets() {
return resultSets;
}
/**
* Gets the resul sets.
*
* @return the resul sets
* @deprecated Use {@link #getResultSets()}
*/
@Deprecated
public String[] getResulSets() {
return resultSets;
}
public BoundSql getBoundSql(Object parameterObject) {
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings == null || parameterMappings.isEmpty()) {
boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
}
// check for nested result maps in parameter mappings (issue #30)
for (ParameterMapping pm : boundSql.getParameterMappings()) {
String rmId = pm.getResultMapId();
if (rmId != null) {
ResultMap rm = configuration.getResultMap(rmId);
if (rm != null) {
hasNestedResultMaps |= rm.hasNestedResultMaps();
}
}
}
return boundSql;
}
private static String[] delimitedStringToArray(String in) {
if (in == null || in.trim().length() == 0) {
return null;
} else {
return in.split(",");
}
}
}
到此为止,所有的statement都被解析出来了。下面我们看一下将namespace和工厂类绑定起来的方法bindMapperForNamespace。
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 && !configuration.hasMapper(boundType)) {
// Spring may not know the real resource namex下。l so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
configuration.addLoadedResource("namespace:" + namespace);
configuration.addMapper(boundType);
}
}
}
public boolean hasMapper(Class<?> type) {
return mapperRegistry.hasMapper(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 {
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);
}
}
}
}
由此,我们可以总结一下Mybatis生成SqlSessionFactory的过程,总的分为一下几个步骤:
- SqlSessionFactoryBuilder中的builder方法i解析mybatis的config配置,并将其中的一些关键属性的配置进行解析并保存的conifig中。
- 解析完配置文件之后,解析mapper配置文件,该配置文件的目的就是将sql语句与java的接口进行映射,同样,我们对该配置文件解析的目的就是绑定这些属性。
- 解析完成后,通过bindMapperForNamspeace()方法进行接口与工厂类的绑定。
- 绑定完成之后,就创建SlqSessionFactory对象。
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
上述代码用来创建DefaultSqlSessionFactory,其中参数就是Configuration。
三、SqlSession创建
在mybatis中,每次操作数据库并进行数据库连接的时候,都需要创建一次SqlSession,即会话。这里使用openSession()方法来进行SqlSession的创建。
我们看一下openSession 相关方法的参数及其实现:
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
@Override
public SqlSession openSession(boolean autoCommit) {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, autoCommit);
}
@Override
public SqlSession openSession(ExecutorType execType) {
return openSessionFromDataSource(execType, null, false);
}
@Override
public SqlSession openSession(TransactionIsolationLevel level) {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), level, false);
}
@Override
public SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level) {
return openSessionFromDataSource(execType, level, false);
}
@Override
public SqlSession openSession(ExecutorType execType, boolean autoCommit) {
return openSessionFromDataSource(execType, null, autoCommit);
}
@Override
public SqlSession openSession(Connection connection) {
return openSessionFromConnection(configuration.getDefaultExecutorType(), connection);
}
@Override
public SqlSession openSession(ExecutorType execType, Connection connection) {
return openSessionFromConnection(execType, connection);
}
通过上述openSession重载方法看得出,打开Session会话需要几个参数,其中有ExecutorType,还有TransactionIsolationLevel属性,所以我们需要创建Transaction。
我们有两种创建Transaction的方式:
属性 | 产生工厂类 | 产生事务 |
---|---|---|
JDBC | JdbcTransactionFactory | JdbcTransaction |
MANAGED | ManagedTransactionFactory | ManagedTransaction |
- 如果配置的是 JDBC,则会使用Connection对象的commit(),rollback(), close()管理事务。
- 如果配置的是Managed,则将事务交给容器来管理,例如JBoss,webLogic。
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
除了需要创建上述的Transactor之外,还需要创建Executor,在mybatis中,Executor共有三种类类型,
public enum ExecutorType {
SIMPLE, REUSE, BATCH
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
// XML 中的development节点
final Environment environment = configuration.getEnvironment();
// 这里根据环境配置,创建的是JdbcTransaction
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
// 通过JdbcTransactionFactory创建JdbcTransaction
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 创建CacheExecutor
final Executor executor = configuration.newExecutor(tx, execType);
//创建SqlSession会话
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
由上代码可以看出,通过对环境变量的配置, Transactional和Executor的配置,成功创建了sqlSesson。
下一步就是通过sqlSession创建Mapper 。
在mybatis中,通过sqlSession的getMapper()通过getMapper方法获取mapper:
@Override
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
这里getMapper获取的是configuration中的项目。
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
在这里,首先从knownMappers里面获取到的是MapperProxyFactory,代理工厂类,使用代理工厂类的newInstance()方法获得。
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
这里通过代理获取代理实例,也就是说,我们在平常使用mybatis的时候,所用的mapper其实都是MapperProxy的代理对象,所以在mapper执行方法的时候,例如selectById
,代理是如何执行的呢?
获取mapper对象之后,下一步就是mapper对象执行sql,按照刚刚所说,MapperProxy在mapper执行的时候,代理执行的是invoke方法。我们来学习一下:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 判断执行SQL还是执行方法
if (Object.class.equals(method.getDeclaringClass())) {
// 直接执行方法
return method.invoke(this, args);
} else {
// 调用cachedInvoker执行sql
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
return MapUtil.computeIfAbsent(methodCache, method, m -> {
// 判断是否似乎默认方法
if (m.isDefault()) {
try {
// 默认方法则执行默认方法Invoker
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
} else {
// 如果不是默认方法,调用PlainMethodInvoker
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}
这里主要是判断执行的是方法还是SQL语句,如果是方法,则直接通过反射调用方法执行;如果不是,则判断是否是默认方法,如果是默认方法,则执行默认方法调用器;如果不是,则构建PlainMethodInvoker来执行sql语句。
PlainMethodInvoker是一个静态内部类,继承自MapperMethodInvoker。
private static class PlainMethodInvoker implements MapperMethodInvoker {
private final MapperMethod mapperMethod;
public PlainMethodInvoker(MapperMethod mapperMethod) {
super();
this.mapperMethod = mapperMethod;
}
// 执行调用
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args);
}
}
这个MapperMethod只要是用于执行sql的类,我们看一下execute方法:
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
在上述execute方法中,分别调用SqlSession中的selectOne等语句完成操作,我们看一下DefaultSqlSession类的实现方法:
我们看到selectOne方法最终还是调用的selectList方法:
@Override
public <T> T selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
List<T> list = this.selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
我们看一下selectList方法:
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
try {
// 获取查询语句
MappedStatement ms = configuration.getMappedStatement(statement);
// 调用Executor的query方法
return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
在selectList中,实际上是调用的Executor的query方法的。
在mybatis中有多个Executor实现类,其中有BatchExecutor,ReuseExecutor,SimpleExecutor,这三个Executor都继承自BaseExecutor,我们看一个SimpleExecutor。
SimpleExecutor继承自BaseExecutor,我们看一下BaseExecutor中的查询方法,即query()。
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
// 创建缓存key,通过sql相应的参数创建缓存key,以使查询的sql以key标记
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 调用query方法
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// queryStack用户记录查询栈,以防止递归查询的时候重复处理缓存
// flushCache == true的时候,先清理本地缓存(一级缓存)
if (queryStack == 0 && ms.isFlushCacheRequired()) {
// 清除本地缓存
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 通过缓存key查询缓存数据
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
// 如果缓存不为空,则直接读取缓存
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 从数据库查询树
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
从数据库读取数据
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
// 先使用缓存占位符为数据占位
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 调用doQuery方法,该方法为一个接口,由SimpleExecutor,ReuseExecutor和BatchExecutor实现,默认为SimpleExecutor
// 同时,在这里没有catch语句,所以其执行过程中,如果出现异常,会抛给用户
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
// 将数据使用键key进行缓存
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
SimpleExecutor中的doQuery()方法
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
这样,mybatis的整个从创建到运行的过程就实现了。我们梳理一下其大体流程: