美文网首页
Spring&Mybaits数据库配置解惑

Spring&Mybaits数据库配置解惑

作者: 阿里加多 | 来源:发表于2018-07-14 14:09 被阅读149次

    一、前言

    一般我们会在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),下面我们从时序图具体看这两个方法内部做了什么:

    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扩展接口里面都做了些什么:


    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的单击我
    想学并发的童鞋可以 单击我

    相关文章

      网友评论

          本文标题:Spring&Mybaits数据库配置解惑

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