目录
传统JDBC的问题如下
mybatis对传统的JDBC的解决方案
Mybaits整体体系图
MyBatis 源码编译
启动流程分析
1、解析全局配置文件
简单总结
2、Mapper.xml文件解析
3、二级缓存的解析(二级缓存一直是开启的,只是我们调用二级缓存需要条件)
4、sql语句的解析
MyBatis执行Sql的流程分析
1.创建SqlSession
Executor
2.用SqlSession操作数据库
通过getMapper的形式调用的话
Mapper方法的执行流程
重要类
调试主要关注点
传统的JDBC
@Test
public void test() throws SQLException {
Connection conn=null;
PreparedStatement pstmt=null;
try {
// 1.加载驱动
Class.forName("com.mysql.jdbc.Driver");
// 2.创建连接
conn= DriverManager.
getConnection("jdbc:mysql://localhost:3306/mybatis_example", "root", "123456");
// SQL语句
String sql="select id,user_name,create_time from t_user where id=?";
// 获得sql执行者
pstmt=conn.prepareStatement(sql);
pstmt.setInt(1,1);
// 执行查询
//ResultSet rs= pstmt.executeQuery();
pstmt.execute();
ResultSet rs= pstmt.getResultSet();
rs.next();
User user =new User();
user.setId(rs.getLong("id"));
user.setUserName(rs.getString("user_name"));
user.setCreateTime(rs.getDate("create_time"));
System.out.println(user.toString());
} catch (Exception e) {
e.printStackTrace();
}
finally{
// 关闭资源
try {
if(conn!=null){
conn.close();
}
if(pstmt!=null){
pstmt.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
1.数据库连接创建,释放频繁造成资源的浪费,从而影响系统性能,使用数据库连接池可以解决问题。
2.sql语句在代码中硬编码,造成代码的不已维护,实际应用中sql的变化可能较大,sql代码和java代码没有分离开来维护不方便。
3.使用preparedStatement向有占位符传递参数存在硬编码问题因为sql中的where子句的条件不确定,同样是修改不方便/
4.对结果集中解析存在硬编码问题,sql的变化导致解析代码的变化,系统维护不方便。
1、数据库连接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库连接池可解决此问题。
解决:在SqlMapConfig.xml中配置数据连接池,使用连接池管理数据库链接。
2、Sql语句写在代码中造成代码不易维护,实际应用sql变化的可能较大,sql变动需要改变java代码。
解决:将Sql语句配置在XXXXmapper.xml文件中与java代码分离。
3、向sql语句传参数麻烦,因为sql语句的where条件不一定,可能多也可能少,占位符需要和参数一一对应。
解决:Mybatis自动将java对象映射至sql语句,通过statement中的parameterType定义输入参数的类型。
4、对结果集解析麻烦,sql变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成pojo对象解析比较方便。
解决:Mybatis自动将sql执行结果映射至java对象,通过statement中的resultType定义输出结果的类型。
一个Mybatis最简单的使用列子如下:
public class App {
public static void main(String[] args) {
//classpath下的mybatis的全局配置文件
String resource = "mybatis-config.xml";
Reader reader;
try {
//将XML配置文件构建为Configuration配置类,这里是加载将我们的配置文件加载进来
reader = Resources.getResourceAsReader(resource);
// 通过加载配置文件流构建一个SqlSessionFactory DefaultSqlSessionFactory
SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
// 数据源 执行器 DefaultSqlSession
SqlSession session = sqlMapper.openSession();
try {
// 执行查询 底层执行jdbc
//User user = (User)session.selectOne("com.tuling.mapper.selectById", 1);
UserMapper mapper = session.getMapper(UserMapper.class);
System.out.println(mapper.getClass());
User user = mapper.selectById(1L);
System.out.println(user.getUserName());
} catch (Exception e) {
e.printStackTrace();
}finally {
session.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
总结下就是分为下面四个步骤:
MyBatis的源码编译比较简单, 随便在网上找一篇博客即可,在这里不多说
mybatis 源码导入IDEA - 未亦末 - 博客园
String resource = "mybatis-config.xml";
//将XML配置文件构建为Configuration配置类
reader = Resources.getResourceAsReader(resource);
// 通过加载配置文件流构建一个SqlSessionFactory DefaultSqlSessionFactory
SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
通过上面代码发现,创建SqlSessionFactory的代码在SqlSessionFactoryBuilder中,进去一探究竟:
//整个过程就是将配置文件解析成Configration对象,然后创建SqlSessionFactory的过程
//Configuration是SqlSessionFactory的一个内部属性
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 {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
//这里是上面方法return return build(parser.parse());之后返回的一个默认的SqlSessionFactroy
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
new XMLConfigBuilder()构造函数(在父类中会创建出 configuration,在初始化configuration的时候会创建出很多默认的typehandler)
XPathParser主要是用来解析xml文件的
下面是解析配置文件的核心方法:
private void parseConfiguration(XNode root) {
try {
/**
* 解析 properties节点
*
* 解析到org.apache.ibatis.parsing.XPathParser#variables
* org.apache.ibatis.session.Configuration#variables
*/
propertiesElement(root.evalNode("properties"));
/**
* 解析我们的mybatis-config.xml中的settings节点
* 具体可以配置哪些属性:http://www.mybatis.org/mybatis-3/zh/configuration.html#settings
*
..............
*
*/
Properties settings = settingsAsProperties(root.evalNode("settings"));
/**
* 基本没有用过该属性
* VFS含义是虚拟文件系统;主要是通过程序能够方便读取本地文件系统、FTP文件系统等系统中的文件资源。
Mybatis中提供了VFS这个配置,主要是通过该配置可以加载自定义的虚拟文件系统应用程序
解析到:org.apache.ibatis.session.Configuration#vfsImpl
*/
loadCustomVfs(settings);
/**
* 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。
* SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING
* 解析到org.apache.ibatis.session.Configuration#logImpl
*/
loadCustomLogImpl(settings);
/**
* 解析我们的别名
*
解析到oorg.apache.ibatis.session.Configuration#typeAliasRegistry.typeAliases
*/
typeAliasesElement(root.evalNode("typeAliases"));
/**
* 解析我们的插件(比如分页插件)
* mybatis自带的
* Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)
解析到:org.apache.ibatis.session.Configuration#interceptorChain.interceptors
*/
pluginElement(root.evalNode("plugins"));
/**
* todo
*/
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// 设置settings 和默认值
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
/**
* 解析我们的mybatis环境
* 解析到:org.apache.ibatis.session.Configuration#environment
* 在集成spring情况下由 spring-mybatis提供数据源 和事务工厂
*/
environmentsElement(root.evalNode("environments"));
/**
* 解析数据库厂商
*
* 解析到:org.apache.ibatis.session.Configuration#databaseId
*/
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
/**
* 解析我们的类型处理器节点
*
解析到:org.apache.ibatis.session.Configuration#typeHandlerRegistry.typeHandlerMap
*/
typeHandlerElement(root.evalNode("typeHandlers"));
/**
* 最最最最最重要的就是解析我们的mapper
*
resource:来注册我们的class类路径下的
url:来指定我们磁盘下的或者网络资源的
class:
若注册Mapper不带xml文件的,这里可以直接注册
若注册的Mapper带xml文件的,需要把xml文件和mapper文件同名 同路径
-->
-->
* package 1.解析mapper接口 解析到:org.apache.ibatis.session.Configuration#mapperRegistry.knownMappers
2.
*/
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
上面解析流程结束后会生成一个Configration对象,包含所有配置信息,然后会创建一个SqlSessionFactory对象,这个对象包含了Configration对象。
对于MyBatis启动的流程(获取SqlSession的过程)这边简单总结下:
核心配置文件中对mapper.xml的读取主要有四种方式
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
/**
* 获取我们mappers节点下的一个一个的mapper节点
*/
for (XNode child : parent.getChildren()) {
/**
* 判断我们mapper是不是通过批量注册的
*
*/
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
/**
* 判断从classpath下读取我们的mapper
*
*/
String resource = child.getStringAttribute("resource");
/**
* 判断是不是从我们的网络资源读取(或者本地磁盘得)
*
*/
String url = child.getStringAttribute("url");
/**
* 解析这种类型(要求接口和xml在同一个包下)
*
*
*/
String mapperClass = child.getStringAttribute("class");
/**
* 我们得mappers节点只配置了
*
*/
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
/**
* 把我们的文件读取出一个流
*/
InputStream inputStream = Resources.getResourceAsStream(resource);
/**
* 创建读取XmlMapper构建器对象,用于来解析我们的mapper.xml文件
*/
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
/**
* 真正的解析我们的mapper.xml配置文件(说白了就是来解析我们的sql)
*/
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.");
}
}
}
}
}
来看下xmlMapperBuilder是怎么解析我们的mapper.xml文件的
private void configurationElement(XNode context) {
try {
/**
* 解析我们的namespace属性
*
*/
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
/**
* 保存我们当前的namespace 并且判断接口完全类名==namespace
*/
builderAssistant.setCurrentNamespace(namespace);
/**
* 解析我们的缓存引用
* 说明我当前的缓存引用和DeptMapper的缓存引用一致
*
解析到org.apache.ibatis.session.Configuration#cacheRefMap<当前namespace,ref-namespace>
异常下(引用缓存未使用缓存):org.apache.ibatis.session.Configuration#incompleteCacheRefs
*/
cacheRefElement(context.evalNode("cache-ref"));
/**
* 解析我们的cache节点
*
解析到:org.apache.ibatis.session.Configuration#caches
org.apache.ibatis.builder.MapperBuilderAssistant#currentCache
*/
cacheElement(context.evalNode("cache"));
/**
* 解析paramterMap节点(该节点mybaits3.5貌似不推荐使用了)
*/
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
/**
* 解析我们的resultMap节点
* 解析到:org.apache.ibatis.session.Configuration#resultMaps
* 异常 org.apache.ibatis.session.Configuration#incompleteResultMaps
*
*/
resultMapElements(context.evalNodes("/mapper/resultMap"));
/**
* 解析我们通过sql节点
* 解析到org.apache.ibatis.builder.xml.XMLMapperBuilder#sqlFragments
* 其实等于 org.apache.ibatis.session.Configuration#sqlFragments
* 因为他们是同一引用,在构建XMLMapperBuilder 时把Configuration.getSqlFragments传进去了
*/
sqlElement(context.evalNodes("/mapper/sql"));
/**
* 解析我们的select | insert |update |delete节点
* 解析到org.apache.ibatis.session.Configuration#mappedStatements
*/
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
都是对mapper.xml里面的标签属性的解析(会将我们的每一个节点封装成一个XNode)
二级缓存的范围是在同一个namespace下,所有的sqlsession范围内有效(必须等sqlSession提交或者是关闭之后才会刷新到二级缓存中)
进入build方法
最后将我们创建出来的二级缓存加入到configuration中
key就是我们的namespace,cache就是我们的SynchronizedCache
流程图
二级缓存结构
二级缓存在结构设计上采用装饰器+责任链模式
1.SynchronizedCache线程同步缓存区
实现线程同步功能,与序列化缓存区共同保证二级缓存线程安全。若blocking=false关闭则SynchronizedCache位于责任链的最前端,否则就位于BlockingCache后面而BlockingCache位于责任链的最前端,从而保证了整条责任链是线程同步的。
源码分析:只是对于操作缓存的方法进行了线程同步功能
2.LoggingCache统计命中率以及打印日志
统计二级缓存命中率并输出打印,由以下源码可知:日志中出现了“Cache Hit Ratio”便表示命中了二级缓存。
public class LoggingCache implements Cache {
private final Log log;
private final Cache delegate;
protected int requests = 0;
protected int hits = 0;
public LoggingCache(Cache delegate) {
this.delegate = delegate;
this.log = LogFactory.getLog(this.getId());
}
public Object getObject(Object key) {
++this.requests;//执行一次查询加一次
Object value = this.delegate.getObject(key);//查询缓存中是否已经存在
if (value != null) {
++this.hits;//命中一次加一次
}
if (this.log.isDebugEnabled()) {//开启debug日志
this.log.debug("Cache Hit Ratio [" + this.getId() + "]: " + this.getHitRatio());
}
return value;
}
private double getHitRatio() {//计算命中率
return (double)this.hits / (double)this.requests;//命中次数:查询次数
3.ScheduledCache过期清理缓存区
@CacheNamespace(flushInterval=100L)设置过期清理时间默认1个小时,
若设置flushInterval为0代表永远不进行清除。
源码分析:操作缓存时都会进行检查缓存是否过期
public class ScheduledCache implements Cache {
private final Cache delegate;
protected long clearInterval;
protected long lastClear;
public ScheduledCache(Cache delegate) {
this.delegate = delegate;
this.clearInterval = 3600000L;
this.lastClear = System.currentTimeMillis();
}
public void clear() {
this.lastClear = System.currentTimeMillis();
this.delegate.clear();
}
private boolean clearWhenStale() {
//判断当前时间与上次清理时间差是否大于设置的过期清理时间
if (System.currentTimeMillis() - this.lastClear > this.clearInterval) {
this.clear();//一旦进行清理便是清理全部缓存
return true;
} else {
return false;
}
}
}
4.LruCache(最近最少使用)防溢出缓存区
内部使用链表(增删比较快)实现最近最少使用防溢出机制
public void setSize(final int size) {
this.keyMap = new LinkedHashMap
5.FifoCache(先进先出)防溢出缓存区
内部使用队列存储key实现先进先出防溢出机制。
public class FifoCache implements Cache {
private final Cache delegate;
private final Deque keyList;
private int size;
public FifoCache(Cache delegate) {
this.delegate = delegate;
this.keyList = new LinkedList();
this.size = 1024;
}
public void putObject(Object key, Object value) {
this.cycleKeyList(key);
this.delegate.putObject(key, value);
}
public Object getObject(Object key) {
return this.delegate.getObject(key);
}
private void cycleKeyList(Object key) {
this.keyList.addLast(key);
if (this.keyList.size() > this.size) {//比较当前队列元素个数是否大于设定值
Object oldestKey = this.keyList.removeFirst();//移除队列头元素
this.delegate.removeObject(oldestKey);//根据移除元素的key移除缓存区中的对应元素
}
}
}
这里是对我们的select,insert,update,delete标签的解析
public void parseStatementNode() {
/**
* 我们的insert|delte|update|select 语句的sqlId
*/
String id = context.getStringAttribute("id");
/**
* 判断我们的insert|delte|update|select 节点是否配置了
* 数据库厂商标注
*/
String databaseId = context.getStringAttribute("databaseId");
/**
* 匹配当前的数据库厂商id是否匹配当前数据源的厂商id
*/
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
/**
* 获得节点名称:select|insert|update|delete
*/
String nodeName = context.getNode().getNodeName();
/**
* 根据nodeName 获得 SqlCommandType枚举
*/
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
/**
* 判断是不是select语句节点
*/
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
/**
* 获取flushCache属性
* 默认值为isSelect的反值:查询:默认flushCache=false 增删改:默认flushCache=true
*/
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
/**
* 获取useCache属性
* 默认值为isSelect:查询:默认useCache=true 增删改:默认useCache=false
*/
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
/**
* resultOrdered: 是否需要处理嵌套查询结果 group by (使用极少)
* 可以将比如 30条数据的三组数据 组成一个嵌套的查询结果
*/
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
/**
* 解析我们的sql公用片段
*
将 解析成sql语句 放在
我们来看下是怎么解析我们的sql语句的
将我们的sql语句解析成一个个的sqlNode并放入到sqlSource中
之后再进行其他的解析
最后将全部东西放入到mappedstatement 中(一个sql语句的就会被解析成一个statement)
mappedstatement的key可以是方法名或者是namespace+方法名,value就是解析出来的各种数据
到这里整个配置文件都已经解析完成
(1)创建事务工厂
(2)创建Execetor(二级缓存条件如果符合的话也会创建cachingExecutor)
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);
/**
* 创建一个sql执行器对象
* 一般情况下 若我们的mybaits的全局配置文件的cacheEnabled默认为ture就返回
* 一个cacheExecutor,若关闭的话返回的就是一个SimpleExecutor
*/
final Executor executor = configuration.newExecutor(tx, execType);
/**
* 创建返回一个DeaultSqlSessoin对象返回
*/
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();
}
}
/**
* 方法实现说明:创建一个sql语句执行器对象
* @author:xsls
* @param transaction:事务
* @param executorType:执行器类型
* @return:Executor执行器对象
* @exception:
* @date:2019/9/9 13:59
*/
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 {
//简单的sql执行器对象
executor = new SimpleExecutor(this, transaction);
}
//判断mybatis的全局配置文件是否开启缓存
if (cacheEnabled) {
//把当前的简单的执行器包装成一个CachingExecutor
executor = new CachingExecutor(executor);
}
/**
* TODO:调用所有的拦截器对象plugin方法
*/
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
Executor分成两大类,一类是CacheExecutor(二级缓存),另一类是普通Executor。
普通Executor又分为三种基本的Executor执行器,SimpleExecutor、ReuseExecutor、
BatchExecutor。
SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完
立刻关闭Statement对象。
ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就
使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map
BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有
sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多
个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()方法批处理。与JDBC批处理相同。
作用范围:Executor的这些特点,都严格限制在SqlSession生命周期范围内。
CacheExecutor其实是封装了普通的Executor,和普通的区别是在查询前先会查询缓存中
是否存在结果,如果存在就使用缓存中的结果,如果不存在还是使用普通的Executor进行
查询,再将查询出来的结果存入缓存。
调用执行器的时候首先委托二级缓存去查询(前提条件是我们的二级缓存要开启),三种执行器都是继承BaseExcutor,二级缓存查询不到会通过BaseExcutor去一级缓存中查询,假设一级缓存查询不到在调用执行器去数据库中查询
这里就将我们的SqlSession创建完毕了
我们的mapperstatement的key是我们的namespace+方法id
接下来,咱们看看StatementHandler 的一个实现类 PreparedStatementHandler(这也是我
们最常用的,封装的是PreparedStatement), 看看它使怎么去处理的
后面就是我们的通过prepareStatement操作sql语句来操作数据库
下面是动态代理类MapperProxy,调用Mapper接口的所有方法都会先调用到这个代理类的
invoke方法(注意由于Mybatis中的Mapper接口没有实现类,所以MapperProxy这个代理对
象中没有委托类,也就是说MapperProxy干了代理类和委托类的事情)。好了下面重点看下
invoke方法。
public class MapperProxy implements InvocationHandler, Serializable {
private static final long serialVersionUID = -6424540398559729838L;
private static final int ALLOWED_MODES = MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
| MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC;
private static Constructor lookupConstructor;
private final SqlSession sqlSession;
private final Class mapperInterface;
/**
* 用于缓存我们的MapperMethod方法
*/
private final Map methodCache;
public MapperProxy(SqlSession sqlSession, Class mapperInterface, Map methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
/**
* 方法实现说明:我们的Mapper接口调用我们的目标对象
* @author:xsls
* @param proxy 代理对象
* @param method:目标方法
* @param args :目标对象参数
* @return:Object
* @exception:
* @date:2019/8/27 19:15
*/
@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);
} else if (method.isDefault()) { //是否接口的默认方法
/**
* 调用我们的接口中的默认方法
*/
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
/**
* 真正的进行调用,做了二个事情
* 第一步:把我们的方法对象封装成一个MapperMethod对象(带有缓存作用的)
*/
final MapperMethod mapperMethod = cachedMapperMethod(method);
/**
*通过sqlSessionTemplate来调用我们的目标方法
* 那么我们就需要去研究下sqlSessionTemplate是什么初始化的
* 我们知道spring 跟mybatis整合的时候,进行了偷天换日
* 把我们mapper接口包下的所有接口类型都变为了MapperFactoryBean
* 然后我们发现实现了SqlSessionDaoSupport,我们还记得在整合的时候,
* 把我们EmployeeMapper(案例class类型属性为MapperFactoryBean)
* 的注入模型给改了,改成了by_type,所以会调用SqlSessionDaoSupport
* 的setXXX方法进行赋值,从而创建了我们的sqlSessionTemplate
* 而在实例化我们的sqlSessionTemplate对象的时候,为我们创建了sqlSessionTemplate的代理对象
* this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class }, new SqlSessionInterceptor());
*/
return mapperMethod.execute(sqlSession, args);
}
MapperProxy的invoke方法非常简单,主要干的工作就是创建MapperMethod对象或者是从
缓存中获取MapperMethod对象。获取到这个对象后执行execute方法。
所以这边需要进入MapperMethod的execute方法:这个方法判断你当前执行的方式是增删改
查哪一种,并通过SqlSession执行相应的操作。(这边以sqlSession.selectOne这种方式进行分析
~)
/**
* 方法实现说明:执行我们的目标方法
* @author:sqlSession:我们的sqlSessionTemplate
* @param args:方法参数
* @return:Object
* @exception:
* @date:2019/9/8 15:43
*/
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
/**
* 判断我们执行sql命令的类型
*/
switch (command.getType()) {
//insert操作
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
//update操作
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
//delete操作
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
//select操作
case SELECT:
//返回值为空
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
//返回值是一个List
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
//返回值是一个map
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
//返回游标
result = executeForCursor(sqlSession, args);
} else {
//查询返回单个
/**
* 解析我们的参数
*/
Object param = method.convertArgsToSqlCommandParam(args);
/**
* 通过调用sqlSessionTemplate来执行我们的sql
* 第一步:获取我们的statmentName(com.tuling.mapper.EmployeeMapper.findOne)
* 然后我们就需要重点研究下SqlSessionTemplate是怎么来的?
* 在mybatis和spring整合的时候,我们偷天换日了我们mapper接口包下的所有的
* beandefinition改成了MapperFactoryBean类型的
* MapperFactoryBean extends SqlSessionDaoSupport的类实现了SqlSessionDaoSupport
* 那么就会调用他的setXXX方法为我们的sqlSessionTemplate赋值
*
*/
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;
}
例如我们这里是一个select查询
sqlSession.selectOne方法会会调到DefaultSqlSession的selectList方法。这个方法获取了
MappedStatement对象,并最终调用了Executor的query方法
然后,通过一层一层的调用(这边省略了缓存操作的环节,会在后面的文章中介绍),最终会
来到doQuery方法, 这儿咱们就随便找个Excutor看看doQuery方法的实现吧,我这儿选择了
SimpleExecutor
后面就跟上面使用sqlsession操作sql操作一样
到此,整个调用流程结束
简单总结
这边结合获取SqlSession的流程,做下简单的总结:
1、SqlSessionFactoryBuilder解析配置文件,包括属性配置、别名配置、拦截器配置、环境(数据源和事务管理器)、Mapper配置等;解析完这些配置后会生成一个Configration对象,这个对象中包含了MyBatis需要的所有配置,然后会用这个Configration对象创建一个SqlSessionFactory对象,这个对象中包含了Configration对象;
2、拿到SqlSessionFactory对象后,会调用SqlSessionFactory的openSesison方法,这个方法会创建一个Sql执行器(Executor组件中包含了Transaction对象),这个Sql执行器会代理你配置的拦截器方法。
3、获得上面的Sql执行器后,会创建一个SqlSession(默认使用DefaultSqlSession),这个SqlSession中也包含了Configration对象和上面创建的Executor对象,所以通过SqlSession也能拿到全局配置;
4、获得SqlSession对象后就能执行各种CRUD方法了。
以上是获得SqlSession的流程
Sql的执行流程:
1、调用SqlSession的getMapper方法,获得Mapper接口的动态代理对象MapperProxy,调用Mapper接口的所有方法都会调用到MapperProxy的invoke方法(动态代理机制);
2、MapperProxy的invoke方法中唯一做的就是创建一个MapperMethod对象,然后调用这个对象的execute方法,sqlSession会作为execute方法的入参;
3、往下,层层调下来会进入Executor组件(如果配置插件会对Executor进行动态代理)的query方法,这个方法中会创建一个StatementHandler对象,这个对象中同时会封装ParameterHandler和ResultSetHandler对象。调用StatementHandler预编译参数以及设置参数值,使用ParameterHandler来给sql设置参数。Executor组件有两个直接实现类,分别是BaseExecutor和CachingExecutor。CachingExecutor静态代理了BaseExecutor。Executor组件封装了Transction组件,Transction组件中又分装了Datasource组件。
4、调用StatementHandler的增删改查方法获得结果,ResultSetHandler对结果进行封
装转换,请求结束。
Executor、StatementHandler 、ParameterHandler、ResultSetHandler,Mybatis的插件会对上面的四个组件进行动态代理
1、MapperRegistry:本质上是一个Map,其中的key是Mapper接口的全限定名,value的MapperProxyFactory;
2、MapperProxyFactory:这个类是MapperRegistry中存的value值,在通过sqlSession获取Mapper时,其实先获取到的是这个工厂,然后通过这个工厂创建Mapper的动态代理类;
3、MapperProxy:实现了InvocationHandler接口,Mapper的动态代理接口方法的调用都会到达这个类的invoke方法;
4、MapperMethod:判断你当前执行的方式是增删改查哪一种,并通过SqlSession执行相应的操作;
5、SqlSession:作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能;
6、Executor:MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护;
7、StatementHandler:封装了JDBC Statement操作,负责对JDBC statement 的操作,如设
置参数、将Statement结果集转换成List集合。
8、ParameterHandler:负责对用户传递的参数转换成JDBC Statement 所需要的参数,
9、ResultSetHandler:负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;
10、TypeHandler:负责java数据类型和jdbc数据类型之间的映射和转换
11、MappedStatement:MappedStatement维护了一条
1、MapperProxy.invoke方法:MyBatis的所有Mapper对象都是通过动态代理生成的,任何方法的调用都会调到invoke方法,这个方法的主要功能就是创建MapperMethod对象,并放进缓存。所以调试时我们可以在这个位置打个断点,看下是否成功拿到了MapperMethod对象,并执行了execute方法。
2、MapperMethod.execute方法:这个方法会判断你当前执行的方式是增删改查哪一
种,并通过SqlSession执行相应的操作。Debug时也建议在此打个断点看下。
3、DefaultSqlSession.selectList方法:这个方法获取了获取了MappedStatement对
象,并最终调用了Executor的query方法;