一、一个简略的Demo
开始之前先创立一张表,搭建一个简略的工程。
-- ------------------------------ Table structure for user-- ----------------------------DROP TABLE IF EXISTS `user`;CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `email` varchar(255) DEFAULT NULL, `age` int(11) DEFAULT NULL, `sex` int(255) DEFAULT NULL, `schoolName` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4;-- ------------------------------ Records of user-- ----------------------------INSERT INTO `user` VALUES ('1', '易哥', 'yeecode@sample.com', '18', '0', 'Sunny School');INSERT INTO `user` VALUES ('2', '莉莉', 'lili@sample.com', '15', '1', 'Garden School');INSERT INTO `user` VALUES ('3', '杰克', 'jack@sample.com', '25', '0', 'Sunny School');INSERT INTO `user` VALUES ('4', '张大壮', 'zdazhaung@sample.com', '16', '0', 'Garden School');INSERT INTO `user` VALUES ('5', '王小壮', 'wxiaozhuang@sample.com', '27', '0', 'Sunny School');INSERT INTO `user` VALUES ('6', '露西', 'lucy@sample.com', '14', '1', 'Garden School');INSERT INTO `user` VALUES ('7', '李二壮', 'lerzhuang@sample.com', '9', '0', 'Sunny School');
1、实体类
package com.gitee.mycode.mybatisdemo.entity;import lombok.Data;/** * @program: mybatisdemo * @description: 用户实体 * @author: Mr.Hu * @create: 2022-05-31 21:19 */@Datapublic class User { private Integer id; private String name; private String email; private Integer age; private Integer sex; private String schoolName;}
2、配置文件
<?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> <typeAliases> <package name="com.gitee.mycode.mybatisdemo.entity"/> </typeAliases> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://127.0.0.1:3306/demo?serverTimezone=UTC"/> <property name="username" value="root"/> <property name="password" value="root"/> </dataSource> </environment> </environments> <mappers> <mapper resource="mapper/UserMapper.xml"/> </mappers></configuration>
3、映射接口文件
package com.gitee.mycode.mybatisdemo.mapper;import com.gitee.mycode.mybatisdemo.entity.User;import org.apache.ibatis.annotations.Mapper;import org.apache.ibatis.annotations.Param;import java.util.List;/** * @program: mybatisdemo * @description: * @author: Mr.Hu * @create: 2022-05-31 21:22 */@Mapperpublic interface UserMapper { List<User> queryUserBySchoolName( User user);}
4、映射文件
<?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.gitee.mycode.mybatisdemo.mapper.UserMapper"> <select id="queryUserBySchoolName" resultType="com.gitee.mycode.mybatisdemo.entity.User"> select * from user <if test="schoolName!=null"> where schoolName=#{schoolName} </if> </select></mapper>
5、外围逻辑
package com.gitee.mycode.mybatisdemo;import com.gitee.mycode.mybatisdemo.entity.User;import com.gitee.mycode.mybatisdemo.mapper.UserMapper;import org.apache.ibatis.io.Resources;import org.apache.ibatis.session.SqlSession;import org.apache.ibatis.session.SqlSessionFactory;import org.apache.ibatis.session.SqlSessionFactoryBuilder;import org.springframework.boot.autoconfigure.SpringBootApplication;import java.io.IOException;import java.io.InputStream;import java.util.List;@SpringBootApplicationpublic class MybatisdemoApplication { public static void main(String[] args) { //第一阶段:mybatis初始化 String resource = "mybatis-config.xml"; //失去配置文件输出流 InputStream inputStream = null; try { inputStream = Resources.getResourceAsStream(resource); } catch (IOException e) { e.printStackTrace(); } SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //第二阶段:数据读写阶段 try (SqlSession session = sqlSessionFactory.openSession()) { UserMapper userMapper = session.getMapper(UserMapper.class); User userParam = new User(); userParam.setSchoolName("Sunny School"); List<User> users = userMapper.queryUserBySchoolName(userParam); for (User user : users) { System.out.println("userName:"+user.getName()+";email:"+user.getEmail()); } } }}
最终我的项目构造和运行后果如下
<img src="https://pic.imgdb.cn/item/629a294609475431292aa084.png" style="zoom:80%" />
二、初始化阶段追踪
从下面demo的外围逻辑能够看出整个运行过程分为两个阶段,初始化阶段和数据读取阶段。初始化阶段的最外围代码为:
inputStream = Resources.getResourceAsStream(resource);
和SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
1、获取InputStream
通过Resources.getResourceAsStream
追踪会发现ClassLoaderWrapper类有这样一段代码
InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) { ClassLoader[] var3 = classLoader; int var4 = classLoader.length; for(int var5 = 0; var5 < var4; ++var5) { ClassLoader cl = var3[var5]; if (null != cl) { InputStream returnValue = cl.getResourceAsStream(resource); if (null == returnValue) { returnValue = cl.getResourceAsStream("/" + resource); } if (null != returnValue) { return returnValue; } } } return null;}
getResourcecAsSream办法会调用传入的每一个类加载器的getResourceAsStream办法来尝试获取配置文件的输出流。在尝试过程中如果获取失败的话,会在传入的地址前加上“/”再获取一次。只有尝试胜利,则表明胜利加载了指定的资源,会将所取得的输出流返回。
2、构建SqlSessionFactory对象
通过对SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
build办法的追踪能够发现
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { SqlSessionFactory var5; try { //创立XMLConfigBuilder对象 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); //通过XMLConfigBuilder对象生成出一个Congfiguration对象之后,传入build办法,构建SqlSessionFactory var5 = this.build(parser.parse()); } catch (Exception var14) { throw ExceptionFactory.wrapException("Error building SqlSession.", var14); } finally { ErrorContext.instance().reset(); try { inputStream.close(); } catch (IOException var13) { ; } } return var5;}
2.1构建Configuration对象
进入parser.parse()办法能够看到上面这段代码
public Configuration parse() { if (this.parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } else { this.parsed = true; this.parseConfiguration(this.parser.evalNode("/configuration")); return this.configuration; }}private void parseConfiguration(XNode root) { try { this.propertiesElement(root.evalNode("properties")); Properties settings = this.settingsAsProperties(root.evalNode("settings")); this.loadCustomVfs(settings); this.loadCustomLogImpl(settings); this.typeAliasesElement(root.evalNode("typeAliases")); this.pluginElement(root.evalNode("plugins")); this.objectFactoryElement(root.evalNode("objectFactory")); this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); this.reflectorFactoryElement(root.evalNode("reflectorFactory")); this.settingsElement(settings); this.environmentsElement(root.evalNode("environments")); this.databaseIdProviderElement(root.evalNode("databaseIdProvider")); this.typeHandlerElement(root.evalNode("typeHandlers")); this.mapperElement(root.evalNode("mappers")); } catch (Exception var3) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3); }}
有没有感觉代码里的这些字符串很眼生?没错,就是咱们demo中配置文件里的那些标签名。可想而知,parse办法作用就是解析配置文件并返回Configuration实例。因而Configuration类中保留了配置文件的所有设置音讯,也保留了映射文件的信息。可见,Configuration类是一个十分重要的类。
2.2、构建SqlSessionFactory对象
执行完parse办法之后,接下来就是将返回的Configuration对象传入build办法,源码如下:
public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config);}
至此,SqlSessionFactory对象创立胜利!
三、数据读写阶段追踪
接着,咱们来到外围逻辑的第二阶段。
//第二阶段:数据读写阶段try (SqlSession session = sqlSessionFactory.openSession()) { UserMapper userMapper = session.getMapper(UserMapper.class); User userParam = new User(); userParam.setSchoolName("Sunny School"); List<User> users = userMapper.queryUserBySchoolName(userParam); for (User user : users) { System.out.println("userName:"+user.getName()+";email:"+user.getEmail()); }}
1、取得SqlSession
咱们追踪sqlSessionFactory.openSession()
能够在DefaultSqlFactory的openSessionFromDataSource办法找到上面这段代码,这是生成SqlSession的外围源码
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; DefaultSqlSession var8; try { Environment environment = this.configuration.getEnvironment(); TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); Executor executor = this.configuration.newExecutor(tx, execType); var8 = new DefaultSqlSession(this.configuration, executor, autoCommit); } catch (Exception var12) { this.closeTransaction(tx); throw ExceptionFactory.wrapException("Error opening session. Cause: " + var12, var12); } finally { ErrorContext.instance().reset(); } return var8;}
从这段代码能够看出,Configuration对象中存储的设置音讯被用来创立各种对象。包含事务工厂TransactionFactory、执行器Executor
以及默认的DefaultSqlSession。进入DefaultSqlSession后能够看到大量的增删改查、提交、回滚等办法。从DefaultSqlSession返回之后,SqlSession session = sqlSessionFactory.openSession()
这段代码执行结束。
2、映射接口文件与映射文件的绑定
下一步就是执行主办法中的UserMapper userMapper = session.getMapper(UserMapper.class);
这段代码,跟进getMappe办法会发现MapperRegistry类中的getMapper办法如下
public <T> T getMapper(Class<T> type, SqlSession sqlSession) { MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } else { try { return mapperProxyFactory.newInstance(sqlSession); } catch (Exception var5) { throw new BindingException("Error getting mapper instance. Cause: " + var5, var5); } }}
上述代码中,getMapper办法通过映射接口信息从所有曾经解析的映射文件找到对应的映射文件,而后依据该映射文件组建并返回接口的一个实现对象。
3、映射接口的代理
那么mapperProxyFactory.newInstance(sqlSession)
返回的对象到底是什么呢?持续追踪代码
protected T newInstance(MapperProxy<T> mapperProxy) { return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);}
可见这里返回的是一个动静代理对象,因而咱们找到MapperProxy类的invoke办法并在其中打上断点。
所以主办法List<User> users = userMapper.queryUserBySchoolName(userParam)
会执行上图的代码。接着会触发MapperMethod对象的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;}
该办法中mybatis依据不同的数据库操作进行了响应的办法解决。以后demo是进行数据库的查问操作,会触发result = executeForMany(sqlSession, args);
语句,executeForMany办法源码如下:
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) { List<E> result; Object param = method.convertArgsToSqlCommandParam(args); if (method.hasRowBounds()) { RowBounds rowBounds = method.extractRowBounds(args); //下一步入口 result = sqlSession.selectList(command.getName(), param, rowBounds); } else { result = sqlSession.selectList(command.getName(), param); } // issue #510 Collections & arrays support if (!method.getReturnType().isAssignableFrom(result.getClass())) { if (method.getReturnType().isArray()) { return convertToArray(result); } else { return convertToDeclaredCollection(sqlSession.getConfiguration(), result); } } return result;}
在该办法中mybatis开始通过SqlSession对象的selectList办法开展后续的查问工作。跟到这里,mybatis曾经实现了为映射接口注入实现的过程。于是,对映射接口中的形象办法调用转变成了数据查问操作。
4、SQL语句的查找
进入result = sqlSession.selectList(command.getName(), param, rowBounds)
,源码如下:
@Overridepublic <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement); //下一步入口 return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); }}
configuration.getMappedStatement(statement)
语句将要执行的MappedStatement对象从Configuration对象存储的映射文件信息中找了进去
每个MappedStatement对象对应了咱们设置的一个数据库操作节点,它次要定义了数据库操作语句、输出/输入参数等信息。
5、查问后果缓存
点进executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER)
能够发现query办法为形象办法,该办法有BaseExecutor和CachingExecutor两种实现形式,在query形象办法上打上断点后从新运行我的项目,会发现断点主动跳进了CachingExecutor类,上面是CachingExecutor类的query办法的代码
@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameterObject); CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); //下一步入口 return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { Cache cache = ms.getCache(); if (cache != null) { flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, boundSql); @SuppressWarnings("unchecked") List<E> list = (List<E>) tcm.getObject(cache, key); if (list == null) { list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } } //下一步入口 return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}
依据以上代码能够看出,mybatis会查看以后操作是否命中缓存。如果是,则从缓存中获取数据后果;否则,便通过delegate调用query办法。
BoundSql是通过层层转化后去除if、where等标签的SQL语句,而CacheKey是本次查问操作计算出来的缓存键。
6、数据库查问
再次进入delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql)
发现它同样是一个形象办法,再次采纳下面的操作,在该办法上打一断点后,发现这次是进入了BaseExecutor类,上面来看看BaseExecutor类的query办法
@Overridepublic <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."); } if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); } List<E> list; try { queryStack++; 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;}
这个办法逻辑有些简单,重点看list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql)
这行外围代码,持续跟进去查看:
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 { //下一步入口 list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { localCache.removeObject(key); } localCache.putObject(key, list); if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list;}
通过代码能够看出,mybatis先在缓存中放入一个占位符,而后调用doQuery办法执行理论查问操作。最初又把缓存中的占位符替换成真正的查问后果。
doQuery是BaseExcutor的形象办法,在该形象办法上打上断点后发现,跳到SimpleExecutor类的doQuery办法
@Overridepublic <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); }}
上述办法生成了一个java.sql.Statement类的对象stmt,Statement类能够执行动态SQL语句并返回后果。
程序先获取到一个StatementHandler对象之后,而后再将查问操作交给StatementHandler对象执行。
StatementHandler对象是一个语句处理器类,其中封装了很多语句操作方法,暂不细说。
接着走到handler.query(stmt, resultHandler)
这一行,最初通过屡次跳转,程序执行到了PreparedStatementHandler类的query办法
@Overridepublic <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { PreparedStatement ps = (PreparedStatement) statement; ps.execute(); return resultSetHandler.handleResultSets(ps);}
这里ps.execute()
真正执行了SQL语句,而后把执行后果交给ResultHandler解决。下图是debug模式下最初查问进去的数据库信息
数据库查问后果在PreparedStatement对象中藏的比拟深,为h>statement>results。数据库指端信息在columnDefinition变量中,数据记录信息在rowData变量中
7、处理结果集
查问到的后果并没有间接返回,而是交给了ResultHandler解决,持续跟进handler.query(stmt, resultHandler)
,其源码如下:
@Overridepublic List<Object> handleResultSets(Statement stmt) throws SQLException { ErrorContext.instance().activity("handling results").object(mappedStatement.getId()); final List<Object> multipleResults = new ArrayList<>(); int resultSetCount = 0; ResultSetWrapper rsw = getFirstResultSet(stmt); List<ResultMap> resultMaps = mappedStatement.getResultMaps(); int resultMapCount = resultMaps.size(); validateResultMapsCount(rsw, resultMapCount); while (rsw != null && resultMapCount > resultSetCount) { ResultMap resultMap = resultMaps.get(resultSetCount); handleResultSet(rsw, resultMap, multipleResults, null); rsw = getNextResultSet(stmt); cleanUpAfterHandlingResultSet(); resultSetCount++; } String[] resultSets = mappedStatement.getResultSets(); if (resultSets != null) { while (rsw != null && resultSetCount < resultSets.length) { ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]); if (parentMapping != null) { String nestedResultMapId = parentMapping.getNestedResultMapId(); ResultMap resultMap = configuration.getResultMap(nestedResultMapId); handleResultSet(rsw, resultMap, null, parentMapping); } rsw = getNextResultSet(stmt); cleanUpAfterHandlingResultSet(); resultSetCount++; } } return collapseSingleResultList(multipleResults);}
在上述办法中查问进去的后果被遍历后放入multipleResults汇合并返回,该汇合中存储的就是这次查问冀望的后果LIst<User>,至于mybatis是如何将数据库输入的记录转化为对象列表的具体过程比拟长,这里就不开展了。次要的就是一下三个办法。
- DefaultResultSetHandler.createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix):该办法创立了输入后果对象,也就是demo中的User对象
- DefaultResultSetHandler.applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix):在主动属性映射性能开启的状况下,该办法将数据记录的赋给输入后果对象
- DefaultResultSetHandler.applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, ResultLoaderMap lazyLoader, String columnPrefix):该办法依照用户的映射设置,给输入后果对象的属性赋值
四、总结
在整个数据库操作阶段,mybatis实现的工作能够分为以下几步:
- 依据配置文件地位,获取他的输出流
- 从配置文件跟节点开始,逐层解析配置文件,也包含相干的映射文件。解析过程中一直将解析后果放入Configuration对象
- 以配置好的Configuration对象为参数,获取一个SqlSessionFactory对象
- 建设连贯数据库的SqlSession
- 查找以后映射接口中形象办法对应的数据库操作节点,依据节点生成接口的实现
- 接口的实现拦挡映射接口中形象办法的调用,并将其转化为数据库查问操作
- 对数据库节点中的操作语句进行屡次解决,最终失去规范的SQL语句
- 尝试从缓存中查找后果,如果找到则返回;如果找不到则持续从数据库中查找
- 从数据库查问后果
处理结果集
- 建设输入对象
- 依据输入后果对输入对象的属性赋值
- 在缓存中记录查问后果
- 返回查问后果