三,MyBatis插件原理及Spring集成

1.MyBatis应用分析与实践
2.MyBatis体系结构与工作原理
3.MyBatis插件原理及Spring集成
4.手写自己的MyBatis框架
本节目标:

1、 掌握插件的使用方法和工作原理

2、 掌握自定义插件的编写方法

3、 掌握 Spring 集成 MyBatis 的原理

一,MyBatis 插件原理与自定义插件

MyBatis 通过提供插件机制,让我们可以根据自己的需要去增强 MyBatis 的功能。 需要注意的是,如果没有完全理解 MyBatis 的运行原理和插件的工作方式,最好不要使用插件,因为它会改变系底层的工作逻辑,给系统带来很大的影响。

1.猜想

MyBatis 的插件可以在不修改原来的代码的情况下,通过拦截的方式,改变四大核心对象的行为,比如处理参数,处理 SQL,处理结果。

  • 第一个问题:不修改对象的代码,怎么对对象的行为进行修改,比如说在原来的方法前面做一点事情,在原来的方法后面做一点事情?

    答案:大家很容易能想到用代理模式,这个也确实是 MyBatis 插件的原理。

  • 第二个问题:

    我们可以定义很多的插件,那么这种所有的插件会形成一个链路,比如我们提交一个休假申请,先是项目经理审批,然后是部门经理审批,再是 HR 审批,再到总经理审批,怎么实现层层的拦截?

    答案:插件是层层拦截的,我们又需要用到另一种设计模式——责任链模式。

如果是用代理模式,我们就要解决几个问题:

  1. 有哪些对象允许被代理?有哪些方法可以被拦截?

    我们应该了解 MyBatis 允许哪些对象的哪些方法允许被拦截,并不是每一个运行的节点都是可以被修改的。只有清楚了这些对象的方法的作用,当我们自己编写插件的时候才知道从哪里去拦截。

    在 MyBatis 官网有答案,我们来看一下:http://www.mybatis.org/mybatis-3/zh/configuration.html#plugins

    对象 描述 可拦截的方法 方法作用
    Executor 上层的对象,SQL执行全过程,包括组装参数,组装结果集返回和执行SQL过程 update 执行 update、insert、delete 操作
    query 执行 query 操作
    flushStatements 在 commit 的时候自动调用,SimpleExecutor、 ReuseExecutor、BatchExecutor 处理不同
    commit 提交事务
    rollback 事务回滚
    getTransaction 获取事务
    close 结束(关闭)事务
    isClosed 判断事务是否关闭
    StatementHandler 执行 SQL 的过程,最常用的拦截对象 prepare (BaseSatementHandler)SQL 预编
    parameterize 设置参数
    batch 批处理
    update 增删改操作
    query 查询操作
    ParameterHandler SQL 参数组装的过程 getParameterObject 获取参数
    setParameters 设置参数
    ResultSetHandler 结果的组装 handleResultSets 处理结果集
    handleOutputParameters 处理存储过程出参

    Executor 会拦截到 CachingExcecutor 或者 BaseExecutor。因为创建 Executor 时是先创建 Executor,再拦截。

  2. 怎么样创建代理?

    如果我们用 JDK 的动态代理,要有一个实现了 InvocationHandler 的代理类,用来包装被代理对象,这个类是我们自己创建还是谁来创建?

  3. 什么时候创建代理对象?是在 MyBatis 启动的时候创建,还是调用的时候创建?

  4. 被代理后,调用的是什么方法?怎么调用到原被代理对象的方法(比如 Executor 的 query()方法)?

要解决后面三个问题,我们先看一下别人的插件是怎么工作的。

2.插件编写与注册

运行自定义的插件,需要 3 步,我们以 PageHelper 为例:

  1. 编写自己的插件类

    • 实现 Interceptor 接

      这个是所有的插件必须实现的接口。

    • 添加@Intercepts({@Signature()}),指定拦截的对象和方法、方法参数 方法名称+参数类型,构成了方法的签名,决定了能够拦截到哪个方法。

      问题:拦截签名跟参数的顺序有关系吗?

    • 实现接口的 3 个方法

      // 用于覆盖被拦截对象的原有方法(在调用代理对象 Plugin 的 invoke()方法时被调用)
      Object intercept(Invocation invocation) throws Throwable;
      // target 是被拦截对象,这个方法的作用是给被拦截对象生成一个代理对象,并返回它
      Object plugin(Object target);
      // 设置参数
      void setProperties(Properties properties);
      
  2. 插件注册,在 mybatis-config.xml 中注册插件

    <plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
         <property name="offsetAsPageNum" value="true"/> ……后面全部省略……
        </plugin>
    </plugins>
    
  3. 插件登记

    MyBatis 启动时扫描 标签 , 注册到 Configuration 对象的 InterceptorChain中。property里面的参数,会调用 setProperties()方法处理。

以上就是编写和使用自定义插件的全部步骤。

3.代理和拦截是怎么实现的?

  • 问题 1:四大对象什么时候被代理,也就是:代理对象是什么时候创建的?

    Executor 是 openSession() 的时候创建的 ;

    StatementHandler 是 SimpleExecutor.doQuery()创建的;

    里面包含了处理参数的 ParameterHandler 和处理结果集的 ResultSetHandler 的创建,创建之后即调用 InterceptorChain.pluginAll(), 返回层层代理后的对象。

  • 问题 2:多个插件的情况下,代理能不能被代理?代理顺序和调用顺序的关系?

    可以被代理,debug 看一下。

  • 问题 3:谁来创建代理对象?

    Plugin类 。 在我们重写的 plugin() 方法里面可以直接调用 return Plugin.wrap(target, this);返回代理对象。

  • 问题 4:被代理后,调用的是什么方法?怎么调用到原被代理对象的方法?

    因为代理类是 Plugin,所以最后调用的是 Plugin 的 invoke()方法。它先调用了定义的拦截器的 intercept()方法。可以通过 invocation.proceed()调用到被代理对象被拦截的方法。

    总结流程如下:

插件调用流程.jpg
对象 作用
Interceptor 自定义插件需要实现接口,实现 3 个方法
InterceptChain 配置的插件解析后会保存在 Configuration
Plugin 用来创建代理对象,包装四大对象
Invocation 对被代理类进行包装,可以调用 proceed()调用到被拦截的方法

4.PageHelper 原理

PageInterceptor 类,用法(EmployeeController.getEmpsWithJson())

PageHelper.startPage(pn, 10);
List<Employee> emps = employeeService.getAll();
PageInfo page = new PageInfo(emps, 10);

先看 PageHelper jar 包中 PageInterceptor 的源码。拦截的是 Executor 的两个 query()方法。在这里对 SQL 进行了改写:

//调用方言获取分页 sql
String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);

跟踪到最后,是在 MySqlDialect. getPageSql()对 SQL 进行了改写,翻页参数是从 一个 Page 对象中拿到的,那么 Page 对象是怎么传到这里的呢?

上一步,AbstractHelperDialect. getPageSql()中:

Page page = getLocalPage();
return getPageSql(sql, page, pageKey);

Page 对象是从一个 ThreadLocal<>变量中拿到的,那它是什么时候赋值的?

回到 EmployeeController. getEmpsWithJson()中,PageHelper.startPage()方法, 把分页参数放到了 ThreadLocal<>变量中。

protected static void setLocalPage(Page page) {
    LOCAL_PAGE.set(page);
}

关键类总结:

对象 作用
PageInterceptor 自定义拦截器
Page 包装分页参数
PageInfo 包装结果
PageHelper 工具类

5.应用场景分析

作用 实现方式
水平分表 对 query update 方法进行拦截 在接口上添加注解,通过反射获取接口注解,根据注解上配置的参数进行分表,修改原 SQL,例 如 id 取模,按月分表
数据加解密 update——加密;query——解密 获得入参和返回值
菜单权限控制 对 query 方法进行拦截在方法上添加注解,根据权限配置,以及用户登录信息,在 SQL 上加上权限过滤条件

二,与 Spring 整合分析

http://www.mybatis.org/spring/zh/index.html

这里我们以传统的 Spring 为例,因为配置更直观,在 Spring 中使用配置类注解是 一样的。

在前面章节,我们基于编程式的工程已经弄清楚了 MyBatis 的工作流程、核心模块和底层原理。编程式的工程,也就是 MyBatis 的原生 API 里面有三个核心对象: SqlSessionFactory、SqlSession、MapperProxy。

大部分时候我们不会在项目中单独使用 MyBatis 的工程,而是集成到 Spring 里面使用,但是却没有看到这三个对象在代码里面的出现。我们直接注入了一个 Mapper接口, 调用它的方法。

所以有几个关键的问题,我们要弄清楚:

1、 SqlSessionFactory 是什么时候创建的?

2、 SqlSession 去哪里了?为什么不用它来 getMapper?

3、 为什么@Autowired 注入一个接口,在使用的时候却变成了代理对象?在IOC 的容器里面我们注入的是什么? 注入的时候发生了什么事情?

关键配置

我们先看一下把 MyBatis 集成到 Spring 中要做的几件事情。

为了让大家看起来更直观,这里我们依旧用传统的 xml 配置给大家来做讲解,当然使用配置类@Configuration 效果也是一样的,对于 Spring 来说只是解析方式的差异。

除了 MyBatis 的依赖之外,我们还需要在 pom 文件中引入 MyBatis 和 Spring 整合 的 jar 包(注意版本!mybatis 的版本和 mybatis-spring 的版本有兼容关系)。

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>2.0.0</version>
</dependency>

然后在 Spring 的 applicationContext.xml 里面配置 SqlSessionFactoryBean,它 是用来帮助我们创建会话的,其中还要指定全局配置文件和 mapper 映射器文件的路径。

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="configLocation" value="classpath:mybatis-config.xml"></property>
    <property name="mapperLocations" value="classpath:mapper/*.xml"></property>
    <property name="dataSource" ref="dataSource"/>
</bean>

然后在 applicationContext.xml 配置需要扫描 Mapper 接口的路径。 在 Mybatis 里面有几种方式,第一种是配置一个 MapperScannerConfigurer。

<bean id="mapperScanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.gupaoedu.crud.dao"/>
</bean>

第二种是配置一个标签:

<mybatis-spring:scan base-package="com.javacoo.crud.dao"/>

还有一种就是直接用@MapperScan 注解,比如我们在 Spring Boot 的启动类上加 上一个注解:

@SpringBootApplication
@MapperScan("com.javacoo.crud.dao")
public class MybaitsApp {
    public static void main(String[] args) {
    SpringApplication.run(MybaitsApp.class, args);
    }
}

这三种方式实现的效果是一样的。

1.创建会话工厂

Spring 对 MyBatis 的对象进行了管理,但是并不会替换 MyBatis 的核心对象。也就意味着:MyBatis jar 包中的 SqlSessionFactory、SqlSession、MapperProxy 这些都会用到。而 mybatis-spring.jar 里面的类只是做了一些包装或者桥梁的工作。 所以第一步,我们看一下在 Spring 里面,工厂类是怎么创建的。 我们在 Spring 的配置文件中配置了一个 SqlSessionFactoryBean,我们来看一下这 个类。

SqlSessionFactoryBean.png

它实现了 InitializingBean 接口,所以要实现 afterPropertiesSet()方法,这个方法会在 bean 的属性值设置完的时候被调用。 另外它实现了 FactoryBean 接口,所以它初始化的时候,实际上是调用 getObject() 方法,它里面调用的也是 afterPropertiesSet()方法。 在 afterPropertiesSet()方法里面:

第一步是一些标签属性的检查,接下来调用了 buildSqlSessionFactory()方法。 然后定义了一个 Configuration,叫做 targetConfiguration。

426 行,判断 Configuration 对象是否已经存在,也就是是否已经解析过。如果已经有对象,就覆盖一下属性。

433 行,如果 Configuration 不存在,但是配置了 configLocation 属性,就根据 mybatis-config.xml 的文件路径,构建一个 xmlConfigBuilder 对象。

436 行,否则,Configuration 对象不存在,configLocation 路径也没有,只能使用默认属性去构建去给 configurationProperties 赋值。 后面就是基于当前 factory 对象里面已有的属性,对 targetConfiguration 对象里面 属性的赋值。

Optional.ofNullable(this.objectFactory).ifPresent(targetConfiguration::setObjectFactory);

这个方法是 Java8 里面的一个判空的方法。如果不为空的话,就会调用括号里面的对象的方法,做赋值的处理。

在第 498 行,如果 xmlConfigBuilder 不为空,也就是上面的第二种情况,调用了xmlConfigBuilder.parse()去解析配置文件,最终会返回解析好的 Configuration 对象。

在 第 507 行 , 如 果 没 有 明 确 指 定 事 务 工 厂 , 默 认 使 用 SpringManagedTransactionFactory 。 它创建的 SpringManagedTransaction 也有 getConnection()和 close()方法

<property name="transactionFactory" value="" />

在 520 行,调用 xmlMapperBuilder.parse(),这个步骤我们之前了解过了,它的作 用是把接口和对应的 MapperProxyFactory 注册到 MapperRegistry 中。

最后调用 sqlSessionFactoryBuilder.build() 返 回 了 一 个 DefaultSqlSessionFactory。

OK,在这里我们完成了编程式的案例里面的第一步,根据配置文件获得一个工厂类, 它是单例的,会在后面用来创建 SqlSession。

用到的 Spring 扩展点总结:

接口 方法 作用
FactoryBean getObject() 返回由 FactoryBean 创建的 Bean 实例
InitializingBean afterPropertiesSet() bean 属性初始化完成后添加操作
BeanDefinitionRegistryPostProcessor postProcessBeanDefinitionRegistry() 注入 BeanDefination 时添加操作

2.创建 SqlSession

Q1:可以直接使用 DefaultSqlSession 吗?

我们现在已经有一个 DefaultSqlSessionFactory,按照编程式的开发过程,我们接 下来就会创建一个 SqlSession 的实现类,但是在 Spring 里面,我们不是直接使用 DefaultSqlSession 的,而是对它进行了一个封装,这个 SqlSession 的实现类就是 SqlSessionTemplate。这个跟 Spring 封装其他的组件是一样的,比如 JdbcTemplate, RedisTemplate 等等,也是 Spring 跟 MyBatis 整合的最关键的一个类。

为什么不用 DefaultSqlSession?它是线程不安全的,注意看类上的注解:

/**
 * The default implementation for {@link SqlSession}.
 * Note that this class is not Thread-Safe.
 *
 * @author Clinton Begin
 */
public class DefaultSqlSession implements SqlSession {

而 SqlSessionTemplate 是线程安全的.

/**
 * Thread safe, Spring managed, {@code SqlSession} that works with Spring transaction management to ensure that that the
 * actual SqlSession used is the one associated with the current Spring transaction. In addition, it manages the session
 * life-cycle, including closing, committing or rolling back the session as necessary based on the Spring transaction
 * configuration.
 * <p>
 ...
 */
public class SqlSessionTemplate implements SqlSession, DisposableBean {

回顾一下 SqlSession 的生命周期:

对象 生命周期
SqlSessionFactoryBuiler 方法局部(method)
SqlSessionFactory (单例) 应用级别(application)
SqlSession 请求和操作(request/method)
Mapper 方法(method)

在编程式的开发中,SqlSession 我们会在每次请求的时候创建一个,但是 Spring 里面只有一个 SqlSessionTemplate(默认是单例的),多个线程同时调用的时候怎么保 证线程安全?

思考:为什么 SqlSessionTemplate 是线程安全的?

思考:在编程式的开发中,有什么方法保证 SqlSession 的线程安全?

SqlSessionTemplate 里面有 DefaultSqlSession 的所有的方法:selectOne()、 selectList()、insert()、update()、delete(),不过它都是通过一个代理对象实现的。这个代理对象在构造方法里面通过一个代理类创建:

this.sqlSessionProxy = (SqlSession) newProxyInstance(
SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class },
new SqlSessionInterceptor());

所有的方法都会先走到内部代理类 SqlSessionInterceptor 的 invoke()方法:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    SqlSession sqlSession = getSqlSession(
    SqlSessionTemplate.this.sqlSessionFactory,
    SqlSessionTemplate.this.executorType,
    SqlSessionTemplate.this.exceptionTranslator);
    try {
    Object result = method.invoke(sqlSession, args)

首先会使用工厂类、执行器类型、异常解析器创建一个 sqlSession,然后再调用 sqlSession 的实现类,实际上就是在这里调用了 DefaultSqlSession 的方法.

Q2:怎么拿到一个 SqlSessionTemplate?

我们知道在 Spring 里面会用 SqlSessionTemplate 替换 DefaultSqlSession,那么 接下来看一下怎么在 DAO 层拿到一个 SqlSessionTemplate。

不知道用过 Hibernate 的同学还记不记得,如果不用注入的方式,我们在 DAO 层 注入一个 HibernateTemplate 的一种方法是什么? ——让我们 DAO 层的实现类去继承 HibernateDaoSupport。

MyBatis 里面也是一样的,它提供了一个 SqlSessionDaoSupport,里面持有一个 SqlSessionTemplate 对象,并且提供了一个 getSqlSession()方法,让我们获得一个 SqlSessionTemplate。

public abstract class SqlSessionDaoSupport extends DaoSupport {

  private SqlSessionTemplate sqlSessionTemplate;

  /**
   * Set MyBatis SqlSessionFactory to be used by this DAO. Will automatically create SqlSessionTemplate for the given
   * SqlSessionFactory.
   *
   * @param sqlSessionFactory
   *          a factory of SqlSession
   */
  public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
    if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {
      this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory);
    }
  }
  ...

也就是说我们让 DAO 层的实现类继承 SqlSessionDaoSupport,就可以获得 SqlSessionTemplate,然后在里面封装 SqlSessionTemplate 的方法。

当然 , 为 了 减 少 重 复 的 代 码 , 我 们 通 常 不 会 让 我 们 的 实 现 类 直 接 去 继 承 SqlSessionDaoSupport,而是先创建一个 BaseDao 继承 SqlSessionDaoSupport。在 BaseDao 里面封装对数据库的操作,包括 selectOne()、selectList()、insert()、delete() 这些方法,子类就可以直接调用。

public class BaseDao extends SqlSessionDaoSupport {
//使用 sqlSessionFactory
@Autowired
private SqlSessionFactory sqlSessionFactory;
@Autowired
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
    super.setSqlSessionFactory(sqlSessionFactory);
}
public Object selectOne(String statement, Object parameter) {
    return getSqlSession().selectOne(statement, parameter);
}
....

然后让我们的实现类继承 BaseDao 并且实现我们的 DAO 层接口,这里就是我们的 Mapper 接口。实现类需要加上@Repository 的注解。

在实现类的方法里面,我们可以直接调用父类(BaseDao)封装的 selectOne()方法, 那么它最终会调用 sqlSessionTemplate 的 selectOne()方法。

@Repository
public class EmployeeDaoImpl extends BaseDao implements EmployeeMapper {
@Override
public Employee selectByPrimaryKey(Integer empId) {
Employee emp = (Employee)
this.selectOne("com.javacoo.crud.dao.EmployeeMapper.selectByPrimaryKey",empId);
return emp;
}

然后在需要使用的地方,比如 Service 层,注入我们的实现类,调用实现类的方法就 行了。我们这里直接在单元测试类里面注入:

@Autowired
EmployeeDaoImpl employeeDao;
@Test
public void EmployeeDaoSupportTest() {
System.out.println(employeeDao.selectByPrimaryKey(1));
}

最终会调用到 DefaultSqlSession 的方法。

Q3:有没有更好的拿到 SqlSessionTemplate 的方法

这么做有一个问题:我们的每一个 DAO 层的接口(Mapper 接口也属于),如果要拿到一个 SqlSessionTemplate,去操作数据库,都要创建实现一个实现类,加上 @Repository 的注解,继承 BaseDao,这个工作量也不小。

另外一个,我们去直接调用 selectOne()方法,还是出现了 Statement ID 的硬编码, MapperProxy 在这里根本没用上。

我们可以通过什么方式,不创建任何的实现类,就可以把 Mapper 注入到别的地方 使用,并且可以拿到 SqlSessionTemplate 操作数据库呢?

这个也确实是我们在 Spring 中的用法。那我们就必要弄清楚,我们只是注入了一个 接口,在对象实例化的时候,是怎么拿到 SqlSessionTemplate 的?当我们调用方法的时候,还是不是用的 MapperProxy?

3.接口的扫描注册

在 Service 层可以使用@Autowired 自动注入的 Mapper 接口,需要保存在 BeanFactory(比如 XmlWebApplicationContext)中。也就是说接口肯定是在 Spring 启动的时候被扫描了,注册过的。

1、 什么时候扫描的?

2、 注册的时候,注册的是什么?这个决定了我们拿到的是什么实际对象。

回 顾 一 下 , 我 们 在 applicationContext.xml 里 面 配 置 了 一 个 MapperScannerConfigurer。

MapperScannerConfigurer 实现了 BeanDefinitionRegistryPostProcessor 接口, BeanDefinitionRegistryPostProcessor 是 BeanFactoryPostProcessor 的子类,可以通过编码的方式修改、新增或者删除某些 Bean 的定义

MapperScannerConfigurer.png

我们只需要重写 postProcessBeanDefinitionRegistry()方法,在这里面操作 Bean 就可以了。

在这个方法里面:

scanner.scan() 方 法 是 ClassPathBeanDefinitionScanner 中 的 , 而 它 的 子 类 ClassPathMapperScanner 覆 盖 了 doScan() 方 法 , 在 doScan() 中 调 用 了 processBeanDefinitions:

public Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
    if (beanDefinitions.isEmpty()) {
    LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages) +
    "' package. Please check your configuration.");
    } else {
    processBeanDefinitions(beanDefinitions);
    }
    return beanDefinitions;
}

它先调用父类的 doScan()扫描所有的接口。

processBeanDefinitions 方法里面,在注册 beanDefinitions 的时候,BeanClass 被改为 MapperFactoryBean(注意灰色的注释)。

private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
GenericBeanDefinition definition;
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (GenericBeanDefinition) holder.getBeanDefinition();
String beanClassName = definition.getBeanClassName();
LOGGER.debug(() -> "Creating MapperFactoryBean with name '" + holder.getBeanName()
+ "' and '" + beanClassName + "' mapperInterface");
// the mapper interface is the original class of the bean
// but, the actual class of the bean is MapperFactoryBean
definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); //
issue #59
definition.setBeanClass(this.mapperFactoryBean.getClass());

问题:

为什么要把 BeanClass 修改成 MapperFactoryBean,这个类有什么作用?

MapperFactoryBean 继 承 了 SqlSessionDaoSupport , 可 以 拿 到 SqlSessionTemplate。

4.接口注入使用

我们使用 Mapper 的时候,只需要在加了 Service 注解的类里面使用@Autowired 注入 Mapper 接口就好了。

@Service
public class EmployeeService {
    @Autowired
    EmployeeMapper employeeMapper;
    public List<Employee> getAll() {
        return employeeMapper.selectByMap(null);
    }
}

Spring 在启动的时候需要去实例化 EmployeeService。

EmployeeService 依赖了 EmployeeMapper 接口(是 EmployeeService 的一个属 性)。

Spring 会根据 Mapper 的名字从 BeanFactory 中获取它的 BeanDefination,再从 BeanDefination 中 获 取 BeanClass , EmployeeMapper 对 应 的 BeanClass 是 MapperFactoryBean(上一步已经分析过)。

接下来就是创建 MapperFactoryBean,因为实现了 FactoryBean 接口,同样是调 用 getObject()方法。

// MapperFactoryBean.java
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}

因 为 MapperFactoryBean 继 承 了 SqlSessionDaoSupport , 所 以 这 个 getSqlSession()就是调用父类的方法,返回 SqlSessionTemplate。

// SqlSessionDaoSupport.java
public SqlSession getSqlSession() {
    return this.sqlSessionTemplate;
}

第二步,SqlSessionTemplate 的 getMapper()方法,里面又有两个方法:

// SqlSessionTemplate.java
public <T> T getMapper(Class<T> type) {
    return getConfiguration().getMapper(type, this);
}

第一步:SqlSessionTemplate 的 getConfiguration()方法:

// SqlSessionTemplate.java
public Configuration getConfiguration() {
    return this.sqlSessionFactory.getConfiguration();
}

进入方法,通过 DefaultSqlSessionFactory,返回全部配置 Configuration:

// DefaultSqlSessionFactory.java
public Configuration getConfiguration() {
    return configuration;
}

第二步:Configuration 的 getMapper()方法:

// Configuration.java
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
}

这 一 步 我 们 很 熟 悉 , 跟 编 程 式 使 用 里 面 的 getMapper 一 样 , 通 过 工 厂 类 MapperProxyFactory 获得一个 MapperProxy 代理对象。

也就是说,我们注入到 Service 层的接口,实际上还是一个 MapperProxy 代理对象。 所以最后调用 Mapper 接口的方法,也是执行 MapperProxy 的 invoke()方法,后面的 流程就跟编程式的工程里面一模一样了。

总结:

对象 生命周期
SqlSessionTemplate Spring 中 SqlSession 的替代品,是线程安全的,通过代理的方式调用 DefaultSqlSession 的方法
SqlSessionInterceptor(内部类) 代理对象,用来代理 DefaultSqlSession,在 SqlSessionTemplate 中使用
SqlSessionDaoSupport 用于获取 SqlSessionTemplate,只要继承它即可
MapperFactoryBean 注册到 IOC 容器中替换接口类,继承了 SqlSessionDaoSupport 用来获取 SqlSessionTemplate,因为注入接口的时候,就会调用它的 getObject()方法
SqlSessionHolder 控制 SqlSession

思考:@MapperScan 注解是怎么解析的?

设计模式总结:

设计模式
工厂 SqlSessionFactory、ObjectFactory、MapperProxyFactory
建造者 XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuidler
单例模式 SqlSessionFactory、Configuration、ErrorContext
代理模式 绑定:MapperProxy <br />延迟加载:ProxyFactory(CGLIB、JAVASSIT) <br />插件:Plugin <br />Spring 集成 MyBaits:SqlSessionTemplate 的内部类 SqlSessionInterceptor <br />MyBatis 自带连接池:PooledDataSource 管理的 PooledConnection <br />日志打印:ConnectionLogger、StatementLogger
适配器模式 logging 模块,对于 Log4j、JDK logging 这些没有直接实现 slf4j 接口的日志组件,需要适配器
模板方法 BaseExecutor 与子类 SimpleExecutor、BatchExecutor、ReuseExecutor
装饰器模式 LoggingCache、LruCache 等对 PerpectualCache 的装饰 CachingExecutor 对其他 Executor 的装饰
责任链模式 InterceptorChain
一些信息
路漫漫其修远兮,吾将上下而求索
码云:https://gitee.com/javacoo
QQ:164863067
作者/微信:javacoo
邮箱:xihuady@126.com
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容