Spring Bean注册解析(一)

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

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,773评论 6 342
  • 本来是准备看一看Spring源码的。然后在知乎上看到来一个帖子,说有一群**自己连Spring官方文档都没有完全读...
    此鱼不得水阅读 6,927评论 4 21
  • 文/优游 图/拍摄 都说人生如戏,戏如人生。看着这被下锅的各样豆角,长长地虚了口气:人如豆角,豆角如人! 从呱...
    优游球阅读 528评论 2 2
  • 解题思路 划一划就出来了,枚举下3和5的索引树就可以看出规律来。第一行和最后一行为一类以初始位置递增(单一递增数列...
    MelodyIsUVoice阅读 249评论 0 0