温馨提示:要怀着 这个世界很美好 的心态去看~

技术经验交流:点击入群

ClassPathXmlApplicationContext的注册方式

源码分析基于Spring4.3

ClassPathXmlApplicationContext入口,最终都会调用到

/*     * 使用给定父级创建新的ClassPathXmlApplicationContext,从给定的XML文件加载定义信息。     * 加载所有的bean 定义信息并且创建所有的单例     * 或者,在进一步配置上下文之后手动调用刷新。*/public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent)throws BeansException {

  super(parent);  setConfigLocations(configLocations);  if (refresh) {    refresh();  }}

上述注释的解释如是说:在容器的启动过程中,初始化过程中所有的bean都是单例存在的

自动刷新

ApplicationContext context = new ClassPathXmlApplicationContext("xxx.xml");

就等同于手动刷新

ApplicationContext context = new ClassPathXmlApplicationContext();context.register("xxx.xml");context.refresh();

上述一共有三条链路,下面来一一分析

加载父子容器

首先是加载并初始化父容器的方法

1、第一个出场的是ClassPathXmlApplicationContext,它是一个独立的应用程序上下文,从类路径获取上下文定义文件,能够将普通路径解析为包含包路径的类路径资源名称。它可以支持Ant-Style(路径匹配原则),它是一站式应用程序的上下文,考虑使用GenericApplicationContext类结合XmlBeanDefinitionReader来设置更灵活的上下文配置。

Ant-Style 路径匹配原则,例如 "mypackages/application-context.xml" 可以用"mypackages/*-context.xml" 来替换。

⚠️注意: 如果有多个上下文配置,那么之后的bean定义将覆盖之前加载的文件。这可以用来通过额外的XML文件故意覆盖某些bean定义

2、随后不紧不慢走过来的不是一个完整的somebody,AbstractXmlApplicationContext, 它是为了方便ApplicationContext的实现而出现的(抽象类一个很重要的思想就是适配)。

AbstractXmlApplicationContext 的最主要作用就是通过创建一个XML阅读器解析ClassPathXmlApplicationContext 注册的配置文件。它有两个最主要的方法 loadBeanDefinitions(DefaultListableBeanFactory beanFactory) 和 loadBeanDefinitions(XmlBeanDefinitionReader reader)

3、下一个缓缓出场的是 AbstractRefreshableConfigApplicationContext ,它就像是中间人的角色,并不作多少工作,很像古代丞相的奏折要呈递给皇上,它的作用就相当于是拿奏折的角色。它用作XML应用程序上下文实现的基类,例如ClassPathXmlApplicationContext、FileSystemXmlApplicationContext和XmlWebApplicationContext

4、当老板的一般都比较听小秘的,那么AbstractRefreshableApplicationContext就扮演了小秘的角色,它是ApplicationContext的基类,支持多次调用refresh()方法,每次都会创建一个新的内部bean factory实例。继承 AbstractRefreshableApplicationContext 需要唯一实现的方法就是loadBeanDefinitions,在每一次调用刷新方法的时候。一个具体的实现是加载bean定义信息的DefaultListableBeanFactory

5、但是只有小秘给老板递交请辞不行,中间还要有技术leader 来纵览大局,向上与老板探讨公司发展计划,在下领导新人做项目打硬仗(这种男人真的很有魅力哈哈哈),但是技术leader也不能干完所有的工作,他还需要交给手下的程序员去帮他完成具体的工作,程序员接到一项工作,看看有没有可复用的项目和开源类库,发现有可用的,直接把"引用"链接过去就可以了。这就是容器的初始化工作,但是这一步的流程还没有结束,你还得时刻记住你是给boss干活的。

public AbstractApplicationContext(@Nullable ApplicationContext parent) {  // 交给其他程序员去完成的工作  this();  // 明确自己的老板是谁  setParent(parent);}

public AbstractApplicationContext() {  this.resourcePatternResolver = getResourcePatternResolver();}

// 返回 ResourcePatternResolver 去解析资源实例中的匹配模式,默认的是 PathMatchingResourcePatternResolver 支持 Ant-Style 模式。protected ResourcePatternResolver getResourcePatternResolver() {  return new PathMatchingResourcePatternResolver(this);}

// 此时的resourceLoader 就是ClassPathXmlApplicationContext 对象。public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) {  Assert.notNull(resourceLoader, "ResourceLoader must not be null");  this.resourceLoader = resourceLoader;}

你需要一些程序员帮你做具体的编码工作,也需要明确你是公司的员工,需要听从老板的,所以你需要明确老板是谁

@Overridepublic void setParent(@Nullable ApplicationContext parent) {  this.parent = parent;  if (parent != null) {    Environment parentEnvironment = parent.getEnvironment();    if (parentEnvironment instanceof ConfigurableEnvironment) {      getEnvironment().merge((ConfigurableEnvironment) parentEnvironment);    }  }}

但是这个时候老板出差了,不在了(因为传过来的parent 是 null),所以你需要自己做一些decision。至此,第一条线路就分析完成了。

配置路径解析

第二条线路,ApplicationContext中的 setConfigLocations(configLocations)

// 参数传过来的是可变参数,可变参数是一个数组,也就是说,你可以传递多个配置文件,用","分隔起来。public void setConfigLocations(@Nullable String... locations) {  if (locations != null) {    Assert.noNullElements(locations, "Config locations must not be null");    // configlocations 是一个可为空的String数组,可以为null,为null可以进行手动注册。    this.configLocations = new String[locations.length];    // 解析数组中的每一个配置文件的路径。    for (int i = 0; i       this.configLocations[i] = resolvePath(locations[i]).trim();    }  }  // 默认是直接创建了一个 ClassPathXmlApplicationContext 的无参数的构造函数,采用手动注册的方式。  else {    this.configLocations = null;  }}

关键点:路径解析方法 : AbstractRefreshableConfigApplicationContext 中的 resolvePath(locations[i]).trim(); 来看看是如何进行路径解析的

// 解析给定的路径,必要时用相应的环境属性值替换占位符。应用于路径配置。protected String resolvePath(String path) {  return getEnvironment().resolveRequiredPlaceholders(path);}

涉及两个方法,AbstractRefreshableConfigApplicationContext 中的getEnvironment() 和 validateRequiredProperties(),先来看第一个

getEnvironment()

// 以配置的形式返回此应用程序上下文的Environment,来进一步自定义// 如果没有指定,则通过初始化默认的环境。@Overridepublic ConfigurableEnvironment getEnvironment() {if (this.environment == null) {  // 使用默认的环境配置  this.environment = createEnvironment();}return this.environment;}

下面来看一下createEnvironment()如何初始化默认的环境:

// 创建并返回一个 StandardEnvironment,子类重写这个方法为了提供// 一个自定义的 ConfigurableEnvironment 实现。protected ConfigurableEnvironment createEnvironment() {        // StandardEnvironment 继承AbstractEnvironment,而AbstractEnvironment        // 实现了ConfigurableEnvironment        return new StandardEnvironment();    }

其实很简单,也只是new 了一个StandardEnvironment() 的构造器而已。StandardEnvironment是什么?非web应用程序的Environment 的标准实现。他实现了AbstractEnvironment 抽象类,下面是具体的继承树:

StandardEnvironment是AbstractEnvironment的具体实现,而AbstractEnvironment又是继承了ConfigurableEnvironment接口,提供了某些方法的具体实现,ConnfigurableEnvironment 继承了Environment,而Environment 和 ConfigurablePropertyResolver 同时继承了PropertyResolver

下面来看一下StandardEnvironment() 的源码:

public class StandardEnvironment extends AbstractEnvironment {

  // 系统属性资源名称    public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";

  // JVM系统属性资源名:    public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";

  //为标准的Java 环境 自定义合适的属性文件    @Override    protected void customizePropertySources(MutablePropertySources propertySources) {        propertySources.addLast(new MapPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));        propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));    }

}

现在读者就会产生疑问,不是说new出来一个标准的StandardEnvironment 实现吗,但是StandardEnvironment并没有默认的构造方法啊?这是什么回事呢?

其实StandardEnvironment 的构造方法是 AbstractEnvironment

public AbstractEnvironment() {  // 实现自定义属性资源的方法,也就是StandardEnvironment中customizePropertySources()  customizePropertySources(this.propertySources);  if (logger.isDebugEnabled()) {    logger.debug("Initialized " + getClass().getSimpleName() + " with PropertySources " + this.propertySources);  }}

上述的customizePropertySources 由StandardEnvironment 来实现,具体如下

@Overrideprotected void customizePropertySources(MutablePropertySources propertySources) {  propertySources.addLast(new       MapPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));  propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,     getSystemEnvironment()));}

由于容器在刚起步的时候 propertySources 是null,所以添加完系统环境(systemEnvironment)和系统属性(systemProperties) 之后,会变成下图所示

如何获取系统属性和如何获取系统环境没有往下跟,有兴趣的读者可以继续沿用。

大致截一个图,里面大概的属性是这样

systemProperties

systemEnvironment

另外一个是 resolveRequiredPlaceholders,它是由 PropertyResolver 超顶级接口定义的方法

// 在给定的text 参数中解析${} 占位符,将其替换为getProperty 解析的相应属性值。// 没有默认值的无法解析的占位符将导致抛出IllegalArgumentException。String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;

由 AbstractPropertyResolver 子类来实现,且看AbstractPropertyResolver 的继承树

具体实现的方法如下:

// 传递进来的文本就是解析过的 配置文件 SimpleName@Overridepublic String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {  if (this.strictHelper == null) {    this.strictHelper = createPlaceholderHelper(false);  }  return doResolvePlaceholders(text, this.strictHelper);}

// 调用createPlaceholderHelperprivate PropertyPlaceholderHelper createPlaceholderHelper(boolean ignoreUnresolvablePlaceholders) {        return new PropertyPlaceholderHelper(this.placeholderPrefix, this.placeholderSuffix,                this.valueSeparator, ignoreUnresolvablePlaceholders);}

----------------------------PropertyPlaceholderHelper-------------------------------

  // PropertyPlaceholderHelper加载的时候会把下面的特殊字符放进去  static {        wellKnownSimplePrefixes.put("}", "{");        wellKnownSimplePrefixes.put("]", "[");        wellKnownSimplePrefixes.put(")", "(");    }

/*    创建一个新的 PropertyPlaceholderHelper 使用提供的前缀 和 后缀     * 参数解释:placeholderPrefix 占位符开头的前缀     *         placeholderSuffix 占位符结尾的后缀     *         valueSeparator 占位符变量和关联的默认值 之间的分隔符     *         ignoreUnresolvablePlaceholders 指示是否应忽略不可解析的占位符。*/

public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix,                                 @Nullable String valueSeparator, boolean ignoreUnresolvablePlaceholders) {

  Assert.notNull(placeholderPrefix, "'placeholderPrefix' must not be null");  Assert.notNull(placeholderSuffix, "'placeholderSuffix' must not be null");  this.placeholderPrefix = placeholderPrefix;  this.placeholderSuffix = placeholderSuffix;  String simplePrefixForSuffix = wellKnownSimplePrefixes.get(this.placeholderSuffix);  if (simplePrefixForSuffix != null && this.placeholderPrefix.endsWith(simplePrefixForSuffix)) {    this.simplePrefix = simplePrefixForSuffix;  }  else {    this.simplePrefix = this.placeholderPrefix;  }  this.valueSeparator = valueSeparator;  this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders;}

解析完成占位符之后,需要做真正的解析,调用AbstractPropertyResolver中的doResolvePlaceholders 方法。

private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) {  return helper.replacePlaceholders(text, new PropertyPlaceholderHelper.PlaceholderResolver() {    @Override    public String resolvePlaceholder(String placeholderName) {      return getPropertyAsRawString(placeholderName);    }  });}

PlaceholderResolver是 PropertyPlaceholderHelper类的内部类,这是一种匿名内部类的写法,它真正调用的就是PropertyPlaceholderHelper`中的 replacePlaceholders 方法,具体如下:

// 将格式为 ${name} 的占位符替换为从提供 PlaceholderResolver 返回的值。public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {  Assert.notNull(value, "'value' must not be null");  return parseStringValue(value, placeholderResolver, new HashSet());}protected String parseStringValue(            String value, PlaceholderResolver placeholderResolver, Set visitedPlaceholders) {        StringBuilder result = new StringBuilder(value);int startIndex = value.indexOf(this.placeholderPrefix);// 判断指定的占位符有无 ${ 存在,没有的话直接返回while (startIndex != -1) {int endIndex = findPlaceholderEndIndex(result, startIndex);if (endIndex != -1) {                String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);                String originalPlaceholder = placeholder;if (!visitedPlaceholders.add(originalPlaceholder)) {throw new IllegalArgumentException("Circular placeholder reference '" + originalPlaceholder + "' in property definitions");                }// Recursive invocation, parsing placeholders contained in the placeholder key.                placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);// Now obtain the value for the fully resolved key...                String propVal = placeholderResolver.resolvePlaceholder(placeholder);if (propVal == null && this.valueSeparator != null) {int separatorIndex = placeholder.indexOf(this.valueSeparator);if (separatorIndex != -1) {                        String actualPlaceholder = placeholder.substring(0, separatorIndex);                        String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());                        propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);if (propVal == null) {                            propVal = defaultValue;                        }                    }                }if (propVal != null) {// Recursive invocation, parsing placeholders contained in the// previously resolved placeholder value.                    propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);                    result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);if (logger.isTraceEnabled()) {                        logger.trace("Resolved placeholder '" + placeholder + "'");                    }                    startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());                }else if (this.ignoreUnresolvablePlaceholders) {// Proceed with unprocessed value.                    startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());                }else {throw new IllegalArgumentException("Could not resolve placeholder '" +                            placeholder + "'" + " in value \"" + value + "\"");                }                visitedPlaceholders.remove(originalPlaceholder);            }else {                startIndex = -1;            }        }return result.toString();    }

直白一点,上述过程就是用来判断有没有 ${} 这个占位符,如果有的话就进入下面的判断逻辑,把${}中的值替换为 PlaceholderResolver 返回的值,如果没有的话,就直接返回。

容器刷新

在经过上述的准备工作完成后,接下来就是整个IOC,DI和AOP的核心步骤了,也是Spring框架的灵魂。由于源码太多,设计范围太广,本篇只分析刷新预处理应该做的事:我们都知道,无论你加载的是哪一种上下文环境,最终都会调用 AbstractApplicationContext 的refresh()方法,此方法是一切加载、解析、注册、销毁的核心方法,采用了工厂的设计思想。

// 完成IoC容器的创建及初始化工作    @Override    public void refresh() throws BeansException, IllegalStateException {        synchronized (this.startupShutdownMonitor) {

      // 1: 刷新前的准备工作。            prepareRefresh();

            // 告诉子类刷新内部bean 工厂。      //  2:创建IoC容器(DefaultListableBeanFactory),加载解析XML文件(最终存储到Document对象中)      // 读取Document对象,并完成BeanDefinition的加载和注册工作            ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

      //  3: 对IoC容器进行一些预处理(设置一些公共属性)            prepareBeanFactory(beanFactory);

            try {

                //  4:  允许在上下文子类中对bean工厂进行后处理。                                postProcessBeanFactory(beanFactory);

                //  5: 调用BeanFactoryPostProcessor后置处理器对BeanDefinition处理                invokeBeanFactoryPostProcessors(beanFactory);

                //  6: 注册BeanPostProcessor后置处理器                registerBeanPostProcessors(beanFactory);

                //  7: 初始化一些消息源(比如处理国际化的i18n等消息源)                initMessageSource();

                //  8: 初始化应用事件多播器                initApplicationEventMulticaster();

                //  9: 初始化一些特殊的bean                onRefresh();

                //  10: 注册一些监听器                registerListeners();

                //  11: 实例化剩余的单例bean(非懒加载方式)        //      注意事项:Bean的IoC、DI和AOP都是发生在此步骤                finishBeanFactoryInitialization(beanFactory);

                //  12: 完成刷新时,需要发布对应的事件                finishRefresh();            }

            catch (BeansException ex) {                if (logger.isWarnEnabled()) {                    logger.warn("Exception encountered during context initialization - " +                            "cancelling refresh attempt: " + ex);                }

                // 销毁已经创建的单例避免占用资源                destroyBeans();

                // 重置'active' 标签。                cancelRefresh(ex);

                // 传播异常给调用者                throw ex;            }

            finally {

                // 重置Spring核心中的常见内省缓存,因为我们可能不再需要单例bean的元数据了...                resetCommonCaches();            }        }    }

刷新容器之刷新预处理

此步骤的主要作用在于:准备刷新的上下文,设置启动的时间和active的标志作为扮演属性资源初始化的角色。

protected void prepareRefresh() {        this.startupDate = System.currentTimeMillis();        this.closed.set(false);        this.active.set(true);

        if (logger.isInfoEnabled()) {            logger.info("Refreshing " + this);        }

        // 初始化environment 上下文中的占位符属性资源        initPropertySources();        // 验证标记为必需的所有属性是否可解析        getEnvironment().validateRequiredProperties();

        // 允许收集早期的ApplicationEvents        this.earlyApplicationEvents = new LinkedHashSet<>();    }

这里面有两处代码需要说明:initPropertySources这个方法是需要子类进行实现的,默认是不会做任何事情的;getEnvironment() 这个方法由于上述的源码分析过程中,已经默认创建了 createEnvironment,所以这段代码是直接返回的

@Overridepublic ConfigurableEnvironment getEnvironment() {  if (this.environment == null) {    this.environment = createEnvironment();  }  return this.environment;}

下面只剩下了validateRequiredProperties()的分析,不着急,看源码不能着急,要怀着这个世界很美好的心情去看。

首先在 ConfigurablePropertyResolver 接口中定义了 validateRequiredProperties 方法

// 验证每一个被setRequiredProperties 设置的属性存在并且解析非空值,会抛出// MissingRequiredPropertiesException 异常如果任何一个需要的属性没有被解析。void validateRequiredProperties() throws MissingRequiredPropertiesException;

在抽象子类AbstractPropertyResolver 中被重写

@Overridepublic void validateRequiredProperties() {  // 属性找不到抛出异常的对象  MissingRequiredPropertiesException ex = new MissingRequiredPropertiesException();  for (String key : this.requiredProperties) {    if (this.getProperty(key) == null) {      ex.addMissingRequiredProperty(key);    }  }  if (!ex.getMissingRequiredProperties().isEmpty()) {    throw ex;  }}

因为在我们的源码分析中,没有看到任何操作是在对 requiredProperties 进行添加操作,也就是如下:

@Overridepublic void setRequiredProperties(String... requiredProperties) {  if (requiredProperties != null) {    for (String key : requiredProperties) {      this.requiredProperties.add(key);    }  }}

所以,此时的 requiredProperties 这个set集合是null, 也就不存在没有解析的元素了。

本篇到此就结束了,下一篇文章会进行源码分析的下一个步骤: 创建IOC容器以及Bean的解析

(点击文字可跳转)

1. 深究Spring中Bean的生命周期

2. 深入SpringBoot核心注解原理

3.线上环境部署概览

4.Springboot Vue shiro 实现前后端分离、权限控制

简述控制反转ioc_阅读Spring源码:IOC控制反转前的处理相关推荐

  1. idea调试源代码c语言,IDEA阅读spring源码并调试

    目标:搭建起Spring源码阅读和代码调试跟踪的环境,顺便建立一个简单的Demo,能够调试Spring的源代码 本节,主要介绍一下Spring源码阅读和调试的相关环境搭建,并使用MVN创建一个非常简单 ...

  2. 如何阅读Spring源码

    如何阅读Spring源码 如果你是一名JAVA开发人员,你一定用过Spring Framework. 作为一款非常经典的开源框架,从2004年发布的1.0版本到现在的5.0版本,已经经历了14年的洗礼 ...

  3. 面试有没有看过spring源码_怎么阅读Spring源码?

    此问必是有心人,有心人必有心答. --题记 当我看到这个问题的时候,不禁心里一问,为何要阅读spring源码? 在我们的生活之中,有形形色色的万物(Object),有飞机,有汽车,有轮船,还有我这个沧 ...

  4. 深圳Java学习:怎么阅读spring源码?

    深圳Java学习:怎么阅读spring源码? 此问必是有心人,有心人必有心答. --题记 当我看到这个问题的时候,不禁心里一问,为何要阅读spring源码? 在我们的生活之中,有形形色色的万物(Obj ...

  5. Spring源码 IOC和循环依赖AOP

    Spring源码 IOC和循环依赖AOP IOC篇 1.BeanFactory IOC容器,以BeanFactory为载体. BeanFactory,是Spring容器.这是由Spring管理,产生S ...

  6. 一、如何阅读Spring源码(全网最简单的方法)

    学习Java最好最有效的方法是学习Spring,但是最笨最没效的方法也是学习Spring. 为什么这么说呢?道理其实很简单 A.Spring很庞大,很完善,也非常的底层,如果我们学会的Spring,那 ...

  7. spring源码刨析总结

    spring源码刨析笔记 1.概述 spring就是 spring Framework Ioc Inversion of Control(控制反转/反转控制) DI Dependancy Inject ...

  8. 连Spring源码都没看过,你怎么敢在简历上写“精通”?

    小A 你好面试官,非常高兴能参加今天的面试 面试官 没事,先做一个自我介绍吧 小A 我叫小A,工作三年了,做过...... 面试官 嗯,好的,看到你的项目这块,在公司主要用的就是spring全家桶相关 ...

  9. 剖析Spring源码:加载IOC容器

    本文接上一篇文章 阅读Spring源码:IOC控制反转前的处理,继续进行下面的分析 首先贴出 Spring bean容器的刷新的核心 11个步骤进行祭拜(一定要让我学会了-阿门) // 完成IoC容器 ...

最新文章

  1. DL之BM:BM的前世今生
  2. 4.2.4 磁盘的管理
  3. CodeForces - 1454E Number of Simple Paths(基环树+思维)
  4. 如何逃离「信息茧房」?
  5. python将数组写入文件_python – 将numpy数组的大小写入二进制文件
  6. video标签:以视频为背景的网页
  7. iOS7以上: 实现如“日历”的 NavigationBar
  8. HLW8032做220V电量采集方案测试
  9. 80386汇编_全局描述表GDT介绍
  10. Delphi第三方组件--Delphi第三方控件大比拼
  11. 学以致用——Excel报表自动化方案 (Automation solution of complicated manual Excel Report)
  12. 个人app开发之找亮点
  13. IPv6双栈技术方案
  14. 看完《硅谷之谜》,马上登机
  15. html div 100 无效,HTML / CSS - IE中div没有100%高度
  16. 简易的C语言判断输入的年份为闰年还是平年
  17. 中国机械对流烘箱行业市场供需与战略研究报告
  18. python简单加密算法_如何制作一个简单的加密/解密程序?
  19. Mvc示例之六---bs软件的路径
  20. python实现类似于visio_9款在线作图工具:那些可以替代Visio的应用

热门文章

  1. 论文简述 | 融合关键点和标记的基于图优化的可视化SLAM
  2. 第八期直播《立体视觉之立体匹配理论与实战》精彩回录
  3. this is incompatible with sql_mode=only_full_group_by
  4. 注意力机制在活体检测中的应用
  5. 数据分析工具Pandas(7):数据清洗、合并、转化和重构
  6. Linux绝对权限和相对权限法,Linux基础学习笔记
  7. TEE综述:植物—土壤反馈(PSF):自然和农业科学间的桥梁
  8. 计算机专业教育,科学网—中国大学计算机教育路在何方? - 吴军的博文
  9. R语言构建文本分类模型:文本数据预处理、构建词袋模型(bag of words)、构建xgboost文本分类模型、基于自定义函数构建xgboost文本分类模型
  10. python使用matplotlib可视化3D曲面图、曲面图表示一个指定的因变量y与两个自变量x和z之间的函数关系