文档章节

Spring Bean注册解析(一)

爱宝贝丶
 爱宝贝丶
发布于 2018/05/12 11:44
字数 3486
阅读 399
收藏 12

       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注册进行管理的类结构图:

IoC容器

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文件,因而可以使用递归的方式对其进行读取。

© 著作权归作者所有

爱宝贝丶

爱宝贝丶

粉丝 324
博文 129
码字总数 427858
作品 0
武汉
程序员
私信 提问
【死磕 Spring】—– IOC 之 IOC 初始化总结

原文出自:http://cmsblogs.com 前面 13 篇博文从源码层次分析了 IOC 整个初始化过程,这篇就这些内容做一个总结将其连贯起来。 在前文提过,IOC 容器的初始化过程分为三步骤:Resource 定位...

chenssy
2018/10/09
0
0
Spring Security3源码分析-FilterChainProxy初始化

很久没有更新博客了,最近对Spring Security做了比较深入的研究。 spring security的教程网上很多: http://lengyun3566.iteye.com/category/153689 http://wenku.baidu.com/view/b0c0dc0b7...

Dead_knight
2014/01/20
1K
2
深入理解Spring源码(一)-IOC容器的定位,载入,注册

前言:Spring源码继承,嵌套层次非常多,读起来非常容易晕,小伙伴们在看文章的时候一定要跟着文章的思路自己去源码里点一点,看一看,并且多看几次。就会越来越清晰。下面开始正题 1.Spring...

Meet相识_bfa5
2018/05/01
0
0
Spring Bean注册解析(一)

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

张旭峰
2018/05/12
0
0
Spring注解解析之ConfigurationClassPostProcessor

概述 Spring中有各种注解,比如@Configuration、@Import、@ComponentScan、@Autowired等等,那Spring是怎么识别和解析这些注解的呢。是通过BeanFactoryPostProcessor和BeanPostProcessor这两...

cregu
04/23
78
0

没有更多内容

加载失败,请刷新页面

加载更多

64.监控平台介绍 安装zabbix 忘记admin密码

19.1 Linux监控平台介绍 19.2 zabbix监控介绍 19.3/19.4/19.6 安装zabbix 19.5 忘记Admin密码如何做 19.1 Linux监控平台介绍: 常见开源监控软件 ~1.cacti、nagios、zabbix、smokeping、ope...

oschina130111
今天
13
0
当餐饮遇上大数据,嗯真香!

之前去开了一场会,主题是「餐饮领袖新零售峰会」。认真听完了餐饮前辈和新秀们的分享,觉得获益匪浅,把脑子里的核心纪要整理了一下,今天和大家做一个简单的分享,欢迎感兴趣的小伙伴一起交...

数澜科技
今天
7
0
DNS-over-HTTPS 的下一代是 DNS ON BLOCKCHAIN

本文作者:PETER LAI ,是 Diode 的区块链工程师。在进入软件开发领域之前,他主要是在做工商管理相关工作。Peter Lai 也是一位活跃的开源贡献者。目前,他正在与 Diode 团队一起开发基于区块...

红薯
今天
12
0
CC攻击带来的危害我们该如何防御?

随着网络的发展带给我们很多的便利,但是同时也带给我们一些网站安全问题,网络攻击就是常见的网站安全问题。其中作为站长最常见的就是CC攻击,CC攻击是网络攻击方式的一种,是一种比较常见的...

云漫网络Ruan
今天
12
0
实验分析性专业硕士提纲撰写要点

为什么您需要研究论文的提纲? 首先当您进行研究时,您需要聚集许多信息和想法,研究论文提纲可以较好地组织你的想法, 了解您研究资料的流畅度和程度。确保你写作时不会错过任何重要资料以此...

论文辅导员
今天
8
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部