对于SqlSessionTemplate的理解

写在开始

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

SqlSessionTemplate

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

1、使用DefaultSqlSession完成update操作

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





    
        UPDATE
        student
        SET
        `name` = #{name}
        WHERE
        id = #{id}
    

进行一次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 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 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文件


    
        UPDATE
        student
        SET
        `name` = #{name}
        WHERE
        id = #{id}
    


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 getMapper(Class 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 getMapper(Class type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
  }

MapperRegistry.java

  public  T getMapper(Class type, SqlSession sqlSession) {
    final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) 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.java

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

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

进入源码以后可以发现,整个getMapper的执行链路是:Configuration.java getMapper-->MapperRegistry.java getMapper--> MapperProxyFactory.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, MapperProxyFactory> knownMappers = new HashMap, MapperProxyFactory>();
    //.....
}

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

public class MapperProxy implements InvocationHandler, Serializable {

 //...
  private final SqlSession sqlSession;
  private final Class mapperInterface;
  private final Map methodCache;

  public MapperProxy(SqlSession sqlSession, Class mapperInterface, Map 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文件中的每个执行逻辑的一个封装(如等等),而每个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 getMapper(Class type) {
      /*注意看,这里传入的是this*/
    return getConfiguration().getMapper(type, this);
  }
        |
        |
        V
Configuration.java

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

  public  T getMapper(Class type, SqlSession sqlSession) {
    final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) 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.java

    public T newInstance(SqlSession sqlSession) {
        final MapperProxy mapperProxy = new MapperProxy(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这个类理解也阐述完了。如果看后觉得有哪些不合理和或者疏漏的,希望大家能够帮助矫正。

你可能感兴趣的:(对于SqlSessionTemplate的理解)