更新
2023年12月5日更新:控制反转和依赖注入有何区别和联系?
开篇
上一篇文章——手写Spring框架——IOC实现(一)对ioc功能进行了分析与极简实现,但是涉及的类非常少,函数的实现也比较简单,并不能体现设计模式的思想,这一篇更多地参考原专栏的实现内容,将极简版ioc实现进行面向对象设计与实现。
因为面向对象设计非常灵活,所以有些拆分不会很细,有些实现也过于简单,所以有些问题暂时先不要较真,后续再慢慢优化。
ioc第二版——极简ioc面相对象实现
上一篇文章已经对ioc的流程进行了简单分析,那么在面向对象设计环节,我们需要将需求描述转化为具体的类的设计。
首先,可以得出BeanDefinition
类,该类抽象了Bean的定义;
其次,之前的ClassPathXmlApplicationContext
做的活有点多,这里可以交给下面3个类去干。
ClassPathXmlResource
:负责解析xml资源,获取标签元素;XmlBeanDefinitionReader
:负责解析xml标签元素,注册Bean;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);
}
}
下面给出类之间的交互关系图:
类之间的交互简单描述:
ClassPathXmlApplicationContext
通过new对象的形式创建ClassPathXmlResource
、XmlBeanDefinitionReader
、 SimpleBeanFactory
三个类;创建XmlBeanDefinitionReader
时将 SimpleBeanFactory
传入其构造函数,赋予其注册Bean的能力。
思考
IoC 的字面含义是“控制反转”,那么它究竟“反转”了什么?又是怎么体现在代码中的?
传统的程序设计中,我们经常手动 new 一个对象。如果在创建对象时,需要依赖其他对象,我们还需要手动管理这些依赖的对象的创建。但是,在 IoC 控制反转中,这个过程被翻转了过来,对象的创建和依赖关系的管理被转移到了 IoC 容器中。
反转的内容:
- 对象的创建和生命周期管理:在没有IoC的情况下,对象的创建和生命周期管理由程序员显式控制。使用IoC后,这些责任被转移到框架或容器上。
- 依赖关系的管理:在传统编程中,组件之间的依赖关系通常在组件内部显式创建。而在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的特性呢?
部分原因:
-
接口继承的优势:通过继承接口,
ApplicationContext
可以在不破坏原有BeanFactory
接口契约的前提下扩展新的功能。这有助于维护向后兼容性,同时允许框架的进一步发展。 -
统一API和类型安全
-
设计原则:这种继承关系遵循了面向对象设计中的“里氏替换原则”(Liskov Substitution Principle)。这意味着在任何需要
BeanFactory
或ListableBeanFactory
的地方,都可以无缝使用ApplicationContext
。 -
框架整体架构考虑:Spring 框架的设计目的是提供一个全面、一致且易于使用的编程模型。将
ApplicationContext
设计为ListableBeanFactory
的一个子接口,是框架设计中对于易用性、功能性和一致性的考虑。
总结
本文通过面向对象的分析,将上一版的核心类ClassPathXmlApplicationContext
进行了初步的拆分。之后,本文简明地描述了控制反转的含义与反转的内容。最后,本文还总结了阅读Spring相关源码过程中遇到的一个问题。下一篇将会进一步拆分类,扩展BeanDefinition的内容,增加对属性的解析和构造方法参数的解析,并且解决循环依赖问题。