查看原文
其他

先从简单的源码入手:MyBatis 工作原理分析

风筝 古时的风筝 2023-08-25

做 Java 开发的同学都清楚,目前最流行的两种 ORM 框架就是 JPA 和 MyBatis。有说法是国内用 mybatis 的多,国外用 JPA 的多,JPA 的底层就是 Hibernate。

大家在开始接触 Java web 开发的时候都会碰到 SSH 和 SSM。前些年比较流行的框架组合是SSH,也就是Struts2、Spring 和Hibernate 的整合。SSM 框架是 Spring 、Spring MVC 和 mybatis 的整合,是最近几年比较流行的。

就我的经验来看,如果项目的数据比较单纯、结构比较简单,用 JPA 可以大大的提高开发效率。相反,如果项目的数据结构比较复杂,反应到 SQL 层面就是需要编写比较复杂灵活的 SQL 语句,比如两三张表联查等,那最好还是选择 mybatis ,更加灵活自由一点。

今天试着来说一下 mybatis 的工作原理。

mybaits 与 SQL 执行过程

第一步:创建数据库连接,有连接池的情况下,就从连接池获取。

想当年在学校使用 vb 的日子,哪里用什么数据库连接池,都是一个连接一个连接的创建和关闭。

第二步:打开连接;

第三步:数据库层进行缓存查询、语法解析、加锁等处理;

第四步:返回数据,mybatis 拿到返回的数据并抽象到 Java 实体;

第五步:关闭连接,或者将连接返还给线程池;

mybatis 参与其中一、二、四、五这几个步骤,来简化我们的数据库操作流程。

mybatis 负责向数据库或向连接池申请数据库连接,然后打开连接,调用数据库 API 执行 SQL,然后拿到返回的数据。

mybaits 在这一过程中要对 SQL 语句进行加工,并对返回的数据进行抽象,抽象到对应的 Java 实体。

纯粹的使用 mybatis

我们在平时的开发中,一般都是用 Spring MCV 或者 Spring Boot,而在这两个框架之下使用的其实并不是单纯的 mybaits,而是 mybatis-spring,它将允许 MyBatis 参与到 Spring 的事务管理之中,创建映射器 mapper 和 SqlSession 并注入到 bean 中,以及将 Mybatis 的异常转换为 Spring 的 DataAccessException。最终,可以做到应用代码不依赖于 MyBatis,Spring 或 MyBatis-Spring。这个我们以后再说,先说如何纯粹的使用 mybatis。

我创建了一个非 Java web 项目,用来演示一下 mybaits 简单的使用方法。数据库是 mysql,仅创建了一个简单的 user 表做测试,数据库命名为 study,sql 如下:

CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(30) DEFAULT NULL, `age` tinyint(4) DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;INSERT INTO `user` VALUES ('1', '古时的风筝', '1');

1、引入 mybatis 包,目前最新版本是 3.5.4。同时,还要引入 mysql 包。

<dependency>   <groupId>org.mybatis</groupId>   <artifactId>mybatis</artifactId>   <version>3.5.4</version></dependency><dependency>   <groupId>mysql</groupId>   <artifactId>mysql-connector-java</artifactId>   <version>5.1.37</version>   <scope>runtime</scope></dependency>

2、声明 user 表对应的 Java 实体类

public class User implements Serializable {   private static final long serialVersionUID = 1L;   private String name;   private Integer age;   public String getName() {       return name;  }   public void setName(String name) {       this.name = name;  }   public Integer getAge() {       return age;  }   public void setAge(Integer age) {       this.age = age;  }   @Override   public String toString() {       return "User{" +               "name='" + name + '\'' +               ", age=" + age +               '}';  }}

3、创建对应的 mapper 接口类,并定义一个 selectOneUser 方法,根据 id 查询用户,返回用户实体对象

public interface UserMapper {   User selectOneUser(int id);}

4、编写对应的 UserMapper.xml 文件,其中有上面接口定义的 selectOneUser 方法的具体 sql 语句。

<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper       PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"       "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="org.kite.purely.mybatis.mapper.UserMapper">   <select id="selectOneUser" resultType="org.kite.purely.mybatis.entity.User">    select * from user where id = #{id}   </select></mapper>

5、数据库相关参数的配置文件 config.properties

jdbcUrl=jdbc:mysql://localhost:3306/studydriverClass=com.mysql.jdbc.Driverjdbc.username=rootjdbc.password=密码

6、定义 mybatis 核心配置文件 mybaits-config.xml,具体配置含义请看文件内注释。

<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE configuration       PUBLIC "-//mybatis.org//DTD Config 3.0//EN"       "http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration>   <!--引入 config.properties 配置文件-->   <properties resource="config.properties">   </properties>      <!--定义 development 作为默认配置环境-->   <environments default="development">       <environment id="development">           <!--事务管理类型为 JDBC-->           <transactionManager type="JDBC"/>           <!--数据源参数设置-->           <dataSource type="POOLED">               <property name="driver" value="${driverClass}"/>               <property name="url" value="${jdbcUrl}"/>               <property name="username" value="${jdbc.username}"/>               <property name="password" value="${jdbc.password}"/>           </dataSource>       </environment>   </environments>      <!--引入 mappers -->   <mappers>       <mapper resource="mappers/UserMapper.xml"/>   </mappers></configuration>

7、在代码中执行,查询并打印返回值

public class App {   public static void main( String[] args ) {       FileReader fileReader = null;       try {           URL url = App.class.getClassLoader().getResource("");           fileReader = new FileReader(url.getPath()+"mybatis-config.xml");      }catch (IOException e){           e.printStackTrace();      }       SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(fileReader,"development");       try (SqlSession sqlSession = factory.openSession()){           UserMapper userMapper = sqlSession.getMapper(UserMapper.class);           User user = userMapper.selectOneUser(1);           System.out.println(user.toString());      }  }}

执行之后,会正常的打印出 id 为 1 的记录。

整个项目结构如下图。

mybaits 两类 xml 文件

mybatis 主要由配置信息和具体的 XML 映射文件组成,也就是上面提到的 mybatis-config.xml 和 UserMapper.xml(以及同类型的映射文件),使用 mybatis 的过程大部分就是完善和添加这两类 xml 文件的过程。

mybatis 全局配置

下面列出了 mybatis-config.xml 配置节点层次结构。

对应到 xml 文件中就是这个样子,以 configuration 为根节点,

<configuration> <!--引入 config.properties 配置文件--> <properties resource="config.properties"> </properties>
<settings> <setting name="cacheEnabled" value="true"/> <setting name="lazyLoadingEnabled" value="true"/> <setting name="multipleResultSetsEnabled" value="true"/> <setting name="useColumnLabel" value="true"/> <setting name="useGeneratedKeys" value="false"/> <setting name="autoMappingBehavior" value="PARTIAL"/> <setting name="autoMappingUnknownColumnBehavior" value="WARNING"/> <setting name="defaultExecutorType" value="SIMPLE"/> <setting name="defaultStatementTimeout" value="25"/> <setting name="defaultFetchSize" value="100"/> <setting name="safeRowBoundsEnabled" value="false"/> <setting name="mapUnderscoreToCamelCase" value="false"/> <setting name="localCacheScope" value="SESSION"/> <setting name="jdbcTypeForNull" value="OTHER"/> <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/> </settings>
<typeAliases> <typeAlias alias="User" type="org.kite.purely.mybatis.entity.User"/> </typeAliases>
<typeHandlers> <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/> </typeHandlers>
<objectFactory type="org.mybatis.example.ExampleObjectFactory"> <property name="someProperty" value="100"/> </objectFactory>
<plugins> <plugin interceptor="org.mybatis.example.ExamplePlugin"> <property name="someProperty" value="100"/> </plugin> </plugins>
<!--定义 development 作为默认配置环境--> <environments default="development"> <environment id="development"> <!--事务管理类型为 JDBC--> <transactionManager type="JDBC"/> <!--数据源参数设置--> <dataSource type="POOLED"> <property name="driver" value="${driverClass}"/> <property name="url" value="${jdbcUrl}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource> </environment> </environments>
<databaseIdProvider type="DB_VENDOR"> <property name="SQL Server" value="sqlserver"/> <property name="DB2" value="db2"/> <property name="Oracle" value="oracle" /> </databaseIdProvider>
<!--引入 mappers --> <mappers> <package name="org.kite.purely.mybatis.mapper"/> </mappers></configuration>

在官网上有对这些配置项的详细解释:https://mybatis.org/mybatis-3/zh/configuration.html,建议使用 mybatis 的同学必须读几遍。

其中有些是必要的,有些是可以选择配置的,比如 dataSource、mappers 是必须的。

settings 都有默认值,typeHandlers 一般也不需要自定义,plugins 可以用作监控,比如记录多少次更新操作、多少次查询操作等。

SQL 映射文件配置

映射文件指的是那些 xxxMapper.xml, 是具体的 Java 实体类和数据库映射配置,比如上面提到的 UserMapper.xml,里面可以定义数据库的增删改查行为,以及查询参数、返回结果与 Java 类型的映射关系。我们系统中具体的数据库查询逻辑都在这里定义。

配置里的顶级元素有如下几个,使用过的同学再熟悉不过了,具体的使用方式在官网有详细说明和例子:https://mybatis.org/mybatis-3/zh/sqlmap-xml.html

  • cache – 对给定命名空间的缓存配置。

  • cache-ref – 对其他命名空间缓存配置的引用。

  • resultMap – 是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象。

  • sql – 可被其他语句引用的可重用语句块。

  • insert – 映射插入语句

  • update – 映射更新语句

  • delete – 映射删除语句

  • select – 映射查询语句

mybatis 执行流程和原理

从这段代码说起

public class App { public static void main( String[] args ) { FileReader fileReader = null; try { URL url = App.class.getClassLoader().getResource(""); fileReader = new FileReader(url.getPath()+"mybatis-config.xml"); }catch (IOException e){ e.printStackTrace(); } SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(fileReader,"development"); try (SqlSession sqlSession = factory.openSession()){ UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user = userMapper.selectOneUser(1); System.out.println(user.toString()); } }}

这段代码是一个简单的使用方法,分为如下几步:

1、 获取 mybatis-config.xml ,并将其填充到一个 FileReader 中;

2、 使用 SqlSessionFactoryBuilder.build(Reader reader, String environment) 这个重载方法生成 SqlSessionFactory 工厂实例;

3、 SqlSessionFactory 实例产生一个 SqlSession,SqlSessionFactory 最好设置为全局的单例模式;

4、 SqlSession 实例调用 getMapper() 方法,获取加工后的 UserMapper 实例,在一次事务结束后要关闭 SqlSession 实例;

5、调用 UserMappper 定义的方法;

SqlSessionFactory 工厂类创建

SqlSessionFactory 是一个工厂类接口,它的作用是产生 SqlSession,SqlSessionFactory 应该在项目中已单例形式存在,不要每次需要 SqlSession 的时候都重新 new SqlSessionFactory,SqlSessionFactory 创建的过程复杂,开销比较大。

SqlSessionFactoryBuilder 类提供了 9 个创建 SqlSessionFactory 的重载方法。

第一部分红色框和第二部分蓝色框都是从 mybatis-config.xml 读取配置,支持 Reader 和 InputStream 两种方式读取文件。

可以选择 environment ,比如我们的项目有开发环境、测试环境、生产环境,每个环境的数据库配置都有所不同,可以提前在 mybatis-config.xml 中配置多个 environment,然后再创建 SqlSessionFactory 的时候指定采用哪个。

还可以单独指定 properties,指定 properties 后会覆盖在 xml 文件中配置的属性。

mybatis 除了支持 xml 方式之外,也可以用纯 Java 代码的方式构造配置项,就是利用最后一个方法,其参数是 Configuration,比如下面这段代码。

DataSource dataSource = BlogDataSourceFactory.getBlogDataSource();TransactionFactory transactionFactory = new JdbcTransactionFactory();Environment environment = new Environment("development", transactionFactory, dataSource);Configuration configuration = new Configuration(environment);configuration.addMapper(BlogMapper.class);SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);


当然了,上面的 8 个方法在获取配置文件中的配置后最终也是要调用这个方法的,最后返回的 SqlSessionFactory 实际是 DefaultSqlSessionFactory 实例,它实现了 SqlSessionFactory 接口。

大致的过程如下:

其中 parseConfiguration() 方法会读取 mybatis-config.xml 中的每一个顶层元素,对其进行相应的解析,下面代码每个调用方法的参数就是要解析的配置文件的对应元素。

private void parseConfiguration(XNode root) { try { 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); // 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); }}

解析完成后将对应的结果赋值给 org.apache.ibatis.session.Configuration 类中对应的属性,比如 protected Environment environment; 就是用来存放配置文件中的 environment 节点内容的。所有的配置文件中可配置的内容都在这个类中有对应的属性,具体代码你可以进去看一下。

构造 Configuration 对象是至关重要的一步,它相当于把你的配置文件抽象到当前上下文中,方便之后随时拿来使用。

SqlSession 创建

SqlSession 完全包含了面向数据库执行 SQL 命令所需的所有方法。你可以通过 SqlSession 实例来直接执行已映射的 SQL 语句。SqlSession 通过 SqlSessionFactory 的 openSession() 方法获取。

@Overridepublic 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); 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(); }}

实际最后返回的对象是 DefaultSqlSession 实例,下图是 DefaultSqlSession 的继承关系。

new DefaultSqlSession(configuration, executor, autoCommit)

从代码可以看出在构造 SqlSession 的过程中除了 configuration 这个参数,还有 executor 和 autoCommit。

executor 是 sql 语句的执行器,包含了执行 sql 语句的方法。共有四类 Executor,分别是 SimpleExecutor、ReuseExecutor、ReuseExecutor 和 ReuseExecutor,默认情况下都是选择 SimpleExecutor 执行器。以下是他们的继承关系图:

autoCommit 参数表示是否自动提交事务。当事务提交之后,才会释放对应的锁。

如何通过 mapper 接口执行具体的 sql

先思考一下,我们定义的 mapper 都是接口类,只有方法,没有实现。方法名在 mapper.xml 中有对应的关系。那么,我们调用 mapper 接口的方法的时候,是怎么对应到 mapper.xml 中的具体 sql 语句的呢。

答案是肯定有 mybatis 帮我们做了什么。一说到这种场景,自然就想到了一种设计模式-「动态代理模式」。

Spring AOP 的原理就是用了动态代理模式。不理解这种模式的同学可以看一下我的旧文 Spring AOP 和动态代理技术

下面我们来理一下这个过程。

在创建 SqlSessionFactory 的过程中会构造 Configuration 对象,其中就包括解析 节点,然后把解析的结果存到 Configuration 对象的属性中。Configuration 对象中有存放 mapper 接口的属性集合mapperRegistry,准确的说不是原始的 mapper 接口类。

//Configuration 类中存放 mapper 接口类集合的属性protected final MapperRegistry mapperRegistry = new MapperRegistry(this);

下面看一下解析 节点的具体的过程,代码如下,已经在代码上添加了注释,mappers 节点下支持 package 和 mapper 子节点,如果你要使用 xml 方式,那么就不能用 package 。

private void mapperElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { //如果使用的是 package 元素,获取 name 属性, //然后查找包下的所有 mapper 接口类,并存入 mapperRegistry 中 //若使用 package 则只能使用注解的方式实现 sql 映射,不能使用 mapper.xml 文件 if ("package".equals(child.getName())) { String mapperPackage = child.getStringAttribute("name"); configuration.addMappers(mapperPackage); } else { String resource = child.getStringAttribute("resource");          String url = child.getStringAttribute("url");          String mapperClass = child.getStringAttribute("class"); // 使用 resource 指定 mapper.xml 所在位置的方式 if (resource != null && url == null && mapperClass == null) { ErrorContext.instance().resource(resource); InputStream inputStream = Resources.getResourceAsStream(resource); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); //解析 resouce 文件,根据文件中的 namespace 反射构造接口 Class // 并添加到 mapperRegistry mapperParser.parse(); } else if (resource == null && url != null && mapperClass == null) { //使用 url 方式,指定的 mapper.xml 是一个项目外目录,比如在服务器的某个目录下 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) { //直接指定 mapperClass 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."); } } } }  }

因为我们是用的 resource 属性配置的,

<mapper resource="mappers/UserMapper.xml"/>

所以,执行的实际上主要是这两行代码:

XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());mapperParser.parse();

主要的行为在 mapperParser.parse() 方法内:

public void parse() { // 判断 mapper.xml 文件是否被加载过,如果没有 if (!configuration.isResourceLoaded(resource)) { configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); bindMapperForNamespace(); }
parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements();}
/*** 解析 mapper.xml 中的各种元素,* 比如 namespace、resultMap、parameterMap、sql 等**/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); }}
/*** 绑定 namespace 对应的 mapper 接口类**/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); } } }}

其中 bindMapperForNamespace() 方法便是根据 mapper.xml 中指定的命名空间找到对应的 mapper 接口类,然后把 mapper 加入到 Configuration 中的结合中,看上面中的 configuration.addMapper(boundType) 就是关键,我们看看这个方法干了什么,就能大概清楚开头提到的那个问题 - mybatis 如何实现的调用接口方法就执行对应的 sql 语句。

Configuration 类中用了以下对象来存储 mapper 接口。

protected final MapperRegistry mapperRegistry = new MapperRegistry(this);


然而添加操作另有玄机

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<>(type)); // 如何是注解方式才会 MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } }}

实际上最终保存 mapper 接口的属性是 knownMappers,它是个什么样子的呢

private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

看到了吗,一个 map,key 是 Class 泛型,也就是对应具体的接口类,value 是一个 MapperProxyFactory 泛型,看名字也能看出个大概,这是一个 mapper 接口代理工厂类。

好,看完生成 mapper 接口类-准确的说是 mapper 接口类的代理工厂类集合后,我们来看一下取的时候,也就是 SqlSession.getMapper 方法,获取对应的 mapper 的过程。

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); }}

上面是 getMapper 接口的最终实现,在 knownMappers 中根据 mapper 接口类 type 获取 MapperProxyFactory 泛型,然后通过 newInstance 方法,返回一个实例,这也是工厂模式的标准写法。

protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);}
public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy);}

最终构造除了一个代理类,所以我们调用 SqlSession.getMapper 方法后,返回的是 mapper 接口的代理 MapperProxy 泛型类。

而最终调用 mapper 接口类的方法,实际上是代理调用了 invoke 方法,并对原始方法进行了一些列加工,mybaits 这里的加工就是找到 mapper 接口方法对应的 mapper.xml 对应的 id 和方法名相同的元素块儿,并对 sql 代码块进行解析。

比如我这个例子中,我执行 UserMapper 接口的 selectOneUser 方法,执行的就是 UserMapper.xml 中对应的这部分:

<select id="selectOneUser" resultType="org.kite.purely.mybatis.entity.User"> select * from user where id = #{id} or id=#{id2}</select>

以下代码是代理类的 invoke 方法。

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else { return cachedInvoker(method).invoke(proxy, method, args, sqlSession); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); }}

判断不是 Object 类,执行以下代码。

return cachedInvoker(method).invoke(proxy, method, args, sqlSession);

这段代码分两部分,首先调用 cachedInvoker(method) 方法,返回的是 PlainMethodInvoker,然后调用它的 invoke 方法

public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable { return mapperMethod.execute(sqlSession, args);}

之后,execute 方法会根据 sql 语句的类型是 INSERT、UPDATE、DELETE、SELECT 做区分,然后执行对应的逻辑。

因为我这个方法是一个 select 方法,所以最后实际执行的方法是 DefaultSqlSession 的 selectOne 方法,command.getName() 就是方法全名,param 是方法参数。

result = sqlSession.selectOne(command.getName(), param);

而 selectOne 方法会调用 selectList 方法,然后根据返回记录数来决定是返回一个列表还是一个单独实体。

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 包含 sql 查询所需的所有属性,比如 resultMap 集合、SqlSource 等。

executor 是 sql 执行器,上面已经介绍了有四种执行器,在默认情况下 mybatis 是开启自身缓存的,所以用到的执行器是 CachingExecutor,这里执行的 query 方法就是 CachingExecutor 的 query 方法。

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.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);}

首先判断是否存在缓存,如果存在,就从缓存直接获取,如果不存在,就执行 delegate 的 query 方法,delegate 是一个执行器,当缓存不存在时,delegate 就是 SimpleExecutor 执行器。

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()); return handler.query(stmt, resultHandler); } finally { closeStatement(stmt); }}

然后 prepareStatement() 是返回数据库连接并把参数设置到 sql 语句的占位符。

最后一步,就是将返回的数据集转换为对应的 Java 实体对象或集合。

public List<Object> handleResultSets(Statement stmt) throws SQLException { ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
final List<Object> multipleResults = new ArrayList<>();
int resultSetCount = 0; ResultSetWrapper rsw = getFirstResultSet(stmt);
List<ResultMap> resultMaps = mappedStatement.getResultMaps(); int resultMapCount = resultMaps.size(); validateResultMapsCount(rsw, resultMapCount); while (rsw != null && resultMapCount > resultSetCount) { ResultMap resultMap = resultMaps.get(resultSetCount); handleResultSet(rsw, resultMap, multipleResults, null); rsw = getNextResultSet(stmt); cleanUpAfterHandlingResultSet(); resultSetCount++; }
String[] resultSets = mappedStatement.getResultSets(); if (resultSets != null) { while (rsw != null && resultSetCount < resultSets.length) { ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]); if (parentMapping != null) { String nestedResultMapId = parentMapping.getNestedResultMapId(); ResultMap resultMap = configuration.getResultMap(nestedResultMapId); handleResultSet(rsw, resultMap, null, parentMapping); } rsw = getNextResultSet(stmt); cleanUpAfterHandlingResultSet(); resultSetCount++; } }
return collapseSingleResultList(multipleResults);  }

这个过程还包括每个数据库字段类型映射到 Java 类型,也就是各种 typeHander。

总结

本篇只分析了 mybatis 工作的主要框架,另外,还有缓存实现、插件实现等诸多细节没有展开。源码这东西,看别人讲不如自己看,不仅能加深对开源框架的理解。而且,我们从优秀的开源代码中可以学到很多东西,比如说设计模式的运用、继承实现关系的运用等等。

跟诸多其他框架比起来,mybatis 算是比较简单的,有时间大家都可以读一读。

参考文章:

https://mybatis.org/mybatis-3/zh/configuration.html

https://mybatis.org/spring/zh/index.html

 


还可以读:

系统内存爆满,原来是线程搞的鬼

线上问题排查神器 Arthas

你了解 Spring Boot Starter 吗


-----------------------

公众号:古时的风筝

一个斜杠程序员,一个纯粹的技术公众号,多写 Java 相关技术文章,不排除会写其他内容。





【今天小雨】


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存