Spring&Mybaits数据库配置解惑

一、前言

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • 驱动对象,设备对象,IRP之间的关系? 类似于程序,窗口,消息三者之间的关系; 每个驱动程序只有一个驱动对象(程序...
    bluewind1230阅读 356评论 0 0
  • 紫微书院 ——上阳宫的城市书房 ...
    拙兰阅读 1,011评论 3 3
  • 2.2 数据挖掘 PLA算法 Python代码 程序运行结果如下: ('final W is :', array(...
    01_小小鱼_01阅读 715评论 0 0