Spring IoC 容器实现原理:工厂模式 + 解析XML + 反射机制
以下实现基于 set 注入的方式,手写一个 Spring 框架。
一、项目初始化
1.1 创建项目/模块
采用 maven 的方式新建项目或模块:qhjspring1.2 引入依赖
1. jdk 版本:17 2. 打包方式采用:jar 3. dom4j + jaxen 依赖:解析 xml 文件 4. junit 依赖:测试类 5. log4j2 依赖:打印日志<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.qhjspring</groupId>
<artifactId>qhjspring</artifactId>
<version>1.0-SNAPSHOT</version>
<!--打成jar包,等测试时引用此包-->
<packaging>jar</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--dom4j是一个能够解析XML文件的java组件。-->
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.2.0</version>
</dependency>
<!--junit依赖-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<!--log4j2的依赖-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.19.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.19.0</version>
</dependency>
</dependencies>
</project>
二、一个常用的 set 注入例子
2.1 创建 Bean
分别创建两个 bean:User 和 Address,以及其 get、set、toString 方法。2.2 创建 xml 配置文件
使用 value 给简单属性赋值,使用 ref 给引用属性赋值:<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean id="userBean" class="com.qhjspring.bean.User">
<property name="name" value="青花椒"/>
<property name="age" value="18"/>
<property name="addr" ref="addrBean"/>
</bean>
<bean id="addrBean" class="com.qhjspring.bean.Address">
<property name="city" value="上海"/>
<property name="street" value="青浦区"/>
<property name="zipcode" value="1000001"/>
</bean>
</beans>
2.3 测试类
```java @Test public void test1() { ApplicationContext context = new ClassPathXmlApplicationContext("qhjspring.xml"); Object userBean = context.getBean("userBean"); System.out.println(userBean); } ```输出结果:
三、手写 Spring
基于以上例子,如果要手写一个 Spring 框架,有四个步骤:- ApplicationContext 接口,提供 getBean() 方法;
- 实现类 ClassPathXmlApplicationContext,加载 xml 配置文件;
- 实例化所有的 Bean;
- 给 Bean 的属性赋值(调用 set 方法)。
⚠️ 其中,第 4、5 步是典型的 “Spring 解决 set + singleton 模式下的循环依赖” 问题:
将 “实例化 Bean” 和 “给 Bean 属性赋值” 两个动作分开。实例化 Bean 后,先不给属性赋值,而是提前将 Bean 对象曝光给外界(类似于缓存),所有单例的 Bean 全部实例化完成后,再调用 set 方法给属性赋值。
3.1 ApplicationContext 接口
ApplicationContext 接口中提供了一个 getBean() 方法,通过该方法可以获取 Bean 对象。/**
* @author qinghuajiao
* @version 1.0
* @description: ApplicantContext接口
* @date 2025/8/28 22:56
*/
public interface ApplicationContext {
/**
* 根据Bean的id获取bean实例
*
* @param beanId Bean的id
* @return
*/
Object getBean(String beanId);
}
3.2 实现类 ClassPathXmlApplicationContext
此类实现 ApplicationContext 接口,核心问题就是:🤔 如何存储 Bean 的集合?如何从集合中获取某个 Bean 实例?
可以采用 Map 集合存储 Bean 实例。Map 集合的 key 存储 beanId,value 存储 Bean 实例:Map<String, Object>。
/**
* @author qinghuajiao
* @version 1.0
* @description: ClassPathXmlApplicationContext类
* @date 2025/8/28 23:01
*/
public class ClassPathXmlApplicationContext implements ApplicationContext {
// 存储Bean的Map集合
Map<String, Object> beanMap = new HashMap<>();
/**
* 解析xml文件,创建所有Bean实例,并将Bean实例存放到Map集合中。
*
* @param configLocation spring配置文件的路径。注意:使用ClassPathXmlApplicationContext,配置文件应当放到类路径下。
*/
public ClassPathXmlApplicationContext(String configLocation) {
}
@Override
public Object getBean(String beanName) {
return beanMap.get(beanName);
}
}
3.3 解析xml文件,实例化 Bean
解析xml文件、实例化Bean需要三步:- 读取文件进行解析(注意要把 xml 文件放到类路径下,否则加载不到);
- 获取解析后的所有 bean 标签(使用反斜杠 // 获取标签);
- 遍历 bean 标签,曝光 Bean
/**
* 解析xml文件,创建所有Bean实例,并将Bean实例存放到beanMap集合中。
*
* @param configLocation spring配置文件的路径。注意:使用ClassPathXmlApplicationContext,配置文件应当放到类路径下。
*/
public ClassPathXmlApplicationContext(String configLocation) {
try {
/**
* 1. 解析xml文件
*/
// 这是dom4j解析xml文件的核心对象
SAXReader reader = new SAXReader();
// 获取一个输入流,指向配置文件
InputStream inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream(configLocation);
// 读取文件
Document document = reader.read(inputStream);
/**
* 2. 获取所有bean标签
*/
// 获取所有的bean标签,使用两个反斜杠//来获取某标签
List<Node> nodes = document.selectNodes("//bean");
/**
* 3. 遍历bean标签,曝光Bean
*/
nodes.forEach(node -> {
try {
// 向下转型:为了使用Element接口里更丰富的方法
Element element = (Element) node;
// 获取id、class属性
String id = element.attributeValue("id");
String className = element.attributeValue("class");
logger.info("beanName={}", id);
logger.info("beanClassName={}", className);
// 通过反射机制创建对象,将其放入到beanMap集合中,提前曝光。
Class<?> clazz = Class.forName(className);
// 获取无参构造方法
Constructor<?> declaredCon = clazz.getDeclaredConstructor();
// 调用无参构造方法实例化Bean
Object bean = declaredCon.newInstance();
// 将Bean曝光,加入到beanMap集合中
beanMap.put(id, bean);
// 记录日志
logger.info("beanMap: {}", beanMap.toString());
} catch (Exception e) {
e.printStackTrace();
}
});
/**
* 4. 再次遍历bean标签,给属性赋值
*/
} catch (Exception e) {
e.printStackTrace();
}
}
3.4 给 Bean 的属性赋值
/**
* 4. 再次遍历bean标签,给属性赋值
*/
nodes.forEach(node -> {
try {
// 向下转型:为了使用Element接口里更丰富的方法
Element element = (Element) node;
// 获取id、class属性
String id = element.attributeValue("id");
String className = element.attributeValue("class");
logger.info("beanName={}", id);
logger.info("beanClassName={}", className);
// 通过反射机制创建对象,将其放入到beanMap集合中,提前曝光。
Class<?> clazz = Class.forName(className);
// 获取该bean标签下所有的属性property标签
List<Element> propertys = element.elements("property");
// 遍历所有的属性标签
propertys.forEach(property -> {
try {
// 获取属性名
String propertyName = property.attributeValue("name");
// 获取属性类型
Field field = clazz.getDeclaredField(propertyName);
logger.info("属性名:{}", propertyName);
// 获取set方法名 set+属性名第一个字母大写+属性名其余字母小写
String setMethodName = "set" + propertyName.toUpperCase().charAt(0) + propertyName.substring(1);
// 获取set方法
Method setMethod = clazz.getDeclaredMethod(setMethodName, field.getType());
// 获取具体的值(简单类型value 和 非简单类型ref)
String value = property.attributeValue("value");
Object actualValue = null;
String ref = property.attributeValue("ref");
/**
* 1️⃣说明这个值是简单类型
*/
if (value != null) {
// 调用set方法(set方法没有返回值)
// 我们myspring框架声明一下:我们只支持这些类型为简单类型
// byte short int long float double boolean char
// Byte Short Integer Long Float Double Boolean Character
// String
// 获取属性类型名
String propertyTypeSimpleName = field.getType().getSimpleName();
switch (propertyTypeSimpleName) {
case "byte":
actualValue = Byte.parseByte(value);
break;
case "short":
actualValue = Short.parseShort(value);
break;
case "int":
actualValue = Integer.parseInt(value);
break;
case "long":
actualValue = Long.parseLong(value);
break;
case "float":
actualValue = Float.parseFloat(value);
break;
case "double":
actualValue = Double.parseDouble(value);
break;
case "boolean":
actualValue = Boolean.parseBoolean(value);
break;
case "char":
actualValue = value.charAt(0);
break;
case "Byte":
actualValue = Byte.valueOf(value);
break;
case "Short":
actualValue = Short.valueOf(value);
break;
case "Integer":
actualValue = Integer.valueOf(value);
break;
case "Long":
actualValue = Long.valueOf(value);
break;
case "Float":
actualValue = Float.valueOf(value);
break;
case "Double":
actualValue = Double.valueOf(value);
break;
case "Boolean":
actualValue = Boolean.valueOf(value);
break;
case "Character":
actualValue = Character.valueOf(value.charAt(0));
break;
case "String":
actualValue = value;
}
setMethod.invoke(beanMap.get(id), actualValue);
}
/**
* 2️⃣说明这个值是非简单类型
*/
if (ref != null) {
// 调用set方法(set方法没有返回值)
setMethod.invoke(beanMap.get(id), beanMap.get(ref));
}
} catch (Exception e) {
e.printStackTrace();
}
});
} catch (Exception e) {
e.printStackTrace();
}
});
四、打包发布、测试
4.1 打包发布
4.2 测试
1. 引入jar包<!--用qhjspring框架,需要引入依赖-->
<dependency>
<groupId>org.qhjspring</groupId>
<artifactId>qhjspring</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
- xml文件
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean id="vip" class="com.powernode.myspring.bean.Vip">
<property name="name" value="jackson"></property>
<property name="age" value="30"></property>
<property name="height" value="1.83"></property>
</bean>
</beans>
- 测试类
import com.qhjspring.core.ApplicationContext;
import com.qhjspring.core.ClassPathXmlApplicationContext;
import org.junit.Test;
/**
* @author qinghuajiao
* @version 1.0
* @description: ClassPathXmlApplicationContext类
* @date 2025/8/28 23:38
*/
public class MySpringTest {
@Test
public void testMySpring(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("myspring.xml");
Object vip = applicationContext.getBean("vip");
System.out.println(vip);
}
}