美文网首页
“王大锤の非诚勿扰” —— Spring IoC / DI 思想

“王大锤の非诚勿扰” —— Spring IoC / DI 思想

作者: 琦小虾 | 来源:发表于2018-09-25 07:12 被阅读0次

    本文参考地址:

    《spring Ioc/DI的理解》
    《关于Spring IOC (DI-依赖注入)你需要知道的一切》
    《一、IOC和DI的概念》
    《深入理解IoC/DI》
    《spring IOC篇二:xml的核心逻辑处理》

    温馨提示:前方内容会引起认真怪和女权者些许不适,请出门左手边右拐。


    一. 王大锤的相亲市场

    我叫王大锤,是个码农,我们这个行业号称“人傻钱多速来”,不信?呵呵呵呵呵呵呵……

    我的职业是码农,工作内容是 new 一个对象,日常聊天是如何找一个对象,睡觉是做梦如何 new 一个白富美对象陪我走上人生巅峰。总之,我没有对象。

    公司的同事连顺看我工作繁忙无暇撩妹,同时又日渐饥渴难耐,最后还是建议我去婚介公司碰碰运气,也许有个好运气,或者找个盘接一下,再不济也能遇见一群饥渴男一起回家组队打 Dota。于是我走到了春天婚介公司,踏上了登上人生巅峰之路。

    我叫王大锤,是个单身狗

    大锤进入了春天婚介公司之后,主要办了三件事:

    1. 进入春天婚介公司
    2. 按照婚介公司要求,填写个人用户简历
    3. 婚介公司告诉大锤,等待我们下一次的联谊事宜:到时候会用很多本公司用户参加,每个用户都有自己的个人条件,届时可进行配对或组队;

    大锤想了想还有点小激动,然后就回了公司,打开了自己的 Markdown,开始写起了一篇控制反转 (IoC)依赖注入 (DI) 相关的教程。

    王大锤的人生巅峰之路

    二. Spring IoC / DI 的简单理解

    《spring Ioc/DI的理解》 一文中,作者用人力资源局的例子方便读者的理解。所以笔者按照自己对 IoC / DI 的理解,也编了一个王大锤婚介公司之旅的故事,用段子的方式写出来,以期加深对自己和读者的印象。

    2.1 IoC 与 DI 的定义

    首先,笔者需要明确说明 IoC 与 DI 的定义。

    控制反转 (Inversion of Control,即 IoC) 是一种设计思想。在 Java 开发中,IoC 意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。同时 IoC 也是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。
    对于 IoC,它最常用的一种手段叫做依赖注入 (Dependency Injection,即 DI)。DI 通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递给它。

    上述定义也许比较难懂,所以笔者讲了开头的故事。故事中的春天婚介公司,就是我们经常使用的经典的 Spring IoC 容器
    将上面段子的名词与 Spring 内容一一对应,则如下所示:

    • 婚介公司 —— Spring IoC 容器
    • 用户征婚简历 —— Spring beans
    • 一次联谊活动 —— xml 配置文件
    • 月老 —— 开发者(我)

    2.2 IoC 的三个经典问题

    对于 IoC 有三个经典问题:谁控制谁?控制了什么?怎么实现了反转?很多博客里都进行了一个回答,笔者也按照自己的故事模式进行一个回答。

    1. “谁控制谁?”IoC / DI 容器控制应用程序
      • 其实可以把 IoC 当做一个存储对象的容器,我们在开发中形成的对象都可以交给 Spring IoC 容器做一个统一的规范管理;
      • 我们在开发中形成的对象可以用一个 Spring bean 来表示,所以可以联想一下,Spring IoC 容器就是婚介公司,每个对象都向这个婚介公司投递了征婚简历,所有用户简历全部由婚介公司调配。所以 IoC 容器就充当了一个婚姻介绍的角色;
    2. 控制了什么?:IoC / DI 容器控制对象本身的创建、实例化,以及控制对象之间的依赖关系
      • 开发之中的对象已经全部交由 IoC 容器来管理了,那我们在获取对象的时候,就得由 IoC 容器来给我们提供;
      • 例:我们想要和一个妹子配对:
        • 平常情况下,需要主动自己上去撩妹自己要(在实际工程中,即调用目标对象的 get API );
        • 现在既然我和妹子都是婚介公司的用户(都注册了 bean),婚介公司控制了我们对于对象的获取,那么就得通过婚介公司来把妹子给你(在实际工程中,即调用 Spring 的 getBean 方法,中间的 BeanDefinition 等细节内容暂且不表);
    3. 怎么实现了反转?:主要体现在控制权的反转。因为现在应用程序不能主动去获取外部资源了,而是被动等待 IoC / DI 容器给它注入它所需要的资源,所以称之为反转。
      • 例:依旧用上面的例子:我们想要和一个妹子配对:
        • 平常情况下,我们直接去找妹子要过来,这种事情是我们自己去做的,控制权在我们手里(实际工程中,就是在 classA 中需要一个 classB 的实例,所以就在 classA 中直接 new 了一个 classB 的实例来使用);
        • 现在既然我和妹子都是婚介公司的用户,那么向婚介公司要求介绍这个妹子,让婚介公司把妹子交给我们(实际工程中,就是通知 Spring IoC 容器“我需要 classB 的实例,你需要给我弄一个,然后把这个实例传给 classA”);
      • 这样一对比,就发现创建权与控制权都从开发者身上转移到了 Spring IoC 容器上,即实现了控制的反转;

    2.3 DI 的三个经典问题

    同样,DI 也存在三个经典问题:谁依赖谁?谁注入了谁?注入了什么?

    1. “谁依赖谁?”应用程序依赖于 IoC 容器
      • 上面也提到了我们找婚介公司介绍妹子配对的流程,可以看出我们用户是依赖于婚介公司的,也就是应用程序依赖于 IoC 容器;
    2. “谁注入了谁?”:IoC 容器把对象注入于应用程序
      • 依旧是我们找婚介公司介绍妹子配对的流程,婚介公司把同为用户的妹子给了我们,就相当于 IoC 容器将对象注入到了应用程序之中;
      • 这种我们需要了对象,IoC 将对象给我们的过程,就是依赖注入。
    3. “注入了什么?”:注入应用程序需要的外部资源,比如有依赖关系的对象;
      • 婚介公司把同为用户的妹子给了我们,就相当于 IoC 容器将对象注入到了应用程序之中;

    此时,笔者可以通过一个类比,来把依赖注入的关系进行说明:

    1. 一个 xml 配置文件中,定义了若干 Spring beans
      • 即在一次联谊活动中,会有很多用户参加;
    2. 对于这些 Spring beans,就是定义 bean 时各种各样的属性定义;
      • 对应于这些用户,就是说每一个用户都有自己的个人条件;
      • 所谓个人条件,就是身长八尺,容貌甚伟,有房有车,Q大H好,医卜星象门门会,钢琴摄影样样通之类的;
    3. 根据开发者在 xml 配置文件中的定义,形成依赖关系。对于一个 bean 的依赖,可以依赖于一个 bean,也可以依赖于多个 bean;例如 bean 定义的 xml 配置文件中,会有类似于 p 命名空间的属性注入 (p:name="qixiaoxia"),或者是 ref 依赖关系注入 (pcbrand-ref=MacBookPro) 之类的配置关系,通过这些配置形成了依赖关系;
      • 对于这次联谊活动,月老牵线,一金童一玉女成功配对,喜结连理,从今以后过上没羞没臊的生活;当然也可能某个老司机勾搭上了多个用户,形成了一个只属于自己的小团伙,从此开启了它的 S8 征战之旅 (RNG IG 加油冲鸭!!!);

    注:在《深入理解IoC/DI》中作者用一问一答的形式阐述了控制、依赖、注入等关系,以及与 IoC/DI 相关内容。本文有类似借鉴。

    三. DI 的实现原理解析

    控制反转 IoC 与依赖注入 DI 之间的关系,控制反转是目的,依赖注入是实现控制反转的手段。前面也提到,如果在传统模式中,A 类依赖于 B 类,就是在 A 类中 new 一个 B 类,或者是用 A 类的 set 方法将 B 类实例的引用注入 A 类。
    但是 IoC 将生成类的方式把传统模式反了过来,即开发人员不需要调用 new,而是在需要类的时候,由框架注入,由 DI 实现。即控制对象生成的权利,从自己转移给了框架(即 Spring),或者比较浅显的理解为转移给了 Spring 的 xml 配置文件。

    笔者为了模拟 DI 依赖注入的实现过程,按照文中的相亲市场写了一个简单的 demo,用来测试依赖注入在源码层面的实现。

    测试源码笔者已经上传到笔者的 Github 上,地址:spring/DI

    在 demo 中笔者设置了两个类:

    • 用户 User
    • 兴趣爱好 Hobby

    其中的 User 类中有三个属性:

    • String name: 姓名
    • Hobby hobby: 兴趣爱好
    • User partner: 伴侣

    每个用户都有自己的姓名,一个爱好,还有配对完毕的伴侣。笔者的 bean 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"
           xmlns:p="http://www.springframework.org/schema/p">
    
        <bean id="qixiaoxia" class="com.grq.spring.DI.User"
              p:name="qixiaoxia"
              p:hobby-ref="qixiaoxiaHobby"
              p:partner-ref="girlFriend"/>
        <bean id="girlFriend" class="com.grq.spring.DI.User"
              p:name="nsy"
              p:partner-ref="qixiaoxia"/>
        <bean id="qixiaoxiaHobby" class="com.grq.spring.DI.Hobby"
              p:name="piano" p:level="Lv.8"/>
    </beans>
    

    Hobby 类定义源码:

    public class Hobby {
        private String name;
        private String level;
        // get, set, 构造函数略
        // ...
        @Override
        public String toString() {
            return "Hobby{" +
                    "name='" + name + '\'' +
                    ", level='" + level + '\'' +
                    '}';
        }
    }
    

    User 类定义源码:

    public class User {
        private String name;
        private Hobby hobby;
        private User partner;
        // set, get, 构造函数略
        // ...
        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    ", hobby=" + hobby +
                    ", partner=\'" + partner.getName() + "\'}";
        }
    }
    

    测试用的 main 方法也很简单:

    public class DITest {
        public static void main(String[] args) {
            BeanFactory factory = new XmlBeanFactory(new ClassPathResource("DITest.xml"));
            User user = (User) factory.getBean("qixiaoxia");
            System.out.println(user);
        }
    }
    

    从 bean xml 配置文件解析内容的方法入口是 XmlBeanDefinitionReader # loadBeanDefinitions 方法,源码及注释如下:

        public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
            // 断言要解析的XML文件配置存在,不能为空
            Assert.notNull(encodedResource, "EncodedResource must not be null");
            // 向日志系统输出日志系统,输出 XML bean 的加载源
            if (logger.isInfoEnabled()) {
                logger.info("Loading XML bean definitions from " + encodedResource.getResource());
            }
            // 获取当前线程里的 ThreadLocal 里的变量集合
            Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
            if (currentResources == null) {
                //如果为空的情况下,重新申请一下 HashSet 集合
                currentResources = new HashSet<EncodedResource>(4);
                this.resourcesCurrentlyBeingLoaded.set(currentResources);
            }
            // 将 encodeResource 填加到当前线程的局部变量集合中
            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());
                    }
                    //========================================
                    // 真正的从指定的 XML 文件中加载 Bean 的定义的关键方法
                    //========================================
                    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();
                }
            }
        }
    

    该方法中,最关键的方法是 doLoadBeanDefinitions 方法,它真正的从指定的 XML 文件中加载了 Bean 的定义。源码及注释如下:

        protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
                throws BeanDefinitionStoreException {
            try {
                // 获取 XML 的验证方式,加载 XML 文件得到对应的 Document
                Document doc = doLoadDocument(inputSource, resource);
                // 根据返回的 Dcoument 注册 Bean 信息
                return registerBeanDefinitions(doc, resource);
            }
            // 若干 catch 方法省略
            // .........................................
        }
    

    doLoadBeanDefinitions 方法由两个方法组成,一个是 doLoadDocument() 方法,其中获取 XML 的验证方式(如确定文件为 DTD 或者 XSD 文件格式等相关信息),并将 xml 文档信息放入 Document 实例对象中。该方法可在《spring IOC篇二:xml的核心逻辑处理》中查阅。

    registerBeanDefinitions 是注册 bean 的内容,其中依赖注入的过程就是在该部分进行的。源码及注释如下:

        public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
            // 使用 DefaultBeanDefinitionDocumentReader 实例化 BeanDefinitionDocumentReader 对象
            BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
            // 记录统计前 BeanDefinition 的加载个数
            int countBefore = getRegistry().getBeanDefinitionCount();
            // 加载以及注册 Bean
            // 这里使用到了单一职责原则,将逻辑处理委托给单一的类进行处理,这个逻辑处理类就是 BeanDefinitionDocumentReader 对象
            documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
            // 统计本次加载 Beanfinition 的个数
            return getRegistry().getBeanDefinitionCount() - countBefore;
        }
    

    registerBeanDefinitions 是一个接口方法,它的具体实现是在 DefaultBeanDefinitionDocumentReader 中实现的:

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

    最后 doRegisterBeanDefinitions 方法才是实际解析 xml 文件内容的核心方法。

        protected void doRegisterBeanDefinitions(Element root) {
            BeanDefinitionParserDelegate parent = this.delegate;
            this.delegate = createDelegate(getReaderContext(), root, parent);
            //======================
            // 处理 profile 属性
            //======================
            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)) {
                        return;
                    }
                }
            }
            // 空代码留给子类去实现模板设计模式
            // 继承 DefaultBeanDefinitionDocumentReader 的子类在 XML 解析前做一些处理,可以实现此方法
            preProcessXml(root);
            
            //==============================
            // 解析除了 profile 以外的默认属性
            //==============================
            parseBeanDefinitions(root, this.delegate);
            
            // 空代码留给子类去实现模板设计模式
            // 继承 DefaultBeanDefinitionDocumentReader 的子类在 XML 解析后做一些处理,可以实现此方法
            postProcessXml(root);
            this.delegate = parent;
        }
    

    在 doRegisterBeanDefinitions 方法中,主要作用有三个:

    • 处理了根节点 root 的 profile 属性;
      • 在该例程中,并没有使用到 profile 属性。
    • 核心方法:调用 parseBeanDefinitions 方法,解析 bean 的基础属性。
    • 在解析 bean 基础属性的上下文处进行预处理 preProcessXml, 后处理 postProcessXml,但两个方法在该类中并没有实际实现,而是采用了模板设计模式,留给继承的子类,实现覆盖该方法;

    核心方法 parseBeanDefinitions 源码如下:

        // 从 XML 文件解析 Bean 的定义
        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)) {
                            // 解析当前节点
                            this.parseDefaultElement(ele, delegate);
                        } else {
                            delegate.parseCustomElement(ele);
                        }
                    }
                }
            } else {
                delegate.parseCustomElement(root);
            }
        }
    
        // 解析 Bean 默认元素
        private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
            if (delegate.nodeNameEquals(ele, "import")) {
                this.importBeanDefinitionResource(ele);
            } else if (delegate.nodeNameEquals(ele, "alias")) {
                this.processAliasRegistration(ele);
            } else if (delegate.nodeNameEquals(ele, "bean")) {
                this.processBeanDefinition(ele, delegate);
            } else if (delegate.nodeNameEquals(ele, "beans")) {
                this.doRegisterBeanDefinitions(ele);
            }
        }
    

    在核心方法 parseBeanDefinitions 中,解析了 import, alias, bean, beans 四种标签。我们的 bean xml 文件基本都是 <bean> 标签,所以其中最核心的方法就是 processBeanDefinition 方法。processBeanDefinition 源码如下:

        protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
            // Bean 定义持有者
            BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
            if (bdHolder != null) {
                // 装饰 Bean 定义,为各个 bean 添加属性信息,其中包含依赖关系的添加
                bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
    
                try {
                    BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, this.getReaderContext().getRegistry());
                } catch (BeanDefinitionStoreException var5) {
                    this.getReaderContext().error("Failed to register bean definition with name '" + bdHolder.getBeanName() + "'", ele, var5);
                }
    
                this.getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
            }
        }
    

    processBeanDefinition 方法源码中,decorateBeanDefinitionIfRequired 方法将 xml 配置文件中各个 bean 的属性“装饰”到该 bean 的定义中,在该方法中实现了 bean 之间的依赖注入。源码如下:

        public BeanDefinitionHolder decorateBeanDefinitionIfRequired(Element ele, BeanDefinitionHolder definitionHolder, BeanDefinition containingBd) {
            BeanDefinitionHolder finalDefinition = definitionHolder;
            // 获取当前 bean 的所有属性值
            NamedNodeMap attributes = ele.getAttributes();
    
            // 遍历所有属性值
            for(int i = 0; i < attributes.getLength(); ++i) {
                Node node = attributes.item(i);
                //===============================================
                // 关键方法,对当前属性值进行装饰(包含依赖关系注入的步骤)
                //===============================================
                finalDefinition = this.decorateIfRequired(node, finalDefinition, containingBd);
            }
    
            NodeList children = ele.getChildNodes();
    
            for(int i = 0; i < children.getLength(); ++i) {
                Node node = children.item(i);
                if (node.getNodeType() == 1) {
                    finalDefinition = this.decorateIfRequired(node, finalDefinition, containingBd);
                }
            }
    
            return finalDefinition;
        }
    

    decorateBeanDefinitionIfRequired 方法中遍历部分的关键方法中,再向下可进入到装饰属性的内容实现方法 decorate。decorate 方法将一个 bean 在 xml 文件中的属性定义赋值进入 BeanDefinition 中,该过程中当然也包含了 DI 依赖注入。decorate 方法是在 SimplePropertyNamespaceHandler 中实现的,源码如下所示:

        public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, ParserContext parserContext) {
            if (node instanceof Attr) {
                Attr attr = (Attr)node;
                // 属性名
                String propertyName = parserContext.getDelegate().getLocalName(attr);
                // 属性值
                String propertyValue = attr.getValue();
                // 属性值集合
                MutablePropertyValues pvs = definition.getBeanDefinition().getPropertyValues();
                // 如果属性值集合中已经包含了当前属性名,则报出错误
                if (pvs.contains(propertyName)) {
                    parserContext.getReaderContext().error("Property '" + propertyName + "' is already defined using both <property> and inline syntax. Only one approach may be used per property.", attr);
                }
    
                //===================================
                // 依赖注入实现:
                //   如果属性名是以 "-ref" 结尾的,则将该属性设置为被依赖的 bean,即 RuntimeBeanReference
                //===================================
                if (propertyName.endsWith("-ref")) {
                    propertyName = propertyName.substring(0, propertyName.length() - "-ref".length());
                    // 将该属性设置为被依赖的 bean,即 RuntimeBeanReference,添加进入属性值集合中
                    pvs.add(Conventions.attributeNameToPropertyName(propertyName), new RuntimeBeanReference(propertyValue));
                } else {
                    pvs.add(Conventions.attributeNameToPropertyName(propertyName), propertyValue);
                }
            }
    
            return definition;
        }
    

    注:关于 RuntimeBeanReference 的内容,可以在文章《Spring Bean 的解析 RuntimeBeanReference》一文中进行查阅了解。

    对于例程中名为 "qixiaoxia" 的 bean 进行调试,在 decorateBeanDefinitionIfRequired 方法中循环遍历 decorate 方法之前的 beandefinition 变量的值如下图所示:

    decorateBeanDefinitionIfRequired 遍历之前的属性值

    经过 decorateBeanDefinitionIfRequired 方法循环赋值之前,如上图所示,propertyValueList 为空集。但在循环赋值后,结果如下图所示:

    decorateBeanDefinitionIfRequired 遍历之后的属性值

    propertyValueList 加入了三个值,这三个值与 xml 配置文件中 "qixiaoxia" bean 的定义相同,而且包含了其中以 "-ref" 为属性名的两个属性。可以对比 bean 的定义,以及上图中循环后的 beandefinition 值的结果:

    <bean id="qixiaoxia" class="com.grq.spring.DI.User"
          p:name="qixiaoxia"
          p:hobby-ref="qixiaoxiaHobby"
          p:partner-ref="girlFriend"/>
    

    这样就将 bean 之间的依赖关系编辑完毕。往后将各个 beanDefinition 存入 Map 中并注册,继续运行 registerBeanDefinition 方法,即可完成 bean 从 xml 配置文件加载的操作。

    至此,DI 依赖注入的源码分析完毕。

    四. 后记

    第一次当标题党,心里还有点小激动呢 ~ 笔者用相亲为主题讲了如何理解 IoC 和 DI,但标题党不能白当,亲还是要相的,万一哪个有趣又美丽小姐姐看上我了呢?

    笔者之自恋,有诗为证:

    钢琴吉他 KTV,
    摄影健身吹牛逼。
    JAVA Python 还有 C,
    有趣灵魂颜值帝。

    最后献上自拍一张,拜个晚年,各位中秋快乐 ~

    自拍(手动滑稽)

    相关文章

      网友评论

          本文标题:“王大锤の非诚勿扰” —— Spring IoC / DI 思想

          本文链接:https://www.haomeiwen.com/subject/mxfkoftx.html