spring自动扫描bean的原理

  • 目的
  • 源码
    • BeanFactoryPostProcessor和BeanDefinitionRegistryPostProcessor
    • @ComponentScan注解的解析

目的

在spring应用中,通过@ComponentScan注解可以声明自动注入要扫描的包,spring会将该包下所有加了@Component注解的bean扫描到spring容器中,本文是spring根据包名进行扫描,获取要注入的业务类对应的源码

源码


public void refresh() throws BeansException, IllegalStateException {synchronized (this.startupShutdownMonitor) {//准备工作包括设置启动时间、是否激活标志位、初始化属性源配置prepareRefresh();/*** 初始化BeanFactory,解析xml格式的配置文件* xml格式的配置,是在这个方法中扫描到beanDefinitionMap中的* 在org.springframework.web.context.support.XmlWebApplicationContext#loadBeanDefinitions(org.springframework.beans.factory.support.DefaultListableBeanFactory)中会创建一个XmlBeanDefinitionReader来解析xml文件* 会把bean.xml解析成一个InputStream,然后再解析成document格式* 按照document格式解析,从root节点进行解析,判断root节点是bean?还是beans?还是import等,如果是bean* 就把解析到的信息包装成beanDefinitionHolder,然后调用DefaultListablebeanFactory的注册方法将bean放到beanDefinitionMap中*/ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();//准备工厂,给BeanFactory设置属性、添加后置处理器等prepareBeanFactory(beanFactory);try {//空方法,留给子类去自定义postProcessBeanFactory(beanFactory);/*** TODO* 完成对bean的扫描,将class变成beanDefinition,并将beanDefinition存到map中** 该方法在spring的环境中执行已经被注册的factory processors;* 执行自定义的processBeanFactory** 在这个方法中,注入bean,分为了三种* 一、普通bean:@Component注解的bean* spring 自己的类,不借助spring扫描,会直接放到beanDefinitionMap** 1.获取到所有的beanFactoryPostProcessor* 2.执行 bean后置处理器的postProcessBeanFactory(configurationClassPostProcessor),该方法会把beanFactory作为入参传到方法里面* 3.从beanFactory中获取到所有的beanName   打断点看一下 org.springframework.context.annotation .ConfigurationClassPostProcessor#processConfigBeanDefinitions** 4.然后将所有的bean包装成beanDefinitionHolder,在后面又根据beanName和bean的metadata包装成了ConfigurationClass* 5.把所有包含@ComponentScan的类取出来,遍历每一个componentScan,调用 ClassPathBeanDefinitionScanner.doScan(basePackages)方法* 6.在doScan方法中,会遍历basePackages,因为一个ComponentScan中可以配置多个要扫描的包* 7.获取每个包下面的 *.class文件,registerBeanDefinition(definitionHolder, this.registry); 这个方法底层就是调用org.springframework.beans.factory.support.DefaultListableBeanFactory#registerBeanDefinition方法  把当前bean put到beanDefinitionMap中** 二、是通过@Import注解注入的bean*     ImportBeanDefinitionRegistrar*     ImportSelector** 三、@Bean注解**** spring在把bean注入到beanDefinitionMaps的同时,会将当前beanName添加到一个list中 beanDefinitionNames,这个list和beanDefinitionMap是同时进行添加的,这个list在后面实例化bean的时候有用到,spring是遍历这个list,拿到每个beanName之后,从beanDefinitionMap中取到对应的beanDefinition*/invokeBeanFactoryPostProcessors(beanFactory);/***  注册beanPostProcessor;在方法里面*  会先把beanPostProcessor进行分类,然后按照beanPostProcessor的name从spring容器中获取bean对象,如果spring容器中没有,就创建;所以如果一个beanDefinition是后置处理器,会这这里进行实例化,然后存放到单实例池中*  然后再调用的是 beanFactory.addBeanPostProcessor(postProcessor);* 把所有的beanPostProcessor放到了beanPostProcessors中,在后面初始化bean的时候,如果需要调用后置处理器,就会遍历这个list,*/registerBeanPostProcessors(beanFactory);//初始化MessageSource组件(该组件在spring中用来做国际化、消息绑定、消息解析)initMessageSource();/*** 注册一个多事件派发器* 先从beanFactory获取,如果没有,就创建一个,并将创建的派发器放到beanFactory中*/initApplicationEventMulticaster();/*** 这是一个空方法,在springboot中,如果集成了Tomcat,会在这里new Tomcat(),new DispatcherServlert();*/onRefresh();/*** 注册所有的事件监听器* 将容器中的事件监听器添加到 applicationEventMulticaster 中**/registerListeners();/*** TODO* 完成对bean的实例化** 主要的功能都在这里面*/finishBeanFactoryInitialization(beanFactory);/*** 当容器刷新完成之后,发送容器刷新完成事件* publishEvent(new ContextRefreshedEvent(this));*/finishRefresh();}catch (BeansException ex) {if (logger.isWarnEnabled()) {logger.warn("Exception encountered during context initialization - " +"cancelling refresh attempt: " + ex);}//当发生异常时,调用bean的销毁方法destroyBeans();cancelRefresh(ex);throw ex;}finally {resetCommonCaches();}}
}

在refresh方法中,完成bean的初始化、动态代理、放入容器等操作;我们着重来说spring是如何扫描bean的,invokeBeanFactoryPostProcessors(beanFactory);在该方法中,完成了对所有要注入到spring容器中的业务类的扫描,将业务类转换成beanDefinition,并存入了BeanDefinitionMap中,这就是该方法完成的操作

BeanFactoryPostProcessor和BeanDefinitionRegistryPostProcessor

在自动注入之前,首先,我们要搞懂这两个接口的关系,前者是父接口,后者是子接口,实现了前者;所以,后者既有postProcessBeanFactory()方法,又有postProcessBeanDefinitionRegistry()方法,为什么要说这两个方法呢?因为在spring源码中,这两者的执行是由顺序的;

@ComponentScan注解的解析

1、@ComponentScan注解是加载配置类上的,所以,我们首先要解析配置类
我们前面有说到过,自动扫描是在invokeBeanFactoryPostProcessors(beanFactory);方法中完成的,所以,我们来看这个方法
跳进这个方法之后,代码有很多,隐藏了很多小的知识点,我们不关心,后面会一一的单独拎出来学习,学习自动扫描的解析,我们只需要关心一行代码

invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);

2、这个是执行BeanDefinitionRegistryPostProcessor接口实现类的postProcessBeanDefinitionRegistry()方法的;从spring启动到现在,beanDefinitionMap中的所有bean,BeanDefinitionRegistryPostProcessor接口的实现类,只有一个***ConfigurationClassPostProcessor***;我在前面的文章中说到过,在spring扫描bean的时候,这个类是特别重要的;
所以,执行到这行代码的时候,会执行ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry()方法;在该方法中,前面是一些判断,关键调用的方法是org.springframework.context.annotation.ConfigurationClassPostProcessor#processConfigBeanDefinitions

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {/*** 这个list用来保存*  添加@Configuration的类*    添加了@Component*     或者@ComponentScan*  或者@Import*     或者@ImportResource*     注解的类** 唯一的区别是:如果类上加了@Configuration,对应的ConfigurationClass是full;否则是lite* * 正常情况下,第一次进入到这里的时候,只有配置类一个bean,因为如果是第一次进入到这里的话,beanDefinitionMap中,只有配置类这一个是我们程序员提供的* 业务类,其他的都是spring自带的后置处理器*/List<BeanDefinitionHolder> configCandidates = new ArrayList<>();//获取在 new AnnotatedBeanDefinitionReader(this);中注入的spring自己的beanPostProcessorString[] candidateNames = registry.getBeanDefinitionNames();for (String beanName : candidateNames) {/*** 根据beanName,从beanDefinitionMap中获取beanDefinition*/BeanDefinition beanDef = registry.getBeanDefinition(beanName);/*** 如果bean是配置类,configurationClass就是full,否则就是lite* 这里,如果当前bean 的configurationClass属性已经被设置值了,说明当前bean已经被解析过来,就无需再次解析*/if (ConfigurationClassUtils.isFullConfigurationClass(beanDef) ||ConfigurationClassUtils.isLiteConfigurationClass(beanDef)) {if (logger.isDebugEnabled()) {logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);}}//校验bean是否包含@Configuration  也就是校验bean是哪种配置类?注解?还是普通的配置类else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));}}// Return immediately if no @Configuration classes were foundif (configCandidates.isEmpty()) {return;}// Sort by previously determined @Order value, if applicable/*** 对config类进行 排序   configCandidates中保存的是项目中的配置类 (AppConfig ....),或者说是加了@Configuration注解的类*/configCandidates.sort((bd1, bd2) -> {int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());return Integer.compare(i1, i2);});// Parse each @Configuration class//实例化ConfigurationClassParser是为了解析各个配置类ConfigurationClassParser parser = new ConfigurationClassParser(this.metadataReaderFactory, this.problemReporter, this.environment,this.resourceLoader, this.componentScanBeanNameGenerator, registry);/*** 这两个set主要是为了去重* 正常情况下,下面的do...while循环中,只会循环处理所有的配置类,因为到目前,还没有普通的bean添加到beanDefinitionMap中*/Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());do {/***  如果将bean存入到beanDefinitionMap第三步**  这里的candidates的个数是由项目中 配置文件的数量来决定的(或者说加了@Configuration或者@ComponentScan或者@Component注解的类)*/parser.parse(candidates);parser.validate();Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());configClasses.removeAll(alreadyParsed);// Read the model and create bean definitions based on its contentif (this.reader == null) {this.reader = new ConfigurationClassBeanDefinitionReader(registry, this.sourceExtractor, this.resourceLoader, this.environment,this.importBeanNameGenerator, parser.getImportRegistry());}/*** 这里的configClasses 就是parse方法中,对import注解进行处理时,存入的;* 这里面存放的是import注入类返回数组对象中的bean(就是实现importSelector接口的类中返回的值,也就是要在import中注入的bean对应的全类名)** 在这个方法里面 完成了对ImportSelector和ImportBeanDefinitionRegistrar注入的bean进行初始化*/this.reader.loadBeanDefinitions(configClasses);alreadyParsed.addAll(configClasses);candidates.clear();if (registry.getBeanDefinitionCount() > candidateNames.length) {String[] newCandidateNames = registry.getBeanDefinitionNames();Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));Set<String> alreadyParsedClasses = new HashSet<>();for (ConfigurationClass configurationClass : alreadyParsed) {alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());}for (String candidateName : newCandidateNames) {if (!oldCandidateNames.contains(candidateName)) {BeanDefinition bd = registry.getBeanDefinition(candidateName);if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&!alreadyParsedClasses.contains(bd.getBeanClassName())) {candidates.add(new BeanDefinitionHolder(bd, candidateName));}}}candidateNames = newCandidateNames;}}while (!candidates.isEmpty());
}

3、这个方法中的部分代码,我删除掉了,因为和本次要看的代码无关
该方法中,完成了以下操作

  1. 从beanDefinitionMap中获取到添加了以下注解的类@Configuration、@ComponentScan、@Component、@Import、@ImportResource
  2. 遍历获取到的bean,解析bean中的@ComponentScan注解,根据该注解,将包下的bean,转换成BeanDefinition,并放入到BeanDefinitionMap中
  3. 处理类上通过@Import引入的ImportSelector接口的实现类和ImportBeanDefinitionRegistry接口的实现类
  4. 处理@Bean注解引入的bean
  5. 处理@ImportResource注解

4、我们需要关心的是对@ComponentScan注解的处理:即

parser.parse(candidates);

这行代码,再往下继续处理,嵌套的层次比较深,我录制了一个GIF,可以看下

在这中间的方法,核心的思想是,从beanDefinition中,获取到componentScan注解的value,即:要扫描的包信息,gif的最后一个方法是

org.springframework.context.annotation.ComponentScanAnnotationParser#parse
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {//这里的scanner就是spring自己new出来的,用来扫描包的scannerClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);.......scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) {@Overrideprotected boolean matchClassName(String className) {return declaringClass.equals(className);}});/***  如果将bean存入到beanDefinitionMap第七步*/return scanner.doScan(StringUtils.toStringArray(basePackages));
}

这个方法中,我删除了一部分代码,要说明的有两个问题

  1. 在前面的文章中,有说到,在AnnotaitonConfigApplicationContext的构造方法中,也会初始化一个ClassPathBeanDefinitionScanner();我当时说过,spring在扫描bean的时候,也会初始化一个,和构造方法中,初始化的不是同一个,这里的代码就可以验证这个结论
  2. 在构造方法中,初始化ClassPathBeanDefinitionScanner对象的时候,会更新一个list集合:includeFilters,这个list是在扫描到bean之后,如果当前bean符合includeFilters,就是需要注入的;如果当前bean符合excludeFilters的规则,就无需注入
  3. excludeFilter就是在这里进行add的,这里add的是当前配置类的beanClassName;也就是说,在扫描出来的bean中,剔除当前配置类,因为当前配置类已经在beanDefinitionMap中,无需再次添加

doScan()的源码,我放张截图

在doScan()方法中,完成了以下操作:

  1. 根据basePackage,获取到所有要注入的bean;由于@ComponentScan注解可以配置多个包,所以这里进行了遍历
  2. 获取到所有的beanDefinition之后,根据bean对应的注解,设置beanDefinition中的属性信息
  3. 将beanDefinition存入到beanDefinitionMap中

我们要关注的是第一步这里:findCandidateComponents(basePackage)
这个方法中,就是完成了真正的所谓的自动扫描

private Set<BeanDefinition> scanCandidateComponents(String basePackage) {Set<BeanDefinition> candidates = new LinkedHashSet<>();try {String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +resolveBasePackage(basePackage) + '/' + this.resourcePattern;Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);boolean traceEnabled = logger.isTraceEnabled();boolean debugEnabled = logger.isDebugEnabled();for (Resource resource : resources) {if (traceEnabled) {logger.trace("Scanning " + resource);}// 判断文件是否可读:这里百度的结果是:如果返回true,不一定可读,但是如果返回false,一定不可读if (resource.isReadable()) {try {MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);/*** isCandidateComponent 也是扫描bean中一个比较核心的方法吧* 由于这里的resource是包下所有的class文件,所以,需要在这个方法中判断是否符合注入条件** 在AnnatationConfigApplication构造函数中,初始化了一个ClassPathBeanDefinitionScanner;* 在初始化这个bean的时候,给一个list中存入了三个类,其中有一个就是Component.class,个人理解:在这个方法中,会* 判断扫描出来的class文件是否有Component注解;需要注意的是@Controller @Service @Repository都是被@Component注解修饰的* 所以,@Controller... 这些注解修饰的bean也会被注入到spring容器中* * excludeFilter是在doScan()方法中赋值的,excludeFilter中包含的是当前配置类的beanClassName;因为当前配置类已经存在于beanDefinitionMap中,无需再次添加*/if (isCandidateComponent(metadataReader)) {ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);sbd.setResource(resource);sbd.setSource(resource);/*** 对scannedGenericBeanDefinition进行判断*/if (isCandidateComponent(sbd)) {if (debugEnabled) {logger.debug("Identified candidate component class: " + resource);}candidates.add(sbd);}}} catch (Throwable ex) {throw new BeanDefinitionStoreException("Failed to read candidate component class: " + resource, ex);}}}} catch (IOException ex) {throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);}return candidates;
}

需要注意的是:这里有两个isCandidateComponent方法,但是完成的功能不同

  • isCandidateComponent(metadataReader):这个方法是在未生成beanDefinition对象之前,从includeFilter和excludeFilter中进行过滤
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {for (TypeFilter tf : this.excludeFilters) {if (tf.match(metadataReader, getMetadataReaderFactory())) {return false;}}for (TypeFilter tf : this.includeFilters) {if (tf.match(metadataReader, getMetadataReaderFactory())) {return isConditionMatch(metadataReader);}}return false;}
  • isCandidateComponent(sbd):是在生成beanDefinition对象之后,判断当前bean是否满足注入的要求
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {AnnotationMetadata metadata = beanDefinition.getMetadata();/*** spring在扫描普通bean,是用的这个方法,mybatis对该方法进行了扩展,mybatis判断一个beanDefinition是否要放到beanDefinitionMap中,是判断当前是否是接口,是否是顶级类(org.mybatis.spring.mapper.ClassPathMapperScanner#isCandidateComponent)** isIndependent:当前类是否独立(顶级类或者嵌套类)* isConcrete: 不是接口,不是抽象类,就返回true* 如果bean是抽象类,且添加了@LookUp注解,也可以注入* 使用抽象类+@LookUp注解,可以解决单实例bean依赖原型bean的问题,这里在spring官方文档中应该也有说明*/return (metadata.isIndependent() && (metadata.isConcrete() ||(metadata.isAbstract() && metadata.hasAnnotatedMethods(Lookup.class.getName()))));}

截止到这里,基本上整个流程先串起来了,spring源码中的细节非常多,在完成扫描的过程中,有许多的知识点,后面会一一来介绍,本文只是讲了spring将bean添加到beanDefinitionMap中的一种方式;
spring将bean添加到beanDefinitionMap中有四种方式

  1. 通过@ComponentScan注解和@Component注解的配合使用,就是本文的源码解析中,所解读的
  2. 在配置类中,通过@Bean注解
  3. 通过@Import注解,引入一个ImportSelector的实现类
  4. 通过@Import注解,引入一个ImportBeanDefinitionRegistrar的实现类
  5. 通过@ImportResource注解,引入一个xml配置文件
    后面会对这几种方式进行一一的学习和解读。
org.springframework.context.support.AbstractApplicationContext#invokeBeanFactoryPostProcessorsorg.springframework.context.support.PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors(org.springframework.beans.factory.config.ConfigurableListableBeanFactory, java.util.List<org.springframework.beans.factory.config.BeanFactoryPostProcessor>)org.springframework.context.support.PostProcessorRegistrationDelegate#invokeBeanDefinitionRegistryPostProcessorsorg.springframework.context.annotation.ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistryorg.springframework.context.annotation.ConfigurationClassPostProcessor#processConfigBeanDefinitionsorg.springframework.context.annotation.ConfigurationClassParser#parse(java.util.Set<org.springframework.beans.factory.config.BeanDefinitionHolder>)org.springframework.context.annotation.ConfigurationClassParser#parse(org.springframework.core.type.AnnotationMetadata, java.lang.String)org.springframework.context.annotation.ConfigurationClassParser#doProcessConfigurationClass   org.springframework.context.annotation.ComponentScanAnnotationParser#parseorg.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScanorg.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#findCandidateComponentsorg.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#scanCandidateComponents

这是整个调用链

spring源码:扫描bean的原理相关推荐

  1. 手撸spring源码分析IOC实现原理

    手撸spring源码分析IOC实现原理 文章出处:https://github.com/fuzhengwei/small-spring 根据小付哥的手撸spring核心源码一步步学习出来的结果收货总结 ...

  2. Spring源码解析-bean实例化

    Spring源码解析-bean实例化 ​ 本文介绍Spring创建 bean 过程中的第一个步骤:实例化 bean. 1. Bean实例化源码 ​ 虽然实例化Bean有多种方式(包括静态工厂和工厂实例 ...

  3. Spring 源码解析 - Bean创建过程 以及 解决循环依赖

    一.Spring Bean创建过程以及循环依赖 上篇文章对 Spring Bean资源的加载注册过程进行了源码梳理和解析,我们可以得到结论,资源文件中的 bean 定义信息,被组装成了 BeanDef ...

  4. Spring源码深度解析,Spring源码以及Bean的生命周期(五)(附代码示例:)

    五)Bean 的生命周期,创建---初始化---销毁的过程 目录 五)Bean 的生命周期,创建---初始化---销毁的过程 一 ,  指定初始化方法 init-method 方法​ 二 ,指定销毁 ...

  5. spring源码阅读--aop实现原理分析

    aop实现原理简介 首先我们都知道aop的基本原理就是动态代理思想,在设计模式之代理模式中有介绍过这两种动态代理的使用与基本原理,再次不再叙述. 这里分析的是,在spring中是如何基于动态代理的思想 ...

  6. Spring源码剖析——Bean的配置与启动

    IOC介绍   相信大多数人在学习Spring时 IOC 和 Bean 算得上是最常听到的两个名词,IOC在学习Spring当中出现频率如此之高必然有其原因.如果我们做一个比喻的话,把Bean说成Sp ...

  7. Spring源码之Bean的注册(使用XML配置的方式)

    本文分析的Spring源码是5.2.2版本,使用Gradle进行管理. 一.Bean的注册,先来看通过XML配置Bean的方式 1.配置applicationContext.xml: <?xml ...

  8. spring源码阅读--@Transactional实现原理

    @Transactional注解简介 @Transactional是spring中声明式事务管理的注解配置方式,相信这个注解的作用大家都很清楚.@Transactional注解可以帮助我们把事务开启. ...

  9. Spring源码之Bean的注册(注解方式)

    1.创建AnnotationConfigApplicationContext AnnotationConfigApplicationContext context = new AnnotationCo ...

  10. Spring源码分析——Bean的生命周期

    文章目录 说明 测试代码 说明 本文从源码的角度分析Spring中Bean的加载过程,本文使用的Spring版本为4.3.25.RELEASE 测试代码 测试代码如下,根据这段简单的测试代码,一步步跟 ...

最新文章

  1. h3c telnet
  2. 简单谈谈Docker镜像的使用方法_docker
  3. C++学习——类的初始化
  4. java 蓝桥杯 基础练习 Sine之舞
  5. [深度学习-原理]GAN(生成对抗网络)的简单介绍
  6. 技巧:在Silverlight中如何访问外部xap文件中UserControl
  7. 谷歌大脑2017总结(Jeff Dean执笔,干货满满,值得收藏)
  8. eclipse 自动提示卡断问题
  9. php 可逆加密方法
  10. excel使用教程_Excel教程大合集:史上最全面的Excel视频教程合集+模板,免费送...
  11. 手把手带你将电脑音乐同步到iPhone 音乐
  12. html5家谱制作模板,Word如何做家谱世系图?
  13. 2022年天津仁爱学院专升本化学工程与工艺专业对口专业限制范围
  14. 高德地图有用的API
  15. 这样的钓鱼邮件,你会中招吗?
  16. WER2019上海世界锦标赛
  17. Oracle数据库 SQL语句总结大赏
  18. windows 复制文件夹命令 xcopy .
  19. 离散数学实验报告 实验3 欧拉路的确定
  20. 048:cesium加载kmz文件,显示图形

热门文章

  1. AWS DeepRacer 强化学习RL,工作流程
  2. 手动安装ipa,通过XCode手动安装包iOS App, ipa Devices and Simulators
  3. 虚拟机一直安装程序正在启动服务器失败,安装使用Vmware出现的问题及解决方法...
  4. 新浪推荐 二面 移动零
  5. 特征的标准化和归一化
  6. MQAM(M元正交幅度调制)
  7. java调用matlab的jar包
  8. 机器学习中性能评估指标中的准确率(Accuracy)、召回率(Recall=TPR)、精确率(Precision)、误报率(FPR)、漏报率(FNR)及其关系
  9. 【机器学习系列】GMM第二讲:高斯混合模型Learning问题,最大似然估计 or EM算法?
  10. 【PRML 学习笔记】附录 - 变分法 (Calculus of Variations)