一、工作流程分析
全局配置文件 mybatis-Config.xml —> Mapper.xml —> Configuration填充配置类 —> SqlSessionFactory(生成session工厂) —> SqlSession(生成session) —> Executor(执行器) —> StatementHandler —> 数据库
[
](https://blog.csdn.net/qq_40111437/article/details/105274919)
1、Demo示例
/**
* 通过 SqlSession.getMapper(XXXMapper.class) 接口方式
* @throws IOException
*/
@Test
public void testSelect() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession(); // ExecutorType.BATCH
try {
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlogById(1);
System.out.println(blog);
} finally {
session.close();
}
}
依赖
<!--mysql的JDBC实现-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
<!--mybatis依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.1</version>
</dependency>
2、解析配置文件并且生成SqlSessionFactory
首先进入构造这个SqlSessionFactory工场的build方法,我们发现它是生成了一个XMLConfigBuilder对象
而XMLConfigBuilder对象里面new了一个全局配置对象,也就是我们Mybatis配置文件的Configuration。
我们再进入这个parser的parse()方法来看看,从if判断我们可以看出这个Mybatis的xml配置文件只能加载一次,否则会丢出异常,然后通过这个XMLConfigBuilder的解析Node方法,把配置文件里面的所有属性以Xnode的形式返回,通过读取根标签configuration开始解析。
看看parseConfiguration方法,我们发现它是把每个配置文件里的属性解析放在了Configuration的属性中,那下面这些其实也就是把我们的配置文件解析成属性,赋值到Configuration中。
我们发现它设置属性的同时,还有几个load操作,这几个操作是给我们的mybatis开启一些其它功能,比如上面的加载客户端的日志。
好,解析完这些属性的时候,中间突然插了一个settingElement方法,我们来看看这个方法做了什么。
这个方法里面就是设置mybatis给我们开启的功能了,如果你配置文件里面配置了,它就会读取替换,没有开启的就会用它默认的参数,比如延迟加载默认为false,比如缓存开启默认为true等,如果不知道某些值得默认属性得话,我们就可以来这里看看。
打开mybatis中文网也能看到这些配置,看看是不是与上面对应。
再看settingElement方法之后得方法,主要看看mapperElement方法,显然这是解析mapper得方法。
我们常说mybatis查找mapper有几种方式,看到这段代码应该就能记住了,首先代码判断了是否是package标签,这也是我们常用得直接扫描整个包下面得mapper,然后如果不是,就解析下面三种标签,resource(直接指定mapper.xml的相对文件路径),url(直接指定mapper.xml的绝对文件路径),class(直接指定类路径,需要mapper类)。 看看mapper文件的解析方式。
mybatis解析完我们整个sql的标签以后,最终它会产生一个Mappedstatement对象,并把解析出来的,看看里面的属性是不是对应map,sqlsource放的是sql语句,statementType放的是curd标签等,果然,它把我们整个mapper里面的sql变成了一个一个的对象。
再回到mapperParse.parse方法,解析完mapper文件以后,还调用了一个bindMapperForNamespace()方法。
boundType获得那个mapper.xml对应的接口的全路径,然后调用了一个addMapper()方法,把这个mapper接口的全路径传进去。
[
](https://blog.csdn.net/qq_40111437/article/details/105274919)
最后,发现它是存了一组这样的键值对,这个mapper的class和这个mapper的代理工厂。
一直走到这里,就发现mybatis已经把我们的配置文件以及mapper.xml都解析完了,并且放在了配置类Configuration里面,最后返回一个DefaultsqlsessionFactory工厂了。
以上总结的 时序图 如下所示:
3、SqlSessionFactory生成session会话
通过上一步,我们发现解析完配置文件以后它会返回一个DefaultSqlSessionFactory对象,我们现在看看这个对象是怎么open一个SqlSession的,先跟踪方法。
通过这个方法,我们看到,它是从我们之前解析的配置文件里面,拿到了environment对象,同时创建了一个事务,然后又从配置中拿到属性,构造了一个执行器。
所以,mybatis里面的事务和执行器都是在openSession里面创建的,我们重点来看看执行器的构造。
在这里我们可以看到,如果我们没有定义执行器的话,它会默认使用一个simple的执行器,同时为了防止有人手贱,设置一个执行器并且赋值null,所以加了一层null判断。
SimpleExecutor:封装了jdbc的statement,使用完后直接关闭。
ReuseExecutor:封装了jdbc的statement,使用完后不直接关闭,放在一个缓存的map里面等待下次使用。
BatchExecutor:封装了jdbc的statement,有缓存功能,同时支持批量操作。
[
](https://blog.csdn.net/qq_40111437/article/details/105274919)
还记得我们之前说到了mybatis的一级缓存和二级缓存,说过了一级缓存是放在baseExecutor中。
MyBatis系列之Mybatis缓存深入了解
可以看看这个执行器的类图,发现它们都是BaseExcutor的子实现类,里面有几个抽象的增删改查方法,如doUpdate(),doQuery(),留给子类去实现,典型的模板模式。
再回到这个方法,添加完执行器以后,它用包装类把这个Executor包装了一下,这里是为了二级缓存的实现,之后还调用了一个插件拦截器链的pluginAll方法,里面是把每个插件都关联一下这个执行器,最后返回这个执行器。
最后又回到一开始那个方法,返回了一个带有Configruation,executor的defaltSqlSession 。
4、Session获取Mapper
上一步我们已经拿到了一个DefaltSqlSession,现在我们再来看看它是怎么得到一个可以调用curd的mapper的,看看它具体做了哪些操作。
看到这里是不是恍然大悟,我们第一次获得一个defaltSqlSessionFactory的时候,最后一步是不是把接口类和对应的代理工厂绑定在了一起,而这里就是通过这个接口类来获取这个对应的工厂,并且把sqlSession作为参数来new一个实例,最后,它又把这个实例传给了另一个newInstance方法,没错,非常明显的jdk动态代理,接口的类加载器,接口和被代理的对象。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/22731784/1644977566438-df6ec837-d8b8-45d3-a101-bb2e6addadbd.png#clientId=u1248b6bc-8d03-4&from=paste&height=397&id=uc6a1b2ec&margin=%5Bobject%20Object%5D&name=image.png&originHeight=478&originWidth=966&originalType=binary&ratio=1&size=432067&status=done&style=none&taskId=ue9df0027-230a-4a84-841c-ab89ef276ee&width=803)
5、代理对象MapperProxy执行具体的sql操作
对最后一行代码进行调试发现,怎么直接去了接口方法,😂😂,当然了!代理对象调用的方法不都在invoke里面吗,此时当然是去看mapperProxy的invoke方法了。
看这个方法,第一步,它首先会去看看它调用的是不是object本身的方法, 如果是就直接调用这个方法;第二步,判断你是不是接口中的defalt方法(jdk1.8中新加了很多接口中得defalut方法),如果是就走下面这个方法,有兴趣的自己可以去看看,否则,它会先拿到一个mapperMethod对象,然后用这个对象调用这个方法,所以重点又来了,这是个啥对象。
来了来了,就在这里了,判断标签并且调用方法。
mappedStatement对象,我们第一步封装sql使用的对象,终于在这里拿到了,通过方法的名字拿到了这个方法,也就是这段sql对应的sql对象,然后用执行器执行query方法,因为开启了缓存,我们进入缓存的执行器看看。
原来,缓存保存一个sql,保存了这个sql的id,分页参数,sql语句以及传参值,这样缓存就能够确保找到这个对应的sql了,好,继续往下走看到这个query方法。
从这一步可以看出,它是先从我们的二级缓存中取这个值,通过我们之前保存的参数,如果取不到,就走下面的方法。
如果没有二级缓存,它就会走一级缓存中的方法,也就是baseExecutor中的query方法,其中走数据库的方法就是上面那个queryFromDatabase了,终于要揭开他的真面目了。
我们看看默认的simpleExecutor执行器是怎么执行的。
发现它创建preparedstatement的时候,先把这些参数赋值,比如配置文件,执行器,sql对象以及分页参数,同时最后还构造了两个handler,一个是参数处理器,一个是结果处理器。
到目前为止,我们已经发现了mybatis里面的四个handler了,分别是executor,执行器的拦截,statement,jdbc的拦截,parameter,参数的拦截以及resultset,返回结果集的拦截。
看到了,下面一个query方法,就是prepareStatement的execute方法了,操作数据库,并且把返回的结果集传给resultSetHandler来处理。
然后就是返回结果集,转换成java对象的操作了。
好,以上的流程就是mybatis怎么通过配置文件和mapper文件执行sql的整体过程了,有兴趣的小伙伴可以继续去看看最后是怎么操作把结果集转换成java对象的,无非就是反射,然后从resultType里面取到类型,然后赋值等一些列操作了。
[
](https://blog.csdn.net/qq_40111437/article/details/105274919)
二、JDK动态代理
接口无法实例化 所以采用jdk动态代理 实例化接口
Proxy.newProxyInstance 方法生成动态代理类 invoke() 可以调用类的方法
程序执行时使用java.lang.reflect包中Proxy类与InvocationHandler接口动态地生成一个实现代理接口的匿名代理类及其对象,无论调用代理对象哪个方法,最终都会执行invoke方法。
JDBC ,mybatis, Hibernate, SpringJDBC区别
三、要点
1、容器Configuration
Configuration 像是Mybatis的总管,Mybatis的所有配置信息都存放在这里,此外,它还提供了设置这些配置信息的方法。Configuration可以从配置文件里获取属性值,也可以通过程序直接设置。
用一句话概述Configuration,他类似Spring中的容器概念,而且是中央容器级别,存储的Mybatis运行所需要的大部分东西。
2、动态SQL模板
使用mybatis,我们大部分时间都在干嘛?在XML写SQL模板,或者在接口里写SQL模板
<?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:命名空间,命名空间唯一 -->
<mapper namespace="UserMapper">
<select id="selectUser" resultType="com.wqd.model.User">
select * from user where id= #{id}
</select>
</mapper>
或者注解
@Mapper
public interface UserMapper {
@Insert("insert into user( name, age) " + "values(#{user.name}, #{user.age})")
void save(@Param("user") User user);
@Select("select * from user where id=#{id}")
User getById(@Param("id")String id);
}
2.1、MappedStatement (映射器)
- 就像使用Spring,我们写的Controller类对于Spring 框架来说是在定义BeanDefinition一样。
- 当我们在XML配置,在接口里配置SQL模板,都是在定义Mybatis的域值MappedStatement
一个SQL模板对应MappedStatement
mybatis 在启动时,就是把你定义的SQL模板,解析为统一的MappedStatement对象,放入到容器Configuration中。每个MappedStatement对象有一个ID属性。这个id同我们平时mysql库里的id差不多意思,都是唯一定位一条SQL模板,这个id 的命名规则:命名空间+方法名
Spring的BeanDefinition,Mybatis的MappedStatement
2.2、两种解析方式
同Spring一样,我们可以在xml定义Bean,也可以java类里配置。涉及到两种加载方式。
xml方式的解析:
提供了XMLConfigBuilder组件,解析XML文件,这个过程既是Configuration容器创建的过程,也是MappedStatement解析过程。
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
Configuration config = parser.parse()
与Spring使用时:
会注册一个MapperFactoryBean,在MapperFactoryBean在实例化,执行到afterPropertiesSet()时,触发MappedStatement的解析
最终会调用Mybatis提供的一MapperAnnotationBuilder 组件,从其名字也可以看出,这个是处理注解形式的MappedStatement
3、SqlSession
3.1、基本介绍
有了SQL模板,传入参数,从数据库获取数据,这就是SqlSession干的工作。
SqlSession代表了我们通过Mybatis与数据库进行的一次会话。使用Mybatis,我们就是使用SqlSession与数据库交互的。
我们把SQL模板的id,即MappedStatement 的id 与 参数告诉SqlSession,SqlSession会根据模板id找到对应MappedStatement ,然后与数据交互,返回交互结果
User user = sqlSession.selectOne("com.wqd.dao.UserMapper.selectUser", 1);
3.2、分类
- DefaultSqlSession:最基础的sqlsession实现,所有的执行最终都会落在这个DefaultSqlSession上,线程不安全
- SqlSessionManager : 线程安全的Sqlsession,通过ThreadLocal实现线程安全。
Sqlsession有点像门面模式,SqlSession是一个门面接口,其内部工作是委托Executor 执行器完成的。
3.3、Executor
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
private Executor executor;//就是他
}
我们调用SqlSession的方法,都是由Executor完成的。
public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
----交给Executor
executor.query(ms, wrapCollection(parameter), rowBounds, handler);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
4、Mapper
4.1、存在的意义
UserMapper userMapper = sqlsession.getMapper(UserMapper.class);
User user = userMapper.getById("51");
Mapper的意义在于,让使用者可以像调用方法一样执行SQL。
区别于,需要显示传入SQL模板的id,执行SQL的方式。
User user = sqlSession.selectOne("com.wqd.dao.UserMapper.getById", 1);
4.2、工作原理
Mapper通过代理机制,实现了这个过程。
1、MapperProxyFactory: 为我们的Mapper接口创建代理。
- 单独使用Mybatis时,Mybatis会调用MapperRegistry.addMapper()方法,为UserDao接口,创建new MapperProxyFactory(type)
- 当和Spring一起使用时,MapperScannerRegistrar组件触发ClassPathMapperScanner组件的doScan方法将UserDao的BeanDefinition 的BeanClass设置为MapperProxyFactory, 在走SpringBean实例化时,就从MapperProxyFactory里获取UserDao的实例对象(即代理对象)。
```java
public T newInstance(SqlSession sqlSession) {
final MapperProxymapperProxy = new MapperProxy (sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy); }
protected T newInstance(MapperProxy
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
MapperProxyFactory通过JDK动态代理技术,在内存中帮我们创建一个代理类出来。(虽然你看不到,但他确实存在)
2、MapperProxy:就是上面创建代理时的增强
```java
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
--------------------------
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
3、MapperMethod
一个业务方法在执行时,会被封装成MapperMethod, MapperMethod 执行时,又会去调用了Sqlsession
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case SELECT:
...
result = sqlSession.selectOne(command.getName(), param);
...
break;
....
}
绕了一周,终究回到了最基本的调用方式上。
result = sqlSession.selectOne(command.getName(), param);
User user = sqlSession.selectOne("com.wqd.dao.UserMapper.getById", 1);
总结下:
- 最基本用法=sqlsession.selectOne(statement.id,参数)
- Mapper=User代理类getById—-》MapperProxy.invoke方法—-》MapperMethod.execute()—-》sqlsession.selectOne(statement.id,参数)
5、缓存
5.1、一级缓存
原来就是HashMap的封装
public class PerpetualCache implements Cache {
private String id;
private Map<Object, Object> cache = new HashMap<Object, Object>();
public PerpetualCache(String id) {
this.id = id;
}
}
在什么位置?作为BaseExecutor的一个属性存在。
public abstract class BaseExecutor implements Executor {
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.localCache = new PerpetualCache("LocalCache");
}
}
Executor上面说过,Sqlsession的能力其实是委托Executor完成的.Executor作为Sqlsession的一个属性存在。
所以:MyBatis一级缓存的生命周期和SqlSession一致。
5.2、二级缓存
基本信息
二级缓存在设计上相对与一级缓存就比较复杂了。
以xml配置为例,二级缓存需要配置开启,并配置到需要用到的namespace中。
<setting name="cacheEnabled" value="true"/>
<mapper namespace="mapper.StudentMapper">
<cache/>
</mapper>
同一个namespace下的所有MappedStatement共用同一个二级缓存。二级缓存的生命周期跟随整个应用的生命周期,同时二级缓存也实现了同namespace下SqlSession数据的共享。
二级缓存配置开启后,其数据结构默认也是PerpetualCache。这个和一级缓存的一样。
但是在构建二级缓存时,mybatis使用了一个典型的设计模式装饰模式,对PerpetualCache进行了一层层的增强,使得二级缓存成为一个被层层装饰过的PerpetualCache,每装饰一层,就有不同的能力,这样一来,二级缓存就比一级缓存丰富多了。
装饰类有:
- LoggingCache:日志功能,装饰类,用于记录缓存的命中率,如果开启了DEBUG模式,则会输出命中率日志
- LruCache:采用了Lru算法的Cache实现,移除最近最少使用的Key/Value
- ScheduledCache: 使其具有定时清除能力
- BlockingCache: 使其具有阻塞能力
层层装饰
private Cache setStandardDecorators(Cache cache) {
try {
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
if (clearInterval != null) {
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
if (readWrite) {
cache = new SerializedCache(cache);
}
cache = new LoggingCache(cache);
cache = new SynchronizedCache(cache);
if (blocking) {
cache = new BlockingCache(cache);
}
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
}
如何工作
二级缓存的工作原理,还是用到装饰模式,不过这次装饰的Executor。使用CachingExecutor去装饰执行SQL的Executor
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);//装饰
} executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
当执行查询时,先从二级缓存中查询,二级缓存没有时才去走Executor的查询
private Executor delegate;
private TransactionalCacheManager tcm = new TransactionalCacheManager();
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
....
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list);
// issue #578 and #116
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
其中TransactionalCacheManager 属性为二级缓存提供了事务能力。
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();
也就是事务提交时才会将数据放入到二级缓存中去}
总结下二级缓存
- 二级缓存是层层装饰
- 二级缓存工作原理是装饰普通执行器
- 装饰执行器使用TransactionalCacheManager为二级缓存提供事务能力
6、插件
一句话总结mybaits插件:代理,代理,代理,还是代理。
Mybatis的插件原理也是动态代理技术。
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
..
executor = new SimpleExecutor(this, transaction);
....
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
插件的入口
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
InterceptorChain
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
以分页插件为例,
创建完Executor后,会执行插件的plugn方法,插件的plugn会调用Plugin.wrap方法,在此方法中我们看到了我们属性的JDK动态代理技术。创建Executor的代理类,以Plugin为增强。
QueryInterceptor
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
public class Plugin implements InvocationHandler {
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
}
最终的执行链:Executor代理类方法—》Plugin.invoke方法—》插件.intercept方法—》Executor类方法