目录
前言
mybatis是什么
MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
这段话来自于Mybatis中文网
我们知道在Javase阶段中,Java如果要将数据持久化的储存下来,要不就是io写到本地磁盘中,要不就是连接上mysql,将数据通过mysql存储到磁盘中。而JDBC的代码是非常的繁琐,哪里使用都需要写上一大段的代码,什么反射获取连接对象,statement传入参数然后执行sql,然后返回结果集进行遍历。
而Mybatis将繁琐的JDBC给进行封装,开发者只需要给一个mapper接口,如果使用注解编写sql语句xml都不需要,但是复杂的sql还是需要使用到xml然后映射到mapper接口,Mybatis帮你解析通过Java的动态代理生成接口的代理类底层帮你于mysql交互。对于返回值,Mybatis只需要你给定一个实体类于mysql表中的映射,Mybatis底层也帮你处理好返回值。
这种映射关系就叫做orm(Object Relational Mapping)框架。
Mybatis的架构
此帖子并不会直接拿源码一行一行的解读,更多的是一个思想的授予,一步一步的推导。从前面我们知道Mybatis是一个orm框架,对于orm框架我们只需要编写他的接口和xml的映射,返回值和实体类的映射。那么了解到这里,假如叫你来设计一个orm框架你会怎么来设计呢?
首先对于Java万物皆对象的语言来说肯定是要来解析Mybatis的配置文件,此时我们可以提供xml写配置文件和直接Java的new对象来写,因为xml其实最后也是解析生成Java对象。
解析完xml后,我们应该干什么呢?
在我们dao层都是接口,看到接口就能明白肯定是要生成代理类。最终干活的都是代理类。
与mysql交互后是不是要解析数据呢?
从整体来说其实就是解析xml,然后动态代理与mysql交互,然后解析数据。
下面的源码环节会来证实这种猜想!
源码解读前的准备
那么我们追源码前需要准备一些什么呢?
有看过博主其他帖子的小伙伴可能知道,追源码共用的几个小步骤:
1. 对框架有一定了解(能推理出上面的架构图,因为这是一个推理过程,追源码一定要带着自己的想法来追,因为框架万变不离其宗,改变的只是代码,思想是改变不了的),对其他的api也有一定的使用了解。
2. 准备一段hello world代码(建议追任何框架源码都从一段hello world开始!)
3. 很关键的一个点就是熟悉使用idea的debug工具。
4. 通过idea生成的类关系图弄清楚类之间的关系。
5. 使用到截图工具将自己的接口和xml代码和测试类main方法代码截图钉在桌面,电脑比较大的可以使用到分屏。这样效率的确高很多。
正文
准备hello world代码
使用maven构建项目,pom文件如下
<dependencies>
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.23</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
测试类的准备
public static void main(String[] args) {
DataSource dataSource = getDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(UserMapper.class);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
System.out.println(mapper.selectAllUser());
// sqlSession.close();
}
private static DataSource getDataSource() {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setUrl("jdbc:mysql://localhost:3306/dingcan?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8");
druidDataSource.setUsername("root");
druidDataSource.setPassword("123456");
return druidDataSource;
}
dao层接口准备
public interface UserMapper {
List<User> selectAllUser();
}
对应的接口映射的xml文件准备(这里注意xml需要跟接口一个路径包下,因为博主使用的Java代码来代替xml配置文件的,xml配置文件可以配置扫描接口xml映射,但是Java代码我没找到对应的方法.... 不过不影响,因为mybatis就是默认扫描接口路径,后面可以证实)
<?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="com.lixuan.orm.helloWolrd.dao.UserMapper">
<select id="selectAllUser" resultType="com.lixuan.orm.helloWolrd.entriy.User">
select * from user
</select>
</mapper>
实体类的准备(你们可以根据我的main方法中数据源来建表,或者自定义,自定义的记得改变实体类的字段,不要犯低级错误来影响时间)
/**
* @author liha
* @version 1.0
* @date 2022/1/29 13:23
*/
@Data
@ToString
public class User {
private String id;
private String userNmae;
private String nickname;
private String password;
}
解读源码
启动源码解读
hello world的源码准备好了后,我们是不是应该找到追寻源码的入口呢?
入口?那不就是第一行开始?
并不是,我们来分析一下
DataSource dataSource = getDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(UserMapper.class);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
System.out.println(mapper.selectAllUser());
数据源的配置,事务工厂,然后环境的准备。
再看到Configuration,他的构造方法是不是把环境传进去,那么我们看看Configuration的构造方法
一些初始化的注册操作,并不是我们的重点。
我们的架构图第一件事不应该是解析xml吗?而xml是跟接口做一个映射关系,所以往下走的addMapper(UserMapper.class)好像跟我们的想法是沾边了。
那么奖励这行一个断点,我们追进去
我们看到Configuration类中维护了很多类,一眼就能明白它肯定是最核心的类。继续往下走
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 {
knownMappers.put(type, new MapperProxyFactory<T>(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 parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
判断是否是接口,是否已经加载过不是我们的核心,我们的核心是什么?解析对不对!
在进入parse()方法之前,我们看一下try和finally代码块,可以学习到这种写法,先put到缓存,如果过程中出现问题,再通过finally代码块remove。
给parse()方法哪行一个断点,不过追进去之前,先看到MapperAnnotationBuilder的构造方法追进去。
内部维护了一个MapperBuilderAssistant对象,翻译过来就是助手。其他就是类信息和共用一个Configuration对象。我们进入到parse()方法中。
public void parse() {
// type是构造方法赋值的类对象。
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) { // 是否解析过
// 解析加载xml
loadXmlResource();
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
parseCache();
parseCacheRef();
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
type是构造方法传进来的类对象,然后这里的configuration也是共用的。
public boolean isResourceLoaded(String resource) {
return loadedResources.contains(resource);
}
先是对判断是否加载过,使用的也是contains检查是否存在于集合中。继续往下走,给loadXmlResource来上一个断点,继续追进去。
private void loadXmlResource() {
// Spring may not know the real resource name so we check a flag
// to prevent loading again a resource twice
// this flag is set at XMLMapperBuilder#bindMapperForNamespace
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
// 字符串的操作,其实从这里我们得出,Mybatis默认找的是接口的同级目录
String xmlResource = type.getName().replace('.', '/') + ".xml";
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e) {
// ignore, resource is not required
}
if (inputStream != null) {
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
xmlParser.parse();
}
}
}
判断肯定是能过的,因为我们并没有解析过,解析过会存入set集合中。跟前面的那个判断共用一个方法。
其实从这里我们得出,Mybatis默认找的接口映射的xml文件是接口的同级目录,后面也是查找这个目录下是否存在这个文件,并且注意到catch代码块中的注释,其实说明了没有这个xml文件也不影响,因为可能使用的是注解写的xml,并且后面会有对这些判空操作,所以这里肯定是不能抛出异常的。
而我们的xml映射文件是跟接口一个目录所以能直接获取到io输出流来给下面的操作解析xml文件。
然后就是new了一个XMLMapperBuilder对象,我们来看看他的构造方法。
在XMLMapperBuilder类中维护了一个XPathParser对象,在构造方法中对XPathParser初始化,而XPathParser内部维护了一个Document,在XpathParser初始化的时候初始化了内部的document,根据io流解析XML(这里使用的是apache公司的xml解析技术,想要看具体的解析可以追进去)生成document。方便后面从document取出标签。
我们继续往后面的解析走。
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
// 解析方法,parser是当前类维护的XPathParser类,XPathParser内部维护了一个document,根据io流解析XML(这里使用的是apache公司的xml解析技术,想要看具体的解析可以追进去)生成document。方便后面从document取出标签。
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
因为上面说过XMLMapperBuilder的构造方法,所以当前的parser对象是在XMLMapperBuilder的构造方法中创建的XPathParser,所以我们看到configurationElement(parser.evalNode("/mapper")); ,之前也说过在XPathParser类中维护了一个document,所以我们推测出parser.evalNode()方法就是从document中根据参数“/mapper”取出mapper标签的所有内容。我们给这行来上一个断点追进去
然后生成XNode对象返回,我们再看看Xnode类中维护了一些什么对象
可以看到我们从Document中取出的数据Node,给了类中的Properties对象attributes,那么我们后面是不是从attributes取值呢?
我们继续往configurationElement()方法里面走
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
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);
}
}
前面看的evalNode()方法的返回值就是XNode,也就是configurationElement()方法的参数,我们看到context.getStringAttribute("namespace")方法中。
这里的确就是从Properties对象中取值。然后进行判断您是否有namespace的一个接口映射。我们接着往下走
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
这些方法跟之前也是一样,也就是通过document获取到参数标签的值,然后存到XNode中,然后再从XNode维护的Properties中获取到值,再把值存到builderAssistant助手中。这里我不细说,大家想追的可以自己细追一下,大致的操作都一样!
并且这里与缓存有关的我会另外写一篇帖子细讲Mybatis的缓存机制。
cache-ref:引用其它命名空间的缓存配置。
cache:该命名空间的缓存配置。
parameterMap:已经废弃,未来可能移除
resultMap:描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素。
sql:抽取出公共的sql语句
Mybatis文档有中文翻译,建议大家养成一个看官方文档的习惯
来到buildStatementFromContext(context.evalNodes("select|insert|update|delete"))方法,给上一个断点,我们追进去。
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
DatabaseId:简单来说就是mybatis支持多种DB厂商的,通过databaseid来识别DB厂商。
Mybatis文档有中文翻译,建议大家养成一个看官方文档的习惯
我们这里没有设置,所以直接跳出if,给buildStatementFromContext()方法来上一个断点。追进去
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
因为一个XML文件可能存在多个增删改查语句,所以for循环遍历这没啥好说的,并且在mybatis中一个增删改查标签对应一个MappedStatement。给statementParser.parseStatementNode();来个断点我们继续往里面走。
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String resultMap = context.getStringAttribute("resultMap");
String resultType = context.getStringAttribute("resultType");
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
Class<?> resultTypeClass = resolveClass(resultType);
String resultSetType = context.getStringAttribute("resultSetType");
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
String resultSets = context.getStringAttribute("resultSets");
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
这里基本还是跟之前操作一样,从XNode的Properties中取值。
resultType、parameterType就生成他们的实体类。
并且看到那几个boolean值的操作,其实哪里就是缓存的一些操作,如果是查询就不flushCache,如果非查询就flushCache,并且看你是否能使用到缓存。对于缓存会另外写篇详细的帖子!
继续往下走
resultSets:在mybatis中执行存储过程,它会执行两个查询并返回多个结果集,所以需要规定多个返回结果集。 keyProperty:自增字段,后面可以通过自增字段获取到自增的值 keyColumn:自增字段数据库的字段Mybatis文档有中文翻译,建议大家养成一个看官方文档的习惯
解析完,就是添加到集合中的操作,进入到addMappedStatement()方法中,也就是生成MappedStatement然后添加到configuration对象中。
这里其实我觉得可以不用解释一句话,因为很明显的东西,小伙伴们可以打开Mybatis官方文档自行查看。
继续走,然后就回到了foreach循环的位置,因为我们就是一条语句所以继续返回。
回到了XMLMapperBuilder类中的parse()方法中
继续往后走, 添加到集合中,证明解析过了,再完后就是bingMapperForNamespace()方法。追进去看看吧其实也就是绑定一下xml中namespace和接口,但是我们已经绑定过了。
继续往下面三个方法看去
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
parsePendingStatements();在之前buildStatementFromContext()方法中已经解析完毕并且添加到configuration中了。
大概就是前面解析的时候如果有ResultMap和cacheref就会对他进行一个处理。在前面解析的时候是把大致的信息添加到configuration中,这里就是把configuration大致的信息获取出来再对它进行一个处理并且添加configuration中,为后面的动态代理处理sql做铺垫。并且做了处理之后会把之前的大概信息给remove。
我们继续走,就回到了MapperAnnotationBuilder类中的parse()方法了
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
loadXmlResource();
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
parseCache();
parseCacheRef();
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
看到parseCache()和parseCacheRef()两个方法。
private void parseCache() {
CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);
if (cacheDomain != null) {
Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size();
Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval();
Properties props = convertToProperties(cacheDomain.properties());
assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), props);
}
}
private void parseCacheRef() {
CacheNamespaceRef cacheDomainRef = type.getAnnotation(CacheNamespaceRef.class);
if (cacheDomainRef != null) {
Class<?> refType = cacheDomainRef.value();
String refName = cacheDomainRef.name();
if (refType == void.class && refName.isEmpty()) {
throw new BuilderException("Should be specified either value() or name() attribute in the @CacheNamespaceRef");
}
if (refType != void.class && !refName.isEmpty()) {
throw new BuilderException("Cannot use both value() and name() attribute in the @CacheNamespaceRef");
}
String namespace = (refType != void.class) ? refType.getName() : refName;
assistant.useCacheRef(namespace);
}
}
这里是看是否存在@CacheNamespace注解和@CacheNamespaceRef注解,我们之前XML解析过cache所以可以得出基于注解的cache优先级要大于xml配置。
对于缓存还是强调一遍,后面会有帖子从源码来讲解缓存。所以这里不多说继续往下走。
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
获取到接口中的方法进行遍历,然后给parseStatement()方法给上一个断点我们追进去。
方法内的内容挺长的,我就不放出来了,但是是不是觉得特别的眼熟呢?
没错他跟前面解析xml标签的方法极其相似,并且我们知道我们的sql语句可以使用注解的形式,也可以使用到xml的形式。所以这里面是对注解的一个解析,并且最后也是通过助手的addMappedStatement()方法将生成MappedStatement对象添加到configuration对象中。
到这里大家伙有没有思考一个问题呢?要是我同时使用注解的形式和XML形式来写sql语句,那么它会是一个优先级的选择还是报错呢?
这里我又给大家做了一个实验,我同时使用到注解和xml,最后接口是抛出异常了。
从异常报告中我们可以看出他是使用到contains判断是否存在了,存在就抛出异常了,并把我们的方法告诉我们。而且从前面解析可以得出,先是解析XML再是解析注解,所以肯定是在解析注解的层面判断的。为了证实我的猜想。
所以我追了一遍,确实是在解析注解的时候进行了一个判断,是解析注解中对ResultMap注解解析中,因为基于注解形式我们一般不适用ResultMap注解,直接通过方法的返回值。所以这里对这个做了判断。在parseStatement()方法中的348行的parseResultMap()方法。这里想看详细的步骤的同学可以自己追一下。
因为这些比较成熟的源码套娃肯定少不了,所以很容易追的头疼,追到迷茫。所以在追源码很迷茫的时候,建议通过日志,一行一行的执行,直到日志出来。然后再debug运行一下,再慢慢思考!
至此启动的源码就已经全部追完,其实启动的源码也就是解析。后面就是mybatis如何动态代理生成代理类并且如何封装jdbc代码执行sql语句,并且如何解析mysql那边返回的数据。
执行流程源码解读
很多小伙伴才开始接触mybatis,就是SqlSessionFactory、SqlSession啥的。使用Sqlsession获取到接口的代理对象,然后执行被代理的方法。所以再追执行流程源码之前先讲解一下Sqlseesion。
使用 MyBatis 的主要 Java 接口就是 SqlSession。你可以通过这个接口来执行命令,获取映射器实例和管理事务。在介绍 SqlSession 接口之前,我们先来了解如何获取一个 SqlSession 实例。SqlSessions 是由 SqlSessionFactory 实例创建的。SqlSessionFactory 对象包含创建 SqlSession 实例的各种方法。而 SqlSessionFactory 本身是由 SqlSessionFactoryBuilder 创建的,它可以从 XML、注解或 Java 配置代码来创建 SqlSessionFactory。
简单来说就是SqlSession就是一次会话,也就是一次java与mysql的交互。而SqlSessionFactory就是生产SqlSession的。
Mybatis文档有中文翻译,建议大家养成一个看官方文档的习惯
这里使用了很明显的建造者模式来创建SqlSessionFactory对象,并且参数是Configuration,在前面的解析过程中,结果集都是存入到Configuration中的。
再来看看SqlSessionFactory如何创建的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);
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();
}
}
Environment 、TransactionFactory 这里就不介绍了,我们来介绍一下Executor,在openSessionFromDataSource()方法的参数中ExecutorType,这里是一个枚举类,默认使用的是普通执行器,还有两种分别是重用和批量执行器。
然后就是创建DefaultSqlSession,我们继续往下走。
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
核心来了,追进去。
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
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);
}
}
knownMappers.get(type);在我们解析的时候有添加到这个map集合中。
所以不为空,所以给mapperProxyFactory.newInstance(sqlSession);来上一个断点,并且追进去
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
MapperProxyFactory工厂创造出MapperProxy,然后继续往newInstance()方法追进去。
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
到这里就是使用JDK自带的动态代理来生成代理类并返回,并且不懂动态代理的同学建议先去通过博客或者课程去了解,我这里不过多讲。
生成了代理类后,看看Mybatis如何执行的,我们执行接口的方法。
追进去。
@Override
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);
}
if判断代理对象是否是原对象显然不是,再判断是否是default修饰的方法显然不是。所以接着往下走。
cachedMapperMethod()方法也就是从map集合缓存中去取,没有就创建,经典缓存思想。
看一下MapperMethod的构造方法把。MapperMethod内部维护了一个SqlCommand,和MethodSignature,也就是sql语句是增删改查和返回值的的类型的值。
然后给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);
}
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;
}
也就是判断是增删改查,内部维护的command在构造方法对其初始化了。
看到switch代码块中的SELECT代码块。
this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
这是内部的维护的MethodSignature构造方法的代码,也就是判断返回值是否是一个集合或者数组,显然我们的是一个集合,所以为true。所以来到executeForMany()方法给个断点追进去。
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
Object param = method.convertArgsToSqlCommandParam(args);
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
result = sqlSession.<E>selectList(command.getName(), param, rowBounds);
} else {
result = sqlSession.<E>selectList(command.getName(), param);
}
// issue #510 Collections & arrays support
if (!method.getReturnType().isAssignableFrom(result.getClass())) {
if (method.getReturnType().isArray()) {
return convertToArray(result);
} else {
return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
}
}
return result;
}
判断是否实现了mybatis内部的分页,当然我们这里是没有实现的。mybatis实现的分页,我的猜想是获取到结果后对集合的处理达到分页效果。
所以我们进入到else代码块中给上一个断点继续追进去。
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
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();
}
}
MappedStatement再熟悉不过了吧,从configuration中获取到MapperStatement对象。
继续往下走,追进query()方法中。
把MappedStatement的数据抽取出来生成其他对象给下面的操作做铺垫,继续往里面的query()走
@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.<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);
}
取缓存,有缓存就直接返回,没有缓存就继续创建。而且换成小伙伴们来设计查询的缓存,肯定是按照sql语句来缓存。这里不过多提。
继续往query()方法走。
打上断点继续走。
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;
}
看到doQuery就知道要执行了(手动滑稽),所以打个断点,继续追进去。
生成StatementHandler来干活, 通过数据源来建立与mysql的连接,connection对象然后生成Statement对象,这些对于懂JDBC的小伙伴来说非常的熟悉不过了。
继续往下走。进入到query()方法中。
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.<E> handleResultSets(ps);
}
Statement和PreparedStatement的区别大致就是PreparedStatement会先与数据库进行预处理,再解析好参数再进行处理,所以比较耗性能,但是他会被缓存下来,所以有重复的操作就可以使用到,而Statement是每次从0到1的创建处理。
然后就是数据源执行,然后就是解析返回的数据。然后就是解析数据并且返回。解析的具体步骤感兴趣的小伙伴可以进给return哪行打上断点追进去。其实无非就是resultType实体类和返回结果中数据库字段的一个映射,如果有resultMap就按照resultMap的来。
后面就一路的返回并且添加到缓存中,就结束了整个的执行流程。
Mybatis的执行流程图
总结
把一个复杂的东西具体化的拆分,然后再自我猜想,再抱着猜想开始追源码。
并且后面会写一篇关于Mybatis缓存的帖子和Spring整合Mybaits的帖子。
个人推荐
暂时没有非常好的课程推荐,但是如果时间比较多,可以去听这位老师的课,是真的学思想,但是得花时间因为课比较长(要有一定的底子才听得懂,要不然听天书)
非常仔细的Mybatis源码解读https://www.bilibili.com/video/BV1kT4y137hk?spm_id_from=333.999.0.0