对于SqlSessionTemplate的理解

写在开始

最近利用闲暇时间猫了一下mybatis和mybatis-spring的源码,看后发现SqlSessionTemplate和MapperFactoryBean这两个类对于mybatis的事务操作起到了关键的作用,因此写个随笔记录一下。本篇主要讲述下我个人对于SqlSessionTemplate的理解,关于MapperFactoryBean后续有时间会再写一篇文章记录一下。

SqlSessionTemplate

SqlSessionTemplate对于Mybatis事务提交起到了一个关键作用。先通过一个示例看一下不使用SqlSessionTemplate情况下通过Mybatis是如何来进行一次update操作。

1、使用DefaultSqlSession完成update操作

我的mapper配置文件如下所示(其他配置文件省略)

<?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="com.jorg.mybatis.mappers.StudentMapper">
    <update id="update" parameterType="java.util.Map">
        UPDATE
        student
        SET
        `name` = #{name}
        WHERE
        id = #{id}
    </update>
</mapper>

进行一次update操作

public class DefaultSqlSessionTest {
    
    private static SqlSessionFactory sqlSessionFactory;

    static {
        initSqlSessionFactoryBySqlSessionFactoryBuilder();
    }

    /**
     * 通过 SqlSessionFactoryBuilder 创建 SqlSessionFactory
     */
    public static void initSqlSessionFactoryBySqlSessionFactoryBuilder(){
        try {
            String resource = "mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            inputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {
        //返回的是DefaultSqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();
        HashMap<String, Object> paramMaps = Maps.newHashMap();
        paramMaps.put("name","hello");
        paramMaps.put("id","100");
        sqlSession.update("update",paramMaps);
        sqlSession.commit();
    }

由于默认情况下autoCommit是关闭的,所以此时若要通过显示的调用DefaultSqlSession的commit方法才能完成真正的更新

2、使用SqlSessionTemplate完成update操作

我的mapper配置文件同上,进行一次update操作。

public class SqlSessionTemplateTest {

    private static SqlSessionFactory sqlSessionFactory;

    static {
        try {
            String resource = "mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            inputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory);
        HashMap<String, Object> paramMaps = Maps.newHashMap();
        paramMaps.put("name","hello");
        paramMaps.put("id","100");
        sqlSessionTemplate.update("update",paramMaps);
    }
}

通过上述代码可以发现,使用SqlSessionTemplate无需通过调用commit方法就可以完成真正的更新。那么它背后具体是怎么实现的呢?来,跟进源码看一下。

public class SqlSessionTemplate implements SqlSession, DisposableBean {

  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) {

    notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
    notNull(executorType, "Property 'executorType' is required");

    this.sqlSessionFactory = sqlSessionFactory;
    this.executorType = executorType;
    this.exceptionTranslator = exceptionTranslator;
    //创建sqlSession代理对象
    this.sqlSessionProxy = (SqlSession) newProxyInstance(
        SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class },
        new SqlSessionInterceptor());
  }
}

上述是SqlSessionTemplate的构造方法(此处只截取了关键代码片段)。当通过调用SqlSessionTemplate(SqlSessionFactory sqlSessionFactory)构造器new对象的时候,其最终内部通过SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,PersistenceExceptionTranslator exceptionTranslator)来完成初始化,这个方法的关键是内部通过调用JDK Proxy.newProxyInstance方法创建了一个SqlSession代理对象并赋值给了sqlSessionProxy。此时,我们需要把目光聚焦到SqlSessionInterceptor的实现逻辑,因为其中包含该代理对象的代理逻辑,其代码逻辑如下:

private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    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);
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          //执行commit
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {
        Throwable unwrapped = unwrapThrowable(t);
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
          sqlSession = null;
          Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
          if (translated != null) {
            unwrapped = translated;
          }
        }
        throw unwrapped;
      } finally {
        if (sqlSession != null) {
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }

发现了没?在代理逻辑中先通过getSqlSession方法获取sqlSession(该方法逻辑后续再展开讲),而后执行method.invoke方法,之后通过isSqlSessionTransactional方法判断当前操作是否是事务操作,如果属于事务操作则执行sqlSession的commit方法,以此完成了事务的提交。

我们再回到之前通过SqlSessionTemplate完成update操作的例子上,先看下sqlSessionTemplate.update("update",paramMaps)源码。

  public int update(String statement, Object parameter) {
    return this.sqlSessionProxy.update(statement, parameter);
  }

看到了没,本质上调用的是sqlSessionProxy这个代理对象的update操作,结合刚才描述的代理逻辑,现在清楚了SqlSessionTemplate无需通过调用commit方法就可以完成真正的更新的背后原理了吧。

3、衍生一下

我们在日常开发中,应该很少以上述的方式来使用mybatis,更多的则是将对应Mapper配置关联到相应的interface,并直接调用interface中定义的方法来操作数据库。如下所示:

我的mapper文件

<mapper namespace="com.jorg.mybatis.transactional.StudentMapper">
    <update id="update">
        UPDATE
        student
        SET
        `name` = #{name}
        WHERE
        id = #{id}
    </update>
</mapper>

StudentMapper.java

public interface StudentMapper {
    int update(@Param("id") Integer id, @Param("name") String name);
}

执行代码

public class SqlSessionTemplateTest {

    private static SqlSessionFactory sqlSessionFactory;

    static {
        try {
            String resource = "mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            inputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
     public static void main(String[] args) {
        SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory);
        StudentMapper studentMapper = sqlSessionTemplate.getMapper(StudentMapper.class);
        studentMapper.update(100,"hello");
    }
}

上述通过interface来执行数据库操作的使用方式应该是最常用的,那么该方式背后又是如何实现的呢?来跟进sqlSessionTemplate.getMapper代码看一下:

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

  @Override
  public Configuration getConfiguration() {
    return this.sqlSessionFactory.getConfiguration();
  }

我们可以看到,这个getMapper方法内部的执行逻辑是:

  1. 先通过sqlSessionFactory.getConfiguration()返回Configuration
  2. 调用Configuration对象的getMapper方法来返回对应的Mapper

这个sqlSessionFactory是哪里来的?返回去看下我们的测试方法(如下所示)。发现了没,是我们自己创建的,并在执行new SqlSessionTemplate(sqlSessionFactory)创建SqlSessionTemplate的时候传进去的。

    private static SqlSessionFactory sqlSessionFactory;

    static {
        try {
            String resource = "mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            inputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

那么sqlSessionFactory中的Configuration是怎么来的呢?这个相对就比较复杂了,主要涉及到Mybatis对我们自定义配置文件的解析逻辑(感兴趣的话可以进入上面代码的SqlSessionFactoryBuilder类的build方法内部去看一下,或者看下这个博客MyBatis 源码分析 - 配置文件解析过程)。
在这里呢,我们可以把Configuration看作是我们自定义的xml配置文件所对应的java对象,即,在Configuration中包含了我们在xml配置文件中配置的所有信息。

那么,我们不妨跟近Configuration的getMapper方法看下里面做了什么。

Configuration.java

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

MapperRegistry.java

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

MapperProxyFactory<T>.java

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

进入源码以后可以发现,整个getMapper的执行链路是:Configuration.java getMapper-->MapperRegistry.java getMapper--> MapperProxyFactory<T>.java newInstance

整理一下整个链路,你发现了什么?没错,动态代理!这里最终返回的是一个interface对应Mapper的一个代理对象。

由于上述的代码只是我截取的部分源码片段,其中有几个Field可能大家感觉比较晕。这里简单描述一下:

  1. Configuration 中的 mapperRegistry :可以把它理解为Mybatis的一个全局Mapper注册中心,它是在Mybaits启动时候解析配置文件的时候生成的;Mybatis会对解析到的每个mapper xml文件中的信息在mapperRegistry中进行注册。
  2. MapperRegistry 中的 knownMappers:它在 MapperRegistry 中是一个HashMap类型,如下所示,它的key是每个mapper xml文件中namespace属性值对应的Interface Mapper(如上述示例中的com.jorg.mybatis.mappers.StudentMapper),它的value对应的就是一个MapperProxyFactory工厂,从名字上看就可以知道,该工厂是用来生产MapperProxy的,对应value的初始化也是在Mybatis解析配置文件阶段。
public class MapperRegistry {
    //....
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
    //.....
}

回到 MapperProxyFactory.java,我们继续看下它生成代理对象的过程,不难发现 MapperProxy 类封装了具体的代理逻辑,跟进其源码:

public class MapperProxy<T> implements InvocationHandler, Serializable {

 //...
  private final SqlSession sqlSession;
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache;

  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
    this.sqlSession = sqlSession;
    this.mapperInterface = mapperInterface;
    this.methodCache = methodCache;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        //如果方法是定义在 Object 类中的,则直接调用
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    //从缓存中获取 MapperMethod 对象,若缓存未命中,则创建 MapperMethod 对象
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }
  //....
}

其中invoke方法体是其代理逻辑,可以看到它最终的调用的是 mapperMethod.execute 方法。mapperMethod获取通过调用一个cachedMapperMethod方法,该方法执行逻辑涉及到二级缓存相关的内容,后面会有时间的情况下会单独写篇文章分析一下。在这里,可以把MapperMethod理解为 Mybatis对mapper文件中的每个执行逻辑的一个封装(如<select></select>,<update></update>等等),而每个MapperMethod的生成也是在Mybatis启动时解析文件时进行的。

下面跟进下 mapperMethod.execute 执行逻辑:

public class MapperMethod {

  private final SqlCommand command;
  private final MethodSignature method;

  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    this.command = new SqlCommand(config, mapperInterface, method);
    this.method = new MethodSignature(config, mapperInterface, method);
  }

  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
      Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName() 
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }
}

看着代码有点多,有点懵逼?莫慌兄弟!没那么复杂。方法体的执行逻辑有一个switch语句,而每个case是啥?INSERT、UPDATE、DELETE、SELECT,没错这对应的就是mapper里的标签啊。回想一下咱们一开始的测试代码的执行的是update操作对吧,那对应的case就是UPDATE了。So,看下UPDATE中的执行逻辑:sqlSession.update(command.getName(), param),像不像在“使用SqlSessionTemplate完成update操作”部分的测试代码里的sqlSessionTemplate.update("update",paramMaps)这个逻辑。有点像???又不太像。不像的地方是之前用的是sqlSessionTemplate,而这里是sqlSession;之前update传参是“update”,而这里是command.getName()。那到底是不是呢??

好了,也不卖关子了。其实这里完全就是一回事,这个sqlSession是怎么来的呢,回过去再顺着之前一路代码跟进的过程看(为了防止你失去信心,我又在下面梳理了一下代码片段):

SqlSessionTemplate.java

  @Override
  public <T> T getMapper(Class<T> type) {
      /*注意看,这里传入的是this*/
    return getConfiguration().getMapper(type, this);
  }
        |
        |
        V
Configuration.java

  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
  }
        |
        |
        V
MapperRegistry.java

  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);
    }
  }
        |
        |
        V
MapperProxyFactory<T>.java

    public T newInstance(SqlSession sqlSession) {
        final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
  }
        |
        |
        V
//此处省略MapperProxy.java

好了,看明白了没?MapperProxy里面的sqlSession是谁?对,就是SqlSessionTemplate!那command.getName()是呢?这里直接告诉你吧,它的值就是“update”,它是什么时候给command这个对象设进去的?也是在Mybatis启动时候解析配置文件的时候设置的。

So,sqlSession.update(command.getName(), param)是完全等于sqlSessionTemplate.update("update",paramMaps)。

结尾

至此,我对SqlSessionTemplate这个类理解也阐述完了。如果看后觉得有哪些不合理和或者疏漏的,希望大家能够帮助矫正。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容