Mybatis 与Spring整合及原理

Mybatis 与Spring原理分析
http://www.mybatis.org/spring/zh/index.html
这里我们以传统的Spring 为例,因为配置更直观,在Spring 中使用配置类注解是一样的。
编程式的工程,也就是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.demo.crud.dao"/>
</bean>

第二种是配置一个<scan>标签:

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

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

@SpringBootApplication
@MapperScan("com.demo.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,我们来看一下这个类。
在这里插入图片描述
它实现了InitializingBean 接口,所以要实现afterPropertiesSet()方法,这个方法会在bean 的属性值设置完的时候被调用。
另外它实现了FactoryBean 接口,所以它初始化的时候,实际上是调用getObject()方法,它里面调用的也是afterPropertiesSet()方法。
在afterPropertiesSet()方法里面:
第一步是一些标签属性的检查,接下来调用了buildSqlSessionFactory()方法。然后定义了一个Configuration,叫做targetConfiguration。

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

if (this.configuration != null) {
  configuration = this.configuration;
  if (configuration.getVariables() == null) {
    configuration.setVariables(this.configurationProperties);
  } else if (this.configurationProperties != null) {
    configuration.getVariables().putAll(this.configurationProperties);
  }
}

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

else if (this.configLocation != null) {
  xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
  configuration = xmlConfigBuilder.getConfiguration();
}

否则,Configuration 对象不存在,configLocation 路径也没有,只能使用默认属性去构建去给configurationProperties 赋值。

else {
  if (LOGGER.isDebugEnabled()) {
    LOGGER.debug("Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
  }
  configuration = new Configuration();
  if (this.configurationProperties != null) {
    configuration.setVariables(this.configurationProperties);
  }
}

后面就是基于当前factory 对象里面已有的属性,对targetConfiguration 对象里面属性的赋值。

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

if (xmlConfigBuilder != null) {
  try {
    xmlConfigBuilder.parse();


    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Parsed configuration file: '" + this.configLocation + "'");
    }
  } catch (Exception ex) {
    throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
  } finally {
    ErrorContext.instance().reset();
  }
}

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

if (this.transactionFactory == null) {
  this.transactionFactory = new SpringManagedTransactionFactory();
}

可以配置:

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

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

try {
  XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
      configuration, mapperLocation.toString(), configuration.getSqlFragments());
  xmlMapperBuilder.parse();
} catch (Exception e) {
  throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
} finally {
  ErrorContext.instance().reset();
}

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

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

用到的Spring 扩展点总结:

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

2、创建SqlSession
Q1:可以直接使用DefaultSqlSession 吗?
我们现在已经有一个DefaultSqlSessionFactory,按照编程式的开发过程,我们接下来就会创建一个SqlSession 的实现类,但是在Spring 里面,我们不是直接使用DefaultSqlSession 的,而是对它进行了一个封装,这个SqlSession 的实现类就是SqlSessionTemplate。这个跟Spring 封装其他的组件是一样的,比如JdbcTemplate,RedisTemplate 等等,也是Spring 跟MyBatis 整合的最关键的一个类。为什么不用DefaultSqlSession?它是线程不安全的,注意看类上的注解:

Note that this class is not Thread-Safe.

而SqlSessionTemplate 是线程安全的。

* Thread safe, Spring managed, {@code SqlSession} that works with  Spring

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;
    public SqlSession getSqlSession() {
    return this.sqlSessionTemplate;
}
前面和后面省略…………

也就是说我们让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.demo.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 的定义。
在这里插入图片描述
我们只需要重写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 的getConfiguration()方法:

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

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

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

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

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

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()方法,后面的流程就跟编程式的工程里面一模一样了。
总结:

对象生命周期
SqlSessionTemplateSpring 中SqlSession 的替代品,是线程安全的,通过代理的方式调用DefaultSqlSession 的方法
SqlSessionInterceptor(内部类)代理对象,用来代理DefaultSqlSession,在SqlSessionTemplate 中使用
SqlSessionDaoSupport用于获取SqlSessionTemplate,只要继承它即可
MapperFactoryBean注册到IOC 容器中替换接口类,继承了SqlSessionDaoSupport 用来获取SqlSessionTemplate,因为注入接口的时候,就会调用它的getObject()方法
SqlSessionHolder控制SqlSession 和事务
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值