MyBatis概述
# Mybatis是做什么的?
MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。
MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML或注解来配置和映射原始类型、
接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
- Pom文件导入
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>x.x.x</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>x.x.x</version>
</dependency>
- mybatis-config配置文件
<?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>
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
<!--实体别名-->
<typeAliases>
<typeAlias type="com.jorry.entity.User" alias="user"/>
</typeAliases>
<!--环境信息-->
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url"
value="jdbc:mysql://localhost:3306/test?useUnicode=true&amp;serverTimezone=GMT%2B8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<!--Mapper映射文件-->
<mappers>
<!-- <package name=""/>-->
<mapper resource="userDaoMapper.xml"/>
</mappers>
</configuration>
- mybatis开发步骤
# 1.entity
# 2.类型别名
# 3.table
# 4.DAO接口
# 5.Mapper文件
# 6.Mapper文件的注册
# 7.API编程
> Api测试
public class ApiTest {
/**
* 用于测试 Mybatis
*/
@Test
public void test() throws IOException {
//第一阶段:Mybatis配置文件初始化阶段
String resource = "mybatis-config.xml";
//获取配置文件输入流
InputStream is = null;
try {
is = Resources.getResourceAsStream(resource);
} catch (IOException e) {
throw new RuntimeException(e);
}
//获取SqlSessionFactory工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
//第二阶段:数据读写阶段
try(SqlSession sqlSession=sqlSessionFactory.openSession()){
//根据动态代理获取接口的实现类
UserDaoMapper userDaoMapper = sqlSession.getMapper(UserDaoMapper.class);
//执行查询操作
List<User> users = userDaoMapper.queryUsers();
users.forEach(System.out::println);
}
}
}
Mybatis的操作主要分为两大阶段:
-
1、Mybatis初始化阶段,该阶段主要对配置文件进行初始化,将mybatis-config.xml文件解析成Configuration对象,这个过程在Mybatis启动过程中只会启动一次。
-
2、第二阶段:数据读写阶段,通过JDK动态代理的方式,获取接口的实现类,获取结果
一、Mybatis初始化阶段
1.1、获取InputStream
String resource = "mybatis-config.xml";
InputStream is = null;
try {
is = Resources.getResourceAsStream(resource);
} catch (IOException e) {
throw new RuntimeException(e);
}
- org.apache.ibatis.io.ClassLoaderWrapper#getResourceAsStream(java.lang.String, java.lang.ClassLoader[])
InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
for (ClassLoader cl : classLoader) {
if (null != cl) {
InputStream returnValue = cl.getResourceAsStream(resource);
if (null == returnValue) {
returnValue = cl.getResourceAsStream("/" + resource);
}
if (null != returnValue) {
return returnValue;
}
}
}
return null;
}
# resource: 配置文件路径
# classLoader:类加载器 负责加载类的对象,一般情况下类加载器会将名称转换为文件名,然后从文件系统中读取该文件的类文件,因此,类加载器具有读取外部资源的能力
# 执行过程:调用getResourceAsStream方法会依次调用传入的每一个类加载器的getResourceAsStream方法来尝试获取配置文件的输入流,在尝试过程中,如果失败的话,会在传入的路径前面加上"/",然后再次尝试。
1.2、配置文件读取
//获取SqlSessionFactory工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
# 这一步是创建一个SqlSessionFactoryBuilder类的实例,然后调用了其build方法
- org.apache.ibatis.session.SqlSessionFactoryBuilder#build(InputStream, String, Properties)
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
//核心代码
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
> 整个方法中最核心部分为:
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
这两句主要进行了两步操作:
-
1、生成了XMLConfiguration对象。并调用其Parse方法,得到了Configuration对象
-
2、调用了SqlSessionFactoryBuilder自身的build方法,传入参数为上一步得到的Configuration对象
首先对XMLConfiguration.parse()进行详解:
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//解析mybatis-config.xml的内容
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
# 以上代码 parseConfiguration(parser.evalNode("/configuration")); 表示解析mybatis-config.xml配置文件的根节点,这里是解析整个配置文件的入口
- parseConfiguration()
private void parseConfiguration(XNode root) {
try {
//解析 properties 标签
propertiesElement(root.evalNode("properties"));
//解析setting标签
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
//解析typeAliases标签 类型别名
typeAliasesElement(root.evalNode("typeAliases"));
//解析plugins标签
pluginElement(root.evalNode("plugins"));
//解析objectFactory标签
objectFactoryElement(root.evalNode("objectFactory"));
//解析objectWrapperFactory标签
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
//解析reflectorFactory标签
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
//解析environments标签
environmentsElement(root.evalNode("environments"));
//解析databaseIdProvider标签
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//解析typeHandlers标签
typeHandlerElement(root.evalNode("typeHandlers"));
//解析 mappers标签
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
- 主要解析以下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>
<properties></properties>
<settings>
<setting name="" value=""/>
</settings>
<!--实体别名-->
<typeAliases>
<typeAlias type="com.jorry.entity.User" alias="user"/>
</typeAliases>
<plugins>
<plugin interceptor=""></plugin>
</plugins>
<objectFactory type=""></objectFactory>
<objectWrapperFactory type=""/>
<reflectorFactory type=""/>
<!--环境信息-->
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url"
value="jdbc:mysql://localhost:3306/test?useUnicode=true&amp;serverTimezone=GMT%2B8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<databaseIdProvider type=""></databaseIdProvider>
<typeHandlers></typeHandlers>
<!--Mapper映射文件-->
<mappers>
<mapper resource="userDaoMapper.xml"/>
</mappers>
</configuration>
# 进入每一个子方法中,解析出的相关信息都放到了Configuration对象中,因此Configuration中保存了配置文件的所有设置信息,也保存了映射文件的信息。
二、数据读写阶段
2.1、获取SqlSession对象
# 在初始化阶段结束之后,我们来对读写阶段进行追踪,初步探索当进行一次数据库的读与写操作时,Mybatis需要进行哪些操作??
SqlSession sqlSession = sqlSessionFactory.openSession()
- 在 DefaultSqlSessionFactory 中找到了openSessionFromDataSource方法,这是生成 SqlSession的核心源码
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level,
boolean autoCommit) {
Transaction tx = null;
try {
//获取环境信息
final Environment environment = configuration.getEnvironment();
//创建事务工厂
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
//获取事务
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//获取Executor,(Mybatis二级缓存就在这个方法里面实现的)
final Executor executor = configuration.newExecutor(tx, execType);
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();
}
}
我们看到 Configuration对象中存储的设置信息被用来创建各种对象,包括事务工厂 TransactionFactory、执行器 Executor及默认的 DefaultSqlSession。
进入 DefaultSqlSession 类,可以看到它提供了查询、增加、更新、删除、提交、回滚等大量的方法。从 DefaultSqlSession 返回后,主方法中"SqlSession session=sqlSessionFactory.openSession()"这句代码就执行完毕了。
有一点需要注意,数据读写阶段是在进行数据读写时触发的,但并不是每次读写都会触发"SqlSession session=sqlSessionFactory.openSession()"操作,因为该操作得到的SqlSession对象可以供多次数据库读写操作复用。
2.2、映射接口文件与映射文件的绑定
# 映射接口文件指:UserDaoMapper.java
# 映射文件指:UserDaoMapper.xml等存有操作Sql语句的文件,最终Mybatis将映射接口文件与映射文件一一对应起来
//根据接口获取对应接口的实现类
UserDaoMapper userDaoMapper = sqlSession.getMapper(UserDaoMapper.class);
# 该操作通过调用Configuration中的getMapper方法转接,最终进入MapperRegistry类中的getMapper方法
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//根据类型获取MapperProxyFactory
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);
}
}
2.3、映射接口的代理
我们从以上分析可知:经过 mapperProxyFactory.newInstance(sqlSession);
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
- 由次可以看出,这是一个基于动态代理的,mapperProxy的invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//判断是Object类的方法,放行
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
//接口代理方法执行
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()) {
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
try {
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
}
return new DefaultMethodInvoker(getMethodHandleJava9(method));
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}
- 然后会触发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;
}
2.4、Sql语句的查找
- DefaultSqlSession类中的 selectList方法,该方法的源码如下
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
dirty |= ms.isDirtySelect();
return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
# 每个MappedStatement对象对应了我们设置了一个数据库操作节点,她主要定义了数据库的操作语句,输入、输出参数等信息
2.5、查询结果缓存
# 对应的数据库操作节点被查找到后,MyBatis 使用执行器开始执行语句。可以看到代码触发操作。
executor.query(ms, wrapCollection(parameter), rowBounds, handler);
- 上述 query方法实际是一个 Executor接口中的抽象方法
# 该抽象类有两个实现,分别是:BaseExecutor和CachingExecutor类中,在抽象类上打断点,可以看到真正的实现类是CachingExecutor
# BoundSql是经过层层转化后去除掉 if、where等标签的 SQL语句
# CacheKey是为该次查询操作计算出来的缓存键。
public <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);
}
- 接下来的流程
@Override
public <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方法。
2.6、数据库查询
- delegate调用的 query方法再次调用了一个 Executor接口中的抽象方法
# 可以知道delegate调用的是抽象方法,我们在抽象类中打上断点,追踪程序的实际流向,我们发现,程序最终停留在BaseExecutor中的query方法中
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.");
}
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();
}
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
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方法是 BaseExecutor类中的抽象方法,实际运行的最终实现
- org.apache.ibatis.executor.SimpleExecutor#doQuery
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);
}
}
<E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException;
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.handleResultSets(ps);
}
# 这里 ps.execute()真正执行了 SQL 语句,然后把执行结果交给 ResultHandler 对象处理。而PreparedStatement类并不是MyBatis中的类,因而ps.execute()的执行不再由MyBatis负责,而是由 com.mysql.cj.jdbc包中的类负责,
2.7、处理结果集
# 查询得到的结果并没有直接返回,而是交给ResultHandler对象进行处理,ResultHandler是结果处理器,用来接收此次查询结果的方法是该接口中的抽象方法handleResultSets
<E> List<E> handleResultSets(Statement stmt) throws SQLException;
- 最终实际执行的方法是 DefaultResultSetHandler中代码
@Override
public 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);
}
> 总结
在整个数据库操作阶段,MyBatis完成的工作可以概述为以下几条。
-
建立连接数据库的 SqlSession。
-
查找当前映射接口中抽象方法对应的数据库操作节点,根据该节点生成接口的实现 。
-
接口的实现拦截对映射接口中抽象方法的调用,并将其转化为数据查询操作。
-
对数据库操作节点中的数据库操作语句进行多次处理,最终得到标准的 SQL语句。
-
尝试从缓存中查找操作结果,如果找到则返回;如果找不到则继续从数据库中查询 。 · 从数据库中查询结果。
-
处理结果集。
-
建立输出对象;
-
根据输出结果对输出对象的属性赋值。
-
在缓存中记录查询结果。
-
返回查询结果。
-
# PerpetualCache 默认实现
装饰器:
FifoCache 先入先出的Cache
LruCache 最少使用
LoggingCache Cache增加日志功能
BlockingCache 保证一个时间只有一个线程到缓存中,查找对应的Key的数据
ScheduledCache 能够自动刷新缓存
SerializedCache 会自动的进行序列化与反序列化
TransactionalCache 只有在事务操作成功时,才会将对应的数据放置在缓存中