硬怼Spring-BeanDefinition(四)

1. 如何使用IOC容器

  1. 在resource中创建app.xml(我是用maven构建的项目)
<?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="dog" class="com.sanjin.blog04.Dog"></bean>
</beans>
  1. app.xml中加载了一个bean,所以我还需要创建一个Dog类
public class Dog {
    public void show() {
        System.out.println("I am a dog!");
    }
}
  1. 将bean加载到Spring容器
public class Main {
    public static void main(String[] args) {
        // 1. 创建资源
        ClassPathResource resource = new ClassPathResource("app.xml");
        // 2. 创建一个factory,用户存储bean(factory内部有个hashMap存放)
        DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
        // 3. 创建一个reader,用户读取资源,读取的bean会放在一个factory中
        //    所以构造器中需要一个 factory
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
        // 4. 加载资源
        reader.loadBeanDefinitions(resource);
        
        // 5. 从Spring容器中获取bean
        Dog dog = factory.getBean(Dog.class);
        dog.show();
    }
}

通过上面一系列代码,我们可以发现初始化一个Spring容器有几个关键点

  • resource:加载哪些Bean,需要资源来指定
  • beanFactory:存放加载的Bean,取Bean也是从这里取
  • reader:解析resource,并注册bean到beanFactory中

整个过程分为资源定位->装载->注册可以用这张图概括:

image

资源定位我们在 硬怼Spring-资源加载(三)探讨过。
文章第二部分就探讨Spring如何从一个xml文件解析出bean

2. BeanDefinition的加载

reader.loadBeanDefinitions(resource);出发
1. reader.loadBeanDefinitions(resource);

@Override
    public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
        return loadBeanDefinitions(new EncodedResource(resource));
    }

只是个代理方法,实际调用另一个重载方法。使用EncodedResource对象作为参数,进入EncodedResource看看,这个类最主要的就是这个方法

/**
     * 将resource转换为Reader,并使用指定的编码方式
     * Open a {@code java.io.Reader} for the specified resource, using the specified
     * {@link #getCharset() Charset} or {@linkplain #getEncoding() encoding}
     * (if any).
     * @throws IOException if opening the Reader failed
     * @see #requiresReader()
     * @see #getInputStream()
     */
    public Reader getReader() throws IOException {
        if (this.charset != null) {
            return new InputStreamReader(this.resource.getInputStream(), this.charset);
        }
        else if (this.encoding != null) {
            return new InputStreamReader(this.resource.getInputStream(), this.encoding);
        }
        else {
            return new InputStreamReader(this.resource.getInputStream());
        }
    }

这个类的主要作用就是将 Resource 转换为 Reader。
2. loadBeanDefinitions(new EncodedResource(resource))

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
        Assert.notNull(encodedResource, "EncodedResource must not be null");
        if (logger.isTraceEnabled()) {
            logger.trace("Loading XML bean definitions from " + encodedResource);
        }

        // this.resourcesCurrentlyBeingLoaded 是一个ThreadLocal类型
        Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
        if (currentResources == null) {
            currentResources = new HashSet<>(4);
            this.resourcesCurrentlyBeingLoaded.set(currentResources);
        }
        // 判断encodedResource是否已经在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());
                }
                // 继续调用 doLoadBeanDefinitions 方法,添加了 inputSource 参数
                return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
            }
            finally {
                inputStream.close();
            }
        }

3. doLoadBeanDefinitions(inputSource, encodedResource.getResource());
相比这次就是真实干事情的方法了,毕竟加了个do。

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
            throws BeanDefinitionStoreException {

        try {
            // Document 就是HTMl中的DOM树,可以用来解析XML或者HTML标签
            Document doc = doLoadDocument(inputSource, resource);
            // 将解析出来的bean注册到factory中
            int count = registerBeanDefinitions(doc, resource);
            if (logger.isDebugEnabled()) {
                logger.debug("Loaded " + count + " bean definitions from " + resource);
            }
            return count;
        }

先说一下Document doc = doLoadDocument(inputSource, resource);这个方法主要作用就是解析我们classpath中的app.xml,抛开Spring不说,我们看看如何使用Java解析xml文件,我还是使用开头的app.xml举例,获取bean标签id="dog"的class值:
Java解析XMl有许多方式,但不管哪种方式,也都是调用不同包下的API。

  • javax.xml包下API
  • org.dom4j包下API
  • org.jdom包下API
    xml文件:


    1.png

我尝试了第一种包下API:

public class PraseXMLTest {
    public static void main(String[] args) {

        // 加载 resource 下的 app.xml 文件
        // ClassPathResource 这个类是 Spring 提供的
        ClassPathResource resource = new ClassPathResource("app.xml");

        // 使用 javax.xml.* 下API
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = null;
        try {
            builder = factory.newDocumentBuilder();
            // builder.parse()有许多重载的方法,返回 Document 对象
            // 通过 Document 我们就可以获取DOM树种所有的元素信息了
            Document document = builder.parse(resource.getInputStream());

            // 下面我们来获取 bean标签的  id class 属性的值

            // 1. 获取所有的bean标签
            NodeList beanNodeList = document.getElementsByTagName("bean");
            for (int i = 0; i < beanNodeList.getLength(); i++) {
                Node beanNode = beanNodeList.item(i);
                // 2.获取 bean 标签的所有属性
                NamedNodeMap attributes = beanNode.getAttributes();
                // 获取 id 属性的值
                Node id = attributes.getNamedItem("id");
                // 获取 class 属性的值
                Node clazz = attributes.getNamedItem("class");
                System.out.println("id 属性值:" + id.getNodeValue());
                System.out.println("class 属性值:" + clazz.getNodeValue());
            }

        } catch (ParserConfigurationException | SAXException | IOException e) {
            e.printStackTrace();
        }
    }
}

打印结果:


1.png

尝试了上面的例子后,我们就可以明白第一个函数
Document doc = doLoadDocument(inputSource, resource);
的作用:解析XMl文件生成一个Document对象,通过这个Document对象就可以获取DOM树中一切元素节点的信息(DOM是HTML中的一个概念)。以便于后面通过反射生成bean对象。Spring内部解析XML使用的API和例子里的相同。

3. BeanDefinition的注册

BeanDefinition的注册流程就包含在registerBeanDefinitions(doc, resource)
这个方法冲名字我们就可以猜出它的作用:从doc(Document)对象中获取bean的配置信息(id,class,lazyInit等等)注册到factory中。
从源码看内部细节:

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
        // documentReader 对象用于解析bean的配置信息,如id,class等等
        BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
        
        // 还记得开头我们的例子中 
        // XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
        // factory 其实是个 DefaultListableBeanDefinition 对象,是Spring默认实现的一个容器
        // getRegistry() 可以获取到我们传入的 factory 对象
        
        // 获取没有注册bean之前 Spring 容器中的 bean 的个数
        int countBefore = getRegistry().getBeanDefinitionCount();
        
        // 获取注册信息,并加载到 factory 中,跟进
        documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
        
        // 返回 factory 加载的 bean 的数量
        return getRegistry().getBeanDefinitionCount() - countBefore;
    }

跟进 documentReader.registerBeanDefinitions(doc, createReaderContext(resource));

public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
        this.readerContext = readerContext;
        doRegisterBeanDefinitions(doc.getDocumentElement());
    }

想必其中玄机就在doRegisterBeanDefinitions中了,继续跟进:
doRegisterBeanDefinitions()源码种我们主要关注parseBeanDefinitions(root, this.delegate);:

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {

        // 通过 Element 解析XML文件
        // 注意此处的 if 判断是否为默认的 namespace
        // 只有XML文件是默认的 namespace Spring才会正常解析
        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)) {
                        // 解析 Element 获取 bean 的配置信息,跟进这个方法
                        parseDefaultElement(ele, delegate);
                    }
                    else {
                        delegate.parseCustomElement(ele);
                    }
                }
            }
        }
        else {
            delegate.parseCustomElement(root);
        }
    }

XML文件 namespace

命名空间都是用来解决命名冲突问题,注意Spring XML 文件的命名空间,比如我们开头使用的app.xml

<?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="dog" class="com.sanjin.blog04.Dog"></bean>
</beans>

xmlns="http://www.springframework.org/schema/beans"
xmlns(XML namespace)属性用于指定命名空间,后面的url标识这个XML属于Spring bean的命名空间,我们的Spring xml只有指定这个命名空间才能被Spring正常解析。

跟进parseDefaultElement(ele, delegate);方法

// 对于不同的标签进行不同的处理
    private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
        if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) { // import 标签
            importBeanDefinitionResource(ele);
        }
        else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) { // alias
            processAliasRegistration(ele);
        }
        else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) { // bean 标签,我们主要关注
            processBeanDefinition(ele, delegate);
        }
        else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) { // beans 标签
            // recurse
            doRegisterBeanDefinitions(ele);
        }
    }

对于不同的XML标签需要用不同方法处理,我们只关心bean标签的处理,所以继续跟进processBeanDefinition(ele, delegate);

protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
        BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
        if (bdHolder != null) {
            // 获取 beanDefinitionHolder
            bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
            try {
                // 将 beanDefinition 注册进 factory 中的 beanDefinitionMap 中,跟进该方法
                // Register the final decorated instance.
                BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
            }
            catch (BeanDefinitionStoreException ex) {
                getReaderContext().error("Failed to register bean definition with name '" +
                        bdHolder.getBeanName() + "'", ele, ex);
            }
            // 派发 注册 事件
            // Send registration event.
            getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
        }
    }

这个方法中,我们主要关心这行代码:
BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
这个方法的两个参数中
第一个参数
bdHolder(beanDefinitionHolder)是一种Holder的设计模式,内部保存了完整的beanDefinition对象和bean的名称以及bean的别名。
跟进delegate.parseBeanDefinitionElement(ele);方法,我们主要关心beanName的生成

public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) {
        // 获取 bean 标签 id属性,只能设置一个
        String id = ele.getAttribute(ID_ATTRIBUTE);
        // 获取 bean 标签 name 属性,可以设置多个,name标识bean的别名
        String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);

        List<String> aliases = new ArrayList<>();
        if (StringUtils.hasLength(nameAttr)) {
            // MULTI_VALUE_ATTRIBUTE_DELIMITERS ->
            // public static final String MULTI_VALUE_ATTRIBUTE_DELIMITERS = ",; ";
            // 分隔 nameAttr 来获取别名
            String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS);
            aliases.addAll(Arrays.asList(nameArr));
        }

        // 1. beanName 默认是 id
        String beanName = id;
        if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) {
            // 若 id 为空,并且 别名不为空,beanName为第一个别名
            beanName = aliases.remove(0);
            if (logger.isTraceEnabled()) {
                logger.trace("No XML 'id' specified - using '" + beanName +
                        "' as bean name and " + aliases + " as aliases");
            }
        }

        if (containingBean == null) {
            // 验证beanName,别名是否唯一
            checkNameUniqueness(beanName, aliases, ele);
        }

        // 根据 XML bean标签配置生成 beanDifinition 对象
        AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);

        if (beanDefinition != null) {
            // 这种情况是 id没有设置,别名也没设置
            if (!StringUtils.hasText(beanName)) {
                try {
                    // containingBean 表示这个 bean 标签是否嵌套 bean 标签,
                    if (containingBean != null) {
                        // 默认生成的 beanName 是 全类名+“#”+十六进制
                        // 如 com.sanjin.blog04.Dog#F3
                        beanName = BeanDefinitionReaderUtils.generateBeanName(
                                beanDefinition, this.readerContext.getRegistry(), true);
                    }
                    else {
                        // 若没有嵌套bean标签,采用这种方式生成beanName,生成的beanName就是
                        // 全类名+“#”+数字,如 com.sanjin.blog04.Dog#0
                        // 数字含义:0 表示 Dog 的第一个对象,1 表示第二对象
                        beanName = this.readerContext.generateBeanName(beanDefinition);
                        
                        // Register an alias for the plain bean class name, if still possible,
                        // if the generator returned the class name plus a suffix.
                        // This is expected for Spring 1.2/2.0 backwards compatibility.
                        
                        // 获取全类名,如 com.sanjin.blog04.Dog
                        String beanClassName = beanDefinition.getBeanClassName();
                        if (beanClassName != null &&
                                beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() &&
                                !this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) {
                            aliases.add(beanClassName);
                        }
                    }
                    if (logger.isTraceEnabled()) {
                        logger.trace("Neither XML 'id' nor 'name' specified - " +
                                "using generated bean name [" + beanName + "]");
                    }
                }
                catch (Exception ex) {
                    error(ex.getMessage(), ele);
                    return null;
                }
            }
            String[] aliasesArray = StringUtils.toStringArray(aliases);
            return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);
        }

        return null;
    }

代码很长,我们只需要记住这个关于beanDefinition 的 beanName结论就行:

  1. 若bean标签设置了 id,id即为beanName
  2. 若bean标签未设置id,设置了name,name第一个值为beanName,其他值为别名(alias)
  3. 若bean标签未设置id,未设置name,则beanName需要分情况:
    3.1 若该bean标签为嵌套bean标签
    beanName为 全类名+“#”+十六进制,如 com.sanjin.blog04.Dog#F3
    3.2 若bean标签不是嵌套bean标签
    beanName为 全类名+“#”+数字,如 com.sanjin.blog04.Dog#0,数字含义:0 表示 Dog 的第一个对象,1 表示第二对象

第二个参数
getReaderContext().getRegistry()返回的就是我们开头 new 出来的 factory,这个factory 就是我们常常所说的 Spring 容器,BeanDefinition的注册就是指把 BeanDefinition注册进 factory中。

此处需要说明一下 BeanDefinition 是个傻子东西

Java是一种面向对象语言,一切皆对象,比如房子我们可以把它当成房子对象,并用宽,高,平方,地理位置等等信息来描述它。那么Java bean 我们如何来描述呢?Spring 使用 BeanDefinition 来描述,比如这个bean是否单例,是否懒加载等等。具体信息可以查看 BeanDefinition 接口源码。
离目标已经很近了,继续跟进BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());

public static void registerBeanDefinition(
            BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
            throws BeanDefinitionStoreException {

        // Register bean definition under primary name.
        // 1. 获取 beanDifinition 的主名称,跟进
        String beanName = definitionHolder.getBeanName();
        // 2. 使用 主名称 注册 beanDefinition,跟进
        registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());

        // Register aliases for bean name, if any.
        // 3. 注册 beanDefinition 的别名
        String[] aliases = definitionHolder.getAliases();
        if (aliases != null) {
            for (String alias : aliases) {
                registry.registerAlias(beanName, alias);
            }
        }
    }

跟进第一个步骤definitionHolder.getBeanName(),来看看Spring 是如何判断一个bean的主名称:

/**
     * Return the primary name of the bean, as specified for the bean definition.
     */
    public String getBeanName() {
        return this.beanName;
    }

直接返回了beanName,说明在new BeanDefinitionHolder时候就已经定义了beanName,关于beanName这个问题很重要,前面我们以及讨论过了。下面看一下registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition())源码,指贴出主要代码

BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
        if (existingDefinition != null) { // 判断该 beanName 是否已经存在

            // 判断是否允许 bean 重写。Spring 有2种情况:
            // 1. 如果在同一个xml文件中有2个 bean 名字相同,Spring 会报错
            // 2. 如果在不同xml文件种有2个 bean 名字相同,Spring 默认会覆盖先前的 bean,
            //      在实际编码种我们也可以设置为 不允许重写
            if (!isAllowBeanDefinitionOverriding()) {
                throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);
            }
// 覆盖之前beanName的BeanDefinition,
            // beanDefinitionMap 保存了 beanDefinition
            this.beanDefinitionMap.put(beanName, beanDefinition);

else {
            if (hasBeanCreationStarted()) { // 若bean已经被创建过
                // Cannot modify startup-time collection elements anymore (for stable iteration)
                // 对beanDefinitionMap加锁
                synchronized (this.beanDefinitionMap) {
                    this.beanDefinitionMap.put(beanName, beanDefinition);
                    List<String> updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1);
                    updatedDefinitions.addAll(this.beanDefinitionNames);
                    updatedDefinitions.add(beanName);
                    this.beanDefinitionNames = updatedDefinitions;
                    removeManualSingletonName(beanName);
                }
            }
            else { // bean 没有被创建过
                // Still in startup registration phase
                this.beanDefinitionMap.put(beanName, beanDefinition);
                this.beanDefinitionNames.add(beanName);
                removeManualSingletonName(beanName);
            }
            this.frozenBeanDefinitionNames = null;
        }

        if (existingDefinition != null || containsSingleton(beanName)) {
            resetBeanDefinition(beanName);
        }

上面就是bean的注册过程

4. 本文总结:

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

推荐阅读更多精彩内容

  • 本来是准备看一看Spring源码的。然后在知乎上看到来一个帖子,说有一群**自己连Spring官方文档都没有完全读...
    此鱼不得水阅读 6,934评论 4 21
  • Spring容器高层视图 Spring 启动时读取应用程序提供的Bean配置信息,并在Spring容器中生成一份相...
    Theriseof阅读 2,811评论 1 24
  • 1、概述     spring的两大核心:IOC(依赖注入)和AOP(面向切面),IOC本质上就是一个线程安全的h...
    ALivn_3cf3阅读 590评论 0 3
  • 郭相麟 人生有几个十年?每一个十年就是一个成长的阶梯! 决定你人生命运的十年,不是你想了什么?而是你认真本着对...
    郭相麟阅读 219评论 0 0
  • 2018年就那样过去了,跨年的方式好像冷静了很多。 寝室六个人,有两个人跟男朋友一起去跨年了。剩下我们四个在寝室,...
    紫薇忘了水葫芦阅读 83评论 0 1