说到mybatis,那可太熟悉了,几乎是所有的java开发从业者的必备框架之一。面试也非常喜欢的mybatis的内部实现和组成。我们在使用mybatis的时候往往都是配合SpringBoot一起。SpringBoot的自动配置会为我们日常的开发提供极大的便利,包括mybatis的配置,SpringBoot都为我们配置好了,我们只需要写写mapper就能优雅的请求到mysql的数据,而不需要走建立连接,配置sql,请求数据,处理结果等复杂流程。所以本文想尽可能的从最初的起点开始,看看mybatis是怎么实现整个流程的。
什么是mybatis它是一款半自动的ORM持久层框架,具有较高的SQL灵活性,我们可以在配置中定制化sql代码,隐藏繁杂的数据连接请求过程,并且支持缓存、延迟加载等特性。
那什么是ORM呢,ORM指的是对象-关系映射(Object-Relational Mapping,简称ORM),可以描述为一个java对象与mysql中的一个表的映射,一个对象就像是表中的一条记录,对象的每个参数就与表中的每个字段保持一致。想象下如果不使用这种框架,每一次请求都需要重新处理返回值,校验等。代码重复率高且不便于维护。至于为什么叫半自动化,因为mybatis的sql是可以由我们自己控制的,那就可以控制返回的数据集,相比Hibernate更加灵活,Hibernate则是一种全自动的ORM框架,使用 Hibernate 查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取。
一个简单的实现-实例代码我们看看mybatis是怎么使用的
项目结构
依赖配置
org.mybatis mybatis 3.4.4 org.projectlombok lombok 1.16.8 junit junit 4.12 mysql mysql-connector-java 8.0.19
User实体对象
package com.df.mybatis.dao.model;
import lombok.Data;
@Data
public class User {
private Long id;
private Long userId;
private String userName;
private Long tel;
private Integer created;
private Integer updated;
private Integer isDeleted;
}
User的查询query
package com.df.mybatis.dao.query;
import lombok.Data;
@Data
public class UserQuery {
private Long id;
private Long userId;
}
UserMapper接口
package com.df.mybatis.dao.mapper;
import com.df.mybatis.dao.model.User;
import com.df.mybatis.dao.query.UserQuery;
import org.apache.ibatis.annotations.Param;
import java.io.IOException;
import java.util.List;
public interface UserMapper {
List selectByCondition(UserQuery userQuery) throws IOException;
}
用于sql映射的xml文件
`User`
mybatis配置 - 需要配置数据源,以及设置映射mapper
UserMapper接口的实现
package com.df.mybatis.dao.Impl;
import com.df.mybatis.dao.mapper.UserMapper;
import com.df.mybatis.dao.model.User;
import com.df.mybatis.dao.query.UserQuery;
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 java.io.IOException;
import java.io.InputStream;
import java.util.List;
public class UserMapperImpl implements UserMapper {
@Override
public List selectByCondition(UserQuery userQuery) throws IOException {
String resource = "properties/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
List userList = sqlSession.selectList("com.df.mybatis.dao.mapper.UserMapper.selectByCondition", userQuery);
sqlSession.close();
return userList;
}
}
最后整个test运行一下
import com.df.mybatis.dao.Impl.UserMapperImpl;
import com.df.mybatis.dao.mapper.UserMapper;
import com.df.mybatis.dao.model.User;
import com.df.mybatis.dao.query.UserQuery;
import java.io.IOException;
import java.util.List;
public class Test {
@org.junit.Test
public void testMybatisSelect() throws IOException {
UserMapper userMapper = new UserMapperImpl();
UserQuery userQuery = new UserQuery();
userQuery.setUserId(1L);
List users = userMapper.selectByCondition(userQuery);
System.out.println(users);
}
}
表结构
CREATE TABLE `User` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `userId` bigint unsigned NOT NULL DEFAULT '0' COMMENT '用户id', `userName` varchar(64) NOT NULL DEFAULT '' COMMENT '用户名', `age` int unsigned NOT NULL DEFAULT '0' COMMENT '年龄', `tel` bigint unsigned NOT NULL DEFAULT '0' COMMENT '手机号', `created` int unsigned NOT NULL DEFAULT '0' COMMENT '记录创建时间', `updated` int unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', `isDeleted` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '是否删除', PRIMARY KEY (`id`), KEY `idx_userId` (`userId`) ) ENGINE=InnoDB AUTO_INCREMENT=117 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';
运行
从结果上看,我们就是执行了一段sql,并得到了一条数据。学习的切入点可以从运行的test文件来看,我们new了一个UserMapperImpl对象,并执行了它的查询方法。在方法的内部,我们可以比较清晰的看到首先定义了我们mybatis配置文件的地址,并转换为了stream流的形式。
String resource = "properties/mybatis-config.xml";
// 读取配置文件成input流
InputStream inputStream = Resources.getResourceAsStream(resource);
我们可以大致看看 Resources.getResourceAsStream(resource); 的内部是怎么解析mybatis配置文件的。
public static InputStream getResourceAsStream(String resource) throws IOException {
// 传入配置文件路径和对应需要的加载器
return getResourceAsStream(null, resource);
}
//分隔符=======================流程分割
// 这里传入的classLoader
public InputStream getResourceAsStream(String resource, ClassLoader classLoader) {
//封装一下目前的各类类加载器数组
return getResourceAsStream(resource, getClassLoaders(classLoader));
}
}
//分隔符=======================流程分割
InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
// 遍历一下可使用的类加载器
for (ClassLoader cl : classLoader) {
if (null != cl) {
// 根据路径查找到xml文件并转换成流的格式
InputStream returnValue = cl.getResourceAsStream(resource);
// 如果找不到资源,,加上前缀"/"并重试
if (null == returnValue) {
returnValue = cl.getResourceAsStream("/" + resource);
}
// 如果成功获取到了资源,进行返回。如果获取不到就使用下一个类加载器继续尝试获取
if (null != returnValue) {
return returnValue;
}
}
}
return null;
}
上述代码通过线程获取的类加载器得到了我们mybatis-config.xml的流数据。后会执行生成SqlSessionFactory的代码,那么SqlSessionFactory到底是什么呢?
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//分隔符=======================流程分割
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
//分隔符=======================流程分割
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 根据上面得到的mybatis-config.xml的流数据得到xml配置文件对象。内部通过XPathParser生成,原流被解析成了一个document(可以根据不同的父,获取子节点的数据,类似于树结构)。在解析流的同时也会注册一些配置项,如JdbcTransactionFactory,Slf4jImpl等。
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 具体需要看一下parser.parse()方法
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
//分隔符=======================流程分割
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//将XPathParser中的/configuration节点解析成Configuration对象。 configuration节点是不是比较熟悉,mybatis-config.xml的最外层就是一个configuration节点,从这里开始解析xml文件内部构造。
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
//分隔符=======================流程分割
// 解析configuration节点内部节点,我们配置的configuration中有environments 和 mappers两个节点,我们重点就看这两个。其他节点如果没有配置,就不会进行解析。
private void parseConfiguration(XNode root) {
try {
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
//分隔符=======================流程分割
// 解析environments节点
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
// 取到目前的默认配置环境
environment = context.getStringAttribute("default");
}
// 遍历 解析每个environment
for (XNode child : context.getChildren()) {
// 取出environment的id属性
String id = child.getStringAttribute("id");
// 选择设置为default的environment
if (isSpecifiedEnvironment(id)) {
// 使用实例化Configuration时设置的typeAliasRegistry进行解析,生成对应type的对象TransactionFactory, 这里我们配置的JDBC,那么会去typeAliasRegistry中取出JDBC对应的JdbcTransactionFactory,反射生成对象。
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
// 同理,也会生成对应type的DataSourceFactory。我们的type设置为Pool,那么生成的应该是PooledDataSourceFactory。并把配置的url、驱动进行解析保存。
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的过程
mapperElement(root.evalNode("mappers"));
//进入解析流程
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
// 我们配置的mappers中的node是mapper
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);
// 这里就跟最初通过路径获取文件流信息一样了,也是xml文件,所以获取的方式也一致
InputStream inputStream = Resources.getResourceAsStream(resource);
// xml配置建立,也是生成了一个XPathParser对象
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 真正的解析过程在这里
mapperParser.parse();
} 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();
} 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.");
}
}
}
}
}
//分隔符=======================流程分割
public void parse() {
// 判断一下configuration中是否已经加载过该资源了 resource:properties/mapper/UserMapper.xml
if (!configuration.isResourceLoaded(resource)) {
// 开始解析UserMapper中的mapper节点,
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
// 绑定mapperxml文件与其接口的关系。并解析其接口,
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
//分隔符=======================流程分割
// 解析mapper节点的流程
private void configurationElement(XNode context) {
try {
// 获取名称空间,也就是对应接口的全类名。com.df.mybatis.dao.mapper.UserMapper
String namespace = context.getStringAttribute("namespace");
// 校验一下名称空间是否是异常的
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
// 到这里开始解析mapper路径下的各个配置
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 解析配置的sql标签,将key为mapper路径+id :node 存入sqlFragments的map
sqlElement(context.evalNodes("/mapper/sql"));
// 这个是我们必写的,也就是设置查询、增加修改删除的地方。
// 内部解析标签内的属性,包括resultType这些,将属性整合处理成MappedStatement,以key:selectByCondition,value:MappedStatement的方式存到mappedStatements的map中
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
}
}
// 解析完毕以后回到最外层,SqlSessionFactory 就是对Configuration做了一层封装。 本质上就是Configuration
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
解析得到SqlSessionFactory(Configuration)之后,下一步就是得到SqlSession,来看看获取SqlSession的过程又经历了什么。
SqlSession sqlSession = sqlSessionFactory.openSession();
//分隔符=======================流程分割
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
//分隔符=======================流程分割
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
// 获取在配置SqlSessionFactory时的一系列配置
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
// 根据transactionFactory 得到我们配置的 JdbcTransaction。
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//根据jdbcTransaction 和 执行类型(SIMPLE) 得到执行执行器
final Executor executor = configuration.newExecutor(tx, execType);
// 将配置、执行器、是否自动提交(默认false)拼装生成DefaultSqlSession
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();
}
}
// 配置Executor 包括事务,本地缓存等等
protected baseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentlinkedQueue();
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
从上面的流程得知,其实sqlSession是对jdbcTransaction(配置的是jdbc类型)的一个封装,包括配置了sqlSession级的本地缓存,延迟队列等…具体还是要看一下是怎么使用sqlSession去执行sql语句的。
ListuserList = sqlSession.selectList("com.df.mybatis.dao.mapper.UserMapper.selectByCondition", userQuery); //分隔符=======================流程分割 @Override public List selectList(String statement, Object parameter) { return this.selectList(statement, parameter, RowBounds.DEFAULT); } //分隔符=======================流程分割 @Override public List selectList(String statement, Object parameter, RowBounds rowBounds) { try { // 看到了熟悉的MappedStatements,以SelectId为key,MappedStatement为value的一个map。这里就通过id:com.df.mybatis.dao.mapper.UserMapper.selectByCondition获取到了对应的MappedStatement。 MappedStatement ms = configuration.getMappedStatement(statement); // 通过executor执行。executor就是上面用transaction封装的对象 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(); } } //分隔符=======================流程分割 @Override public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { //得到可以执行的sql语句,并且会对#{}进行解析,将#{}替换成?的格式,并且对需要替换的参数进行获取。 BoundSql boundSql = ms.getBoundSql(parameterObject); CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); // 执行查询方法 return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } //分隔符=======================流程分割 // 在真正执行查询逻前还有一部分缓存获取等前置操作,直接跳转到查询的代码 public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); // 得到StatementHandler,handler内封装了需要执行的sql,参数配置、resultSet、mappedStatement等信息 StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); // 根据jdbcTransaction中的dataSource获取连接,并设置隔离级别,设置是否自动提交 stmt = prepareStatement(handler, ms.getStatementLog()); // 执行查询 return handler. query(stmt, resultHandler); } finally { closeStatement(stmt); } } //分隔符=======================流程分割 @Override public List query(Statement statement, ResultHandler resultHandler) throws SQLException { PreparedStatement ps = (PreparedStatement) statement; // 执行查询的过程 ps.execute(); // 处理结果 return resultSetHandler. handleResultSets(ps); }
整体看下来,就是对我们最古老写的代码的封装,本质还是通过PreparedStatement进行查询,只是不需要我们每次都配置这么底层的东西,方便我们的开发。
最后就是对sqlSession的关闭啦。
sqlSession.close();结束
今天通过一个mybatis最简单的查询例子,简单的了解了一下内部的实现原理。当然在日常的开发中,会结合SpringBoot等开源框架使用,这些就等到下一次再解析吧~



