美文网首页mybatis
Mybatis 源码分析(与Spring 结合)—— sql 执

Mybatis 源码分析(与Spring 结合)—— sql 执

作者: habit_learning | 来源:发表于2019-03-21 17:40 被阅读0次

    SqlSessionFactory 生成

    SqlSessionFactory 的生成是通过 SqlSessionFactoryBean 生成的。在讲源码之前,我们先看看 Mybatis 在 Springboot 中的配置:

        @Bean(name = "dataSource")
        @ConfigurationProperties(prefix = "spring.datasource.cashier")
        public DataSource dataSource() {
            return new DruidDataSource();
        }
    
        @Bean(name = "sqlSessionFactory")
        public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) throws Exception {
            SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
            sessionFactoryBean.setDataSource(dataSource);
            org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
            configuration.setMapUnderscoreToCamelCase(Boolean.TRUE);
            configuration.setUseGeneratedKeys(Boolean.TRUE);
            sessionFactoryBean.setConfiguration(configuration);
            sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
                    .getResources("classpath:yongda.mapper/*.xml"));
            return sessionFactoryBean.getObject();
        }
    

    上面配置中,将数据源DataSource,配置信息Configuration等属性赋值给SqlSessionFactoryBean,然后调用sessionFactoryBean.getObject()方法,接下来我们看看这个方法做了什么。

    public SqlSessionFactory getObject() throws Exception {
        if (this.sqlSessionFactory == null) {
          afterPropertiesSet();
        }
    
        return this.sqlSessionFactory;
      }
    

    直接返回SqlSessionFactory对象,其初始化操作是在afterPropertiesSet()方法中,里面会调用buildSqlSessionFactory()SqlSessionFactory赋值:

    protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
      ......
      return this.sqlSessionFactoryBuilder.build(configuration);
    }
    
     public SqlSessionFactory build(Configuration config) {
        return new DefaultSqlSessionFactory(config);
      }
    

    我们可以看到,生成了DefaultSqlSessionFactory作为SqlSessionFactory

    MapperScannerRegistrar

    MapperScannerRegistrar,Mappper 接口的扫描配置类。

    我们程序一般会使用@MapperScan(basePackages = "xxx")来指定需要扫描的 Mapper 接口路径。

    我们可以看其结构:

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Documented
    @Import(MapperScannerRegistrar.class)
    public @interface MapperScan {
    
      /**
       * Alias for the {@link #basePackages()} attribute. Allows for more concise
       * annotation declarations e.g.:
       * {@code @EnableMyBatisMapperScanner("org.my.pkg")} instead of {@code
       * @EnableMyBatisMapperScanner(basePackages= "org.my.pkg"})}.
       */
      String[] value() default {};
    
      /**
       * Base packages to scan for MyBatis interfaces. Note that only interfaces
       * with at least one method will be registered; concrete classes will be
       * ignored.
       */
      String[] basePackages() default {};
    
      /**
       * Type-safe alternative to {@link #basePackages()} for specifying the packages
       * to scan for annotated components. The package of each class specified will be scanned.
       * <p>Consider creating a special no-op marker class or interface in each package
       * that serves no purpose other than being referenced by this attribute.
       */
      Class<?>[] basePackageClasses() default {};
    
      /**
       * The {@link BeanNameGenerator} class to be used for naming detected components
       * within the Spring container.
       */
      Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
    
      /**
       * This property specifies the annotation that the scanner will search for.
       * <p>
       * The scanner will register all interfaces in the base package that also have
       * the specified annotation.
       * <p>
       * Note this can be combined with markerInterface.
       */
      Class<? extends Annotation> annotationClass() default Annotation.class;
    
      /**
       * This property specifies the parent that the scanner will search for.
       * <p>
       * The scanner will register all interfaces in the base package that also have
       * the specified interface class as a parent.
       * <p>
       * Note this can be combined with annotationClass.
       */
      Class<?> markerInterface() default Class.class;
    
      /**
       * Specifies which {@code SqlSessionTemplate} to use in the case that there is
       * more than one in the spring context. Usually this is only needed when you
       * have more than one datasource.
       */
      String sqlSessionTemplateRef() default "";
    
      /**
       * Specifies which {@code SqlSessionFactory} to use in the case that there is
       * more than one in the spring context. Usually this is only needed when you
       * have more than one datasource.
       */
      String sqlSessionFactoryRef() default "";
    
      /**
       * Specifies a custom MapperFactoryBean to return a mybatis proxy as spring bean.
       *
       */
      Class<? extends MapperFactoryBean> factoryBean() default MapperFactoryBean.class;
    
    }
    

    这里使用了@Import注解来引入配置类,相当于 Spring 中的 <import/>标签。@Import注解的参数可以是一个 @Configuration 配置类,也可以是一个ImportSelector接口,也可以是ImportBeanDefinitionRegistrar接口。如果是一个 @Configuration 配置类,会将类中 @Bean 修饰的 bean 注册到 IOC 容器;如果是ImportSelector接口的实现类,那就会根据实现的逻辑对 @Configuration 配置类进行筛选;如果是一个ImportBeanDefinitionRegistrar接口实现类,那么也会根据该实现类的逻辑来创建 Bean。

    我们看引入的MapperScannerRegistrar结构:

    public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
    
      private ResourceLoader resourceLoader;
    
      /**
       * {@inheritDoc}
       */
      @Override
      public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    
        AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
        ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
    
        // this check is needed in Spring 3.1
        if (resourceLoader != null) {
          scanner.setResourceLoader(resourceLoader);
        }
    
        Class<? extends Annotation> annotationClass = annoAttrs.getClass("annotationClass");
        if (!Annotation.class.equals(annotationClass)) {
          scanner.setAnnotationClass(annotationClass);
        }
    
        Class<?> markerInterface = annoAttrs.getClass("markerInterface");
        if (!Class.class.equals(markerInterface)) {
          scanner.setMarkerInterface(markerInterface);
        }
    
        Class<? extends BeanNameGenerator> generatorClass = annoAttrs.getClass("nameGenerator");
        if (!BeanNameGenerator.class.equals(generatorClass)) {
          scanner.setBeanNameGenerator(BeanUtils.instantiateClass(generatorClass));
        }
    
        Class<? extends MapperFactoryBean> mapperFactoryBeanClass = annoAttrs.getClass("factoryBean");
        if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) {
          scanner.setMapperFactoryBean(BeanUtils.instantiateClass(mapperFactoryBeanClass));
        }
    
        scanner.setSqlSessionTemplateBeanName(annoAttrs.getString("sqlSessionTemplateRef"));
        scanner.setSqlSessionFactoryBeanName(annoAttrs.getString("sqlSessionFactoryRef"));
    
        List<String> basePackages = new ArrayList<String>();
        for (String pkg : annoAttrs.getStringArray("value")) {
          if (StringUtils.hasText(pkg)) {
            basePackages.add(pkg);
          }
        }
        for (String pkg : annoAttrs.getStringArray("basePackages")) {
          if (StringUtils.hasText(pkg)) {
            basePackages.add(pkg);
          }
        }
        for (Class<?> clazz : annoAttrs.getClassArray("basePackageClasses")) {
          basePackages.add(ClassUtils.getPackageName(clazz));
        }
        scanner.registerFilters();
        scanner.doScan(StringUtils.toStringArray(basePackages));
      }
    
      /**
       * {@inheritDoc}
       */
      @Override
      public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
      }
    
    }
    

    我们发现,MapperScannerRegistrarImportBeanDefinitionRegistrar 的实现类,而ImportBeanDefinitionRegistrar接口的作用是,它允许我们直接通过BeanDefinitionRegistry 对象注册 bean。其提供的 API 为 ImportBeanDefinitionRegistrar#registerBeanDefinitions,而此 API 被调用的时机是在解析导入的@Configuration配置类的核心方法ConfigurationClassPostProcessor#processConfigBeanDefinitions中,具体源码就不深入分析,感兴趣的同学自行了解。

    接下来我们看MapperScannerRegistrar 注册 Mapper 接口的流程。首先根据标注的@MapperScan获取 basePackage,之后通过ClassPathMapperScanner去扫描包,获取所有 Mapper 接口类的BeanDefinition,之后具体配置,设置 beanClass 为MapperFactoryBean,设置 sqlSessionFactory 属性为上面生成的DefaultSqlSessionFactory,通过ClassPathBeanDefinitionScanner父类进行 bean 注册,自动注入的时候,就会调用MapperFactoryBeangetObject方法获取实际类型的实例。

    如果是与 Spring 集成的话,配置文件如下:

        <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
            <property name="basePackage" value="xxx" />
        </bean>
    

    这里的MapperScannerConfigurer实现的功能和MapperScannerRegistrar是一致的,我就不累述了。

    MapperFactoryBean 父类 SqlSessionDaoSupport-持久层创建

    private SqlSession sqlSession;
    
      private boolean externalSqlSession;
    
      public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
        if (!this.externalSqlSession) {
          //包装成 SqlSessionTemplate 对象
          this.sqlSession = new SqlSessionTemplate(sqlSessionFactory);
        }
      }
    

    上面我们已经介绍了,MapperScannerRegistrar会给扫描到的 Mapper 替换为 MapperFactoryBean,并且会给其设置 sqlSessionFactory 属性。mybatis 持久层的操作都会被包装成SqlSessionTemplate对象。

    SqlSessionTemplate 构造函数

    基本都是由SqlSessionFactory作为入参:

    public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        //默认的 ExecutorType 为 ExecutorType.SIMPLE
        this(sqlSessionFactory, sqlSessionFactory.getConfiguration().getDefaultExecutorType());
      }
    
    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;
        //对数据库的操作也会包装成代理的形式,所有的 CRUD 操作则都由 sqlSessionProxy 对象来完成
        this.sqlSessionProxy = (SqlSession) newProxyInstance(
            SqlSessionFactory.class.getClassLoader(),
            new Class[] { SqlSession.class },
            new SqlSessionInterceptor());
      }
    

    这里代理了SqlSession,后续所有的 CRUD 操作都是基于SqlSession的。

    MapperFactoryBean

    它实现了FactoryBean,故是一个工厂 Bean,在初始化阶段,返回的实例类型就是getObject()返回值类型:

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

    getSqlSession 得到的是之前创建的SqlSessionTemplate,于是我们直接看SqlSessionTemplte.getMapper(type)方法:

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

    SqlSessionTemplate什么都没做,把包袱甩给了Configuration, 接下来就看看Configuration。源码如下:

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

    Configuration不要这烫手的山芋,接着甩给了MapperRegistry, 那咱看看MapperRegistry。 源码如下:

      /**
       * 烂活净让我来做了,没法了,下面没人了,我不做谁来做
       * @param type
       * @param sqlSession
       * @return
       */
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        //能偷懒的就偷懒,俺把粗活交给 MapperProxyFactory 去做
        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是个苦B的人,粗活最终交给它去做了。咱们看看源码:

    /**
       * 别人虐我千百遍,我待别人如初恋
       * @param mapperProxy
       * @return
       */
      @SuppressWarnings("unchecked")
      protected T newInstance(MapperProxy<T> mapperProxy) {
        //动态代理我们写的 dao 接口
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
      }
      
      public T newInstance(SqlSession sqlSession) {
        final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
      }
    

    通过以上的动态代理,为每个 dao 接口,生成一个MapperProxy代理,咱们就可以方便地使用 dao 接口啦。至此,程序启动阶段关于 Mybatis 做的事情就完事了,接下来,我们看看执行一条 SQL 时的处理流程。

    MapperProxy

    当调用 dao 层的方法时,会走其代理类 MapperProxyinvoke方法:

      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (Object.class.equals(method.getDeclaringClass())) {
          try {
            return method.invoke(this, args);
          } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
          }
        }
        final MapperMethod mapperMethod = cachedMapperMethod(method);
        //二话不说,主要交给 MapperMethod 自己去管
        return mapperMethod.execute(sqlSession, args);
      }
    

    MapperMethod:

      public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;
        if (SqlCommandType.INSERT == command.getType()) {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = rowCountResult(sqlSession.insert(command.getName(), param));
        } else if (SqlCommandType.UPDATE == command.getType()) {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = rowCountResult(sqlSession.update(command.getName(), param));
        } else if (SqlCommandType.DELETE == command.getType()) {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = rowCountResult(sqlSession.delete(command.getName(), param));
        } else if (SqlCommandType.SELECT == command.getType()) {
          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 {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = sqlSession.selectOne(command.getName(), param);
          }
        } else {
          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;
      }
    

    看着代码不少,不过其实就是先判断 CRUD 类型,然后根据类型去选择到底执行sqlSession中的哪个方法,绕了一圈,又转回sqlSession了,这里的sqlSession就是程序在启动阶段创建的sqlSessionTemplate。当sqlSession调用具体的 CURD 方法时,会调用其代理类SqlSessionInterceptor.invoke()方法:

    private class SqlSessionInterceptor implements InvocationHandler {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
          //通过 SqlSessionUtils.getSqlSession() 获得真实处理 CRUD 的持久层,默认为 DefaultSqlSession
          SqlSession sqlSession = getSqlSession(
              SqlSessionTemplate.this.sqlSessionFactory,
              SqlSessionTemplate.this.executorType,
              SqlSessionTemplate.this.exceptionTranslator);
          try {
            //执行 CRUD 操作
            Object result = method.invoke(sqlSession, args);
            //非事务处理的数据操作需要强制 commit
            if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
              // force commit even on non-dirty sessions because some databases require
              // a commit/rollback before calling close()
              sqlSession.commit(true);
            }
            return result;
          } catch (Throwable t) {
            Throwable unwrapped = unwrapThrowable(t);
            if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
              // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
              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) {
              //关闭 sqlSession
              closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
            }
          }
        }
      }
    

    创建sqlSession的步骤是通过SqlSessionUtils.getSqlSession()来完成的,默认生成的是DefaultSqlSession,下面我们拿 SELECT 方式为例,直接进入DefaultSqlSession.selectList()

     @Override
      public <E> List<E> selectList(String statement) {
        return this.selectList(statement, null);
      }
    
      @Override
      public <E> List<E> selectList(String statement, Object parameter) {
        return this.selectList(statement, parameter, RowBounds.DEFAULT);
      }
    
      @Override
      public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
        try {
          // 将 sql 语句转换为 MappedStatement
          MappedStatement ms = configuration.getMappedStatement(statement);
          //CRUD 实际上是交给 Excetor 去处理
          return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
        } catch (Exception e) {
          throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
        } finally {
          ErrorContext.instance().reset();
        }
      }
    

    至此,sql 执行流程已经完毕,下面我们通过流程图来看看整个流程:


    彩蛋

    Mybatis 的二级缓存机制

    一级缓存:基于 HashMap 的本地缓存,它的生命周期是和 SqlSession一致的,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认一级缓存是开启的。

    二级缓存:也是基于 HashMap 的本地缓存,不同在于其存储作用域为Mapper级别的,如果多个SqlSession之间需要共享缓存,则需要使用到二级缓存,默认不打开二级缓存。

    开启二级缓存的方式:
    MyBatis 配置文件中通过:

    <settings>
        <setting name = "cacheEnabled" value = "true" />
    </settings>
    

    还需要在 Mapper 的 xml 配置文件中加入<cache>标签。

    开启二级缓存之后的数据查询流程:二级缓存 -> 一级缓存 -> 数据库。

    缓存更新机制:当某一个作用域(一级缓存SqlSession/二级缓存Mapper)进行了C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。

    由上面我们知道,SqlSession是由SqlSessionUtils.getSqlSession()来创建的:

    public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
          PersistenceExceptionTranslator exceptionTranslator) {
    
        notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
        notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
    
        SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
    
        SqlSession session = sessionHolder(executorType, holder);
        if (session != null) {
          return session;
        }
    
        LOGGER.debug(() -> "Creating a new SqlSession");
        session = sessionFactory.openSession(executorType);
    
        registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
    
        return session;
      }
    

    首先从SqlSessionHolder中获取SqlSession,如果SqlSessionHolder中没有,则新建一个。所以,我们看sessionHolder(executorType, holder)方法是如何获取SqlSession的:

    private static SqlSession sessionHolder(ExecutorType executorType, SqlSessionHolder holder) {
        SqlSession session = null;
        // 验证当前线程是否存在事务
        if (holder != null && holder.isSynchronizedWithTransaction()) {
          if (holder.getExecutorType() != executorType) {
            throw new TransientDataAccessResourceException(
                "Cannot change the ExecutorType when there is an existing transaction");
          }
    
          holder.requested();
    
          LOGGER.debug(() -> "Fetched SqlSession [" + holder.getSqlSession() + "] from current transaction");
          session = holder.getSqlSession();
        }
        return session;
      }
    

    由上面可知,只有当前线程存在事务时,SqlSession就会被复用,否则将新建一个。

    相关文章

      网友评论

        本文标题:Mybatis 源码分析(与Spring 结合)—— sql 执

        本文链接:https://www.haomeiwen.com/subject/tkqqvqtx.html