Mybatis相关文章:
- Mybatis 源码类功能介绍
- 博主自己源码工程,做了大量注释:gitee.com/old_yogurt/...
Mybatis 源码执行流程
Mybatis 源码执行流程大致分为下面几步:
- 1、获取配置文件流;
- 2、解析XML配置文件,构建
SqlSessionFactory
; - 3、利用
SqlSessionFactory
创建SqlSession
会话; - 4、利用
SqlSession
创建Mapper接口
的代理对象MapperProxy
; - 5、然后在执行
Mapper接口
的 SQL查询时,转而利用代理对象执行 JDBC的SQL底层操作。
下面的5个小结会对着5个步骤分别分析。
我们这里只是直接利用Mybatis框架进行操作,没有说利用Spring整合Mybatis之后操作数据库,其实Spring整合Mybatis也是对下面几步的封装。
java
@Test
public void testMyBatisBuild() throws IOException {
// 1、获取配置文件流;
InputStream input = Resources.getResourceAsStream("SqlSessionConfig.xml");
// 2、解析XML配置文件,构建 `SqlSessionFactory`;
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(input);
// 3、利用`SqlSessionFactory`创建 `SqlSession` 会话;
SqlSession sqlSession = sessionFactory.openSession();
// 4、利用`SqlSession` 创建 `Mapper接口` 的代理对象 `MapperProxy`;
TestMapperDao mapper = sqlSession.getMapper(TestMapperDao.class);
// 5、然后在执行 `Mapper接口`的 SQL查询时,转而利用代理对象执行 JDBC的SQL底层操作。
System.out.println(mapper.selectByPrimaryKey(1));
}
一、获取配置文件流
java
// 1、获取配置文件流;
InputStream input = Resources.getResourceAsStream("SqlSessionConfig.xml");
这个没什么好说的,就是将配置文件以流的形式读入内存。
二、构建 SqlSessionFactory
java
// 2、解析XML配置文件,构建 `SqlSessionFactory`;
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(input);
其实这里.build(input)
利用了设计模式的建造者模式构建对象;我们进入 .build(input)
方法:
java
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
/**
* 该方法的作用就是:建造一个SqlSessionFactory对象
* 建造一个SqlSessionFactory对象 需要经历下面三步:
* (1)传入配置文件,创建一个 XMLConfigBuilder类准备对配置文件展开解析。
* (2)解析配置文件,得到配置文件对应的 Configuration对象。
* (3)根据 Configuration对象,获得一个 DefaultSqlSessionFactory。
* @param inputStream Mybatis配置文件
* @param environment 环境变量
* @param properties 属性信息
* @return
*/
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
//不用说,下面两句是核心代码 XMLConfigBuilder(XML配置文件的 构造者)
// 1.传入配置文件,创建一个XMLConfigBuilder类
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 2、解析配置文件,得到配置文件对应的Configuration对象
// 3、根据Configuration对象,获得一个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.
}
}
}
}
流程分为三步:
- 1、传入配置文件,创建一个XMLConfigBuilder类;
- 2、解析配置文件,得到配置文件对应的Configuration对象;
- 3、利用Configuration对象,获得一个DefaultSqlSessionFactory。
下面将分三小节分别讲解。
2.1 创建XMLConfigBuilder类
java
// 1.传入配置文件,创建一个XMLConfigBuilder类
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
java
public class XMLConfigBuilder extends BaseBuilder {
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
//
this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
}
-
进入:
new XPathParser(inputStream, true, props, new XMLMapperEntityResolver())
- 我们主要是进入到 下面类型的第一个构造方法,然后再看一下
this.document
this.document = createDocument(new InputSource(inputStream));
这里就是将 配置文件的输入流构建成 原生XML Document对象。- 下面同时也列出了 XPathParser类中的一些属性信息;目的是为了对 XML整个解析过程有更清晰的认识。
javapublic class XPathParser { // 代表要解析的整个XML文档 private final Document document; // 是否开启验证 private boolean validation; // EntityResolver,通过它可以声明寻找DTD文件的方法,例如通过本地寻找,而不是只能通过网络下载dtd文件 private EntityResolver entityResolver; // MyBatis配置文件中的properties信息 private Properties variables; // javax.xml.xpath.XPath工具 private XPath xpath; //这玩意是解析XML的 public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) { commonConstructor(validation, variables, entityResolver); // document : 就是 将xml文件节点,内容解析,构建成Document对象 this.document = createDocument(new InputSource(inputStream)); } private Document createDocument(InputSource inputSource) { // important: this must only be called AFTER common constructor 重要提示:这只能在公共构造函数之后调用 // 也就是说在构造方法中,先调用下面的commonConstructor()方法对XPathParser该类中的属性进行初始化操作之后; // 才能调用该方法,原因也很简单,下面会用到该类中的属性信息。 try { // DOM文档创建器的工厂 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setValidating(validation); factory.setNamespaceAware(false); factory.setIgnoringComments(true); factory.setIgnoringElementContentWhitespace(false); factory.setCoalescing(false); factory.setExpandEntityReferences(true); // DOM文档创建器 DocumentBuilder builder = factory.newDocumentBuilder(); builder.setEntityResolver(entityResolver); builder.setErrorHandler(new ErrorHandler() { @Override public void error(SAXParseException exception) throws SAXException { throw exception; } @Override public void fatalError(SAXParseException exception) throws SAXException { throw exception; } @Override public void warning(SAXParseException exception) throws SAXException { } }); return builder.parse(inputSource); // 这里就是将xml内容解析成 Document对象 } catch (Exception e) { throw new BuilderException("Error creating document instance. Cause: " + e, e); } } }
- 我们主要是进入到 下面类型的第一个构造方法,然后再看一下
-
好,我们继续回到 XMLConfigBuilder 类中
this
javaprivate XMLConfigBuilder(XPathParser parser, String environment, Properties props) { // 初始化父类的构造方法 BaseBuilder, // 初始化:configuration(配置类) , typeAliasRegistry(类型别名注册表),typeHandlerRegistry(类型处理器注册表) super(new Configuration()); ErrorContext.instance().resource("SQL Mapper Configuration"); // 一些属性信息可能是以 配置文件的方式写的,这里的props就是属性信息 this.configuration.setVariables(props); this.parsed = false; this.environment = environment; this.parser = parser; }
-
这里面比较重要的一行:
java// 初始化XMLConfigBuilder父类的构造方法 BaseBuilder, // 初始化:configuration(配置类) , typeAliasRegistry(类型别名注册表),typeHandlerRegistry(类型处理器注册表) // 创建 Configuration对象,这里先初始化Configuration里面的 typeAliasRegistry注册表 // 然后放入 BaseBuilder构造方法 super(new Configuration());
-
OK ,XMLConfigBuilder对象构建完成。
2.2 构建Configuration对象
parser.parse()
这里是 解析XML配置文件,继续初始化 Configuration对象。
parse()
还是在 XMLConfigBuilder
对象中。
java
public class XMLConfigBuilder extends BaseBuilder {
/**
* ★ 解析配置文件的入口方法
* @return Configuration对象
*/
public Configuration parse() {
// 不允许重复解析
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// 开始解析,根节点是configuration
// parser.evalNode("/configuration") 会将 XML节点解析,构建成Node对象,然后再封装成Mybatis定义的XNode对象
// 然后parseConfiguration(parser.evalNode("/configuration")) 继续解析,并放入到 configuration对象对应的属性中!
parseConfiguration(parser.evalNode("/configuration"));
// 这个 configuration属性 可是在BaseBuilder中的!!!
return configuration;
}
}
我们查看:parseConfiguration(parser.evalNode("/configuration"));
下面我们就可以看到,XML文件中各种节点属性信息、值都会被解析,然后放入到 configuration
对象中。
java
/**
* 这里是解析配置文件的起始方法,解析的所有信息都放入到了configuration中
* @param root
*/
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings); //所有的 settings 节点中的属性都放在这这里解析了
// read it after objectFactory and objectWrapperFactory issue #631
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);
}
}
以解析 mappers
节点为例,分析对一个节点是如何解析的:mapperElement(root.evalNode("mappers"));
java
/**
* 对配置文件中的 mappers节点进行解析
* ege:
* <mappers>
* <mapper resource="com.yogurt.example.mapper.TestMapperDao"/>
* <package name="com.yogurt.example.mapper"/>
* </mappers>
* @param parent mappers节点内容
*/
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
//下面对 mappers节点下不同的子节点进行不同的处理
//1.对package子节点的处理
if ("package".equals(child.getName())) {
// 取出包的路径
String mapperPackage = child.getStringAttribute("name");
// 全部加入Mappers中
configuration.addMappers(mapperPackage);
} else { //2.对 mapper 字节的处理
// 下面三个 resource,url,mapperClass只能有一个生效,
// 就是获取的 mapper节点上的属性,用于获取 xxxMapper.xml的路径,
// 然后下面的(if/else if/else)就是针对节点属性不同进行不同的处理。
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) { // resource
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
// ★使用XMLMapperBuilder解析Mapper文件解析: ★ 这应该解析的是对应路径中的接口 继续解析(就是对XML的接口映射文件继续解析)
XMLMapperBuilder mapperParser =
new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) { // url
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
// ★使用XMLMapperBuilder解析Mapper文件
XMLMapperBuilder mapperParser =
new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) { // 配置的不是Mapper文件,而是Mapper接口
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.");
}
}
}
}
}
2.3 获取DefaultSqlSessionFactory对象
build(parser.parse());
java
public class SqlSessionFactoryBuilder {
/**
* 根据配置信息建造一个SqlSessionFactory对象;
* 这里可以看到构建的对象是SqlSessionFactory子类实现类: DefaultSqlSessionFactory
* @param config 配置信息
* @return SqlSessionFactory对象
*/
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
}
OK,SqlSessionFactory
构建完成。
三、创建 SqlSession
会话
java
// 3、利用 SqlSessionFactory 创建 SqlSession 会话;
SqlSession sqlSession = sessionFactory.openSession();
java
public class DefaultSqlSessionFactory implements SqlSessionFactory {
@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);
// 创建执行器
final Executor executor = configuration.newExecutor(tx, execType);
// 创建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 类是一个错误上下文,它能够提前将一些背景信息保存下来。
// 这样在真正发生错误时,便能将这些背景信息提供出来,进而给我们的错误排查带来便利。
//
// 这里整个 初始化Session完成,就将该上下文信息重置(清理)
ErrorContext.instance().reset();
}
}
}
四、创建代理对象 MapperProxy
java
// 4、利用`SqlSession` 创建 `Mapper接口` 的代理对象 `MapperProxy`;
TestMapperDao mapper = sqlSession.getMapper(TestMapperDao.class);
我们可以看到: sqlSession 类型是 :DefaultSqlSession
往里跟:sqlSession.getMapper(TestMapperDao.class);
java
/**
* SqlSession的默认实现。请注意,此类不是线程安全的
*
* 可以看到该类提供了 查询、增加、更新、删除、提交、回滚等方法。
* 其实这就就是提供给用户,对外的。
* 执行工作不在这里执行,而是交给了executor包
*
* session包是整个 MyBatis应用的对外接口包,而 executor包是最为核心的执行器包。
* DefaultSqlSession 类做的主要工作则非常简单------把接口包的工作交给执行器包处理。
*/
public class DefaultSqlSession implements SqlSession {
// 配置信息
private final Configuration configuration;
// 执行器 (下面的操作语句都交由执行去执行)
private final Executor executor;
// 是否自动提交
private final boolean autoCommit;
// 缓存是否已经污染
private boolean dirty;
// 游标列表
private List<Cursor<?>> cursorList;
@Override
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
}
继续往下跟:configuration.getMapper(type, this);
java
public Configuration{
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 在 mapper的注册表中 找到指定映射接口的映射文件,并根据映射文件信息为该映射接口 生成一个代理实现
return mapperRegistry.getMapper(type, sqlSession);
}
}
跟:
java
/**
* 用于解决:抽象方法与数据库操作节点之间的关联,就是通过该MapperRegistry里面的 knownMappers属性
* 1.因为 MapperMethod 解决了 如果通过一个方法去执行SQL(就是将一个SQL转为为一个方法)
* 2.因为 MapperProxy、MapperProxyFactory 通过动态代理解决了
* 映射接口(ege:UserMapper.java)的方法去执行(数据库操作的方法);
* (因为 UserMapper.java本身是一个接口,没有具体的实现类,如果找到SQL数据库操作,就是通过MapperProxyFactory、MapperProxy)
*
*/
public class MapperRegistry {
private final Configuration config;
// knownMappers维护了 映射接口与MapperProxyFactory关联起来
// key: 映射接口 (UserMapper.class)
// value: 对应的 MapperProxyFactory对象
// 而这个MapperProxyFactory 作为代理工厂,里面有封装了 MapperProxy,这个MapperProxy,一个对一个,对应MapperMethod;
// 同时MapperProxyFactory工厂里面还有一个 methodCache(这是一个map)维护了当前 代理接口的所有方法(MapperMethod)
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
/**
* 找到指定映射接口的映射文件,并根据映射文件信息为该映射接口 生成一个代理实现
* @param type 映射接口
* @param sqlSession sqlSession
* @param <T> 映射接口类型
* @return 代理实现对象
*/
/**
* TestMapperDao mapper = sqlSession.getMapper(TestMapperDao.class);
* 这里sqlSession.getMapper(***.class)最终也会来到这里
*/
@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 1、找出指定映射接口的代理工厂
// ★ 这里我们主要看属性: knownMappers的 (key,value)值
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
//2、通过mapperProxyFactory返回一个对应映射接口的代理器实例
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
}
-
1、
knownMappers.get(type);
我们代码跟到这里看一下:可以看到,每一个 Mapper接口都有与之对应的代理工厂。
-
2、利用对应的代理工厂创建代理对象:
mapperProxyFactory.newInstance(sqlSession);
跟着代码可以来到 MapperProxyFactory类,来到代理创建的位置。
javapublic class MapperProxyFactory<T> { // mapperInterface: // 对应SQL的Java映射接口(ege: UserMapper.java) // methodCache: // 该map缓存的是映射接口的方法,但是这里没有put操作, // 也就说,这里一个mapperInterface,一个methodCache,而且methodCache里面只对应mapperInterface里面的一个方法,没有多个。 private final Class<T> mapperInterface; private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>(); public T newInstance(SqlSession sqlSession) { //这里就是为 映射接口创建代理对象 final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } /** * 这里返回的应该是一个 代理(映射接口)对象 */ @SuppressWarnings("unchecked") protected T newInstance(MapperProxy<T> mapperProxy) { // 三个参数分别是: // 创建代理对象的类加载器、要代理的接口(映射接口)、代理类的处理器(映射接口中的方法所绑定的SQL)。 return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } }
至此,代理对象创建完成,代理对象的类型为 :MapperProxy,我们可以返回 main 方法中查看,可以看到此时的 TestMapperDao
已经是代理类型了,那么,下一步,执行mapper.selectByPrimaryKey(1)
就是由代理对象执行了。
五、代理对象执行目标方法
java
// 5、然后在执行 `Mapper接口`的 SQL查询时,转而利用代理对象执行 JDBC的SQL底层操作。
System.out.println(mapper.selectByPrimaryKey(1));
java
/**
* 首先是MapperMethod已经完成方法和SQL语句之间绑定,将数据库操作(SQL
* 语句)转化(这里的转化有反转之意)为MapperMethod中的execute()方法。
*
* 该类基于动态代理将针对 映射接口的方法(mapper.java中的方法)调用转接成了
* 对MapperMethod对象execute方法的调用,进而实现了数据库操作。
* 该类实现了InvocationHandler,这意味着当使用它的实例替代被代理对象后,
* 对被代理对象的方法调用会被转接到MapperProxy中invoke方法上 (√ 没毛病)
*/
public class MapperProxy<T> implements InvocationHandler, Serializable {
private static final long serialVersionUID = -6424540398559729838L;
private final SqlSession sqlSession;
// mapperInterface:映射接口,methodCache:映射接口中的方法所对应的SQL方法
private final Class<T> mapperInterface;
// 该Map的键为映射接口中的方法,值为MapperMethod对象。
// 通过该属性,完成了MapperProxy内(即映射接口内)方法和MapperMethod的绑定
// 也就是一个 抽象方法(ege:UserMapper.java中的一个抽象方法)对应一个MapperMethod对象
// 这个map作为:映射接口方法和MapperMethod,第一次调用会put进入,再次调用的话就直接在该map中寻找了
private final Map<Method, MapperMethod> methodCache;
/**
* 这里是 代理方法
* @param proxy
* @param method 被代理的方法
* @param args 被代理方法的参数
* @return 方法执行结果
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) { // 继承自Object的方法
// 直接执行原有方法
return method.invoke(this, args);
} else if (method.isDefault()) {
// 执行默认方法
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
// 1、找到对应的MapperMethod对象
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 调用MapperMethod中的execute方法
// 2、就是用于执行数据库操作的方法!
return mapperMethod.execute(sqlSession, args);
}
}
5.1 找到对应的MapperMethod对象
5.2 用于执行数据库操作的方法
java
mapperMethod.execute(sqlSession, args);
java
/**
* 每个 MapperMethod对象都对应了一个数据库操作节点(即一次SQL操作,换句话说,一个接口映射方法就对应一个MapperMethod),
* 调用 MapperMethod实例中的 execute方法就可以触发节点中的 SQL语句。
*
* MapperMethod类将一个数据库操作语句和一个Java方法绑定在一起:
* MethodSignature属性保存了这个方法的详细信息;SqlCommand属性持有这个方法对应的SQL语句。
* 所以,对接方法的操作:MethodSignature,对接SQL的操作在SqlCommand;
* 所以,只要调用 MapperMethod对象的 execute方法,就可以触发具体的数据库操作,★于是数据库操作就被转化为了方法。
*/
public class MapperMethod {
// SqlCommand、MethodSignature这两个是该类(MapperMethod)中的两个内部类
// SqlCommand内部类指代一条SQL语句
// ege:
// {
// "name":"org.apache.ibatis.binding.BoundBlogMapper.selectBlogUsingConstructorWithResultMap",
// "type":"SELECT"
// }
private final SqlCommand command;
// MethodSignature 内部类的属性详细描述了一个方法的细节。
private final MethodSignature method;
/**
* 每个MapperMethod对象都对应了一个数据库操作节点,调用MapperMethod实例中的execute方法就可以触发节点中的SQL语句。
* 这个方法会被执行:
* 可以看到,这里里面由于 增、删、改、查
* @param sqlSession
* @param args
* @return
*/
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()) { // 方法返回值为void,且有结果处理器
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) { //多条结果查询
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);
// ★单条数据结果查询,就跟着这个方法往下走就行了。
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()) {
// 查询结果为null,但返回类型为基本类型。因此返回变量无法接收查询结果,抛出异常。
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
}
我们这里是单条数据结果查询:就是其为例。
java
result = sqlSession.selectOne(command.getName(), param);
sqlSession
类型是 DefaultSqlSession
java
public class DefaultSqlSession implements SqlSession {
@Override
public <T> T selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
// 接着跟这一行
List<T> list = this.selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(),
but found: " + list.size());
} else {
return null;
}
}
@Override
public <E> List<E> selectList(String statement, Object parameter) {
// 接着跟这一行,走到最终下面的方法
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
/**
* 查询结果列表
* @param <E> 返回的列表元素的类型
* @param statement SQL语句
* @param parameter 参数对象
* @param rowBounds 翻页限制条件
* @return 结果对象列表
*/
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
// 每一个MappedStatement对象对应了我们设置的一个数据库操作节点,它主要定义了数据库操作语句、输入/输出参数等信息
// configuration.getMappedStatement(statement) : 将 要执行的MappedStatement对象从Configuration对象存储的映射文件信息中找出来
//
// 获取查询语句
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();
}
}
}
5.2.1 交由执行器,执行SQL操作
java
// 交由执行器进行查询
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
看 executor
类型为 CachingExecutor
,我们往下跟:
java
public class CachingExecutor implements Executor {
// 被装饰的执行器
private final Executor delegate;
// 事务缓存管理器
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject,
RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
//1. BoundSql是经过层层转化后去除掉 if、where等标签的 SQL语句
BoundSql boundSql = ms.getBoundSql(parameterObject);
//2. CacheKey是为该次查询操作计算出来的缓存键
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
//3. 执行查询操作
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
}
-
1、
boundSql
值: -
2、key值
CacheKey是为该次查询操作计算出来的缓存键
-
3、查询数据库中的数据
这里涉及到:
先去 二级缓存 查找数据,如果有,返回
二级缓存没有,去一级缓存查找,一级缓存有,返回
- 一级缓存没有,执行数据查询操作。
如果我们设置了缓存的话, 那么我们就要在执行数据库操作之后,将其放入到缓存中。
5.2.1.1 二级缓存 查找数据
java
/**
* 查询数据库中的数据
* @param ms 映射语句
* @param parameterObject 参数对象
* @param rowBounds 翻页限制条件
* @param resultHandler 结果处理器
* @param key 缓存的键
* @param boundSql 查询语句
* @param <E> 结果类型
* @return 结果列表
* @throws SQLException
*/
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject,
RowBounds rowBounds, ResultHandler resultHandler,
CacheKey key, BoundSql boundSql) throws SQLException {
// 获取MappedStatement对应的缓存,可能的结果有:该命名空间的缓存、共享的其它命名空间的缓存、无缓存
Cache cache = ms.getCache();
// 如果映射文件未设置<cache>或<cache-ref>则,此处cache变量为null
if (cache != null) { // 存在缓存
// 根据要求判断语句执行前是否要清除二级缓存,如果需要,清除二级缓存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) { // 该语句使用缓存且没有输出结果处理器
// 二级缓存不支持含有输出参数的CALLABLE语句,故在这里进行判断
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 从缓存中读取结果
List<E> list = (List<E>) tcm.getObject(cache, key);
// 说明缓存中没有相应数据
if (list == null) {
// 交给被包装的执行器执行
// 首先: 当前类CachingExecutor本身是二级缓存,然后 来到这里说明在二级缓存中没有找到数据,
// 这里应该就会调用做为一级缓存的BaseExecutor查询数据库。
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);
}
5.2.1.2 一级缓存 查找数据
-
二级缓存没有的话,执行
delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
去一级缓存查找数据;
java这里是到了 执行的基类:`org.apache.ibatis.executor.BaseExecutor` /** * 查询数据库中的数据; * 该方法大致的流程是:会尝试读取一级缓存,而在缓存中无结果时,则会调用queryFromDatabase方法进行数据库中结果的查询 * @param ms 映射语句 * @param parameter 参数对象 * @param rowBounds 翻页限制条件 * @param resultHandler 结果处理器 * @param key 缓存的键 * @param boundSql 查询语句 * @param <E> 结果类型 * @return 结果列表 * @throws SQLException */ @SuppressWarnings("unchecked") @Override 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) { // 本地缓存中有结果,则对于CALLABLE语句还需要绑定到IN/INOUT参数上 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(); } // issue #601 deferredLoads.clear(); // 如果本地缓存的作用域为STATEMENT,则立刻清除本地缓存 if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // issue #482 clearLocalCache(); } } return list; }
5.2.1.3 数据库查找数据
java
// 本地缓存没有结果,故需要查询数据库
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
/**
* 从数据库中查询结果
* @param ms 映射语句
* @param parameter 参数对象
* @param rowBounds 翻页限制条件
* @param resultHandler 结果处理器
* @param key 缓存的键
* @param boundSql 查询语句
* @param <E> 结果类型
* @return 结果列表
* @throws SQLException
*/
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler,
CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
// MyBatis先在缓存中放置一个占位符,然后调用doQuery方法实际执行查询操作。
// 最后,又把缓存中的占位符替换成真正的查询结果(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;
}
/**
* 该方法中,不论下面的那个实现类实现的次方法,都有这样几步:
* 1. 生成Statement对象stmt;Statement类并不是MyBatis中的类,而是JDK java.sql包中的类,Statement类能够执行静态SQL语句并返回结果。
* 2. 通过Configuration的newStatementHandler方法获得了一个StatementHandler对象handler,
* 然后将查询操作交给StatementHandler对象进行,StatementHandler是一个语句处理器类,其中封装了很多语句操作方法。
* 3. handler.query(stmt, resultHandler);
* 会进入到 StatementHandler接口下面的某一个实现类中(这里进入PreparedStatementHandler)
* 里面的 ps.execute(), ps类型是 PreparedStatement(这个已经是 com.mysql.cj.jdbc包中的类负责)的了
*/
protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
throws SQLException;
我们这里单个查询的实现类是:org.apache.ibatis.executor.SimpleExecutor#doQuery
java
public class SimpleExecutor extends BaseExecutor {
@Override
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());
// SQL语句的处理方式:
// handler有下面4种类型:
// CallableStatementHandler,PreparedStatementHandler,
// SimpleStatementHandler,RoutingStatementHandler
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
}
这里是:PreparedStatementHandler
类型
java
public class PreparedStatementHandler extends BaseStatementHandler {
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
//这里是 com.mysql.cj.jdbc包中的类负责;
//执行的结果,我们可以在 ps对象中找到: (这里是debug模式,查看ps对象中的值)
// ->h
// ->statement
// ->results
// ->(catalog:数据库名;
// connection:连接对象,里面有数据库明,地址,端口,账号,密码等信息;
// rowData:rows记录了该次数据库查询的结果信息)
ps.execute();
//这里最终数据库查询得到的结果交给 ResultHandler对象处理
return resultSetHandler.handleResultSets(ps);
}
}
OK,到这里就要调用 JDBC的PreparedStatement执行SQL(数据库操作)了。
然后再层层返回,执行流程走完。
完结撒花。
六、目标方法的注册
这里单独介绍一下 Mapper接口如何和代理类绑定起来的;以及如何注册到 Configuration中的,因为只有这些Mapper接口信息以及接口中的方法信息注册了,我们才能在会话中用。
其实就是在 2.2节中解析mapper
节点的一个步骤:
java
// ★使用XMLMapperBuilder解析Mapper文件解析: ★ 这应该解析的是对应路径中的接口 继续解析(就是对XML的接口映射文件继续解析)
XMLMapperBuilder mapperParser =
new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
XMLMapperBuilder
就是 Mapper文件方式的解析类。
我们进入该类的 parse()
方法:
java
/**
* 解析映射文件
*/
public void parse() {
//该节点是否被解析过
if (!configuration.isResourceLoaded(resource)) {
// 处理mapper节点
configurationElement(parser.evalNode("/mapper"));
// 加入到已解析列表
configuration.addLoadedResource(resource);
// 将mapper注册给 configuration
bindMapperForNamespace();
}
// 下面分别用来处理暂时性失败的<resultMap>、<cache-ref>、SQL语句
// ege:
// <resultMap id="girlUserMap" type="Girl" extends="userMap">
// <result property="email" column="email"/>
// </resultMap>
// <resultMap id="userMap" type="User" autoMapping="false">
// <id property="id" column="id" javaType="Integer"/>
// <result property="name" column="name"/>
// </resultMap>
// 由于映射文件(xxxMapper.xml)节点的解析顺序是由上往上的;
// 当先解析girlUserMap的时候,extends="userMap"引用的是id="userMap";
// 可是,id="userMap"的resultMap(由于在下面)还未被读入,此时就会出现暂时性的错误。
// 这里就是处理这种场景的!!!
// (由于已经在第一遍解析时读入了所有节点,因此第二遍解析的时候可以解决这种依赖。)
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
关于 mapper解析过程中的细枝末节,这里不做关注,想看的话,可以到博主源码工程中去看。
这里直接看 bindMapperForNamespace();
注册 对应Mapper的代理工厂
java
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
//ignore, bound type is not required
}
if (boundType != null) {
if (!configuration.hasMapper(boundType)) {
// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
configuration.addLoadedResource("namespace:" + namespace);
// 这里注册!!!
configuration.addMapper(boundType);
}
}
}
}
跟:configuration.addMapper(boundType);
这里,我们要注意,这里是 Configuration类中的 方法,并且这里是 mapperRegistry
属性中添加 mapper 的代理工厂。
好的, 找到了,与第四节创建代理对象时,先从mapperRegistry
注册表中获取对应mapper的代理工厂构成对应关系了。
java
public class Configuration {
// 这里是注册 mapper的代理工厂的。
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
// 第四节时候用的这个方法获取的!!!
// 与第四节创建代理对象时,先从`mapperRegistry`注册表中获取对应mapper的代理工厂构成对应关系了。
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
}
跟:mapperRegistry.addMapper(type);
注意看里面的 knownMappers
属性。
java
/**
* 用于解决:抽象方法与数据库操作节点之间的关联,就是通过该MapperRegistry里面的 knownMappers属性
* 1.因为 MapperMethod 解决了 如何通过一个方法去执行SQL(就是将一个SQL转为为一个方法)
* 2.因为 MapperProxy、MapperProxyFactory 通过动态代理解决了
* 映射接口(ege:UserMapper.java)的方法去执行(数据库操作的方法);
* (因为 UserMapper.java本身是一个接口,没有具体的实现类,如何找到SQL数据库操作,就是通过MapperProxyFactory、MapperProxy)
*
*/
public class MapperRegistry {
// knownMappers维护了 映射接口与MapperProxyFactory关联起来
// key: 映射接口 (UserMapper.class)
// value: 对应的 MapperProxyFactory对象
// 而这个MapperProxyFactory 作为代理工厂,里面又封装了 MapperProxy,这个MapperProxy一个对一个,对应MapperMethod;
// 同时MapperProxyFactory工厂里面还有一个methodCache(这是一个map)维护了当前 代理接口的所有方法(MapperMethod)
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
/**
* 该方法就是为映射接口 创建代理工厂并将该工厂加入 knownMappers中
* 然后后面在 上面的getMapper()方法中使用该工厂(是这样使用的:在getMapper()方法中①+②(②就是通过代理工厂创建代理对象))
*/
public <T> void addMapper(Class<T> type) {
// 只有是接口的时候才可以添加
if (type.isInterface()) {
//判断是否已经注册过了
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
//★添加 Mapper对应的代理的工厂
knownMappers.put(type, new MapperProxyFactory<>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
// 必须在运行解析器之前添加类型,否则映射器解析器可能会自动尝试绑定。如果类型已知,则不会尝试。
// 这里应该是注解方式的处理 : 是的没错
// 这里是构造一个 MapperAnnotationBuilder,
// 如果 映射接口中有注解方式的SQL语句:@Select/@SelectProvider...删改查;就会用该类中的parse()方法进行解析
//
// ★不,不,解析的不止是注解方式的,还有配置文件方式,parse()会有判断步骤分开处理。
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
}
这里,可以看到注册了 Mapper接口对应的代理工厂。
java
//★添加 Mapper对应的代理的工厂
knownMappers.put(type, new MapperProxyFactory<>(type));
其实这里就完结了!!!