手写Spring框架——IOC实现(二)

更新

2023年12月5日更新:控制反转和依赖注入有何区别和联系?

开篇

上一篇文章——手写Spring框架——IOC实现(一)对ioc功能进行了分析与极简实现,但是涉及的类非常少,函数的实现也比较简单,并不能体现设计模式的思想,这一篇更多地参考原专栏的实现内容,将极简版ioc实现进行面向对象设计与实现。

因为面向对象设计非常灵活,所以有些拆分不会很细,有些实现也过于简单,所以有些问题暂时先不要较真,后续再慢慢优化。

ioc第二版——极简ioc面相对象实现

上一篇文章已经对ioc的流程进行了简单分析,那么在面向对象设计环节,我们需要将需求描述转化为具体的类的设计。

首先,可以得出BeanDefinition类,该类抽象了Bean的定义;
其次,之前的ClassPathXmlApplicationContext做的活有点多,这里可以交给下面3个类去干。

  1. ClassPathXmlResource:负责解析xml资源,获取标签元素;
  2. XmlBeanDefinitionReader:负责解析xml标签元素,注册Bean;
  3. SimpleBeanFactory:工厂类,收集BeanDefinition定义(原料),创建Bean,获取Bean等

简单分析接口的抽象
读取资源文件获取类信息这一抽象行为中,资源文件的格式可能会发生变化,资源文件的来源可能会发生变化,比如通过扫描类的注解获取类信息,从网络中获取,从数据库中读取等。但是资源的内容(指类信息,是封装成BeanDefinition前的一种抽象描述)的可迭代性是不变的;所以可以抽象出Resource接口,代码如下:

public interface Resource extends Iterator<Object> {

}

所以ClassPathXmlResource的具体方法实现就是完成项目文件中xml资源文件的读取,获得类信息;具体代码如下:

@Slf4j
public class ClassPathXmlResource implements Resource{

    Document document;
    Element rootElement;
    Iterator<Element> elementIterator;

    public ClassPathXmlResource(String fileName) {
        SAXReader saxReader = new SAXReader();
        URL xmlPath = this.getClass().getClassLoader().getResource(fileName);
        //将配置文件装载进来,生成一个迭代器,可以用于遍历
        try {
            this.document = saxReader.read(xmlPath);
            this.rootElement = document.getRootElement();
            this.elementIterator = this.rootElement.elementIterator();
        }catch (Exception e) {
            log.error("读取xml配置发生错误:", e);
        }
    }


    @Override
    public boolean hasNext() {
        return elementIterator.hasNext();
    }

    @Override
    public Object next() {
        return elementIterator.next();
    }

    @Override
    public void remove() {
        elementIterator.remove();
    }

}

当前版本并不打算进行特别细的拆分,所以这里抽象的BeanFactory接口功能比较多,既可以注册Bean定义(原料),又可以获取Bean。代码如下:

public interface BeanFactory {
    Object getBean(String beanName) throws BeansException;
    void registerBeanDefinition(BeanDefinition beanDefinition);
}

那么SimpleBeanFactory将会实现上面的功能,具体代码如下:

@Slf4j
public class SimpleBeanFactory implements BeanFactory{
    private final List<BeanDefinition> beanDefinitions = new ArrayList<>();
    private final List<String> beanNames = new ArrayList<>();
    private final Map<String, Object> singletons = new HashMap<>();
    public SimpleBeanFactory() {

    }

    /**
     * 核心方法
     * 注册Bean和获取Bean
     */
    @Override
    public Object getBean(String beanName) throws BeansException {
        // 先尝试直接拿Bean实例
        Object singleton = singletons.get(beanName);
        //如果此时还没有这个Bean的实例,则获取它的定义来创建实例
        if (singleton == null) {
            int index = beanNames.indexOf(beanName);
            if (index == -1 ) {
                throw new BeansException("BeanDefinition不存在!");
            }else {
                // 获取Bean的定义
                BeanDefinition beanDefinition = beanDefinitions.get(index);
                // Bean实例化
                try {
                    singleton = Class.forName(beanDefinition.getClassName()).getDeclaredConstructor().newInstance();
                } catch (InstantiationException | IllegalAccessException | InvocationTargetException |
                         NoSuchMethodException | ClassNotFoundException e) {
                    log.error("创建Bean实例出错:", e);
                }
                // 注册Bean实例
                singletons.put(beanName, singleton);
            }
        }

        return singleton;
    }

    @Override
    public void registerBeanDefinition(BeanDefinition beanDefinition) {
        this.beanDefinitions.add(beanDefinition);
        this.beanNames.add(beanDefinition.getId());
    }
}

因为 XmlBeanDefinitionReader解析资源后需要注册Bean,所以需要传入BeanFactory的对象,获取注册Bean的能力,其实现代码如下:

public class XmlBeanDefinitionReader {
    BeanFactory beanFactory;

    public XmlBeanDefinitionReader(BeanFactory beanFactory) {
        this.beanFactory = beanFactory;
    }

    public void loadBeanDefinitions(Resource resource) {
        while (resource.hasNext()) {
            Element element = (Element) resource.next();
            String beanId = element.attributeValue("id");
            String beanClassName = element.attributeValue("class");
            BeanDefinition beanDefinition = new BeanDefinition(beanId, beanClassName);
            this.beanFactory.registerBeanDefinition(beanDefinition);
        }
    }
}

最后,给出ClassPathXmlApplicationContext最终的实现:

public class ClassPathXmlApplicationContext implements BeanFactory {

    BeanFactory beanFactory;

    //context负责整合容器的启动过程,读外部配置,解析Bean定义,创建BeanFactory
    public ClassPathXmlApplicationContext(String fileName) {
        // 创建资源
        Resource resource = new ClassPathXmlResource(fileName);
        // 创建依赖
        SimpleBeanFactory beanFactory = new SimpleBeanFactory();
        // 解析xml元素,并将解析的元素转换为BeanDefinition
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
        reader.loadBeanDefinitions(resource);
        this.beanFactory = beanFactory;
    }

    // //context再对外提供一个getBean,底下就是调用的BeanFactory对应的方法
    @Override
    public Object getBean(String beanName) throws BeansException {
        return beanFactory.getBean(beanName);
    }

    @Override
    public void registerBeanDefinition(BeanDefinition beanDefinition) {
        this.beanFactory.registerBeanDefinition(beanDefinition);
    }
}

下面给出类之间的交互关系图:
ioc-oop
类之间的交互简单描述:
ClassPathXmlApplicationContext通过new对象的形式创建ClassPathXmlResourceXmlBeanDefinitionReaderSimpleBeanFactory三个类;创建XmlBeanDefinitionReader时将 SimpleBeanFactory传入其构造函数,赋予其注册Bean的能力。

思考

IoC 的字面含义是“控制反转”,那么它究竟“反转”了什么?又是怎么体现在代码中的?

传统的程序设计中,我们经常手动 new 一个对象。如果在创建对象时,需要依赖其他对象,我们还需要手动管理这些依赖的对象的创建。但是,在 IoC 控制反转中,这个过程被翻转了过来,对象的创建和依赖关系的管理被转移到了 IoC 容器中

反转的内容:

  1. 对象的创建和生命周期管理:在没有IoC的情况下,对象的创建和生命周期管理由程序员显式控制。使用IoC后,这些责任被转移到框架或容器上。
  2. 依赖关系的管理:在传统编程中,组件之间的依赖关系通常在组件内部显式创建。而在IoC中,这些依赖关系通常由外部容器动态注入,减少了组件之间的耦合。

在代码中的体现:

// 传统方式,组件自己控制依赖
class TraditionalComponent {
    private Dependency dependency = new ConcreteDependency();

    public void doSomething() {
        dependency.performTask();
    }
}
***************************************************************
// 使用依赖注入,控制权被反转
interface Dependency {
    void performTask();
}

class ConcreteDependency implements Dependency {
    public void performTask() {
        // 任务实现
    }
}

@Service
class IoCComponent {
		@AutoWired
    private Dependency dependency;

    public void doSomething() {
        dependency.performTask();
    }
}

class Main {
    public static void main(String[] args) {
        // 依赖的创建和组件的创建是分离的
        Dependency dependency = new ConcreteDependency();
        IoCComponent component = new IoCComponent(dependency);
        component.doSomething();
    }
}

在这个例子中:

  • 传统方式TraditionalComponent 类自己创建并管理 Dependency 的实例。
  • IoC方式IoCComponent 类的依赖 Dependency 由外部(如主函数或框架)提供,而不是由组件自身创建。

这种方式的好处是,代码变得更加模块化,组件的测试和维护也变得更加容易。通过这种方式,我们可以看到“控制反转”的直接体现:即控制对象创建和依赖管理的责任从组件转移到了外部。

控制反转和依赖注入有何区别和联系?

控制反转的英文翻译是 Inversion Of Control,缩写为 IOC。控制反转并不是一种具体的实现技巧,而是一个比较笼统的设计思想,一般用来指导框架层面的设计。下面来看一个典型的通过框架来实现“控制反转”的例子:

/*IOC之前的测试方法,平铺直叙地写*/
public class UserServiceTest {
  public static boolean doTest() {
    // ... 
  }
  
  public static void main(String[] args) {//这部分逻辑可以放到框架中
    if (doTest()) {
      System.out.println("Test succeed.");
    } else {
      System.out.println("Test failed.");
    }
  }
}
// IOC,将控制逻辑交给框架执行,自己只实现doTest方法
// 下面给出IOC框架内容 
public abstract class TestCase {
	// run方法是控制逻辑,已经被实现,doTest等待子类实现
    public void run() {
        if (doTest()) {
            System.out.println("Test succeed.");
        } else {
            System.out.println("Test failed.");
        }
    }
    public abstract boolean doTest();
}
// TestCase容器实现
public class JunitApplication{
    private static final List<TestCase> testCases = new ArrayList<>();

    public static void register(TestCase testCase) {
        testCases.add(testCase);
    }

    public static void main(String[] args) {
	    // UserServiceTest 需要user实现
        JunitApplication.register(new UserServiceTest());
        for (TestCase testCase : testCases) {
            testCase.run();
        }
    }
}
// UserServiceTest继承TestCase
public class UserServiceTest extends TestCase{
    @Override
    public boolean doTest() {
        UserInfo info = new UserInfo();
        info.setUserId(1L);
        UserService service = new UserServiceImpl();
        UserInfo user = service.getUserById(1L);
        return user.getUserId() == info.getUserId();
    }
}

框架提供了一个可扩展的代码骨架,用来组装对象、管理整个执行流程。用户利用框架进行开发的时候,只需要往预留的扩展点上(继承TestCase),添加跟自己业务相关的代码,就可以利用框架来驱动整个程序流程的执行。

这里的“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程可以通过框架来控制。流程的控制权从程序员“反转”到了框架。

实际上,实现控制反转的方法有很多,除了刚才例子中所示的类似于模板设计模式的方法之外,还有下面要说的依赖注入等方法。所以我们会说控制反转是一种设计思想。

依赖注入 DI,不是一种设计思想,而是一种具体的编码技巧。

具体理解依赖注入:不通过 new() 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。

在实际的软件开发中,项目中往往会涉及几十、上百、甚至几百个类,类对象的创建和依赖注入会变得非常复杂。如果对象的创建都是靠我们自己写代码来完成,容易出错且开发成本也比较高。而对象创建和依赖注入的工作,本身跟具体的业务无关,我们完全可以抽象成框架来自动完成。
这个框架就是“依赖注入框架”。我们只需要通过依赖注入框架提供的扩展点,简单配置一下所有需要创建的类对象、类与类之间的依赖关系,就可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事情。

现成的依赖注入框架有很多,比如 Google Guice、Java Spring、Pico Container、Butterfly Container 等。

总结:控制反转是一种思想,依赖注入是一种具体实现方法;

问题

注意:本问题来源阅读spring相关源码

为什么ApplicationContext的实现类要实现接口BeanFactory而不仅仅是将BeanFactory作为自己的成员变量(组合的方式)?
或者说,为什么要赋予ApplicationContext BeanFactory的特性呢?

部分原因:

  1. 接口继承的优势:通过继承接口,ApplicationContext 可以在不破坏原有 BeanFactory 接口契约的前提下扩展新的功能。这有助于维护向后兼容性,同时允许框架的进一步发展。

  2. 统一API和类型安全

  3. 设计原则:这种继承关系遵循了面向对象设计中的“里氏替换原则”(Liskov Substitution Principle)。这意味着在任何需要 BeanFactoryListableBeanFactory 的地方,都可以无缝使用 ApplicationContext

  4. 框架整体架构考虑:Spring 框架的设计目的是提供一个全面、一致且易于使用的编程模型。将 ApplicationContext 设计为 ListableBeanFactory 的一个子接口,是框架设计中对于易用性、功能性和一致性的考虑。

总结

本文通过面向对象的分析,将上一版的核心类ClassPathXmlApplicationContext进行了初步的拆分。之后,本文简明地描述了控制反转的含义与反转的内容。最后,本文还总结了阅读Spring相关源码过程中遇到的一个问题。下一篇将会进一步拆分类,扩展BeanDefinition的内容,增加对属性的解析和构造方法参数的解析,并且解决循环依赖问题。

  • 22
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值