很显然,实际生产中我们使用的最多的ORM框架是Mybatis,本文假设你已经是一个Mybatis老玩家。

ORM

ORM意为对象关系映射,使用面向对象的思想去描述各实体与各实体之间的关系,并且与数据库中的表建立映射关系。实体在数据库中描述为表结构(具体一条表记录对应的是具体实体),在编程中描述为类(具体对象对应具体实体),而描述关系一般使用字段(数据库)与属性(编程)。总的来说,ORM就是通过编程思想去描述数据库的一些概念,这样开发者就不用分心在这俩个概念上面。我在很多地方都有提到应该简化开发,让开发者专注在业务与合作而非一些细枝末节的东西上面。技术是为产品服务的,只有做出产品来了技术才有意义,毕竟我们不是搞科研的。

Mybatis如何实现

和绝大多数古老的框架一样,最开始使用Mybatis是需要一个xml配置文件的。但是,正如开头所述,你应该是一个Mybatis老玩家了,这些东西自然不需要更多赘述。本文的代码也全部在Springboot环境下的。Mybatis对配置有一个等价配置类(org.apache.ibatis.session.Configuration)去描述,不论是配置XML还是构造配置类对象他们的主要目的是通过读取配置构造一个org.apache.ibatis.session.SqlSessionFactoryBuilder对象,而生成这个SqlSessionFactoryBuilder对象的目的是生成SqlSessionFactory
注:SqlSessionFactory是各长生命周期的对象,一般情况下一个应用下的一个数据源就对应一个SqlSessionFactory。在SqlSessionFactoryBuilder构建出SqlSessionFactory之后SqlSessionFactoryBuilder就没有用了,他应该被释放掉。

//方式一
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//方式二
DataSource dataSource = BlogDataSourceFactory.getBlogDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(BlogMapper.class);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);

在Springboot中,SqlSessionFactoryBuilder的地位被SqlSessionFactoryBean替代了。不管怎样构建SqlSessionFactoryBean,对其配置的本质还是相当于构造一个配置类Configuration,然后通过这个配置对象产生SqlSessionFactory对象(SqlSessionFactoryBean.getObject())。DefaultSqlSessionFactory对象是依赖Configuration对象的:

public class DefaultSqlSessionFactory implements SqlSessionFactory {
  private final Configuration configuration;
  //。。。。。。。。。。。。。。。。。。
}
  @Bean
  @ConditionalOnMissingBean
  public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
    factory.setDataSource(dataSource);
    factory.setVfs(SpringBootVFS.class);
    if (StringUtils.hasText(this.properties.getConfigLocation())) {
      factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
    }
    applyConfiguration(factory);
    if (this.properties.getConfigurationProperties() != null) {
      factory.setConfigurationProperties(this.properties.getConfigurationProperties());
    }
    if (!ObjectUtils.isEmpty(this.interceptors)) {
      factory.setPlugins(this.interceptors);
    }
    if (this.databaseIdProvider != null) {
      factory.setDatabaseIdProvider(this.databaseIdProvider);
    }
    if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
      factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
    }
    if (this.properties.getTypeAliasesSuperType() != null) {
      factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
    }
    if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
      factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
    }
    if (!ObjectUtils.isEmpty(this.typeHandlers)) {
      factory.setTypeHandlers(this.typeHandlers);
    }
    if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
      factory.setMapperLocations(this.properties.resolveMapperLocations());
    }
    Set<String> factoryPropertyNames = Stream
        .of(new BeanWrapperImpl(SqlSessionFactoryBean.class).getPropertyDescriptors()).map(PropertyDescriptor::getName)
        .collect(Collectors.toSet());
    Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
    if (factoryPropertyNames.contains("scriptingLanguageDrivers") && !ObjectUtils.isEmpty(this.languageDrivers)) {
      // Need to mybatis-spring 2.0.2+
      factory.setScriptingLanguageDrivers(this.languageDrivers);
      if (defaultLanguageDriver == null && this.languageDrivers.length == 1) {
        defaultLanguageDriver = this.languageDrivers[0].getClass();
      }
    }
    if (factoryPropertyNames.contains("defaultScriptingLanguageDriver")) {
      // Need to mybatis-spring 2.0.2+
      factory.setDefaultScriptingLanguageDriver(defaultLanguageDriver);
    }

    return factory.getObject();
  }

注:applyConfiguration()方法会构造一个Configuration对象放到factory里面,其他的配置比如TypeHandler,Plugin都是注入到这个MybatisAutoConfiguration里面的。还有就是一些property设置,总之前面一大段与你写XML与构造Configuration对象的目的是一致的。getObject()方法会根据配置得到SqlSessionFactory对象:

  public SqlSessionFactory getObject() throws Exception {
    if (this.sqlSessionFactory == null) {
      //这里内部是调用buildSqlSessionFactory()构造SqlSessionFactory对象
      afterPropertiesSet();
    }
    return this.sqlSessionFactory;
  }

上面的获得SqlSessionFactory对象告一段落。另外还有一个获取Mapper的过程,在我们开始探讨获取Mapper的过程之前还需要了解SqlSessionTemplate:它是由Spring管理的线程安全的Template对象。

public class SqlSessionTemplate implements SqlSession, DisposableBean {
    //前文的DefaultSqlSessionFactory,通过构造器传入
    private final SqlSessionFactory sqlSessionFactory;
    //默认是SIMPLE,是从DefaultSqlSessionFactory的Configuration属性里面获取的这个配置
    private final ExecutorType executorType;
    //实际上是SqlSessionManager对象,线程安全就是通过它实现的。
    private final SqlSession sqlSessionProxy;
    //处理执行异常
    private final PersistenceExceptionTranslator exceptionTranslator;
    。。。。。。。。。。。。。
}

public class SqlSessionManager implements SqlSessionFactory, SqlSession {
    //前文的DefaultSqlSessionFactory;SqlSessionFactory的职能最后会委托给这个对象
    private final SqlSessionFactory sqlSessionFactory;
    //SqlSession是个Jdk代理对象,SqlSession的职能交给它完成
    private final SqlSession sqlSessionProxy;
    //线程隔离就是通过它来实现的
    private final ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<>();
    //代理类的拦截器,SqlSession的工作是它完成的
    private class SqlSessionInterceptor implements InvocationHandler {
        public SqlSessionInterceptor() {}
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
          //优先使用当前线程的SqlSession
          final SqlSession sqlSession = SqlSessionManager.this.localSqlSession.get();
          if (sqlSession != null) {
            try {
              return method.invoke(sqlSession, args);
            } catch (Throwable t) {
              throw ExceptionUtil.unwrapThrowable(t);
            }
          } else {
            //找不到就临时开一个用
            try (SqlSession autoSqlSession = openSession()) {
              try {
                final Object result = method.invoke(autoSqlSession, args);
                autoSqlSession.commit();
                return result;
              } catch (Throwable t) {
                autoSqlSession.rollback();
                throw ExceptionUtil.unwrapThrowable(t);
              }
            }
          }
        }
   }
    。。。。。。。。。。。。。。。。。。。。。。。。。
}

注:SqlSessionTemplate的正确用法是先调用它的startManagedSession(args)开启一个与当前线程绑定的SqlSession,然后再调用它的SqlSession相应的方法去执行,最后commit或者rollback。
上述SqlSessionTemplate保证了在多线程环境下SqlSessionTemplate依然是单例的。
接下来是产生Mapper代理对象的过程,由几个重要的类:

public class MapperProxy<T> implements InvocationHandler, Serializable {
    //在Spring中实际是前文提到的SqlSessionTemplate,它保证了MapperProxy也可以是单例的
    private final SqlSession sqlSession;
    //代理的接口
    private final Class<T> mapperInterface;
    。。。。。。。。。。。。。。。。。。
}
//接口无法通过直接getBean,他需要通过工厂获取代理对象
public class MapperProxyFactory<T> {
    //哪个对象接口的工厂
    private final Class<T> mapperInterface;
}
//注册到Spring容器的Mapper实际类型
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
    //也就是前文中的那个sqlSessionTemplate
    private SqlSessionTemplate sqlSessionTemplate;
    private Class<T> mapperInterface;
    。。。。。
    /**
     * 委托给SqlSession;实际委托链是:MapperFactoryBean——》SqlSessionTemplate ——》DefaultSqlSessionFactory——》Configuration——》MapperRegistry(它有缓存功能,从XML解析到Mapper的映射关系会存到里面)
     */
    @Override
    public T getObject() throws Exception {
        return getSqlSession().getMapper(this.mapperInterface);
    }
    。。。。。。。。。
}

过程是这样的@MapperScan会导入MapperScannerRegistrar(关于Spring如何处理这个请点击这里); MapperScannerRegistrar会注册一个MapperScannerConfigurer到容器,它本质是一个BeanFactoryPostProcessor。MapperScannerConfigurer将扫描Mapper的工作委托给ClassPathMapperScanner,将扫描到的Mapper做成BeanDefinition注册到Spring容器。ClassPathMapperScanner有个重要的的方法processBeanDefinitions(),他会对扫描后的BeanDefinition进行后续处理:

 private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
       .........................
      // 这里会将Mapper注册类型为MapperFactoryBean,并且将接口类型传进去了。
      definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); // issue #59
      definition.setBeanClass(this.mapperFactoryBeanClass);
      .......................
}

注:MapperRegistry里面的mapper与MapperProxyFactory之间的映射是通过xml解析的,当然,实际产生mapper代理对xml不是强依赖的,也就是说在生产mapper代理的过程中是不会检查对应的mapper配置的,假如在执行过程中没有找到方法绑定的Sql就会报错。

生产mapper,调用链是:getBean——》doGetBean——》getObjectForBeanInstance——》super.getObjectForBeanInstance——》getObjectFromFactoryBean——》getObjectFromFactoryBean——》doGetObjectFromFactoryBean——FactoryBean.getObject

注:如果getBean的beanname带有factory标志:&,则会直接取出FactoryBean对象。

从上面你可以知道Mybatis生产mapper实际是通过生产代理对象做到的,关键就是MapperProxy与MapperProxyFactory,而Mybatis的配置统一为一个Configuration类,通过它构建SqlSessionFactory对象,SqlSessionFactory对象会根据配置获取SqlSession(openSession(),SqlSession代表一个socket连接会话,所有的Sql都需要通过它执行。

一条Sql在Mybatis的执行过程是怎么样的

在这个部分主要通过Sql执行讲清楚Mybatis相关的一系列接口,API,类。

基础用法:xml,无xml,保留mapperxml:

//xml
    @Test
    public void testInit() throws IOException {
        //从xml文件中读取配置
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-cfg.xml"));
        NacosMapper nacosMapper =sqlSessionFactory.openSession().getMapper(NacosMapper.class);
        nacosMapper.allUsers();
     }
//无xml
    @Test
    public void testInit() throws IOException {
        //mysql数据源
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUser("root");
        dataSource.setPassword("123456");
        dataSource.setUrl("jdbc:mysql://localhost:3306/nacos");
        TransactionFactory transactionFactory = new JdbcTransactionFactory();
        Environment environment = new Environment("development", transactionFactory, dataSource);
        
        Configuration configuration = new Configuration(environment);
        //添加mapper,这里需要@Select @Update等等注解或者配合Sql API做到无xml
        configuration.addMapper(NacosMapper.class);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
        
        NacosMapper nacosMapper =sqlSessionFactory.openSession().getMapper(NacosMapper.class);
        nacosMapper.allUsers();
     }
//保留mapper.xml
    @Test
    public void testInit() throws IOException {
        //MySQL数据源
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUser("root");
        dataSource.setPassword("123456");
        dataSource.setUrl("jdbc:mysql://localhost:3306/nacos");
        TransactionFactory transactionFactory = new JdbcTransactionFactory();
        Environment environment = new Environment("development", transactionFactory, dataSource);
        
        Configuration configuration = new Configuration(environment);
        //这里实际上就是从配置中将mapper与statement解析出来
        InputStream inputStream = Resources.getResourceAsStream("mapper/NacosMapper.xml");
        XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, "mapper/NacosMapper.xml", configuration.getSqlFragments());
        mapperParser.parse();
        
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
        
        NacosMapper nacosMapper =sqlSessionFactory.openSession().getMapper(NacosMapper.class);
        nacosMapper.allUsers();
     }

从上面可以看出,Mybatis的配置方式是很灵活的,不论是怎样组合,你总能找到解决方式。所有配置的本质都是构建合适的Configuration对象去构建合适的SqlSessionFactory对象,毕竟DefaultSqlSessionFactory只有一个必须的Configuration对象属性需要传入构造。

样例:
NacosMapper.xml

<?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.example.demo.mapper.NacosMapper">
  <select id="allUsers" resultType="com.example.demo.entity.Users">
    select * from users
  </select>
  <select id="selectByUsername" resultType="com.example.demo.entity.Users" databaseId="mysql">
    select * from users 
    <where>
        <if test="username != null">username = #{username}</if>
    </where>
  </select>
</mapper>

对应接口:

public interface NacosMapper {
    List<Users> allUsers();
    List<Users> selectByUsername(String username);
}

执行:

@SpringBootApplication
@MapperScan(basePackages = "com.example.demo.mapper")
public class DemoApplication {
    
    public static void main(String[] args) {
        
        ConfigurableApplicationContext app = SpringApplication.run(DemoApplication.class, args);
        NacosMapper nacosMapper =(NacosMapper) app.getBean("nacosMapper");
        nacosMapper.hashCode();
        List<Users> users = nacosMapper.selectByUsername("nacos");
        System.out.println("----"+users);
        
    }

}

或者你可以不写xml:

public interface NacosMapper {
    @Select("select * from users")
    List<Users> allUsers();
    
    @Select(databaseId ="mysql", 
            value={"<script>",
                    "select * from users ",
                "    <where>",
                "        <if test=\"username != null\">username = #{username}</if>",
                "    </where>",
                "    </script>"})
    List<Users> selectByUsername(String username);
}

上文中提到Springboot生产SqlSessionFactory的过程,他是由MybatisAutoConfiguration完成的,MybatisAutoConfiguration一些东西:

  public MybatisAutoConfiguration(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider,
      ObjectProvider<TypeHandler[]> typeHandlersProvider, ObjectProvider<LanguageDriver[]> languageDriversProvider,
      ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider,
      ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {

  }

可以看到它对以下类的对象有依赖:

  • MybatisProperties:一些配置比如mapper.xml位置,cfg.xml位置等等。它内部有一个Configurations属性你也可以对他就行配置
  • Interceptor:Mybatis Plugin的实现,这是个非常重要的扩展点,像tkmapper,pagehelper都使用插件实现。
  • TypeHandler:类型转换器,jdbc类型(数据库类型)与java类型互相转换要使用到的对象,基本上mybatis自带的的类型转换器已经够用了,我能想到的需要使用自定义类型转换器的地方大概只有自定义枚举的时候要用
  • LanguageDriver:语法解析器,mybatis自带两种:XMLLanguageDriver(XML语法解析器),RawLanguageDriver(原生语法解析器,它不支持动态SQL)。sql内部使用了<>标签的都是动态sql,而TEXT和CDATA区需要另外解析才能判断是否是动态SQL。你可以想象到Mybatis是将xml解析为一棵语法树,然后通过处理这棵树得到SQL。
  • ResourceLoader:属于Spring的东西。在SpringIOC的朴素解释有提到。就是加载资源用的。
  • DatabaseIdProvider:数据库厂牌标识提供器。它需要与mapper里面的databaseId属性向配合才能发挥效用。默认是空,表示所有数据库都可以执行该sql。实际作用是处理方言。
  • ConfigurationCustomizer:为了处理上面没有考虑到的扩展点而设计的一个接口,如下:

    @FunctionalInterface
    public interface ConfigurationCustomizer {
    void customize(Configuration configuration);
    }

    注:ObjectProvider时是ObjectFactory的子接口,它主要提供了一个类似与Optional的功能(延迟加载,异步)。具体请自行搜索,网民必修课——搜索引擎使用
    另外,获取SqlSessionFactory bean是依赖datasource的,它也算是一个扩展点:

     @Bean
     @ConditionalOnMissingBean
     public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {}

    前文提到,SqlSessionFactory是通过FactoryBean的getObject()方法获取的,内部调用buildSqlSessionFactory获得SqlSessionFactory对象:

    protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
    
      final Configuration targetConfiguration;
    
      XMLConfigBuilder xmlConfigBuilder = null;
      if (this.configuration != null) {
        //在上文sqlSessionFactory方法中构造的configuration
        targetConfiguration = this.configuration;
        //properties,理论上它也应该是空的。
        if (targetConfiguration.getVariables() == null) {
          targetConfiguration.setVariables(this.configurationProperties);
        } else if (this.configurationProperties != null) {
          targetConfiguration.getVariables().putAll(this.configurationProperties);
        }
      } else if (this.configLocation != null) {
        //如果配置了cfg.xml,则解析获取配置,从中产生Configuration,理论上不可能还需要cfg.xml
        xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
        targetConfiguration = xmlConfigBuilder.getConfiguration();
      } else {
        //理论上不会到这里,毕竟它是从sqlSessionFactory过来的。当然要是自定义就不一定了。
        LOGGER.debug(
            () -> "Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
        targetConfiguration = new Configuration();
        Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables);
      }
      //在处理ResultSet需要创建对象时会用到ObjectFactory,默认的已经够用了,有俩个方法分别调用了无参构造与有参构造。
      //Springboot中如果要自定义,则需要借助ConfigurationCustomizer或者自己自定义SqlSessionFactory的Bean注入到容器。
      Optional.ofNullable(this.objectFactory).ifPresent(targetConfiguration::setObjectFactory);
      Optional.ofNullable(this.objectWrapperFactory).ifPresent(targetConfiguration::setObjectWrapperFactory);
      Optional.ofNullable(this.vfs).ifPresent(targetConfiguration::setVfsImpl);
      //处理别名在实际使用中基本上用不上,有俩个方面,1.全限定出冲突的概率低,2.全限定名的后面的方法名Mybatis会做别名处理。
      if (hasLength(this.typeAliasesPackage)) {
        scanClasses(this.typeAliasesPackage, this.typeAliasesSuperType).stream()
            .filter(clazz -> !clazz.isAnonymousClass()).filter(clazz -> !clazz.isInterface())
            .filter(clazz -> !clazz.isMemberClass()).forEach(targetConfiguration.getTypeAliasRegistry()::registerAlias);
      }
      if (!isEmpty(this.typeAliases)) {
        Stream.of(this.typeAliases).forEach(typeAlias -> {
          targetConfiguration.getTypeAliasRegistry().registerAlias(typeAlias);
          LOGGER.debug(() -> "Registered type alias: '" + typeAlias + "'");
        });
      }
      //plugin(也就是Interceptor),包括4种,下文会讲到。
      if (!isEmpty(this.plugins)) {
        Stream.of(this.plugins).forEach(plugin -> {
          targetConfiguration.addInterceptor(plugin);
          LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");
        });
      }
      //TypeHandler,类型处理器,下文会讲到。
      if (hasLength(this.typeHandlersPackage)) {
        scanClasses(this.typeHandlersPackage, TypeHandler.class).stream().filter(clazz -> !clazz.isAnonymousClass())
            .filter(clazz -> !clazz.isInterface()).filter(clazz -> !Modifier.isAbstract(clazz.getModifiers()))
            .forEach(targetConfiguration.getTypeHandlerRegistry()::register);
      }
      
      if (!isEmpty(this.typeHandlers)) {
        Stream.of(this.typeHandlers).forEach(typeHandler -> {
          targetConfiguration.getTypeHandlerRegistry().register(typeHandler);
          LOGGER.debug(() -> "Registered type handler: '" + typeHandler + "'");
        });
      }
      //实际上EnumTypeHandler也是一个TypeHandler,在有些时候它可能不满足你的需要。
      targetConfiguration.setDefaultEnumTypeHandler(defaultEnumTypeHandler);
      //sql转换器,后文会提到。
      if (!isEmpty(this.scriptingLanguageDrivers)) {
        Stream.of(this.scriptingLanguageDrivers).forEach(languageDriver -> {
          targetConfiguration.getLanguageRegistry().register(languageDriver);
          LOGGER.debug(() -> "Registered scripting language driver: '" + languageDriver + "'");
        });
      }
      Optional.ofNullable(this.defaultScriptingLanguageDriver)
          .ifPresent(targetConfiguration::setDefaultScriptingLanguage);
      //厂牌,默认为空,后文会提到。
      if (this.databaseIdProvider != null) {// fix #64 set databaseId before parse mapper xmls
        try {
          targetConfiguration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
        } catch (SQLException e) {
          throw new NestedIOException("Failed getting a databaseId", e);
        }
      }
    
      Optional.ofNullable(this.cache).ifPresent(targetConfiguration::addCache);
    
      if (xmlConfigBuilder != null) {
        try {
          xmlConfigBuilder.parse();
          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();
        }
      }
      //在Spring环境下默认的transactionFactory要比自定义要好。另外Environment与DatabaseId都有标识作用。
      targetConfiguration.setEnvironment(new Environment(this.environment,
          this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory,
          this.dataSource));
      //mapper.xml配置,关于Mapper前文已经提到了。
      if (this.mapperLocations != null) {
        if (this.mapperLocations.length == 0) {
          LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found.");
        } else {
          for (Resource mapperLocation : this.mapperLocations) {
            if (mapperLocation == null) {
              continue;
            }
            try {
              //解析mapper.xml
              XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
                  targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
              xmlMapperBuilder.parse();
            } catch (Exception e) {
              throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
            } finally {
              ErrorContext.instance().reset();
            }
            LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");
          }
        }
      } else {
        LOGGER.debug(() -> "Property 'mapperLocations' was not specified.");
      }
    
      return this.sqlSessionFactoryBuilder.build(targetConfiguration);
    }

    在创建SqlSessionFactory的过程中,以下一个扩展点是直接被使用了的:
    1.ConfigurationCustomizer:它是在Configuration对象创建完成后立即执行的(applyConfiguration方法里面),它也是最早被使用的一个扩展点。这里我们利用它替换了默认的ObjectFactory:

    @Component
    public class TestConfigurationCustomizer implements ConfigurationCustomizer {
      @Override
      public void customize(Configuration configuration) {
          configuration.setObjectFactory(new UserObjectFactory());
      }
    
    }
    
    public class UserObjectFactory extends DefaultObjectFactory {
      private static final long serialVersionUID = 1L;
      @Override
      public <T> T create(Class<T> type) {
          System.out.println("-------------create----------------");
          return super.create(type);
      }
    
    }

    ObjectFactory、Cache等等在Springboot项目里面都没有扩展,但是你能通过ConfigurationCustomizer对其进行设置。

2.DatabaseIdProvider:这是第二个被使用的扩展(在buildSqlSessionFactory方法里面),这里将厂牌设置为了mysql,这样的话DatabaseId为空或者为mysql都能被执行,这里可以看出,一个SqlSessionFactory的databaseId是一开始确定好的。

@Component
public class MysqlDatabaseIdProvider implements DatabaseIdProvider {
    @Override
    public String getDatabaseId(DataSource dataSource) throws SQLException {
        System.out.println("---------getDatabaseId-------------");
        return "mysql";
    }
}

像Interceptor、TypeHandler等等都是直接将实例保存到Configuration对象里面的,前期准备好configuration就可以直接注入SqlSessionTemplate了。

3.LanguageDriver:这个对象是在MapperFactoryBean对象初始化完成后被使用的,具体如下:

    @Override
    public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {
        // 这是个模板方法,定义在DaoSupport类中
        checkDaoConfig();
        try {
            initDao();
        }
        catch (Exception ex) {
            throw new BeanInitializationException("Initialization of DAO failed", ex);
        }
    }
    //在MapperFactoryBean的具体实现。
  protected void checkDaoConfig() {
    super.checkDaoConfig();

    notNull(this.mapperInterface, "Property 'mapperInterface' is required");

    Configuration configuration = getSqlSession().getConfiguration();
    if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
      try {
        //内部最终委托给MapperRegistry的addMapper方法
        configuration.addMapper(this.mapperInterface);
      } catch (Exception e) {
        logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);
        throw new IllegalArgumentException(e);
      } finally {
        ErrorContext.instance().reset();
      }
    }
  }
//MapperRegistry的addMapper实现
  public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        knownMappers.put(type, new MapperProxyFactory<>(type));
        // 此处会解析Mapper相关的注解,这里面就用到了LanguageDriver(parseStatement方法里面)
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

在这里我们使用自定义在LanguageDriver去模拟:

@Component
public class JsonLanguageDriver extends XMLLanguageDriver {
    @Override
    public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject,
            BoundSql boundSql) {
        System.out.println("---------createParameterHandler------------");
        return super.createParameterHandler(mappedStatement, parameterObject, boundSql);
    }
    @Override
    public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
        System.out.println("---------createSqlSource------------");
        return super.createSqlSource(configuration, script, parameterType);
    }
    @Override
    public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
        System.out.println("---------createSqlSource------------");
        return super.createSqlSource(configuration, script, parameterType);
    }
}

mapper是可以通过注解@Lang来指定LanguageDriver的。XMLLanguageDriver内部解析工作是交给XMLScriptBuilder处理的,XMLScriptBuilder继承自BaseBuilder,再你有自定义语法格式的时候可能会用上。各个XML节点(CDATA,TEXT,WHERE,IF等等)的处理是交给不同的NodeHandler实现的,你无法扩展这个private内部接口。
注:mapper.xml的解析是在buildSqlSessionFactory实现的,这一点与注解是不一样的。用到了一个关键工具类:XMLMapperBuilder。

到这里。整个创建时的过程与创建时各个扩展的调用就完成了。下面是使用时过程及扩展调用。
4.Executor.class的Interceptor。org.apache.ibatis.executor.Executor里面的方法都可以进行拦截,具体有哪些方法你可以自己看文档或者源码。很显然插件功能是通过AOP(动态代理)实现的。这里是一个简单的演示:

@Component
@Intercepts(@Signature(method = "query", type = Executor.class, args = { MappedStatement.class, Object.class,
        RowBounds.class, ResultHandler.class}))
public class ExecutorPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("----------run ExecutorPlugin------------");
        return invocation.proceed();
    }
}

注:Executor有三类:SIMPLE(默认,普通的执行器),REUSE(复用,执行器会重用预处理语句(PreparedStatement)),BATCH(批处理,执行器不仅重用语句还会执行批量更新)。它可以通过defaultExecutorType(Configuration对象)设置默认。它的具体效果需要你自行实验,这里不再展开。在实际使用的是CachingExecutor,CachingExecutor功能还是委托给那三个中的其中一个。这个阶段还拿不到sql。
5.ParameterHandler.class的Interceptor。和上面一样org.apache.ibatis.executor.parameter.ParameterHandler有多个方法可以拦截,你可以自己看源码。这个阶段SQL已经准备好了,在传入参数之前。这里是一个简单的演示:

@Component
@Intercepts(@Signature(method = "setParameters", type = ParameterHandler.class, args = { PreparedStatement.class}))
public class ParameterHandlerPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("----------run ParameterHandlerPlugin------------");
        return invocation.proceed();
    }
}

6.BaseTypeHandler(TypeHandler的一个模板,你可以继承它实现自己的TypeHandler)的setNonNullParameter方法。这里将参数设置到PreparedStatement中。下面是简单演示:

@Component
public class StringTypeHandler extends BaseTypeHandler<String> {
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
            throws SQLException {
        System.out.println("------------setNonNullParameter--------------");
        ps.setString(i, parameter);
    }
。。。。。。。。。。。。。。。。。。
}

注:MyBatis 不会通过检测数据库元信息来决定使用哪种类型,所以你必须在参数和结果映射中指明字段是 VARCHAR 类型, 以使其能够绑定到正确的类型处理器上。这是因为 MyBatis 直到语句被执行时才清楚数据类型。这也就意味着你如果双向都用同一个TypeHandler就需要为该字段设置类型。

7.StatementHandler.class的Interceptor。和前文一样,里面有多个方法,你需要自己看源码。这个阶段SQL准备好了,参数也好了就差执行了。下面是简单演示:

@Component
@Intercepts(@Signature(method = "query", type = StatementHandler.class, args = { Statement.class,ResultHandler.class}))
public class StatementHandlerPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("----------run StatementHandlerPlugin------------");
        return invocation.proceed();
    }
}

8.ResultSetHandler.class的Interceptor。和前文一样,里面有多个方法,你需要自己看源码。这个阶段已经可以拿到resultset,该resultset还未处理。下面是简单演示:

@Component
@Intercepts(@Signature(method = "handleResultSets", type = ResultSetHandler.class, args = { Statement.class}))
public class ResultSetHandlerPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("----------run ResultSetHandlerPlugin------------");
        return invocation.proceed();
    }
}

9.ObjectFactory。调用create方法得到对象,这里是List类型和Users类型的对象。这个对象可能会被调用多次,每次创建对象都会用到。下面是样例:

public class UserObjectFactory extends DefaultObjectFactory {
    private static final long serialVersionUID = 1L;
    @Override
    public <T> T create(Class<T> type) {
        System.out.println("-------------create----------------");
        return super.create(type);
    }

}

10.BaseTypeHandler的getNullableResult方法。前面已经有对象了,现在你需要将每个字段设置到属性里面,这里就会用到TypeHandler,下面是个枚举类型转换的演示:

@Component
@MappedJdbcTypes(JdbcType.BOOLEAN)//这里必须要指定,上文有提
public class EnabledEnumTypeHandler extends BaseTypeHandler<Enabled> {
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Enabled parameter, JdbcType jdbcType)
            throws SQLException {
        System.out.println("--------EnabledEnumTypeHandler----------");
        ps.setBoolean(i, parameter == Enabled.YES ? true : false);
    }

    @Override
    public Enabled getNullableResult(ResultSet rs, String columnName) throws SQLException {
        System.out.println("--------EnabledEnumTypeHandler----------");
        boolean s = rs.getBoolean(columnName);
        return s ? Enabled.YES : Enabled.NO;
    }

    @Override
    public Enabled getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        System.out.println("--------EnabledEnumTypeHandler----------");
        boolean s = rs.getBoolean(columnIndex);
        return s ? Enabled.YES : Enabled.NO;
    }

    @Override
    public Enabled getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        System.out.println("--------EnabledEnumTypeHandler----------");
        boolean s = cs.getBoolean(columnIndex);
        return s ? Enabled.YES : Enabled.NO;
    }
}

注:@Intercepts是可以配置拦截多个签名的方法的。
注:插件是在创建Executor的时候代理的,是在执行过程中创建的:Configuration里面的方法。

  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    //全部插件代理
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

注:正如前文所述,各个接口拦截的位置的有多个的,所以上面的顺序仅供参考。更多配置比如properties,settings等等等等请自行翻看Mybatis官方文档

到这一步全流程就已经完成了。可以看到Mybatis是非常灵活的,但是它也有些不足,比如不能xml自定义标签处理等等(这个问题也许在这里会有处理方案的启示)。你也可以看到Mybatis不仅仅是ORM它还做了很多其他工作。

总结

回到开头,Mybatis在ORM方面交给TypeHandler与ObjectFactory处理了(当然里面还会涉及到比如注解处理的东西,mybatis帮我们屏蔽了)。其他的比如插件、设置、缓存等等等等是为了mybatis的扩展性与健壮性而生的。和其他框架源码一样,上面所说的绝大多数知识对业务开发时没有帮助的,如果非得找出一个益处的话那就是可以更好的使用该框架也可以从源码中学到设计系统的方法。我依然保持这个观点:拖慢项目的最大的问题是合作,而非技术。

注:转载请标明出处与作者。有意见或建议请评论留言。

标签: mybatis

添加新评论