前言
又迎来了一年一度的金三银四,虽然说今年的大环境不好,但是招聘还是在火热进行中。
面试过 Java 工程师的小伙伴都知道,Spring 中的 IOC 是面试高频题,面试官上来就问,知道什么是 IOC 吗,IOC 是如何初始化的,以及 Bean 的生命周期等相关内容。
那么接下来,让我们使用 ChatGPT 并结合自己的理解来讲述一下 IOC 的相关内容。
什么是 IOC?
什么是 IOC?ChatGPT 是这么说的:
Spring IOC(Inversion of Control,控制反转)是 Spring 框架的核心特性之一,它的基本思想是将对象的创建和对象间的关系由程序员手动管理转移到框架中自动管理。
在传统的开发模式中,程序员通过 new
关键字手动创建对象,并在程序中直接使用这些对象。而在 Spring IOC 中,程序员不需要手动创建对象,而是将对象的创建交给 Spring 容器进行管理。Spring 容器会根据配置文件中的规则自动创建对象并管理对象之间的关系,程序员只需要使用这些对象即可,从而将程序中的控制权反转到了框架中。
通过 Spring IOC,程序员可以更加专注于业务逻辑的实现,而不需要关注对象的创建和管理,从而提高了开发效率和程序的可维护性。
一个关于 IOC 的实际例子是一个家庭使用了自动化家居系统。这个系统可能会有不同的设备、传感器和控制器,例如智能灯泡、温度传感器、音乐播放器等。在传统的编程模式中,每个设备都需要直接连接到主控制器,并由主控制器直接管理所有设备的功能。但使用 IOC 模式后,每个设备可以被视为独立的组件,它们可以通过一个通用的接口来与其他组件交互。这样,当需要添加或删除设备时,只需对该设备进行必要的配置,而不需要修改整个系统的代码。
例如,当用户想要调整房间的温度时,他可以直接与智能温度传感器进行交互,传感器可以将数据发送到主控制器,然后主控制器可以向空调发送指令来改变室内温度。这里的关键在于,每个组件都被视为独立的实体,它们可以相互通信,而无需直接与其他组件或主控制器进行耦合。这种设计模式提高了系统的可维护性和可扩展性。
具体可以参考这篇 Spring IoC有什么好处呢?,博主提炼了一下:
控制反转(Inversion of Control) 就是依赖倒置原则的一种代码设计的思路。具体采用的方法就是所谓的依赖注入(Dependency Injection) ,这几种概念的关系大概如下:
什么是依赖倒置原则? 假设我们设计一辆汽车,结构如下所示:
这样的设计看起来没问题,但是可维护性却很低,在后面会进行解释。那现在换一种思路进行设计:
这就是依赖倒置原则 —— 把原本的高层建筑依赖底层建筑倒置过来,变成底层建筑依赖高层建筑。高层建筑决定需要什么,底层去实现这样的需求,但是高层并不用管底层是怎么实现的。这样就不会出现前面的 “牵一发动全身” 的情况。
为了方便理解,先定义四个 Class,车,车身,底盘,轮胎。然后初始化这辆车,最后跑这辆车。代码结构如下:
这就是车子的第一个设计思路,上层建筑依赖下层建筑,即每一个类的构造函数都直接调用了底层代码的构造函数。这样就会出现很严重的问题,比如说我现在要把车的轮胎从30改成动态尺寸的,那么我们需要修改整个上层的所有构造函数!
这样的设计基本是不可维护的,因为在实际工程中,有些类会有几千个底层,如果要一一修改,所耗费的成本太大了。
那我们通过 IOC,对车子进行第二种思路的设计,重写车类的定义:
这次再把轮胎尺寸变成动态的,如果要让整个系统顺利运行,只需要做如下修改:
这样子就不需要修改上层的其他代码,因此 IOC 提高了系统的可维护性和可扩展性。
IOC 的初始化过程
IOC 是如何进行初始化的?ChatGPT 是这样子描述的:
Spring IOC 容器的初始化过程主要分为以下六个步骤:
- 资源定位:Spring IOC 容器会根据配置文件的路径,找到配置文件并加载到内存中。一般是从 XML 文件中进行读取。
- 载入配置文件:Spring IOC 容器会读取配置文件中的 Bean 定义信息,并将其封装成对应的 BeanDefinition 对象。例如读取 property 元素并注入到 BeanDefinition 实例中。
- 实例化 Bean 对象:Spring IOC 容器根据 BeanDefinition 对象中的信息,使用 Java 反射机制实例化 Bean 对象,并将其放入 IOC 容器中。
- 依赖注入:Spring IOC 容器会根据 BeanDefinition 对象中的依赖关系,注入 Bean 之间的依赖关系。
- Bean 初始化:如果 Bean 实现了特定的接口,Spring IOC 容器会在 Bean 实例化之后调用这些接口中的方法进行初始化。
- 容器初始化完成:Spring IOC 容器初始化完成后,可以通过
getBean()
方法获取容器中的 Bean 对象。
TIPs:
单例 bean 的初始化以及依赖注入一般都在容器初始化阶段进行,只有懒加载(lazy-init为true)的单例 bean 是在应用第一次调用
getBean()
时进行初始化和依赖注入。// AbstractApplicationContext // Instantiate all remaining (non-lazy-init) singletons. finishBeanFactoryInitialization(beanFactory); 复制代码
这里顺带提一下 Bean 的生命周期,如下图所示:
Bean 的生命周期一般包含以下阶段:
- 实例化:当程序调用某个类的构造方法时,将创建该类的实例。这个实例就是一个 Bean。
- 属性赋值:在实例化后,程序可以使用
set
方法或者直接为属性赋值,来设置 Bean 的属性值。 - 初始化:在属性赋值后,Spring 会通过回调函数(如
init-method
)或者注解(如@PostConstruct
)来完成 Bean 的初始化操作,比如建立与其他 Bean 的关联关系等。 - 使用:在初始化完成后,Bean 就可以被应用程序使用了。
- 销毁:当应用程序不再需要 Bean 的时候,Spring 会调用回调函数(如
destroy-method
)或者注解(如@PreDestroy
)来完成 Bean 的销毁操作,比如清理资源、断开数据库连接等。
TIPs:
需要注意的是,Spring 容器掌握着 Bean 的生命周期,所以所有的 Bean 操作都必须在 Spring 容器的管理下进行。同时要避免出现循环依赖,否则可能导致 Bean 的实例化失败。
手撕阉割版 IOC
先来一个简单的练练手,主打的就是一个凸显 IOC 特性:
public class IOCContainer {private Map<String, Object> beans; // 存储对象实例的 Mappublic IOCContainer() {beans = new HashMap<>();}// 注册 bean 到容器中public void registerBean(String name, Object bean) {beans.put(name, bean);}// 获取 bean 对象public Object getBean(String name) {return beans.get(name);}
}
复制代码
在上述代码中,将 Map 对象 beans
当成是 IOC 容器,用来存储注册进行来的 Bean,registerBean()
用来将 Bean 注册到容器当中,getBean()
方法则是从容器中获取已经注册的 Bean 对象。
那接下来写个测试用例,运行一下这个代码:
public class MyClass {public void sayHello() {System.out.println("Hello World! --sid10t.");}
}// 在 main 方法中使用 IOC 容器
public static void main(String[] args) {IOCContainer container = new IOCContainer();MyClass myClass = new MyClass();container.registerBean("myClass", myClass); // 将 MyClass 的实例注册到 IOC 容器中MyClass instance = (MyClass) container.getBean("myClass"); // 从 IOC 容器中获取 MyClass 的实例instance.sayHello(); // 调用 MyClass 实例的方法
}
复制代码
虽然这是一个非常简单的 IOC 容器实现的示例,但它清晰地表达了 IOC 的基本思想:通过将类的创建和依赖注入交给容器来管理,从而解耦应用程序中各个组件之间的依赖关系。在实际生产环境中,我们可能会需要更加完善的 IOC 容器实现,但这个示例可以作为一个基础来理解 IOC 容器的工作原理。
接下来,来点进阶挑战,通过 Java 反射机制,在原有的基础上,实现 Bean 的属性注入:
private Object createBean(Class<?> clazz, Map<String, Object> properties) throws Exception {try {// 构造函数实例化对象Constructor<?>[] constructors = clazz.getDeclaredConstructors();Constructor<?> constructor = null;for (Constructor<?> c : constructors) {if (c.getParameterCount() == 0) {constructor = c;break;}}if (constructor == null) {throw new RuntimeException("No default constructor found for bean: " + clazz);}Object bean = constructor.newInstance();// 注入依赖for (Field field : clazz.getDeclaredFields()) {if (properties.containsKey(field.getName())) {field.setAccessible(true);field.set(bean, properties.get(field.getName()));}}return bean;} catch (Exception e) {throw new RuntimeException("Failed to create bean: " + clazz, e);}
}
复制代码
通过 getDeclaredConstructors()
获取类的构造器,再通过 getParameterCount() == 0
来获取到默认构造器,即无参构造器,再使用 getDeclaredFields()
获取到相应的属性名,通过 set()
进行属性注入。
最后写一个测试用例运行一下代码:
public static void main(String[] args) throws Exception {MyIOCContainer container = new MyIOCContainer();// 注册 UserDTO 和 UserServiceMap<String, Object> userDTOProperties = new HashMap<>();userDTOProperties.put("url", "jdbc:mysql://localhost:3306/test");userDTOProperties.put("username", "root");userDTOProperties.put("password", "123456");container.registerBean("userDTO", UserDTO.class, userDTOProperties);Map<String, Object> userServiceProperties = new HashMap<>();userServiceProperties.put("userDTO", container.getBean("userDTO"));container.registerBean("userService", UserService.class, userServiceProperties);// 获取 UserService 并调用方法UserService userService = (UserService) container.getBean("userService");User user = new User("sid10t.", 18);userService.addUser(user);
}
复制代码
补充:
考虑到有些读者可能不知道怎么设计 User,UserDTO 和 UserService 类,就贴一下代码作为参考,其实是自己随意发挥就行,没有那么严谨;
User.java
public class User {private String name;private int age;public User() {}// Getter and Setter
}
复制代码
UserDTO.java
public class UserDTO {private String url;private String username;private String password;public UserDTO() {}// Getter and Setter
}
复制代码
UserService.java
public class UserService {private UserDTO userDTO;public UserService() {}public UserService(UserDTO userDTO) {this.userDTO = userDTO;}public void addUser(User user) {System.out.println("UserService.addUser: " + user.getName() + ", " + user.getAge());System.out.println("url: " + userDTO.getUrl() + ", username: " + userDTO.getUsername() + ", password: " + userDTO.getPassword());// 这里省略了其他方法调用}// Getter and Setter
}
复制代码
最后来点复杂的,在 Spring 框架中实现 Bean 的注入,作用域,初始化和销毁方法等功能,资源定位和载入配置文件这里就不赘述了:
public class IOCContainer {private Map<String, BeanDefinition> beanDefinitionMap = new HashMap<>();private Map<String, Object> singletonBeanMap = new HashMap<>();// 资源定位...// 载入配置文件...public void register(Class<?> beanClass, String beanName, Scope scope) {beanDefinitionMap.put(beanName, new BeanDefinition(beanClass, scope));}public <T> T getBean(String beanName) {BeanDefinition beanDefinition = beanDefinitionMap.get(beanName);if (beanDefinition == null) {throw new RuntimeException("No such bean: " + beanName);}if (beanDefinition.getScope() == Scope.SINGLETON) {Object singletonBean = singletonBeanMap.get(beanName);if (singletonBean != null) {return (T) singletonBean;}}Object bean = createBean(beanDefinition);if (beanDefinition.getScope() == Scope.SINGLETON) {singletonBeanMap.put(beanName, bean);}return (T) bean;}private Object createBean(BeanDefinition beanDefinition) {Class<?> beanClass = beanDefinition.getBeanClass();try {// 构造函数实例化对象Constructor<?>[] constructors = beanClass.getDeclaredConstructors();Constructor<?> constructor = null;for (Constructor<?> c : constructors) {if (c.getParameterCount() == 0) {constructor = c;break;}}if (constructor == null) {throw new RuntimeException("No default constructor found for bean: " + beanClass);}Object bean = constructor.newInstance();// 注入依赖Field[] fields = beanClass.getDeclaredFields();for (Field field : fields) {if (field.isAnnotationPresent(Autowired.class)) {String fieldName = field.getName();Object dependencyBean = getBean(fieldName);field.setAccessible(true);field.set(bean, dependencyBean);}}// 执行初始化方法Method[] methods = beanClass.getDeclaredMethods();for (Method method : methods) {if (method.isAnnotationPresent(InitMethod.class)) {method.invoke(bean);}}return bean;} catch (Exception e) {throw new RuntimeException("Failed to create bean: " + beanClass, e);}}public void close() {for (Object singletonBean : singletonBeanMap.values()) {Class<?> beanClass = singletonBean.getClass();Method[] methods = beanClass.getDeclaredMethods();for (Method method : methods) {if (method.isAnnotationPresent(DestroyMethod.class)) {try {method.invoke(singletonBean);} catch (Exception e) {throw new RuntimeException("Failed to invoke destroy method for bean: " + beanClass, e);}}}}}
}