从源码分析spring整合mybatis

Posted 编程之艺术

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从源码分析spring整合mybatis相关的知识,希望对你有一定的参考价值。


从源码分析spring整合mybatis


回顾上一篇文章中,spring整合mybatis的三种配置mapper接口bean的方式:

  • mapper接口有实现类

  • mapper接口无实现类

  • 配置mapper接口的扫描


首先来回顾一下spring整合mybatis的配置:

  1. 第一步:配置数据源dataSource,class为c3p0的ComboPooledDataSource类。

    <!-- 使用c3p0连接池配置数据源,需要导入c3p0的jar包 -->
        <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" >
            <!-- 是否开启自动提交,默认为false -->
            <property name="autoCommitOnClose" value="false"/>
            <property name="driverClass" value="${dataSource.driver}"/>
            <property name="jdbcUrl" value="${dataSource.url}"/>
            <property name="user" value="${dataSource.username}"/>
            <property name="password" value="${dataSource.password}"/>
            <!-- 池的配置 -->
            <!--初始化时获取多少个连接,取值应在minPoolSize与maxPoolSize之间。默认值: 3 -->
            <property name="initialPoolSize" value="${c3p0.initialPoolSize}"/>
            <property name="maxPoolSize" value="${c3p0.maxPoolSize}"/>
            <property name="minPoolSize" value="${c3p0.minPoolSize}"/>
            <property name="maxStatements" value="${c3p0.maxStatements}"/>
            <!--当连接池中的连接耗尽的时候c3p0一次同时获取的连接数。默认值: 3 -->
            <property name="acquireIncrement" value="${c3p0.acquireIncrement}"/>
            <!--最大空闲时间,指定多少秒内未使用则连接被丢弃。若为0则永不丢弃。默认值: 0 -->
            <property name="maxIdleTime" value="${c3p0.maxIdleTime}"/>
            <!--指定多少秒检查一次所有连接池中的空闲连接。默认值: 0 -->
            <property name="idleConnectionTestPeriod" value="${c3p0.idleConnectionTestPeriod}"/>
        </bean>


  2. 第二步:配置sqlSessionFactory,class是mybatis-spring提供的SqlSessionFactoryBean类,该类实现了FactoryBean接口。

    <!-- sqlsessionfactory,mybatis-spring提供的SqlSessionFactoryBean类,该类实现了FactoryBean接口 -->
        <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
            <property name="dataSource" ref="dataSource"/>
            <!-- mybatis的核心配置文件路径,该文件中只保留mybatis的一些设置 -->
            <property name="configLocation" value="classpath:mybatis-config.xml"/>
            <!-- 接口映射文件路径,*匹配任意字符 -->
            <property name="mapperLocations" value="classpath:mapper/*.xml"/>
        </bean>


  3. 第三步:1).mapper接口有实现类

    <bean id="userDao" class="wjy.sm.dao.impl.UseDaoImpl">
            <property name="sqlSessionFactory" ref="sqlSessionFactory"/>
            <!--<property name="sqlSessionTemplate" ref=""/>-->
        </bean>


    2).mapper接口无实现类
    <bean id="userDao" class="org.mybatis.spring.mapper.MapperFactoryBean">
            <!-- 注入Mapper接口,也可以构造方法注入 -->
            <!--<property name="mapperInterface" value="wjy.sm.dao.UserDao"/>-->
            <constructor-arg index="0" value="wjy.sm.dao.UserDao"/>
            <!-- 注入SqlSessionFactory -->
            <property name="sqlSessionFactory" ref="sqlSessionFactory"/>
        </bean>
    3).配置mapper接口的扫描
    <!-- 配置mapper接口的扫描 -->
        <!-- bean的名称为接口的类名,首字母小写 -->
        <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
            <property name="basePackage" value="wjy.sm.dao"/>
        </bean>

通过这三步配置我们就可以以spring的方法获取到mapper接口的实例对象来操作了。

从mapper接口有实现类入手


我们从第一种有接口实现类的方法进行深入源码。

就以例子中的userDao这个bean为例,看下UserDao接口的实现类UseDaoImpl。要使用mapper接口的实现类来配置bean,那么就需要这个实现类不仅需要实现该mapper接口,还需要继承自SqlSessionDaoSupport这个类,我们知道spring整合mybatis需要一个额外jar包,这个jar包是mybatis-spring.*.jar,而SqlSessionDaoSupport是mybatis-spring.*.jar包提供的。


[UseDaoImpl.java]

package wjy.sm.dao.impl;

import org.mybatis.spring.support.SqlSessionDaoSupport;
import wjy.sm.bean.User;
import wjy.sm.dao.UserDao;

public class UseDaoImpl extends SqlSessionDaoSupport implements UserDao {

    public User getUserByUsername(String username) {
        UserDao userDao =  this.getSqlSession().getMapper(UserDao.class);
        return userDao.getUserByUsername(username);
    }

    public User findUserById(int id) {
        UserDao userDao =  this.getSqlSession().getMapper(UserDao.class);
        return userDao.findUserById(id);
    }

    public int savaUserGetId(User user) {
        UserDao userDao =  this.getSqlSession().getMapper(UserDao.class);
        return userDao.savaUserGetId(user);
    }
}


在实现类中,我们可以使用this.getSqlSession()获取到一个SqlSession。还记得mybatis单独使用的过程吗?先来回顾一下吧,看下面代码,首先通过mybatis的核心配置文件获取数据库连接配置信息,将配置信息作为参数调用SqlSessionFactoryBuilder实例的build方法构建一个SqlSessionFactory实例,再通过SqlSessionFactory实例的openSession方法获取到SqlSession对象,拿到SqlSession实例就可以通过getMapper方法获取实现指定mapper接口的一个实例来操作数据了。


try {
    //通过配置文件获取数据库连接配置信息
    //通过配置信息构建一个SqlSessionFactory
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config.xml"));
    //通过SqlSessionFactory打开一个数据库会话
    SqlSession sqlSession = sqlSessionFactory.openSession();
    //通过sqlSession的getMapper方法获取实现了指定接口的一个实例对象,至于底层是怎么实现的就需要自己去研究mybatis的源码了,这也不是本篇文章讨论的话题
    UserDao userDao = sqlSession.getMapper(UserDao.class);
    userDao.savaUserGetId(new User());
}catch (IOException e){
    e.printStackTrace();
}


回顾mybatis的单独使用之后我们回到主题来,接着看下SqlSessionDaoSupport这个类的源码,看看this.getSqlSession()是如何获取到一个SqlSession实例的。


[SqlSessionDaoSupport源码]

public abstract class SqlSessionDaoSupport extends DaoSupport {
    private SqlSession sqlSession;
    private boolean externalSqlSession;

    public SqlSessionDaoSupport() {
    }

    public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
        if (!this.externalSqlSession) {
            this.sqlSession = new SqlSessionTemplate(sqlSessionFactory);
        }

    }

    public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
        this.sqlSession = sqlSessionTemplate;
        this.externalSqlSession = true;
    }

    public SqlSession getSqlSession() {
        return this.sqlSession;
    }

    protected void checkDaoConfig() {
        Assert.notNull(this.sqlSession, "Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required");
    }
}


SqlSessionDaoSupport为spring注入提供了两个setter方法,一个是注入SqlSessionFactory实例的,另一个是注入SqlSessionTemplate实例的。SqlSessionFactory我们都很熟悉了,因为只有通过SqlSessionFactory我们才能获取到SqlSession,相对来说SqlSessionTemplate就显得很陌生,先暂时忽略SqlSessionTemplate这个东东吧。


SqlSessionDaoSupport类定义了一个私有的类型为SqlSession的属性sqlSession,还实现了getter方法返回sqlSession属性的值。我们来看看sqlSession被实例化的过程,先看setSqlSessionFactory方法体内的这句代码


this.sqlSession = new SqlSessionTemplate(sqlSessionFactory);


哎,想避都避不开这个SqlSessionTemplate类,这么陌生,感觉很讨厌。既然避不开,那就客服它吧。


package org.mybatis.spring;

import ....

public class SqlSessionTemplate implements SqlSessionDisposableBean {


好,看到SqlSessionTemplate是实现SqlSession接口的就行了,其它的暂时就先不管了。现在回到前面提的问题SqlSessionDaoSupport实例的this.getSqlSession()是如何获取到一个SqlSession实例的?那么你明白了吗?就是new一个SqlSessionTemplate对象啊,SqlSessionTemplate是实现SqlSession接口的。


我们在配置userDao的时候注入了sqlSessionFactory,所以当执行到setSqlSessionFactory这个方法的时候就实例化了sqlSession属性,当我们在UseDaoImpl中调用this.getSession的时候就可以获取到Session的实例了。


<bean id="userDao" class="wjy.sm.dao.impl.UseDaoImpl">
        <property name="sqlSessionFactory" ref="sqlSessionFactory"/>
        <!--<property name="sqlSessionTemplate" ref=""/>-->
    </bean>


我们就拿UserDaoImpl的一个方法来说


public User getUserByUsername(String username) {
        UserDao userDao =  this.getSqlSession().getMapper(UserDao.class);
        return userDao.getUserByUsername(username);
    }


通过this.getSqlSession()获取到SqlSession实例,当然,这就是一个SqlSessionTemplate实例,所以当我们继续调用其getMapper方法的实现,走的就是SqlSessionTemplate的方法。


[SqlSessionTemplate类的getMapper方法源码]

public <T> T getMapper(Class<T> type) {
        return this.getConfiguration().getMapper(type, this);
    }
public Configuration getConfiguration() {
        return this.sqlSessionFactory.getConfiguration();
    }


到这里我们已经获取到UserDao的实例了。


现在把UserDaoImpl的getUserByUsername改为如下

public User getUserByUsername(String username) {
        return this.getSqlSession().selectOne("wjy.sm.dao.UserDao.getUserByUsername",username);
    }


直接调用的this.getSqlSession()方法获取到的SqlSession实例(即SqlSessionTemplate实例)的selectOne方法,这是实现SqlSession接口的方法。


先来看下SqlSession的源码吧:

package org.apache.ibatis.session;

import ......

public interface SqlSession extends Closeable {
    <T> T selectOne(String var1);
    <T> T selectOne(String var1, Object var2);
    <E> List<E> selectList(String var1);
    <E> List<E> selectList(String var1, Object var2);
    <E> List<E> selectList(String var1, Object var2, RowBounds var3);
    <K, V> Map<K, V> selectMap(String var1, String var2);
    <K, V> Map<K, V> selectMap(String var1, Object var2, String var3);
    <K, V> Map<K, V> selectMap(String var1, Object var2, String var3, RowBounds var4);
    void select(String var1, Object var2, ResultHandler var3);
    void select(String var1, ResultHandler var2);
    void select(String var1, Object var2, RowBounds var3, ResultHandler var4);
    int insert(String var1);
    int insert(String var1, Object var2);
    int update(String var1);
    int update(String var1, Object var2);
    int delete(String var1);
    int delete(String var1, Object var2);
    void commit();
    void commit(boolean var1);
    void rollback();
    void rollback(boolean var1);
    List<BatchResult> flushStatements();
    void close();
    void clearCache();
    Configuration getConfiguration();
    <T> T getMapper(Class<T> var1);
    Connection getConnection();
}



跟进selectOne方法,也就是SqlSessionTemplate的selectOne方法。

public <T> T selectOne(String statement, Object parameter) {
        return this.sqlSessionProxy.selectOne(statement, parameter);
    }


最终调用的是sqlSessionProxy的selectOne方法,这里出现了一个sqlSessionProxy属性,那么就有必要先找到这个属性是何时被实例化的,以及它的类型。


[SqlSessionTemplate源码]

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.mybatis.spring;

import ......

public class SqlSessionTemplate implements SqlSessionDisposableBean {
    private final SqlSessionFactory sqlSessionFactory;
    private final ExecutorType executorType;
    private final SqlSession sqlSessionProxy;
    private final PersistenceExceptionTranslator exceptionTranslator;

    public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        this(sqlSessionFactory, sqlSessionFactory.getConfiguration().getDefaultExecutorType());
    }

    public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType) {
        this(sqlSessionFactory, executorType, new MyBatisExceptionTranslator(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), true));
    }

    public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
        Assert.notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
        Assert.notNull(executorType, "Property 'executorType' is required");
        this.sqlSessionFactory = sqlSessionFactory;
        this.executorType = executorType;
        this.exceptionTranslator = exceptionTranslator;
        this.sqlSessionProxy = (SqlSession)Proxy.newProxyInstance(SqlSessionFactory.class.getClassLoader(), new Class[]{SqlSession.class}, new SqlSessionTemplate.SqlSessionInterceptor());
    }
    ...................................
}


跟着构造函数走,最终调用Proxy.newProxyInstance实例化sqlSessionProxy的。

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


这句代码应该很熟悉了吧?不就是jdk提供的动态代理吗,既然有Proxy.newProxyInstance的出现就必然会有InvocationHandler的实现。

private class SqlSessionInterceptor implements InvocationHandler {
        private SqlSessionInterceptor() {
        }

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

            Object unwrapped;
            try {
                Object result = method.invoke(sqlSession, args);
                if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
                    sqlSession.commit(true);
                }
                unwrapped = result;
            } catch (Throwable var11) {
                ......
                throw (Throwable)unwrapped;
            } finally {
                if (sqlSession != null) {
                    SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
                }

            }

            return unwrapped;
        }
    }


SqlSessionInterceptor为了容易看我去掉了一些源码,也就是说SqlSessionTemplate类中定义的类型为SqlSession的属性sqlSessionProxy是通过动态代理来创建的,而每次调用sqlSessionProxy这个实例实现SqlSession接口中的方法都会被SqlSessionInterceptor的invoke方法拦截下来。再通过SqlSessionUtils的getSqlSession获取一个SqlSession来执行目标方法,最终将执行结果返回。

SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);


当函数调用完成时,最终走SqlSessionUtils.closeSqlSession方法将sqlSession关闭掉。

finally {
     if (sqlSession != null) {
          SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
     }
}


再看mapper接口无实现类


这需要从无实现类的配置中入手。

<!-- 配置userDao,底层使用动态代理创建注入的接口类型的实现类 -->
    <bean id="userDao" class="org.mybatis.spring.mapper.MapperFactoryBean">
        <!-- 注入Mapper接口,也可以构造方法注入 -->
        <!--<property name="mapperInterface" value="wjy.sm.dao.UserDao"/>-->
        <constructor-arg index="0" value="wjy.sm.dao.UserDao"/>
        <!-- 注入SqlSessionFactory -->
        <property name="sqlSessionFactory" ref="sqlSessionFactory"/>
    </bean>


我们来回顾一下上一篇文章的知识点,因为可能很多人没看过。

这里有必要介绍一个知识点,即FactoryBean。

Spring中有两种类型的Bean,一种是普通Bean,其返回的对象是指定类的一个实例,另一种是实现FactoryBean接口的Bean,实现FactoryBean接口的Bean跟普通Bean不同,其返回的对象是该实现FactoryBean接口的实例的getObject方法所返回的对象。
FactoryBean通常是用来创建比较复杂的Bean,一般的Bean直接用xml配置即可,但如果一个Bean的创建过程中涉及到很多其他的bean和复杂的逻辑,这时需要用FactoryBean。

[FactoryBean源码]


package org.springframework.beans.factory;

public interface FactoryBean<T{
    getObject() throws Exception;

    Class<?> getObjectType();

    boolean isSingleton();
}


现在回到主题来,看看userDao这个bean的配置。这个配置可以简单理解为:配置了一个类型为MapperFactoryBean的bean,id为userDao,因为MapperFactoryBean是实现FactoryBean接口的,所以取id为userDao的bean的时候[比如getBean("userDao")],取得的不是这个MapperFactoryBean实例,而是其getObject()方法返回的实例。

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

    public Class<T> getObjectType() {
        return this.mapperInterface;
    }

    public boolean isSingleton() {
        return true;
    }

MapperFactoryBean实现FactoryBean接口的三个接口源码。


那么我们是不是只需要关注MapperFactoryBean的getObject这个方法就行了呢?肯定是的。

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


我们需要先从MapperFactoryBean源码入手,首先是这个类是否继承了什么类,实现了什么接口。

public class MapperFactoryBean<Textends SqlSessionDaoSupport implements FactoryBean<T{
}


从源码看出,MapperFactoryBean不仅是实现了FactoryBean接口,还是继承自SqlSessionDaoSupport的,这不就是跟前面的内容接轨了嘛,this.getSqlSession()就是SqlSessionDaoSupport的方法,获取到SqlSession之后,调用getMapper方法获取到注入的接口(mapperInterface)的实例并返回,这是mybatis完成的工作。


好了,spring整合mybatis就分析到这里了,如有不明白的地方欢迎留言探讨,如果有说错的地方还望指正。


从源码分析spring整合mybatis
《Spring与Mybatis整合事务管理》的补充

这是上一篇《Spring与Mybatis整合事务管理》就是事务管理配置的一点小补充。

<tx:annotation-driven/>如果不指定transaction-manager,默认会使用id设为transactionManager的事务管理器。如果不指定transaction-manager,且事务管理器的id也不为transactionManager就需要在@Transactional注解中配置transactionManager = "事务管理器的id",否则就会抛出异常。如

@Transactional(transactionManager="dataSourceTransactionManager");

<!-- 开启spring事务,依赖spring-jdbc的jar包 -->
    <!-- 事务管理器 -->
    <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- 注入数据源 -->
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!-- 开启事务注解功能 -->
    <!--
        如果不指定transaction-manager,默认会使用id设为transactionManager的事务管理器,
        如果不指定transaction-manager,且事务管理器的id也不为transactionManager就,
        就需要在@Transactional注解中配置transactionManager = "事务管理器的id",否则就会抛出异常。
        @Transactional(transactionManager="dataSourceTransactionManager");
     -->

    <tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>


从源码分析spring整合mybatis

上一篇《Spring与Mybatis整合事务管理》文中的例子源码已经上传到Github了,想参考的可以复制链接到浏览器下载源码:https://github.com/wujiuye/SpringMybatisDemo




从源码分析spring整合mybatis



以上是关于从源码分析spring整合mybatis的主要内容,如果未能解决你的问题,请参考以下文章

Mybatis整合Spring实现事务管理的源码分析

mybatis源码阅读mybatis与spring整合原理

MyBatis源码分析环境准备

MyBatis源码分析 : 环境准备

Spring整合MyBatisMyBatis独立使用

Spring+SpringMVC+MyBatis+Maven框架整合