美文网首页
Mybatis原理分析

Mybatis原理分析

作者: Typhoon叔牙 | 来源:发表于2022-01-14 15:39 被阅读0次

        到目前位置在国内使用最广泛和最流行的持久层框架非Mybatis莫属,但是从最近一次jvm生态报告中,mybatis在java体系的使用率并不高:


    image.png
    orm-providers-hibernate-jdbc-spring-template-eclipse-link-mybatis.png

        可以清晰地看到mybatis不占上风,作为开发人员的常识,貌似国内java开发人员占比比较高,但是其实并不然,我们通过另一张统计报告图来看一下java开发人员在全球范围内的占比:


    image.png
    image.png
        很明显北美的活跃度最高,老外更喜欢优雅的jpa编程方式,也可能他们对mybatis的认知还停留在ibatis时代,另外国内之所以流行,应该有一大部分原因是大厂带的节奏。比如某云曾说过:某里每年向社会输出1000名在某里工作十年以上的人才。那么他们从大厂出来后到其他中小型企业也都是中高层管理者,把大厂的规则辐射到中小厂也就无可厚非了。

    一、使用

        为了全面拥抱springboot和降低接入复杂度,mybatis也推出了mybatis-spring-boot-starter,是开发者通过很少的配置就能使用mybatis能力.

    1.引入依赖

    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
    </dependency>
    

    2.添加配置

    spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
    spring.datasource.username=root
    spring.datasource.password=root
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    
    mybatis.config-location=classpath:mybatis/mybatis-config.xml
    mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
    

    3.开启Mapper接口扫描

        在启动类上添加@MapperScan注解:

    @SpringBootApplication
    @MapperScan("xxx.xxx.mapper")
    public class Application {
        //....
    }
    

    4.编写Mapper接口和xml实现

    public interface UserInfoMapper {
        UserInfoDO findByUid(@Param("uid") String uid);
    }
    
    <mapper namespace="xxx.UserInfoMapper">
        <resultMap id="BaseResultMap" type="xxx.UserInfoDO">
                <id property="id" column="ID"/><!--自增ID-->
                <result property="uid" column="UID"/>
                ....
        </resultMap>
        <select id="findByUid" resultMap="BaseResultMap">
            SELECT <include refid="BaseColumnList"/>
            FROM user_info
            where UID = #{uid}
        </select>
    </mapper>
    

        这样我们就可以想使用其他bean一样,直接将Mapper接口注入到业务中正常使用了,那我们就应该思考一个问题,为什么我明明定义的是一个接口,而具体操作数据的sql实现在xml文件中,但是可以直接调用Mapper接口并且实现数据操作?我们带着问题继续分析.

    二、源码&原理解析

        接着前一节的问题,我们可以做出以下猜想:

    • Mapper接口在应用启动的时候被解析成具体的实现,并注册到spring容器中
    • Xml文件里边的sql实现在启动的时候被解析,然后放到一个大Map中
    • 在应用启动完成或者Mapper接口在调用的时候把具体方法实现和Xml里边的数据操作进行绑定
    • bean注入的时候进行一些初始化或者绑定操作

    1.Mapper初始化

        基于以上三个点,也给我们提供了思路,我们直接从@MapperScan注解入手:

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Documented
    @Import(MapperScannerRegistrar.class)
    @Repeatable(MapperScans.class)
    public @interface MapperScan {
      //包路径,等价于basePackages
      String[] value() default {};
      //包路径
      String[] basePackages() default {};
      ...
    }
    

        注解定义了要扫描的包路径,并且导入了MapperScannerRegistrar,看一下实现:


    MapperScannerRegistrar.png
    public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
    
      @Override
      public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        AnnotationAttributes mapperScanAttrs = AnnotationAttributes
            .fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
        if (mapperScanAttrs != null) {
          registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry,
              generateBaseBeanName(importingClassMetadata, 0));
        }
      }
    
      void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
          BeanDefinitionRegistry registry, String beanName) {
    
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
        builder.addPropertyValue("processPropertyPlaceHolders", true);
    
    
        List<String> basePackages = new ArrayList<>();
        basePackages.addAll(
            Arrays.stream(annoAttrs.getStringArray("value")).filter(StringUtils::hasText).collect(Collectors.toList()));
    
        basePackages.addAll(Arrays.stream(annoAttrs.getStringArray("basePackages")).filter(StringUtils::hasText)
            .collect(Collectors.toList()));
    
        basePackages.addAll(Arrays.stream(annoAttrs.getClassArray("basePackageClasses")).map(ClassUtils::getPackageName)
            .collect(Collectors.toList()));
    
        if (basePackages.isEmpty()) {
          basePackages.add(getDefaultBasePackage(annoMeta));
        }
    
        builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(basePackages));
    
        registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
    
      }
    
      ...
    
    }
    

        MapperScannerRegistrar实现了ImportBeanDefinitionRegistrar接口,用来动态注册bean,被ConfigurationClassPostProcessor调用,在当前类实现中定义了MapperScannerConfigurer并注册到BeanDefinitionRegistry,接着看一下MapperScannerConfigurer实现:

    MapperScannerConfigurer.png
    public class MapperScannerConfigurer
        implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
      ...
      @Override
      public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        // left intentionally blank
      }
      @Override
      public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
        if (this.processPropertyPlaceHolders) {
          processPropertyPlaceHolders();
        }
    
        ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
        scanner.setAddToConfig(this.addToConfig);
        scanner.setAnnotationClass(this.annotationClass);
        scanner.setMarkerInterface(this.markerInterface);
        scanner.setSqlSessionFactory(this.sqlSessionFactory);
        scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
        scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
        scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
        scanner.setResourceLoader(this.applicationContext);
        scanner.setBeanNameGenerator(this.nameGenerator);
        scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
        if (StringUtils.hasText(lazyInitialization)) {
          scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
        }
        scanner.registerFilters();
        scanner.scan(
            StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
      } 
    }
    

        MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor接口,而BeanDefinitionRegistryPostProcessor又继承了BeanFactoryPostProcessor接口,BeanFactoryPostProcessor的作用是在bean的定义信息已经加载但还没有初始化的时候执行方法postProcessBeanFactory()方法,而BeanDefinitionRegistryPostProcessor是在BeanFactoryPostProcessor的前面执行,MapperScannerConfigurer中postProcessBeanFactory没有实现,实现了postProcessBeanDefinitionRegistry方法,创建了ClassPathMapperScanner扫描器,注册过滤器并且扫描包路径下的候选Mapper接口.

        接着看一下ClassPathMapperScanner实现:

    ClassPathMapperScanner.png
    public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
      ...
      @Override
      public Set<BeanDefinitionHolder> doScan(String... basePackages) {
        Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
    
        if (beanDefinitions.isEmpty()) {
          LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages)
              + "' package. Please check your configuration.");
        } else {
          processBeanDefinitions(beanDefinitions);
        }
        return beanDefinitions;
      }
    
      private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
        GenericBeanDefinition definition;
        for (BeanDefinitionHolder holder : beanDefinitions) {
          definition = (GenericBeanDefinition) holder.getBeanDefinition();
          String beanClassName = definition.getBeanClassName();
          LOGGER.debug(() -> "Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + beanClassName
              + "' mapperInterface");
    
          // the mapper interface is the original class of the bean
          // but, the actual class of the bean is MapperFactoryBean
          definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); // issue #59
          definition.setBeanClass(this.mapperFactoryBeanClass);
    
          definition.getPropertyValues().add("addToConfig", this.addToConfig);
    
          boolean explicitFactoryUsed = false;
          if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
            definition.getPropertyValues().add("sqlSessionFactory",
                new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
            explicitFactoryUsed = true;
          } else if (this.sqlSessionFactory != null) {
            definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
            explicitFactoryUsed = true;
          }
    
          if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
            if (explicitFactoryUsed) {
              LOGGER.warn(
                  () -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
            }
            definition.getPropertyValues().add("sqlSessionTemplate",
                new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
            explicitFactoryUsed = true;
          } else if (this.sqlSessionTemplate != null) {
            if (explicitFactoryUsed) {
              LOGGER.warn(
                  () -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
            }
            definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
            explicitFactoryUsed = true;
          }
    
          if (!explicitFactoryUsed) {
            LOGGER.debug(() -> "Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
            definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
          }
          definition.setLazyInit(lazyInitialization);
        }
      }
    }
    

        在Spring IOC 容器初始化阶段就会调用ClassPathBeanDefinitionScanner扫描包下所有类,并将符合过滤条件的类注册到IOC 容器内,ClassPathMapperScanner继承ClassPathBeanDefinitionScanner并重写了doScan方法,用来扫描mybatis定义的路径下的包并注册到容器中。

        需要注意的是这里的GenericBeanDefinition定义的BeanClass是MapperFactoryBean,也就是说这里调用父类的doScan扫描到Mapper接口的定义之后,把BeanDefinition修改成了MapperFactoryBean,而MapperFactoryBean本质上是FactoryBean在调用getObject()的时候才会返回原始的bean,看一下MapperFactoryBean继承关系和实现:


    MapperFactoryBean.png
    public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
      private Class<T> mapperInterface;
      @Override
      protected void checkDaoConfig() {
        super.checkDaoConfig();
        notNull(this.mapperInterface, "Property 'mapperInterface' is required");
        Configuration configuration = getSqlSession().getConfiguration();
        if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
          try {
            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();
          }
        }
      }
      /**
       * {@inheritDoc}
       */
      @Override
      public T getObject() throws Exception {
        return getSqlSession().getMapper(this.mapperInterface);
      }
     }
    

        到这里Mapper接口的初始化操作就完成了,但是注入到Spring Ioc容器中的是包装了Mapper的MapperFactoryBean,而如果某个Bean被定义成FactoryBean的话,在通过@Autowired方式注入的时候会在AutowiredAnnotationBeanPostProcessor#postProcessProperties注入:

    public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
        InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
        try {
            metadata.inject(bean, beanName, pvs);
        }
        catch (BeanCreationException ex) {
            throw ex;
        }
        catch (Throwable ex) {
            throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex);
        }
        return pvs;
    }
    

        具体注入原理和时机此篇不做描述,注入的时候会调用getObject方法返回原始bean.

        Mapper的注册流程如下:


    image.png

    2.XML操作指令解析

        前边分析了Mapper的注册流程和原理,但是发现Mapper注册成MapperFactoryBean之后并没有和具体的数据操作指令关联起来,如果不关联只是一个孤零零的空壳子Mapper肯定无法实现数据操作,既然我们基于mybatis-spring-boot-starter实现和分析,那么基本确定里边肯定会有一些配置,没错那就是MybatisAutoConfiguration:

    @org.springframework.context.annotation.Configuration
    @ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
    @ConditionalOnSingleCandidate(DataSource.class)
    @EnableConfigurationProperties(MybatisProperties.class)
    @AutoConfigureAfter(DataSourceAutoConfiguration.class)
    public class MybatisAutoConfiguration implements InitializingBean {
    
      @Override
      public void afterPropertiesSet() {
        checkConfigFileExists();
      }
    
      private void checkConfigFileExists() {
        if (this.properties.isCheckConfigLocation() && StringUtils.hasText(this.properties.getConfigLocation())) {
          Resource resource = this.resourceLoader.getResource(this.properties.getConfigLocation());
          Assert.state(resource.exists(), "Cannot find config location: " + resource
              + " (please add config file or check your Mybatis 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.properties.resolveMapperLocations())) {
          factory.setMapperLocations(this.properties.resolveMapperLocations());
        }
    
        return factory.getObject();
      }
      ...
    }
    

        MybatisAutoConfiguration在DataSourceAutoConfiguration数据源配置之后配置,检查有没有mybatis相关属性配置,并且创建SqlSessionFactory,SqlSessionFactory是通过SqlSessionFactoryBean配置生成的,看一下实现:


    SqlSessionFactoryBean.png
    public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {
    
      @Override
      public void afterPropertiesSet() throws Exception {
        notNull(dataSource, "Property 'dataSource' is required");
        notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
        state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
                  "Property 'configuration' and 'configLocation' can not specified with together");
    
        this.sqlSessionFactory = buildSqlSessionFactory();
      }
    
      @Override
      public SqlSessionFactory getObject() throws Exception {
        if (this.sqlSessionFactory == null) {
          afterPropertiesSet();
        }
    
        return this.sqlSessionFactory;
      }
    
      protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
    
        final Configuration targetConfiguration;
    
        XMLConfigBuilder xmlConfigBuilder = null;
        if (this.configuration != null) {
          targetConfiguration = this.configuration;
          if (targetConfiguration.getVariables() == null) {
            targetConfiguration.setVariables(this.configurationProperties);
          } else if (this.configurationProperties != null) {
            targetConfiguration.getVariables().putAll(this.configurationProperties);
          }
        } else if (this.configLocation != null) {
          xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
          targetConfiguration = xmlConfigBuilder.getConfiguration();
        } else {
          LOGGER.debug(() -> "Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
          targetConfiguration = new Configuration();
          Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables);
        }
    
        Optional.ofNullable(this.objectFactory).ifPresent(targetConfiguration::setObjectFactory);
        Optional.ofNullable(this.objectWrapperFactory).ifPresent(targetConfiguration::setObjectWrapperFactory);
        Optional.ofNullable(this.vfs).ifPresent(targetConfiguration::setVfsImpl);
    
        if (hasLength(this.typeAliasesPackage)) {
          scanClasses(this.typeAliasesPackage, this.typeAliasesSuperType)
              .forEach(targetConfiguration.getTypeAliasRegistry()::registerAlias);
        }
    
        if (!isEmpty(this.typeAliases)) {
          Stream.of(this.typeAliases).forEach(typeAlias -> {
            targetConfiguration.getTypeAliasRegistry().registerAlias(typeAlias);
            LOGGER.debug(() -> "Registered type alias: '" + typeAlias + "'");
          });
        }
    
        if (!isEmpty(this.plugins)) {
          Stream.of(this.plugins).forEach(plugin -> {
            targetConfiguration.addInterceptor(plugin);
            LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");
          });
        }
    
        if (hasLength(this.typeHandlersPackage)) {
          scanClasses(this.typeHandlersPackage, TypeHandler.class).stream()
              .filter(clazz -> !clazz.isInterface())
              .filter(clazz -> !Modifier.isAbstract(clazz.getModifiers()))
              .filter(clazz -> ClassUtils.getConstructorIfAvailable(clazz) != null)
              .forEach(targetConfiguration.getTypeHandlerRegistry()::register);
        }
    
        if (!isEmpty(this.typeHandlers)) {
          Stream.of(this.typeHandlers).forEach(typeHandler -> {
            targetConfiguration.getTypeHandlerRegistry().register(typeHandler);
            LOGGER.debug(() -> "Registered type handler: '" + typeHandler + "'");
          });
        }
    
        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();
          }
        }
    
        targetConfiguration.setEnvironment(new Environment(this.environment,
            this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory,
            this.dataSource));
    
        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 {
                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);
      }
    
    }
    

        SqlSessionFactoryBean也是一个FactoryBean,通过getObject方法返回SqlSessionFactory,SqlSessionFactoryBean实现了InitializingBean接口通过afterPropertiesSet方法创建SqlSessionFactory,在buildSqlSessionFactory方法中执行了解析xml的逻辑:

      try {
        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();
      }
    

        继续看XMLMapperBuilder#parse实现:

      public void parse() {
        if (!configuration.isResourceLoaded(resource)) {
          configurationElement(parser.evalNode("/mapper"));
          configuration.addLoadedResource(resource);
          bindMapperForNamespace();
        }
    
        parsePendingResultMaps();
        parsePendingCacheRefs();
        parsePendingStatements();
      }
    

        先检查xml资源是否解析加载,如果没有则执行解析并置为加载,并执行绑定操作,然后解析通过集成用到的父文件的节点信息,我们主要看看configurationElement和bindMapperForNamespace, 先看configurationElement:

      private void configurationElement(XNode context) {
        try {
          String namespace = context.getStringAttribute("namespace");
          if (namespace == null || namespace.equals("")) {
            throw new BuilderException("Mapper's namespace cannot be empty");
          }
          builderAssistant.setCurrentNamespace(namespace);
          cacheRefElement(context.evalNode("cache-ref"));
          cacheElement(context.evalNode("cache"));
          parameterMapElement(context.evalNodes("/mapper/parameterMap"));
          resultMapElements(context.evalNodes("/mapper/resultMap"));
          sqlElement(context.evalNodes("/mapper/sql"));
          buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
        } catch (Exception e) {
          throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
        }
      }
    

        获取namespace属性,也就是对应的Mapper接口全路径,然后解析缓存相关节点/ResultMap节点/ParameterMap节点/sql片段,最后解析sql操作指令:

      private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
        for (XNode context : list) {
          final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
          try {
            statementParser.parseStatementNode();
          } catch (IncompleteElementException e) {
            configuration.addIncompleteStatement(statementParser);
          }
        }
      }
    

        获取sql指令节点并执行解析:

      public void parseStatementNode() {
        String id = context.getStringAttribute("id");
        String databaseId = context.getStringAttribute("databaseId");
        if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
          return;
        }
        String nodeName = context.getNode().getNodeName();
        SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
        boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
        boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
        boolean useCache = context.getBooleanAttribute("useCache", isSelect);
        boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
    
        // Include Fragments before parsing
        XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
        includeParser.applyIncludes(context.getNode());
    
        String parameterType = context.getStringAttribute("parameterType");
        Class<?> parameterTypeClass = resolveClass(parameterType);
    
        String lang = context.getStringAttribute("lang");
        LanguageDriver langDriver = getLanguageDriver(lang);
    
        // Parse selectKey after includes and remove them.
        processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
        // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
        KeyGenerator keyGenerator;
        String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
        keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
        if (configuration.hasKeyGenerator(keyStatementId)) {
          keyGenerator = configuration.getKeyGenerator(keyStatementId);
        } else {
          keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
              configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
              ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
        }
        SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
        StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
        Integer fetchSize = context.getIntAttribute("fetchSize");
        Integer timeout = context.getIntAttribute("timeout");
        String parameterMap = context.getStringAttribute("parameterMap");
        String resultType = context.getStringAttribute("resultType");
        Class<?> resultTypeClass = resolveClass(resultType);
        String resultMap = context.getStringAttribute("resultMap");
        String resultSetType = context.getStringAttribute("resultSetType");
        ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
        String keyProperty = context.getStringAttribute("keyProperty");
        String keyColumn = context.getStringAttribute("keyColumn");
        String resultSets = context.getStringAttribute("resultSets");
    
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
            fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
            resultSetTypeEnum, flushCache, useCache, resultOrdered,
            keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
      }
    

        把sql指令节点的属性和内容解析完成之后调用addMappedStatement方法转换成指令对象并维护id与其映射关系:

      public MappedStatement addMappedStatement(
          String id,
          SqlSource sqlSource,
          StatementType statementType,
          SqlCommandType sqlCommandType,
          Integer fetchSize,
          Integer timeout,
          String parameterMap,
          Class<?> parameterType,
          String resultMap,
          Class<?> resultType,
          ResultSetType resultSetType,
          boolean flushCache,
          boolean useCache,
          boolean resultOrdered,
          KeyGenerator keyGenerator,
          String keyProperty,
          String keyColumn,
          String databaseId,
          LanguageDriver lang,
          String resultSets) {
    
        if (unresolvedCacheRef) {
          throw new IncompleteElementException("Cache-ref not yet resolved");
        }
        id = applyCurrentNamespace(id, false);
        boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
        MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
            .resource(resource)
            .fetchSize(fetchSize)
            .timeout(timeout)
            .statementType(statementType)
            .keyGenerator(keyGenerator)
            .keyProperty(keyProperty)
            .keyColumn(keyColumn)
            .databaseId(databaseId)
            .lang(lang)
            .resultOrdered(resultOrdered)
            .resultSets(resultSets)
            .resultMaps(getStatementResultMaps(resultMap, resultType, id))
            .resultSetType(resultSetType)
            .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
            .useCache(valueOrDefault(useCache, isSelect))
            .cache(currentCache);
    
        ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
        if (statementParameterMap != null) {
          statementBuilder.parameterMap(statementParameterMap);
        }
        MappedStatement statement = statementBuilder.build();
        configuration.addMappedStatement(statement);
        return statement;
      }
    

        组装成MappedStatement之后放到内部维护的Map<String, MappedStatement>中备用.

        然后看bindMapperForNamespace方法:

      private void bindMapperForNamespace() {
        String namespace = builderAssistant.getCurrentNamespace();
        if (namespace != null) {
          Class<?> boundType = null;
          try {
            boundType = Resources.classForName(namespace);
          } catch (ClassNotFoundException e) {
            //ignore, bound type is not required
          }
          if (boundType != null) {
            if (!configuration.hasMapper(boundType)) {
              // Spring may not know the real resource name so we set a flag
              // to prevent loading again this resource from the mapper interface
              // look at MapperAnnotationBuilder#loadXmlResource
              configuration.addLoadedResource("namespace:" + namespace);
              configuration.addMapper(boundType);
            }
          }
        }
      }
    

        加载namespace指定的Mapper接口并添加到ConfigurationMapperRegistry中:

      public <T> void addMapper(Class<T> type) {
        mapperRegistry.addMapper(type);
      }
    

        接着看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));
            // It's important that the type is added before the parser is run
            // otherwise the binding may automatically be attempted by the
            // mapper parser. If the type is already known, it won't try.
            MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
            parser.parse();
            loadCompleted = true;
          } finally {
            if (!loadCompleted) {
              knownMappers.remove(type);
            }
          }
        }
      }
    

        namespace指定的类是接口才处理,如果已经添加过了那么就报错终止(开发阶段可能不同的xml把namespace写成同一个接口),然后为Mapper创建MapperProxyFactory并添加到MapperRegistry的map中,然后创建MapperAnnotationBuilder解析Mapper接口方法上的注解,如果方法上使用了@Select/@Insert/@Update/@Delete,并且也同事使用了xml定义,那么会优先使用注解方式,也看一下MapperProxyFactory实现:

    public class MapperProxyFactory<T> {
    
      private final Class<T> mapperInterface;
      private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();
    
      public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
      }
    
      public Class<T> getMapperInterface() {
        return mapperInterface;
      }
    
      public Map<Method, MapperMethod> getMethodCache() {
        return methodCache;
      }
    
      @SuppressWarnings("unchecked")
      protected T newInstance(MapperProxy<T> mapperProxy) {
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
      }
    
      public T newInstance(SqlSession sqlSession) {
        final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
      }
    
    }
    

        其实就是对于Mapper接口的基于jdk动态代理实现,动态代理的InvocationHandler是MapperProxy,具体动态代理的实现和调用链路我们接下来分析.

        XML解析流程如下:

    image.png

    3.Mapper注入&使用

        前边有讲述到Mapper会被解析成MapperFactoryBean注册到容器中,在使用的时候通过@Autowired注入会调用MapperFactoryBean#getObject方法返回:

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

        然后会调用到SqlSession的默认实现类DefaultSqlSession#getMapper方法:

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

        接着调用Configuration#getMapper方法:

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

        继续调用MapperRegistry#getMapper方法:

      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,通过newInstance动态代理返回Mapper接口实例:

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

        前边有描述到MapperProxy是一个InvocationHandler,继续看重载方法newInstance:

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

        看到这里是不是眼前一亮,没错这就是jdk自带的动态代理,返回Mapper的动态代理类,那么动态代理类实例调用的时候会调用InvocationHandler的invoke方法,我们继续看MapperProxy的invoke实现:

      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
          if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
          } else {
            return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
          }
        } catch (Throwable t) {
          throw ExceptionUtil.unwrapThrowable(t);
        }
      }
    

        如果Mapper是类则走if逻辑,如果是接口走else逻辑调用cachedInvoker..invoke,先看cachedInvoker实现:

      private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
        try {
          MapperMethodInvoker invoker = methodCache.get(method);
          if (invoker != null) {
            return invoker;
          }
          return methodCache.computeIfAbsent(method, m -> {
            if (m.isDefault()) {
              try {
                if (privateLookupInMethod == null) {
                  return new DefaultMethodInvoker(getMethodHandleJava8(method));
                } else {
                  return new DefaultMethodInvoker(getMethodHandleJava9(method));
                }
              } catch (IllegalAccessException | InstantiationException | InvocationTargetException
                  | NoSuchMethodException e) {
                throw new RuntimeException(e);
              }
            } else {
              return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
            }
          });
        } catch (RuntimeException re) {
          Throwable cause = re.getCause();
          throw cause == null ? re : cause;
        }
      }
    

        如果方法调用命中缓存,直接返回invoker,否则构造invoker放入缓存并返回,从而避免每次Mapper的方法调用都要解析构造method与具体sql的映射关系,如果方法是接口默认方法,这里的实现区分了java8和java9以上版本,主要是为了使用MethodHandle这种比传统反射更高效的方式来实现方法调用,最后封装成DefaultMethodInvoker并返回:

      private static class DefaultMethodInvoker implements MapperMethodInvoker {
        private final MethodHandle methodHandle;
    
        public DefaultMethodInvoker(MethodHandle methodHandle) {
          super();
          this.methodHandle = methodHandle;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
          return methodHandle.bindTo(proxy).invokeWithArguments(args);
        }
      }
    

        否则是接口抽象方法那么久封装成PlainMethodInvoker并返回:

      private static class PlainMethodInvoker implements MapperMethodInvoker {
        private final MapperMethod mapperMethod;
    
        public PlainMethodInvoker(MapperMethod mapperMethod) {
          super();
          this.mapperMethod = mapperMethod;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
          return mapperMethod.execute(sqlSession, args);
        }
      }
    

        很明显我们数据库操作依赖很多外部资源,基本都是定义抽象方法,然后实现交给xml的具体sql,回到前边的invoker,最终调用的是MapperMethod的execute方法:

     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);
              if (method.returnsOptional()
                  && (result == null || !method.getReturnType().equals(result.getClass()))) {
                result = Optional.ofNullable(result);
              }
            }
            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;
      }
    

        这里就看到了熟悉的crud相关指令实现,那insert操作为例:

    case INSERT: {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.insert(command.getName(), param));
            break;
          }
    

        会调用DefaultSqlSession的insert方法并组装结果:

      public int update(String statement, Object parameter) {
        try {
          dirty = true;
          MappedStatement ms = configuration.getMappedStatement(statement);
          return executor.update(ms, wrapCollection(parameter));
        } catch (Exception e) {
          throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
        } finally {
          ErrorContext.instance().reset();
        }
      }
    

        从Configuration的mappedStatements中获取对应sql并执行:

      public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
        Statement stmt = null;
        try {
          Configuration configuration = ms.getConfiguration();
          StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
          stmt = prepareStatement(handler, ms.getStatementLog());
          return handler.update(stmt);
        } finally {
          closeStatement(stmt);
        }
      }
    

        最终会调用到jdbc定义的Statement#execute,然后返回结果.

        Mapper注入和调用流程如下:

    image.png
    image.png

    三、总结

        mybatis使用抽象一下有几个点:

    1. XML文件解析,sql指令解析
    2. Mapper接口扫描并注册到容器
    3. Mapper接口注入以及与sql指令绑定
    4. Mapper接口动态代理与解析执行

    相关文章

      网友评论

          本文标题:Mybatis原理分析

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