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 扩展点总结:
接口 | 方法 | 作用 |
---|---|---|
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?它是线程不安全的,注意看类上的注解:
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()方法,后面的流程就跟编程式的工程里面一模一样了。
总结:
对象 | 生命周期 |
---|---|
SqlSessionTemplate | Spring 中SqlSession 的替代品,是线程安全的,通过代理的方式调用DefaultSqlSession 的方法 |
SqlSessionInterceptor(内部类) | 代理对象,用来代理DefaultSqlSession,在SqlSessionTemplate 中使用 |
SqlSessionDaoSupport | 用于获取SqlSessionTemplate,只要继承它即可 |
MapperFactoryBean | 注册到IOC 容器中替换接口类,继承了SqlSessionDaoSupport 用来获取SqlSessionTemplate,因为注入接口的时候,就会调用它的getObject()方法 |
SqlSessionHolder | 控制SqlSession 和事务 |