1. 传统方式的源码剖析
1.1 初始化过程
// 1.读取配置文件,加载为字节输入流(还没解析)InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);if (in == null) {throw new IOException("Could not find resource " + resource);}return in;}// 2.解析配置文件,封装成Configuration对象;创建DefaultSqlSessionFactory对象SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);// 2.1 XML解析,创建DefaultSqlSessionFactory对象public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {try {// XMLConfigBuilder是专门解析Mybatis配置文件的类XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);// 执行XML解析,创建DefaultSqlSessionFactory对象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.}}}// 2.2 parser.parse解析public Configuration parse() {// 如果不是第一次解析,则抛出只能解析一次异常if (parsed) {throw new BuilderException("Each XMLConfigBuilder can only be used once.");}parsed = true;// 解析XML的configuration节点(Mybatis配置文件的根节点)parseConfiguration(parser.evalNode("/configuration"));return configuration;}// 2.3 具体的解析过程private void parseConfiguration(XNode root) {try {//issue #117 read properties first// 解析<properties/>标签propertiesElement(root.evalNode("properties"));// 解析<settings/>标签Properties settings = settingsAsProperties(root.evalNode("settings"));loadCustomVfs(settings);// 解析<typeAliases/>标签typeAliasesElement(root.evalNode("typeAliases"));// 解析<plugins/>标签pluginElement(root.evalNode("plugins"));objectFactoryElement(root.evalNode("objectFactory"));objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));reflectorFactoryElement(root.evalNode("reflectorFactory"));settingsElement(settings);// read it after objectFactory and objectWrapperFactory issue #631environmentsElement(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);}}public SqlSessionFactory build(Configuration config) {// 构建者设计模式return new DefaultSqlSessionFactory(config);}
1.2 sql执行流程
SqlSession介绍
- SqlSession是⼀个接⼝,它有两个实现类:DefaultSqlSession (默认)和SqlSessionManager (弃⽤,不做介绍)。
SqlSession是MyBatis中⽤于和数据库交互的顶层类,通常将它与ThreadLocal绑定,⼀个会话使⽤⼀个SqlSession,并且在使⽤完毕后需要close
public class DefaultSqlSession implements SqlSession {private final Configuration configuration;private final Executor executor;}
SqlSession中的两个最重要的参数,configuration与初始化时的相同,Executor为执⾏器
- 通过sqlsessionFactory生产了DefaultSqlSession实例对象,设置了事务不自动提交,完成了executor的创建 ```java SqlSession sqlSession = sqlSessionFactory.openSession();
@Override public SqlSession openSession() { return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); }
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 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(); } }
3. 根据statementId从configuration中获取到获取到MappedStatement对象对象,然后将查询交给executor执行器```javaList<User> users = sqlSession.selectList("com.lagou.entity.User.findAll");@Overridepublic <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {try {// 获取到MappedStatement对象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();}}
- executor执行器查询,创建一级缓存的cacheKey,判断缓存是否存在,不存在则像数据库查询
```java
@Override
public
List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { // 根据传入的参数动态获得sql语句,封装在BoundSql中 BoundSql boundSql = ms.getBoundSql(parameter); // 为本次查询创建缓存的key CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql); }
@Override
public
5. executor执行器像数据库查询```javaprivate <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;}
真正向数据库查询,执行jdbc的组件StatementHandler
@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.<E>query(stmt, resultHandler);} finally {closeStatement(stmt);}}
StatementHandler通过ParameterHandler设置参数
- StatementHandler通过resultSetHandler进行返回结果集的处理
2. Mapper代理方式的源码剖析
2.1 getMapper
- 使用JDK动态代理对mapper接口产生代理对象
```java //DefaultSqlSession 中的 getMapper publicUserDao mapper = sqlSession.getMapper(UserDao.class);
T getMapper(Class type) { return configuration. getMapper(type, this); }
//configuration 中的给 getMapper
public
//MapperRegistry 中的 getMapper
public
- e, e); } }
//MapperProxyFactory 类中的 newInstance ⽅法
public T newInstance(SqlSession sqlSession) {
//创建了 JDK动态代理的Handler类
final MapperProxy
//MapperProxy 类,实现了 InvocationHandler 接⼝
public class MapperProxy
2. 通过代理对象调用方法时,都会执行MapperProxy中的invoke方法<a name="ywXEt"></a>## 3. 二级缓存源码剖析1. 二级缓存的构建在一级缓存之上,收到查询请求时,先查询二级缓存,未命中再查询一级缓存,还未命中再查询数据库。二级缓存 ---》一级缓存 ----》数据库2. sqlsession1查询完结果后,需要commit或者close结果才会被缓存到二级缓存。```javapublic void test1() throws IOException {InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);SqlSession sqlSession1 = build.openSession();SqlSession sqlSession2 = build.openSession();List<User> users1 = sqlSession1.selectList("com.lagou.entity.User.findAll");System.out.println(users1);//如果不先commmit或者close那么二级缓存还没有储存结果sqlSession1.commit();List<User> users2 = sqlSession2.selectList("com.lagou.entity.User.findAll");System.out.println(users2);}
启用二级缓存(分三步走;第一步和第三步都是默认为true,所以可以只配置第二步
再sqlMapConfig.xml中开启全局二级缓存配置
<settings><setting name="cacheEnabled" value="true"/></settings>
在Mapper中配置二级缓存标签
<cache></cache>
在具体CURD标签上配置 useCache=true
<select id="findById" resultType="com.lagou.pojo.User" useCache="true">select * from user where id = #{id}</select>
解析
标签过程 查询过程解析:分析为什么收到查询请求时先走二级缓存再走一级缓存,两次查询之间需要commit一次二级缓存才会有数据
从sqlSession.selectList()入手
Overridepublic <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {try {MappedStatement ms = configuration.getMappedStatement(statement);// 执行executor的query方法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();}}
当开启二级缓存时,executor的实现类不再是BaseExecutor,而是CachingExecutor ```java @Override public
List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameterObject); // 创建缓存的CacheKey CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
// 这个方法分析了如果二级缓存存在,新请求先查询二级缓存再查询一级缓存
@Override
public
3. 如上,注意⼆级缓存是从 MappedStatement 中获取的。由于 MappedStatement 存在于全局配置中,可以多个 CachingExecutor 获取到,这样就会出现线程安全问题。除此之外,若不加以控制,多个事务共⽤⼀个缓存实例,会导致脏读问题。⾄于脏读问题,需要借助其他类来处理,也就是上⾯代码中tcm 变量对应的类型。下⾯分析⼀下。4. 上面代码的tcm实际上是TransactionalCacheManager类,维护了Cache与TransactionalCache的映射关系,TransactionalCache就是解决二级缓存的脏读问题的。TransactionalCache类维护的几个属性:```java// 实际的二级缓存,tcm.getObject()就是查询这个private final Cache delegate;private boolean clearOnCommit;// 在事务未提交时,所有从数据库查询的结果存到此集合中,tcm.putObject()是先保存到这private final Map<Object, Object> entriesToAddOnCommit;// 在事务未提交时,当缓存未命中,Cachekey将缓存到此集合中private final Set<Object> entriesMissedInCache;
- TransactionalCache的查询是直接从实际二级缓存delegate查的,存储的时候存的是entriesToAddOnCommit,这也就解释了为什么两个查询之间如果事务没有提交,二级缓存是不会命中的 ```java public Object getObject(Object key) { // issue #116 Object object = delegate.getObject(key); if (object == null) { entriesMissedInCache.add(key); } // issue #146 if (clearOnCommit) { return null; } else { return object; } }
@Override public void putObject(Object key, Object object) { entriesToAddOnCommit.put(key, object); }
// 执行了commit那么二级缓存才真正存了刚刚查询的结果 public void commit() { if (clearOnCommit) { delegate.clear(); } // 把entriesToAddOnCommit和entriesMissedInCache的值刷入delegate flushPendingEntries(); reset(); }
6. 二级缓存查询过程总结:在CachingExecutor的执行查询中,先通过TransactionalCacheManager类来维护Cache和TransactionalCache的映射关系,二级缓存的存储与获取实际是通过TransactionalCache类来进行的,里面维护了真正的二级缓存Cache,存储二级缓存的时候实际先放到TransactionalCache.entriesToAddOnCommit的一个map集合中,但是查询二级缓存的时候是直接通过TransactionalCache.delegate去查询的,所以这个二级缓存查询后,设置的缓存值没有立即生效,主要是直接存到delegate会产生脏读问题。6. 二级缓存生效机制,只要看sqlSession的commit()方法做了什么```java@Overridepublic void commit(boolean force) {try {// 主要是这句executor.commit(isCommitOrRollbackRequired(force));dirty = false;} catch (Exception e) {throw ExceptionFactory.wrapException("Error committing transaction.Cause: " + e, e);} finally {ErrorContext.instance().reset();}}// CachingExecutor.commit()@Overridepublic void commit(boolean required) throws SQLException {delegate.commit(required);tcm.commit();// 在这⾥}// TransactionalCacheManager.commit()public void commit() {for (TransactionalCache txCache : transactionalCaches.values()) {txCache.commit();// 在这⾥}}// TransactionalCache.commit()public void commit() {if (clearOnCommit) {delegate.clear();}flushPendingEntries();//这⼀句reset();}// TransactionalCache.flushPendingEntries()private void flushPendingEntries() {for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {// 在这⾥真正的将entriesToAddOnCommit的对象逐个添加到delegate中,只有这时,⼆级缓存才真正的⽣效delegate.putObject(entry.getKey(), entry.getValue());}for (Object entry : entriesMissedInCache) {if (!entriesToAddOnCommit.containsKey(entry)) {delegate.putObject(entry, null);}}}
- 二级缓存的刷新,主要看sqlSession的update方法
```java
public int update(String statement, Object parameter) {
int var4;
try {
} catch (Exception var8) {this.dirty = true;MappedStatement ms = this.configuration.getMappedStatement(statement);var4 = this.executor.update(ms, this.wrapCollection(parameter));
} finally {throw ExceptionFactory.wrapException("Error updating database. Cause:"+ var8, var8);
} return var4; }ErrorContext.instance().reset();
public int update(MappedStatement ms, Object parameterObject) throws SQLException { this.flushCacheIfRequired(ms); return this.delegate.update(ms, parameterObject); }
private void flushCacheIfRequired(MappedStatement ms) { //获取MappedStatement对应的Cache,进⾏清空 Cache cache = ms.getCache(); //SQL需设置flushCache=”true” 才会执⾏清空 if (cache != null && ms.isFlushCacheRequired()) { this.tcm.clear(cache); } }
8. MyBatis⼆级缓存只适⽤于不常进⾏增、删、改的数据,⽐如国家⾏政区省市区街道数据。⼀但数据变更,MyBatis会清空缓存。因此⼆级缓存不适⽤于经常进⾏更新的数据。9. 总结:在⼆级缓存的设计上,MyBatis⼤量地运⽤了装饰者模式,如CachingExecutor, 以及各种Cache接⼝的装饰器。1. ⼆级缓存实现了Sqlsession之间的缓存数据共享,属于namespace级别2. ⼆级缓存具有丰富的缓存策略。3. ⼆级缓存可由多个装饰器,与基础缓存组合⽽成4. ⼆级缓存⼯作由 ⼀个缓存装饰执⾏器CachingExecutor和 ⼀个事务型预缓存TransactionalCache完成。<a name="nS9BZ"></a>## 4. 延迟加载源码分析<a name="S6Cnp"></a>### 4.1 什么是延迟加载1. 延迟加载:就是需要用到数据的时候才去加载,不需要用到的数据就不去加载,也叫懒加载。2. 优点:先从单表查询,需要时再去关联表查询,大大提高数据库性能,因为查询单表比查询多表快的多。3. 缺点:因为用到数据时才去数据库查询,这样在大批量数据查询时,因为查询工作也需要消耗时间,所以可能造成用户等待时间过长,用户体验下降。4. 在多表中:1. 一对多,多对多通常建议延迟加载。2. 一对一,多对一通常情况采用立即加载。5. 注意:延迟加载是采用嵌套查询实现的。<a name="M3bam"></a>### 4.2 实现延迟加载1. 局部延迟加载:在association和collection标签中都有⼀个fetchType属性,通过修改它的值,可以修改局部的加载策略。```xml<!-- 开启⼀对多 延迟加载 --><resultMap id="userMap" type="user"><id column="id" property="id"></id><result column="username" property="username"></result><result column="password" property="password"></result><result column="birthday" property="birthday"></result><!--fetchType="lazy" 懒加载策略fetchType="eager" ⽴即加载策略--><collection property="orderList" ofType="order" column="id"select="com.lagou.dao.OrderMapper.findByUid" fetchType="lazy"></collection></resultMap><select id="findAll" resultMap="userMap">SELECT * FROM `user`</select>
全局延迟加载:在Mybatis的核⼼配置⽂件中可以使⽤setting标签修改全局的加载策略。
<settings><!--开启全局延迟加载功能--><setting name="lazyLoadingEnabled" value="true"/></settings>
-
4.3 延迟加载的原理实现
它的原理是,使⽤ CGLIB 或 Javassist( 默认 ) 创建⽬标对象的代理对象。当调⽤代理对象的延迟加载属性的 getting ⽅法时,进⼊拦截器⽅法。⽐如调⽤ a.getB().getName() ⽅法,进⼊拦截器的invoke(…) ⽅法,发现 a.getB() 需要延迟加载时,那么就会单独发送事先保存好的查询关联 B对象的 SQL ,把 B 查询上来,然后调⽤ a.setB(b) ⽅法,于是 a 对象 b 属性就有值了,接着完成 a.getB().getName() ⽅法的调⽤。这就是延迟加载的基本原理。
- 总结:延迟加载主要是通过动态代理的形式实现,通过代理拦截到指定⽅法,执⾏数据加载。
