Spring&Mybaits資料庫配置解惑

weixin_34146805發表於2018-07-14

一、前言

一般我們會在datasource.xml中進行如下配置,但是其中每個配置項原理和用途是什麼,並不是那麼清楚,如果不清楚的話,在使用時候就很有可能會遇到坑,所以下面對這些配置項進行一一解說

(1)配置資料來源
?xml version="1.0" encoding="UTF-8" standalone="no"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd   
                        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd   
                        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd   
                        http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-4.0.xsd   
                        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">

    <!-- (1) 資料來源 -->
    <bean id="dataSource"
        class="com.alibaba.druid.pool.DruidDataSource" init-method="init"
        destroy-method="close">
        <property name="driverClassName"
            value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://127.0.0.1:3306/test" />
        <property name="username" value="root" />
        <property name="password" value="123456" />
        <property name="maxWait" value="3000" />
        <property name="maxActive" value="28" />
        <property name="initialSize" value="2" />
        <property name="minIdle" value="0" />
        <property name="timeBetweenEvictionRunsMillis" value="300000" />
        <property name="testOnBorrow" value="false" />
        <property name="testWhileIdle" value="true" />
        <property name="validationQuery" value="select 1 from dual" />
        <property name="filters" value="stat" />
    </bean>

    <!-- (2) session工廠 -->
    <bean id="sqlSessionFactory"
        class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="mapperLocations"
            value="classpath*:mapper/*Mapper*.xml" />
        <property name="dataSource" ref="dataSource" />
    </bean>

    <!-- (3) 配置掃描器,掃描指定路徑的mapper生成資料庫操作代理類 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="annotationClass"
            value="javax.annotation.Resource"></property>
        <property name="basePackage" value="com.zlx.user.dal.sqlmap" />
        <property name="sqlSessionFactory" ref="sqlSessionFactory" />
    </bean>


</beans>
  • 其中(1)是配置資料來源,這裡使用了druid連線池,使用者可以根據自己的需要配置不同的資料來源,也可以選擇不適用資料庫連線池,而直接使用具體的物理連線。

  • 其中(2)建立sqlSessionFactory,用來在(3)時候使用。

  • 其中(3)配置掃描器,掃描指定路徑的mapper生成資料庫操作代理類

二、SqlSessionFactory內幕

第二節配置中配置SqlSessionFactory的方式如下:

<!-- (2) session工廠 -->
    <bean id="sqlSessionFactory"
        class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="mapperLocations"
            value="classpath*:mapper/*Mapper*.xml" />
        <property name="dataSource" ref="dataSource" />
    </bean>

其中mapperLocations配置mapper.xml檔案所在的路徑,dataSource配置資料來源,下面我們具體來看SqlSessionFactoryBean的程式碼,SqlSessionFactoryBean實現了FactoryBean和InitializingBean擴充套件介面,所以具有getObject和afterPropertiesSet方法(具體可以參考:https://gitbook.cn/gitchat/activity/5a84589a1f42d45a333f2a8e),下面我們從時序圖具體看這兩個方法內部做了什麼:

5879294-11d42ad8bca3e59d
enter image description here

如上時序圖其中步驟(2)程式碼如下:

 protected SqlSessionFactory buildSqlSessionFactory() throws IOException {

    Configuration configuration;

    XMLConfigBuilder xmlConfigBuilder = null;
    ...
    //(3.1)
    if (this.transactionFactory == null) {
      this.transactionFactory = new SpringManagedTransactionFactory();
    }
    //(3.2)
    configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));
    //(3.3)
    if (!isEmpty(this.mapperLocations)) {
      for (Resource mapperLocation : this.mapperLocations) {
        if (mapperLocation == null) {
          continue;
        }

        try {
          XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
              configuration, mapperLocation.toString(), configuration.getSqlFragments());
          xmlMapperBuilder.parse();
        } catch (Exception e) {
          throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
        } finally {
          ErrorContext.instance().reset();
        }

        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Parsed mapper file: '" + mapperLocation + "'");
        }
      }
    } else {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Property 'mapperLocations' was not specified or no matching resources found");
      }
    }
   //3.9
    return this.sqlSessionFactoryBuilder.build(configuration);
  }
  • 如上程式碼(3.1)建立了一個Spring事務管理工廠,這個後面會用到。

  • 程式碼(3.2)設定configuration物件的環境變數,其中dataSource為demo中配置檔案中建立的資料來源。

  • 程式碼(3.3)中mapperLocations是一個陣列,為demo中配置檔案中配置的滿足classpath:mapper/Mapper*.xml條件的mapper.xml檔案,本demo會發現存在
    [file[/Users/zhuizhumengxiang/workspace/mytool/distributtransaction/transactionconfig/transaction-demo/deep-learn-java/Start/target/classes/mapper/CourseDOMapper.xml],
    file[/Users/zhuizhumengxiang/workspace/mytool/distributtransaction/transactionconfig/transaction-demo/deep-learn-java/Start/target/classes/mapper/UserDOMapper.xml]] 兩個檔案

程式碼(3.3)迴圈遍歷每個mapper.xml,然後呼叫XMLMapperBuilder的parse方法進行解析。

XMLMapperBuilder的parse程式碼中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);
      ...
      //(3.4)
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      //(3.5)
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      //(3.6)
      sqlElement(context.evalNodes("/mapper/sql"));
      //(3.7)
      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);
    }
  • 程式碼(3.4)解析mapper.xml中/mapper/parameterMap標籤下內容,本demo中的XML檔案中沒有配置這個。

  • 程式碼(3.5)解析mapper.xml中/mapper/resultMap標籤下內容,然後存放到Configuration物件的resultMaps快取裡面,這裡需要提一下,所有的mapper.xml檔案共享一個Configuration物件,所有mapper.xml裡面的resultMap都存放到同一個Configuration物件的resultMaps裡面,其中key為mapper檔案的namespace和resultMap的id組成,比如UserDoMapper.xml:

<mapper namespace="com.zlx.user.dal.sqlmap.UserDOMapper" >
  <resultMap id="BaseResultMap" type="com.zlx.user.dal.dao.UserDO" >
    <id column="id" property="id" jdbcType="INTEGER" />
    <result column="age" property="age" jdbcType="INTEGER" />
  </resultMap>

其中key為com.zlx.user.dal.sqlmap.CourseDOMapper.BaseResultMap,value則為存放一個map,map裡面是column與property的對映。

  • 程式碼(3.6)解析mapper.xml中/mapper/sql下的內容,然後儲存到Configuration物件的sqlFragments快取中,sqlFragments也是一個map,比如UserDoMapper.xml中的一個sql標籤:
<sql id="Base_Column_List" >
    id, age
</sql>

其中key為com.zlx.user.dal.sqlmap.CourseDOMapper.Base_Column_List,value作為一個記錄sql標籤內容的XNode節點。

  • 程式碼(3.7)解析mapper.xml中select|insert|update|delete增刪改查的語句,並封裝為MappedStatement物件儲存到Configuration的mappedStatements快取中,mappedStatements也是一個map結構,比如:比如UserDoMapper.xml中的一個select標籤:

<select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
    select 
    <include refid="Base_Column_List" />
    from user
    where id = #{id,jdbcType=INTEGER}
  </select>

其中key為com.zlx.user.dal.sqlmap.CourseDOMapper.selectByPrimaryKey,value為標籤內封裝為MappedStatement的物件。

至此configurationElement解析XML的步驟完畢了,下面我們看時序圖中步驟(12)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)) {
          //(3.8)
          configuration.addLoadedResource("namespace:" + namespace);
          configuration.addMapper(boundType);
        }
      }
    }
  }

其中程式碼(3.8)註冊mapper介面的Class物件到configuration中的mapperRegistry管理的快取knownMappers中,knownMappers是個map,其中key為具體mapper介面的Class物件,value為mapper介面的代理物件MapperProxyFactory。

注:SqlSessionFactoryBean作用之一是掃描配置的mapperLocations路徑下的所有mapper.xml 檔案,並對其進行解析,然後把解析的所有mapper檔案的資訊儲存到一個全域性的configuration物件的具體快取中,然後註冊每個mapper.xml對應的介面類到configuration中,併為每個介面類生成了一個代理bean.

然後時序圖步驟15建立了一DefaultSqlSessionFactory物件,並且傳遞了上面全域性的configuration物件。

步驟16則返回建立的DefaultSqlSessionFactory物件。

三、MapperScannerConfigurer內幕

第二節中MapperScannerConfigurer的配置方式如下:

    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="annotationClass"
            value="javax.annotation.Resource"></property>
        <property name="basePackage" value="com.zlx.user.dal.sqlmap" />
        <property name="sqlSessionFactory" ref="sqlSessionFactory" />
    </bean>

其中sqlSessionFactory設定為第4節建立的DefaultSqlSessionFactory,basePackage為mapper介面類所在目錄,annotationClass這是為註解@Resource,後面會知道標示只掃描basePackage路徑下標註@Resource註解的mapper介面類。

MapperScannerConfigurer 實現了 BeanDefinitionRegistryPostProcessor, InitializingBean介面,所以會重寫下面方法:

(5.1)
//在bean註冊到ioc後建立例項前修改bean定義和新增bean註冊,這個是在context的refresh方法被呼叫
void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException;

(5.2)
//set屬性設定後被呼叫
void afterPropertiesSet() throws Exception;

更多關於Spring擴充套件介面的知識可以移步(https://gitbook.cn/gitchat/activity/5a84589a1f42d45a333f2a8e

下面我們從時序圖看這看postProcessBeanDefinitionRegistry和afterPropertiesSet擴充套件介面裡面都做了些什麼:


5879294-e0377d1b319d5e08
enter image description here

其中afterPropertiesSet程式碼如下:

  public void afterPropertiesSet() throws Exception {
    notNull(this.basePackage, "Property 'basePackage' is required");
  }

可知是校驗basePackage是否為null,為null會丟擲異常。因為MapperScannerConfigurer作用就是掃描basePackage路徑下的mapper介面類然後生成代理,所以不允許basePackage為null。

postProcessBeanDefinitionRegistry的程式碼如下:

public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    if (this.processPropertyPlaceHolders) {
      processPropertyPlaceHolders();
    }

    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
    ...
    //5.3
    scanner.setAnnotationClass(this.annotationClass);

    //5.4
    scanner.setSqlSessionFactory(this.sqlSessionFactory);
    ...
    //5.5
    scanner.registerFilters();
    //5.6
  scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
 }
  • 程式碼(5.3)設定註解類,這裡設定的為@Resource註解,(5.4)設定sqlSessionFactory到ClassPathMapperScanner。

  • 程式碼(5.5)根據設定的@Resource設定過濾器,程式碼如下:

public void registerFilters() {
    boolean acceptAllInterfaces = true;

    if (this.annotationClass != null) {
      addIncludeFilter(new AnnotationTypeFilter(this.annotationClass));
      acceptAllInterfaces = false;
    }

    ...
  }

public void addIncludeFilter(TypeFilter includeFilter) {
    this.includeFilters.add(includeFilter);
  }

可知具體是把@Resource註解作為了一個過濾器

  • 程式碼(5.6)具體執行掃描,其中basePackage為我們設定的com.zlx.user.dal.sqlmap,basePackage設定的時候允許設定多個包路徑並且使用 ,; \t\n進行分割,加上上面的過濾條件,就是說對basePackage路徑下標註@Resource註解的mapper介面類進行代理。

具體執行掃描的是doScan方法,其程式碼如下:

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
        Assert.notEmpty(basePackages, "At least one base package must be specified");
        Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
        for (String basePackage : basePackages) {
        //具體掃描符合條件的bean
            Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
            for (BeanDefinition candidate : candidates) {
                ...
                if (checkCandidate(beanName, candidate)) {
                    BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
                    definitionHolder =
                            AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
                    beanDefinitions.add(definitionHolder);
                    //註冊到IOC容器
                    registerBeanDefinition(definitionHolder, this.registry);
                }
            }
        }
        return beanDefinitions;
}

如上程式碼可知是對每個包路徑分別進行掃描,然後對符合條件的介面bean註冊到IOC容器。

這裡我們看下findCandidateComponents的邏輯:

private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
        Set<BeanDefinition> candidates = new LinkedHashSet<>();
        try {
            //5.8
            String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
                    resolveBasePackage(basePackage) + '/' + this.resourcePattern;
            Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
            ...
            //5.9
            for (Resource resource : resources) {
                if (traceEnabled) {
                    logger.trace("Scanning " + resource);
                }
                if (resource.isReadable()) {
                    try {
                        //5.10
                        MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
                        if (isCandidateComponent(metadataReader)) {
                            ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
                            sbd.setResource(resource);
                            sbd.setSource(resource);
                            if (isCandidateComponent(sbd)) {
                                //5.11
                                candidates.add(sbd);
                            }
                            else {
                                
                            }
                        }
                        ...
                    }
                    ...
                }
                ...
            }
        }
        ...
        return candidates;
    }

如上程式碼其中(5.8)是根據我們設定的basePackage得到一個掃描路徑,這裡根據我們demo設定的值,拼接後packageSearchPath為classpath*:com/zlx/user/dal/sqlmap/**/*.class,這裡掃描出來的檔案為:

file[/Users/zhuizhumengxiang/workspace/mytool/distributtransaction/transactionconfig/transaction-demo/deep-learn-java/Start/target/classes/com/zlx/user/dal/sqlmap/CourseDOMapper.class]
file[/Users/zhuizhumengxiang/workspace/mytool/distributtransaction/transactionconfig/transaction-demo/deep-learn-java/Start/target/classes/com/zlx/user/dal/sqlmap/CourseDOMapperNoAnnotition.class]
file[/Users/zhuizhumengxiang/workspace/mytool/distributtransaction/transactionconfig/transaction-demo/deep-learn-java/Start/target/classes/com/zlx/user/dal/sqlmap/UserDOMapper.class]

然後isCandidateComponent方法執行具體對上面掃描到的檔案進行過濾,其程式碼:

protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
        ...
        for (TypeFilter tf : this.includeFilters) {
            if (tf.match(metadataReader, getMetadataReaderFactory())) {
                return isConditionMatch(metadataReader);
            }
        }
        return false;
}

上面我們講解過新增了一個@Resource註解的過濾器,這裡執行時候器match方法如下:

public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory)
            throws IOException {

        if (matchSelf(metadataReader)) {
            return true;
        }
        
        ...
        return false;

}
    //判斷介面類是否有@Resource註解
    protected boolean matchSelf(MetadataReader metadataReader) {
        AnnotationMetadata metadata = metadataReader.getAnnotationMetadata();
        return metadata.hasAnnotation(this.annotationType.getName()) ||
                (this.considerMetaAnnotations && metadata.hasMetaAnnotation(this.annotationType.getName()));
    }

經過過濾後CourseDOMapperNoAnnotition.class介面類被過濾了,因為其沒有標註@Resource註解。只有CourseDOMapper和UserDOMapper兩個標註@Resource的類註冊到了IOC容器。

如上時序圖註冊後,還需要執行processBeanDefinitions對滿足過濾條件的CourseDOMapper和UserDOMapper的bean定義進行修改,以便生成代理類,processBeanDefinitions程式碼如下:

 private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
    GenericBeanDefinition definition;
    for (BeanDefinitionHolder holder : beanDefinitions) {
      definition = (GenericBeanDefinition) holder.getBeanDefinition();

      // (5.12)
      definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59
      definition.setBeanClass(this.mapperFactoryBean.getClass());

     ...
     //5.13
     if (this.sqlSessionFactory != null) {
        definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
        explicitFactoryUsed = true;
      }
     ...
    }
  }

如上程式碼(5.12)修改bean定義的BeanClass為MapperFactoryBean,然後設定MapperFactoryBean的泛型建構函式引數為真正的被代理介面。也就是如果當前bean定義是com.zlx.user.dal.sqlmap.CourseDOMapper介面的,則設定當前bean定義的BeanClass為MapperFactoryBean,並設定com.zlx.user.dal.sqlmap.CourseDOMapper為MapperFactoryBean的建構函式引數。

程式碼(5.13)設定session工廠到bean定義。

注:MapperScannerConfigurer的作用是掃描指定路徑下的Mapper介面類,並且可以制定過濾策略,然後對符合條件的bean定義進行修改以便在bean建立時候生成代理類,最終符合條件的mapper介面都會被轉換為MapperFactoryBean,MapperFactoryBean中並且維護了第4節生成的DefaultSqlSessionFactory。

最後

更多本地事務諮詢可以單擊我
更多分散式事務諮詢可以單擊我
更多Spring事務配置解惑單擊我

想了解更多關於粘包半包問題單擊我
更多關於分散式系統中服務降級策略的知識可以單擊 單擊我
想系統學dubbo的單擊我
想學併發的童鞋可以 單擊我

相關文章