一、加载xml的入口
mybatis配置xml路径的类,一般都在SqlSessionFactoryBean。怎么加载它呢?有两种方式:
- xml形式:
<bean id="xxxSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"
p:dataSource-ref="xxxDataSource" p:mapperLocations="classpath*:mapper/*.xml">
<property name="configuration">
<bean class="org.apache.ibatis.session.Configuration">
<property name="mapUnderscoreToCamelCase" value="true" />
</bean>
</property>
</bean>
- @Configuration注解形式
@Bean(name = "xxxSqlSessionFactory")
public SqlSessionFactory xxxSqlSessionFactory(@Qualifier("xxxDataSource") DataSource dataSource){
SqlSessionFactory sqlSessionFactory = null;
try {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean() ;
sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources("classpath*:mapper/crt/*.xml"));
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactory = sqlSessionFactoryBean.getObject();
sqlSessionFactory.getConfiguration().setMapUnderscoreToCamelCase(true);
} catch (Exception e) {
LOGGER.error(e.getMessage(),e);
throw new CouponBizException(CouponBizCodeEnum.SYSTEM_ERROR) ;
}
return sqlSessionFactory ;
}
不管哪种方式,最终都会执行到sqlSessionFactoryBean.getObject()方法,得到一个SqlSessionFactory接口的实现类,具体是哪个实现类,下面会讲到。好了,入口知道了,我们就开始继续深入吧~
二、加载Configuration
应用在启动的时候,Spring容器会触发sqlSessionFactoryBean.getObject()方法得到单例对象,以便放到容器中。看下getObject方法:
@Override
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
afterPropertiesSet();
}
return this.sqlSessionFactory;
}
第一次必然会调用afterPropertiesSet()。继续深入看下源码:
@Override
public void afterPropertiesSet() throws Exception {
notNull(dataSource, "Property 'dataSource' is required");
notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
"Property 'configuration' and 'configLocation' can not specified with together");
this.sqlSessionFactory = buildSqlSessionFactory();
}
sqlSessionFactory接口的实现类,就在buildSqlSessionFactory()里面。继续深入:
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
Configuration configuration;
//说明了mapper接口的配置,默认用xml来实现
XMLConfigBuilder xmlConfigBuilder = null;
//上面的xml配置,我们已经传入configuration了,这里必然会进来
if (this.configuration != null) {
configuration = this.configuration;
//property字段,我们并没有配置多余其他字段,所以这里都不会进来
if (configuration.getVariables() == null) {
configuration.setVariables(this.configurationProperties);
} else if (this.configurationProperties != null) {
configuration.getVariables().putAll(this.configurationProperties);
}
} else if (this.configLocation != null) {
xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
configuration = xmlConfigBuilder.getConfiguration();
} else {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
}
configuration = new Configuration();
if (this.configurationProperties != null) {
configuration.setVariables(this.configurationProperties);
}
}
//xml的配置未配置该<property />标签,无需进来
if (this.objectFactory != null) {
configuration.setObjectFactory(this.objectFactory);
}
//xml的配置未配置该<property />标签,无需进来
if (this.objectWrapperFactory != null) {
configuration.setObjectWrapperFactory(this.objectWrapperFactory);
}
//xml的配置未配置该<property />标签,无需进来
if (this.vfs != null) {
configuration.setVfsImpl(this.vfs);
}
//xml的配置未配置该<property />标签,无需进来
if (hasLength(this.typeAliasesPackage)) {
String[] typeAliasPackageArray = tokenizeToStringArray(this.typeAliasesPackage,
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
for (String packageToScan : typeAliasPackageArray) {
configuration.getTypeAliasRegistry().registerAliases(packageToScan,
typeAliasesSuperType == null ? Object.class : typeAliasesSuperType);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Scanned package: '" + packageToScan + "' for aliases");
}
}
}
//xml的配置未配置该<property />标签,无需进来
if (!isEmpty(this.typeAliases)) {
for (Class<?> typeAlias : this.typeAliases) {
configuration.getTypeAliasRegistry().registerAlias(typeAlias);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Registered type alias: '" + typeAlias + "'");
}
}
}
//xml的配置未配置该<property />标签,无需进来
if (!isEmpty(this.plugins)) {
for (Interceptor plugin : this.plugins) {
configuration.addInterceptor(plugin);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Registered plugin: '" + plugin + "'");
}
}
}
//xml的配置未配置该<property />标签,无需进来
if (hasLength(this.typeHandlersPackage)) {
String[] typeHandlersPackageArray = tokenizeToStringArray(this.typeHandlersPackage,
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
for (String packageToScan : typeHandlersPackageArray) {
configuration.getTypeHandlerRegistry().register(packageToScan);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Scanned package: '" + packageToScan + "' for type handlers");
}
}
}
//xml的配置未配置该<property />标签,无需进来
if (!isEmpty(this.typeHandlers)) {
for (TypeHandler<?> typeHandler : this.typeHandlers) {
configuration.getTypeHandlerRegistry().register(typeHandler);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Registered type handler: '" + typeHandler + "'");
}
}
}
//xml的配置未配置该<property />标签,无需进来
if (this.databaseIdProvider != null) {//fix #64 set databaseId before parse mapper xmls
try {
configuration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
} catch (SQLException e) {
throw new NestedIOException("Failed getting a databaseId", e);
}
}
//xml的配置未配置该<property />标签,无需进来
if (this.cache != null) {
configuration.addCache(this.cache);
}
//xmlConfigBuilder尚未初始化,无需进来
if (xmlConfigBuilder != null) {
try {
xmlConfigBuilder.parse();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Parsed configuration file: '" + this.configLocation + "'");
}
} catch (Exception ex) {
throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
} finally {
ErrorContext.instance().reset();
}
}
//xml的配置未配置该<property />标签,所以必为null,需要进来
if (this.transactionFactory == null) {
//默认的transactionFactory是SpringManagedTransactionFactory
this.transactionFactory = new SpringManagedTransactionFactory();
}
//创建Environment,其实就是为了封装dataSource和transactionFactory
configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));
//xml的配置了mapperLocations,所以不为null,需要进来
if (!isEmpty(this.mapperLocations)) {
//遍历每个mapper文件
for (Resource mapperLocation : this.mapperLocations) {
if (mapperLocation == null) {
continue;
}
try {
//为每个xml文件,初始化一个xmlMapperBuilder。它含有configuration,目的就是容纳下面parse xml得到的结果
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
configuration, mapperLocation.toString(), configuration.getSqlFragments());
//开始解析xml文件,并把解析出来的所有标签,放到configuration对应的字段上
xmlMapperBuilder.parse();
} catch (Exception e) {
throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
} finally {
ErrorContext.instance().reset();
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Parsed mapper file: '" + mapperLocation + "'");
}
}
} else {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Property 'mapperLocations' was not specified or no matching resources found");
}
}
return this.sqlSessionFactoryBuilder.build(configuration);
}
有点长。。。
看完里面的注释,也就都明白了。加载configuration的核心就在xmlMapperBuilder.parse()方法里面。
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
然后进入configurationElement方法:
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"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
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);
}
}
可以看到先解析<mapper> .... </mapper>标签里面的东东,然后递归继续解析mapper里面的<resultMap>....</resultMap>,以及<sql> ....</sql>。我们来看一下,mapper的xml标签格式好了。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.xxx.xxx.xxxMapper">
<resultMap id="xxxMap" type="com.xxx.xxx">
<id column="id" property="id"/>
<result column="uid" property="uid" />
<result column="created_time" property="createdTime" />
<result column="updated_time" property="updatedTime" />
....
</resultMap>
<sql id="Base_Column_List">
id, uid....
</sql>
<insert id="insert" parameterType="com.xxx.xxx">
insert into xxx values()
</insert>
<select id="getById" parameterType="java.util.Map" resultType="com.xxx.xxx">
select <include refid="Base_Column_List" /> from xxx where id=#{id}
</select>
<update id="updateStatusById" parameterType="java.util.Map">
update xxxx
set status= #{targetStatus, jdbcType=NUMERIC}
where id = #{id} and status = #{sourceStatus}
</update>
</mapper>
可以看到解析的入口就在<mapper>标签,而它恰好就是mapper xml文件的格式。然后依次按照规范解析其余标签,以及标签里面的属性信息,放到Configuration对应的字段。
下面我们以解析<select>标签为例,解析源码:
public void parseStatementNode() {
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);
String resultMap = context.getStringAttribute("resultMap");
String resultType = context.getStringAttribute("resultType");
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
Class<?> resultTypeClass = resolveClass(resultType);
String resultSetType = context.getStringAttribute("resultSetType");
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
String nodeName = context.getNode().getNodeName();
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 = 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;
}
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
获取<select>标签上面的属性,很多属性是不是很陌生。但是最常见的几个属性,我们一定知道。比如:id, parameterMap, resultMap。其实这些属性在mybatis的dtd描述文件里面就有的,不信我们点击xml的<select>标签,是可以点击进去的。看到如下:
<!ELEMENT select (#PCDATA | include | trim | where | set | foreach | choose | if | bind)*>
<!ATTLIST select
id CDATA #REQUIRED
parameterMap CDATA #IMPLIED
parameterType CDATA #IMPLIED
resultMap CDATA #IMPLIED
resultType CDATA #IMPLIED
resultSetType (FORWARD_ONLY | SCROLL_INSENSITIVE | SCROLL_SENSITIVE) #IMPLIED
statementType (STATEMENT|PREPARED|CALLABLE) #IMPLIED
fetchSize CDATA #IMPLIED
timeout CDATA #IMPLIED
flushCache (true|false) #IMPLIED
useCache (true|false) #IMPLIED
databaseId CDATA #IMPLIED
lang CDATA #IMPLIED
resultOrdered (true|false) #IMPLIED
resultSets CDATA #IMPLIED
>
id, parameterMap, resultMap等等每一个属性都有定义好,只不过解析的时候拿出来就好了。最终所有字段解析完成后,会调用如下:
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
该方法会生成MappedStatement,它会接收<select>标签所有属性,包括:真正的sql语句。但是我们的sql语句是动态的,也就是说有条件的,只有在真正执行的才能确定sql语句。那么xml的静态的sql语句是怎么保存的呢?答案就在sqlSource字段。debug查看,如下:
内容被切割为不同类型的对象了,比如:StaticTextSqlNode对象存放固定sql语句,如where前面的语句,它肯定是固定不变的,无需动态生成。而IfSqlNode它有test字段String类型,又有contents字段,而contents又是一个StaticTextSqlNode属于静态不变的语句。
可以看出mybatis的动态sql,会在初始化的时候生成sqlSource这种模板,后面再运行的时候,会根据sql请求参数,匹配这个sqlSource,最终生成要执行的sql。
MappedStatement生成完成后,执行configuration.addMappedStatement(statement),加入到configuration。
总结:到这里我们就知道了xml的配置根据标签,一个一个去解析。解析完成后,最终会生成MappedStatement对象,然后把它添加到configuration。整个解析完成后,configuration是不是就拥有了所有xml的配置信息了,包括:sql语句,以及sql的返回值字段到对象的映射关系。这些sql执行和sql结果映射需要的东西,全部都在configuration里面了。
下一节准备分析:
- Mybatis中Sql解析执行的原理是什么?
- Mybatis中Executor接口有几种实现方式
未完待续。。。
三、Mybatis中Sql解析执行的原理是什么?
3.1 先来介绍sql 解析的原理
可以看下面的这篇文章:
Mybatis解析动态sql原理分析
其实上面讲的更加透彻,从sqlNode的多个实现类来解释。mybatis根据不同标签,把sql语句切分不同部分,然后对各个部分分别处理。如果是静态文本,就用StaticTextSqlNode,如果是if标签用IfSqlNode,其他类推。就像上一节截图那样,最终sql语句会被解析问sqlSource,传递给configuration。
3.2 再来介绍sql 执行的原理
当然可以先看这个文章:mybatis调用过程
上面的文章是从mapper整个调用过程来切入的,这节我们只介绍sql具体执行,是其中的一小块内容。首先我们从下面的源码切入:
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
第一步肯定就是生成动态sql了,如下:
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;
}
继续跟进getBoundSql方法:
@Override
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}
rootSqlNode.apply(context)一般情况是MixedSqlNode,进入该类:
@Override
public boolean apply(DynamicContext context) {
for (SqlNode sqlNode : contents) {
sqlNode.apply(context);
}
return true;
}
会根据sql语句不同部分,分别调用apply。apply方法完成后,最终会成为一个如下的sql语句:
select count(1) from xxx e left join yyy on e.order_no = rff.biz_no
where e.sob_id = #{xxx} and e.deal_date >= #{yyy} and e.deal_date <= #{dealDateEnd} and e.active=1
但是参数值并没有写进去,说明还不是最终要执行的sql。然后由sqlSourceParse.parse方法,会把所有'#{‘开头的匹配出来,变成'?',得到如下的sql:
select count(1) from xxx e left join yyyy rff on e.order_no = rff.biz_no
where e.sob_id = ? and e.deal_date >= ? and e.deal_date <= ? and e.active=1
and e.staff_id = ?
这个sql还是没有参数,还不是最终执行的sql。但是它得到sqlSource变成了StaticSqlSource类型了,因为动态sql其实已经构造完成,它就变成了静态的了,只需要绑定参数即可。
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.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
到这里传递进来的BoundSql都是带有"?"的sql语句,看下prepareStatement方法:
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
参数的绑定就发生在 handler.parameterize(stmt)里面,它会匹配参数里面的key和查询条件的key,然后把对应的value构造到sql中。
就不深入进去了,太多了。。。
至于sql的执行,mybatis解析完成动态sql后,它的职责就完成了。剩下的就交给dataSource的Connection去真正执行网络请求,通过JDBC组件,构建mysql应用能够识别的应用层协议报文,发送给服务器。然后服务器查询得到结果后,返回给mybatis。
JDBC是如何初始化连接的,如何握手验证密码的,以及如何调用的,并不在本文讨论范围。
当然JDBC里面有一堆控制,比如超时,重试,事务等等控制,并不仅仅只有网络传输。
四、Mybatis如果进行ORM转换,把数据库返回的column-value对象,通过映射转换为我们POJO呢?
我们如下的代码:
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.<E> handleResultSets(ps);
}
ps.execute(),这里应该是阻塞式调用。函数返回,表示ps已经收到结果了。如果对JDBC是真正如何执行sql的,强烈建议跟进去execute方法,看看mysql厂商的驱动是怎么构造mysql应用层协议的。然后协议的响应,你在客户端肯定是看不着的,你得去研究mysql的源码才能知道。
我们这里就不展开JDBC了。剩下的我们就开始处理返回的结果,mybatis如何映射为POJO?
很明显转换就在handleResultSets方法。
最后给调用handleRowValues方法:
public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
if (resultMap.hasNestedResultMaps()) {
ensureNoRowBounds();
checkResultHandler();
handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
} else {
handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
}
}
最终到typeHandler.getResult(rsw.getResultSet(), columnName)方法:
typeHandler.getResult(rsw.getResultSet(), columnName)
typeHandler有很多实现类,比如:IntegerTypeHandler,ObjectTypeHandler等等,它会那映射关系,一个一个取出对应列的值。
END