Spring是通过IoC容器对Bean进行管理的,而Bean的初始化主要分为两个过程:Bean的注册和Bean实例化。Bean的注册主要是指Spring通过读取配置文件获取各个bean的声明信息,并且对这些信息进行注册的过程。Bean的实例化则指的是Spring通过Bean的注册信息对各个Bean进行实例化的过程。本文主要讲解Spring是如何注册Bean,并且为后续的Bean实例化做准备的。

       Spring提供了BeanFactory对Bean进行获取,但Bean的注册和管理并不是在BeanFactory中进行的,而是在BeanDefinitionRegistry中进行的,这里BeanFactory只是提供了一个查阅的功能。如果把整个IoC容器比作一个图书馆的话,BeanFactory只是提供给学生查阅书籍的管理员,而BeanDefinitionRegistry则是注册所有图书信息的图书管理软件。Spring的Bean信息是注册在一个个BeanDefinitioin中的,其就相当于一本本的图书,在图书管理软件中是注册备案了的。如下是IoC容器对Bean注册进行管理的类结构图:

1. bean声明示例

       首先我们看下如下利用配置文件声明一个Bean,并且通过ClassPathXmlApplicationContext读取该Bean的过程:

public class MockBusinessObject {}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="mockBO" class="MockBusinessObject"/>
</beans>

       通过上述方式,我们就创建了一个MockBusinessObject的实例,通过如下代码我们即可获取该实例,并且使用该实例完成我们所需要的工作:

public class BeanApp {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
MockBusinessObject business = context.getBean(MockBusinessObject.class);
System.out.println(business);
}
}

       这里我们选用的IoC容器是ClassPathXmlApplicationContext,Spring有两种类型的Bean工厂:ApplicationContext和BeanFactory。这里ApplicationContext是继承自BeanFactory的,因而其具有BeanFactory的全部功能。ApplicationContext和BeanFactory的主要区别有两点:①ApplicationContext在注册Bean之后还会立即初始化各个Bean的实例,BeanFactory只有在调用getBean()方法时才会开始实例化各个Bean;②ApplicationContext会自动检测配置文件中声明的BeanFactoryPostProcessor和BeanPostProcessor等实例,并且在实例化各个Bean的时候会自动调用这些配置文件中声明的辅助bean实例,而BeanFactory必须手动调用其相应的方法才能将声明的辅助Bean添加到IoC容器中。

2. 源码解析

2.1 初始化BeanFactory信息

       我们这里以ClassPathXmlApplicationContext为例,首先查看在构造该实例时Spring所做的工作:

public ClassPathXmlApplicationContext(
String[] configLocations, boolean refresh, @Nullable ApplicationContext parent)
throws BeansException { super(parent);
setConfigLocations(configLocations); // 设置属性文件路径等
if (refresh) {
refresh(); // bean的注册和初始化
}
}

       通过跟踪其源码,我们最终看到上述代码,setConfigLocations()方法主要是对设置配置文件的路径,并且会对配置文件路径中的占位符使用属性文件中相关的属性进行替换。这里的refresh()方法则主要是进行bean的注册和初始化的,跟踪其代码如下:

@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// 准备Bean初始化相关的环境信息,其内部提供了一个空实现的initPropertySources()方法用于提供给用户一个更改相关环境信息的机会
prepareRefresh(); // 创建BeanFactory实例,并且注册xml文件中相关的bean信息
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // 注册Aware和Processor实例,并且注册了后续处理请求所需的一些Editor信息
prepareBeanFactory(beanFactory); try {
// 提供的一个空方法,用于供给子类对已经生成的BeanFactory的一些信息进行定制
postProcessBeanFactory(beanFactory); // 调用BeanFactoryPostProcessor及其子接口的相关方法,这些接口提供了一个入口,提供给了调用方一个修改已经生成的BeanDefinition的入口
invokeBeanFactoryPostProcessors(beanFactory); // 对BeanPostProcessor进行注册
registerBeanPostProcessors(beanFactory); // 初始化国际化所需的bean信息
initMessageSource(); // 初始化事件广播器的bean信息
initApplicationEventMulticaster(); // 提供的一个空方法,供给子类用于提供自定义的bean信息,或者修改已有的bean信息
onRefresh(); // 注册事件监听器
registerListeners(); // 对已经注册的非延迟(配置文件指定)bean的实例化
finishBeanFactoryInitialization(beanFactory); // 清除缓存的资源信息,初始化一些声明周期相关的bean,并且发布Context已被初始化的事件
finishRefresh();
} catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
} // 发生异常则销毁已经生成的bean
destroyBeans(); // 重置refresh字段信息
cancelRefresh(ex); throw ex;
} finally {
// 初始化一些缓存信息
resetCommonCaches();
}
}
}

       可以看到,refresh()方法主要做了如下几个工作:

  • BeanFactory的初始化,并且加载配置文件中相关bean的信息;
  • BeanFactoryPostProcessor和BeanPostProcessor和调用;
  • 初始化国际化信息;
  • 注册和调用相关的监听器;
  • 实例化注册的Bean信息;

       对于bean所需实例化信息的注册,我们主要关注obtainFreshBeanFactory()方法,逐步跟踪其代码如下:

protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
refreshBeanFactory(); // 初始化BeanFactory并加载xml文件信息
// 获取已生成的BeanFactory
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
if (logger.isDebugEnabled()) {
logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory);
}
return beanFactory;
}

       这里我们跟踪refreshBeanFactory()方法如下:

@Override
protected final void refreshBeanFactory() throws BeansException {
if (hasBeanFactory()) { // 如果BeanFactory已经创建则对其进行销毁
destroyBeans();
closeBeanFactory();
}
try {
// 创建BeanFactory实例
DefaultListableBeanFactory beanFactory = createBeanFactory();
beanFactory.setSerializationId(getId()); // 为当前BeanFactory设置一个标识id
customizeBeanFactory(beanFactory); // 设置BeanFacotry的定制化属性信息
loadBeanDefinitions(beanFactory); // 加载xml文件信息
synchronized (this.beanFactoryMonitor) {
this.beanFactory = beanFactory;
}
}
catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
}
}

       可以看到,这里的xml加载主要是在loadBeanDefinitions()方法中,跟踪该方法如下:

@Override
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
// 创建一个XmlBeanDefinitionReader用于读取xml文件中的属性
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory); // 设置一些环境变量相关的信息
beanDefinitionReader.setEnvironment(this.getEnvironment());
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this)); // 提供的一个可供子类继承的方法,用于定制XmlBeanDefinitionReader相关的信息
initBeanDefinitionReader(beanDefinitionReader);
// 加载xml文件中的信息
loadBeanDefinitions(beanDefinitionReader);
}

        可以看到,这里xml文件中的bean信息,Spring主要是委托给了XmlBeanDefinitionReader来进行。如下是继续跟踪loadBeanDefinitions()方法的代码:

protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {
Resource[] configResources = getConfigResources();
if (configResources != null) {
reader.loadBeanDefinitions(configResources);
}
String[] configLocations = getConfigLocations();
if (configLocations != null) {
reader.loadBeanDefinitions(configLocations);
}
}
@Override
public int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException {
Assert.notNull(resources, "Resource array must not be null");
int counter = 0;
for (Resource resource : resources) {
counter += loadBeanDefinitions(resource);
}
return counter;
}

       这里XmlBeanDefinitionReader会依次读取所指定的每个配置文件的bean信息,继续跟踪loadBeanDefinitions()如下:

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
Assert.notNull(encodedResource, "EncodedResource must not be null");
if (logger.isInfoEnabled()) {
logger.info("Loading XML bean definitions from " + encodedResource.getResource());
} Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
if (currentResources == null) {
currentResources = new HashSet<>(4);
this.resourcesCurrentlyBeingLoaded.set(currentResources);
}
if (!currentResources.add(encodedResource)) {
throw new BeanDefinitionStoreException(
"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
}
try {
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
finally {
inputStream.close();
}
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"IOException parsing XML document from " + encodedResource.getResource(), ex);
}
finally {
currentResources.remove(encodedResource);
if (currentResources.isEmpty()) {
this.resourcesCurrentlyBeingLoaded.remove();
}
}
}

       上述代码中,主要是将xml文件转换为了一个InputStream,最终通过调用doLoadBeanDefinitions()方法进行bean信息的注册。如下是doLoadBeanDefinitions()方法的实现:

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
try {
Document doc = doLoadDocument(inputSource, resource);
return registerBeanDefinitions(doc, resource);
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (SAXParseException ex) {
throw new XmlBeanDefinitionStoreException(resource.getDescription(), "Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex);
}
catch (SAXException ex) {
throw new XmlBeanDefinitionStoreException(resource.getDescription(), "XML document from " + resource + " is invalid", ex);
}
catch (ParserConfigurationException ex) {
throw new BeanDefinitionStoreException(resource.getDescription(), "Parser configuration exception parsing XML from " + resource, ex);
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(resource.getDescription(), "IOException parsing XML document from " + resource, ex);
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(resource.getDescription(), "Unexpected exception parsing XML document from " + resource, ex);
}
}

        上述代码中,首先将资源文件转换为一个Document对象,该对象中保存有各个xml文件中各个节点和子节点的相关信息。通过转换得到的Document对象,通过registerBeanDefinitions()方法完成Bean的注册,如下是registerBeanDefinitions()方法的代码:

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
int countBefore = getRegistry().getBeanDefinitionCount();
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
return getRegistry().getBeanDefinitionCount() - countBefore;
}

        如下是BeanDefinitionDocumentReader.registerBeanDefinitions()方法的实现:

@Override
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
logger.debug("Loading bean definitions");
Element root = doc.getDocumentElement();
doRegisterBeanDefinitions(root);
}

2.2 解析xml文件

       这里首先通过Document对象获取到xml文件的根节点信息,然后通过doRegisterBeanDefinitions()方法转换节点的bean信息:

protected void doRegisterBeanDefinitions(Element root) {
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(getReaderContext(), root, parent); if (this.delegate.isDefaultNamespace(root)) {
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
if (logger.isInfoEnabled()) {
logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec + "] not matching: " + getReaderContext().getResource());
}
return;
}
}
} preProcessXml(root);
parseBeanDefinitions(root, this.delegate);
postProcessXml(root); this.delegate = parent;
}

       在doRegisterBeanDefinitions()方法中,其首先获取当前xml文件是否为默认的命名空间,也即是否使用的是Spring的xsd文件声明的bean,如果是的,则获取当前是否有指定profile相关的信息,并且在环境变量中获取当前是哪种profile,与命名空间中指定的profile进行比较,如果profile不匹配,则过滤掉当前的xml文件。

        下面的preProcessorXml()和postProcessorXml()方法是两个空方法,用于供给子类实现从而对获取到的Document对象进行定制。真正的bean节点的读取在parseBeanDefinitions()方法中:

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
if (delegate.isDefaultNamespace(ele)) {
parseDefaultElement(ele, delegate);
}
else {
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}

       这里在读取bean节点的时候分为了两种情形进行读取:①默认的命名空间,也即Spring所提供的xsd命名空间的bean读取;②自定义的命名空间定义的bean读取。关于自定义命名空间bean的读取我们在后续文章中会进行讲解,本文主要讲解使用Spring默认命名空间所定义的bean的读取。如下是parseDefaultElement()方法的实现:

private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
importBeanDefinitionResource(ele);
} else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
processAliasRegistration(ele);
} else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
processBeanDefinition(ele, delegate);
} else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
doRegisterBeanDefinitions(ele);
}
}

       可以看到,在对xml节点的读取的时候,其分为了四种情形:①读取import节点所指定的xml文件信息;②读取alias节点的信息;③读取bean节点指定的信息;④读取嵌套bean的信息。由于这里对bean节点的解析是较为复杂,并且最为重要的,本文主要对其余三种节点的解析进行讲解,对bean节点的解析将放入下一篇文章进行讲解。

2.2.1 读取import节点指定的bean信息
protected void importBeanDefinitionResource(Element ele) {
String location = ele.getAttribute(RESOURCE_ATTRIBUTE);
if (!StringUtils.hasText(location)) {
getReaderContext().error("Resource location must not be empty", ele);
return;
} // 处理import节点指定的路径中的属性占位符,将其替换为属性文件中指定属性值
location = getReaderContext().getEnvironment().resolveRequiredPlaceholders(location); Set<Resource> actualResources = new LinkedHashSet<>(4); // 处理路径信息,判断其为相对路径还是绝对路径
boolean absoluteLocation = false;
try {
absoluteLocation = ResourcePatternUtils.isUrl(location) || ResourceUtils.toURI(location).isAbsolute();
} catch (URISyntaxException ex) {} if (absoluteLocation) { // 如果是绝对路径,则直接读取该文件
try {
// 递归调用loadBeanDefinitions()方法加载import所指定的文件中的bean信息
int importCount = getReaderContext().getReader().loadBeanDefinitions(location, actualResources);
if (logger.isDebugEnabled()) {
logger.debug("Imported " + importCount + " bean definitions from URL location [" + location + "]");
}
} catch (BeanDefinitionStoreException ex) {
getReaderContext().error(
"Failed to import bean definitions from URL location [" + location + "]", ele, ex);
}
}
else {
try {
int importCount;
Resource relativeResource = getReaderContext().getResource()
.createRelative(location); // 判断是否为相对路径
// 如果是相对路径,则调用loadBeanDefinitions()方法加载该文件中的bean信息
if (relativeResource.exists()) {
importCount = getReaderContext().getReader()
.loadBeanDefinitions(relativeResource);
actualResources.add(relativeResource);
}
else {
// 如果相对路径,也不是绝对路径,则将该路径当做一个外部url进行请求读取
String baseLocation = getReaderContext().getResource()
.getURL().toString();
// 继续调用loadBeanDefinitions()方法读取下载得到的xml文件信息
importCount = getReaderContext().getReader().loadBeanDefinitions(
StringUtils.applyRelativePath(baseLocation, location), actualResources);
}
if (logger.isDebugEnabled()) {
logger.debug("Imported " + importCount + " bean definitions from relative location [" + location + "]");
}
} catch (IOException ex) {
getReaderContext().error("Failed to resolve current resource location", ele, ex);
} catch (BeanDefinitionStoreException ex) {
getReaderContext().error("Failed to import bean definitions from relative location [" + location + "]",
ele, ex);
}
}
Resource[] actResArray = actualResources.toArray(new Resource[actualResources.size()]);
// 调用注册的对import文件读取完成事件的监听器
getReaderContext().fireImportProcessed(location, actResArray, extractSource(ele));
}

       可以看到,对import节点的解析,主要思路还是判断import节点中指定的路径是相对路径还是绝对路径,如果都不是,则将其作为一个外部URL进行读取,最终将读取得到的文件还是使用loadBeanDefinitions()进行递归调用读取该文件中的bean信息。

2.2.2 读取alias节点指定的信息
protected void processAliasRegistration(Element ele) {
String name = ele.getAttribute(NAME_ATTRIBUTE); // 获取name属性的值
String alias = ele.getAttribute(ALIAS_ATTRIBUTE); // 获取alias属性的值
boolean valid = true;
if (!StringUtils.hasText(name)) {
getReaderContext().error("Name must not be empty", ele);
valid = false;
}
if (!StringUtils.hasText(alias)) {
getReaderContext().error("Alias must not be empty", ele);
valid = false;
}
if (valid) {
try {
// 注册别名信息
getReaderContext().getRegistry().registerAlias(name, alias);
}
catch (Exception ex) {
getReaderContext().error("Failed to register alias '" + alias +
"' for bean with name '" + name + "'", ele, ex);
}
// 激活对alias注册完成进行监听的监听器
getReaderContext().fireAliasRegistered(name, alias, extractSource(ele));
}
}

如下是registerAlias()方法的最终实现:

@Override
public void registerAlias(String name, String alias) {
Assert.hasText(name, "'name' must not be empty");
Assert.hasText(alias, "'alias' must not be empty");
if (alias.equals(name)) {
this.aliasMap.remove(alias);
}
else {
String registeredName = this.aliasMap.get(alias);
if (registeredName != null) {
if (registeredName.equals(name)) {
// 如果已注册,则直接返回
return;
}
if (!allowAliasOverriding()) {
throw new IllegalStateException("Cannot register alias '" + alias + "' for name '" + name + "': It is already registered for name '" + registeredName + "'.");
}
}
// 检查是否有循环别名注册
checkForAliasCircle(name, alias);
// 将别名作为key,目标bean名称作为值注册到存储别名的Map中
this.aliasMap.put(alias, name);
}
}

       可以看到,别名的注册,其实就是将别名作为一个key,将目标bean的名称作为值,存储到一个Map中。这里需要注意的是,目标bean的名称也可能是一个别名。

2.2.3 读取嵌套beans信息

       对于嵌套beans的解析,可以看到,其调用的是doRegisterBeanDefinitions()方法,该方法正是前面我们讲解的开始对bean解析的方法,因而这里其实是使用递归对嵌套bean进行解析的。这里需要说明的是,一个xml文件,其根节点其实就是一个beans节点,而嵌套beans节点的节点名也是beans,因而嵌套beans其实也可以理解为一份单独引入的xml文件,因而可以使用递归的方式对其进行读取。

Spring Bean注册解析(一)的更多相关文章

  1. Spring Bean注册解析(二)

           在上文Spring Bean注册解析(一)中,我们讲解了Spring在注册Bean之前进行了哪些前期工作,以及Spring是如何存储注册的Bean的,并且详细介绍了Spring是如何解析 ...

  2. Spring Bean注册和加载

    Spring解密 - XML解析 与 Bean注册 Spring解密 - 默认标签的解析 Spring解密 - 自定义标签与解析 Spring解密 - Bean的加载流程

  3. spring bean注册之bean工厂方式

    一般我们在spring中注册一个bean,直接 <bean id="websocket" class="com.sdyy.common.spring.websock ...

  4. Spring Bean 标签解析

    上一篇文章讲到了标签在 parseDefaultElement 方法中进行解析,本篇文章将讲解这部分内容 bean 标签解析 查看 processBeanDefinition 方法,针对各个操作作具体 ...

  5. Spring bean注册

    DefaultListableBeanFactory中: DefaultListableBeanFactory实现了BeanDefinitionRegistry,这个接口的实现完成BeanDefini ...

  6. 【转】Spring Bean属性解析

    转载自:http://wenku.baidu.com/view/30c7672cb4daa58da0114ae2.html Bean所以属性一览: <bean id="beanId&q ...

  7. 09.Spring Bean 注册 - BeanDefinitionRegistry

    基本概念 BeanDefinitionRegistry ,该类的作用主要是向注册表中注册 BeanDefinition 实例,完成 注册的过程. 它的接口定义如下: public interface ...

  8. spring bean注册和实例化

    1.左边3个接口定义了基本的Ioc容器的2.HierarchicalBeanFactory增加了getParentBeanFactory()具备了双亲Ioc的管理能力3.ConfigurableBea ...

  9. Spring自定义标签解析与实现

           在Spring Bean注册解析(一)和Spring Bean注册解析(二)中我们讲到,Spring在解析xml文件中的标签的时候会区分当前的标签是四种基本标签(import.alias ...

随机推荐

  1. Oracle DataGuard启动与关闭顺序

    (一)Active DataGuard启动顺序(1)启动监听,先启从库再起主库 lsnrctl start (2)启动数据库,先启动备库在启主库 --先启备库 sql>startup nomou ...

  2. volatile关键字到底做了什么?

    话不多说,直接贴代码 class Singleton { private static volatile Singleton instance; private Singleton(){} //双重判 ...

  3. Oracle 的PL/SQL语言使用

    --PL/SQL语言(procedure language 过程化语言) --1.声明类型 declare k number; m ; --Character String buffer too sm ...

  4. c#一些处理解决方案(组件,库)

    1.关系数据库 postgresql,mysql,oracle,sqlserver 2.本地数据库 sqlite,berkeleydb,litedb 3.缓存数据库 redis,mongdb 4.数据 ...

  5. Qt5显示中文字符

    在cpp文件或.h文件中顶行输入: #pragma execution_character_set("utf-8")

  6. HCDA day1

    OSI有几层: OSI将计算机网络体系结构(architecture)划分为以下七层: 图1.OSI模型 物理层: 将数据转换为可通过物理介质传送的电子信号 相当于邮局中的搬运工人. 物理层(Phys ...

  7. MySQL5.5安装(Windows版本)

    1. 官网下载mysql5.5 下载地址:http://dev.mysql.com/downloads/mysql/5.5.html#downloads 2. 安装mysql5.5(安装之前,请关闭杀 ...

  8. MongoDB怎么用?

    MongoDB简介 MongoDB 是一个基于分布式文件存储的数据库.由 C++ 语言编写.旨在为 WEB 应用提供可扩展的高性能数据存储解决方案. MongoDB 是一个介于关系数据库和非关系数据库 ...

  9. Oracle Data Provider for .NET Support for Microsoft .NET Core

    Oracle Data Provider for .NET Support for Microsoft .NET Core的官方地址,记录下来,按照官方描述,会在2017年底左右发布,暂时还没有看到相 ...

  10. echarts 去掉上面的小图标

    在option里找到toolbox,删除对应的代码即可: toolbox: { y : -30, show : true, feature : { mark : '辅助线开关', markUndo : ...