手写Spring框架Day1

ylnxwlp 菲比啾比!

开始探究Spring啦!先从最简单最原理的部分写起。

第一步 创建Spring容器

日常使用的有根据路径拿到配置的容器类(ClassPathXmlApplicationContext),也有根据java配置类拿到配置的容器类(AnnotationConfigApplicationContext)。今天手写的是根据配置的Spring容器。
创建容器类SwissApplicationContext。里面先写上一个成员变量,也就是指定的配置类:

1
private Class configClass;

创建一个类用来当作配置类,命名为AppConfig。
配置类嘛,肯定有个配置类的注解,标志一下扫描的路径,也就是@ComponentScan。写出这个注解,加到配置类的头上:

1
2
3
@ComponentScan("com.swiss.service")
public class AppConfig {
}

然后我们创建一个用来当作bean的类UserService。当然,要注册成bean,最简单的一个注解就是@Component了。同样创建出来加到UserService头上。
这两个注解目前都只有一个默认的value属性,一个定义扫描路径,一个定义bean的名字。
现在可以回到容器SwissApplicationContext上了。容器容器,肯定是要拿bean的嘛,所以肯定有一个getBean方法。所以创建一个暂时返回null的getBean,后续补充。

1
2
3
public Object getBean(String beanName) {
return null;
}

新建Test类,把容器new出来。

1
SwissApplicationContext swissApplicationContext = new SwissApplicationContext(AppConfig.class);

然后写一个getBean:

1
UserService userService = (UserService) swissApplicationContext.getBean("userService");

当然这个getBean只是一个暂时的。毕竟bean的类型还需要确定嘛。
到这里,前置的Spring容器创建准备就做好了。

第二步 开始扫描

我们知道,在容器创建的时候,就要开始进行bean的创建了。那么第一步肯定是发现bean吧,所以肯定在构造函数里面开始进行扫描。
这个时候就用到了配置类:配置类的顶着@ComponentScan注解,告诉Spring容器应该在哪里去扫描bean,就像上面写的一样:

1
@ComponentScan("com.swiss.service")

所以首先就得获得这个配置类告诉我们的包路径了。容器里面已经有一个配置类的成员变量实例了,直接拿来用:

1
if (configClass.isAnnotationPresent(ComponentScan.class)) {}

存在ComponentScan注解,再获取扫描包路径:

1
2
ComponentScan componentScanAnnotation = (ComponentScan) configClass.getAnnotation(ComponentScan.class);
String path = componentScanAnnotation.value(); //获得扫描路径 com.swiss.service

但是现在就有一个问题了:扫描的内容应该是编译出来的class文件,因为运行时环境只加载.class文件,并且Spring依赖Java反射机制,所以这个路径需要做一些处理才能得到真正的编译出class文件的路径。

step1:把路径里面的”.”改为”/“。

1
path = path.replace(".", "/");//com/swiss/service

step2:(最重要)通过类加载器得到class资源存放路径,因为JVM会通过类路径(classpath)来查找.class文件和资源文件。

1
2
ClassLoader classLoader = SwissApplicationContext.class.getClassLoader();
URL resource = classLoader.getResource(path);

如果路径中有空格等非全英文路径情况,记得URL解析一下:

1
2
3
4
5
6
7
String decodedPath;
try {
decodedPath = URLDecoder.decode(resource.getFile(), String.valueOf(StandardCharsets.UTF_8));
} catch (UnsupportedEncodingException e) {
System.out.println("URL错误!");
return;
}

如果没有的话,直接File file = new File(resource.getFile());即可,否则传入解析后的URL。
这个时候存放class文件的File文件夹就被我们获取到了。我们可以遍历文件,判断哪些是.class文件,然后进一步处理。

1
2
3
4
5
6
7
8
9
if (file.isDirectory()) {
File[] list = file.listFiles();
for (File f : list) {
String fileName = f.getAbsolutePath();
if (fileName.endsWith(".class")) {

}
}
}

接着,如果是class文件,要怎么办呢?就得开始确定这个类是否为bean了吧。
我们知道,bean是通过@Component注解指定的,所以我们只需要确定类上有没有这个注解。那又怎么拿到这个class对象呢?
找找前面,我们发现有一个classloader已经被我们定义好了。那直接把这个类的全限定名丢进去load,不就得到这个class对象了吗?
这个时候,我们就可以判断是否有注解声明啦。

1
2
3
4
5
6
7
8
9
10
11
12
13
try {
//这边需要传入一个类的全限定名字,如com.swiss.service.UserService,这边为了省事,就写死,实际上应该要去完成一个灵活的路径拆分得到全限定名字
String className = fileName.substring(fileName.indexOf("com"), fileName.indexOf(".class"));
className = className.replace("\\", ".");
System.out.println(className);
Class<?> clazz = classLoader.loadClass(className);
if (clazz.isAnnotationPresent(Component.class)) {
//说明这是一个bean
}
} catch (ClassNotFoundException e) {
System.out.println("获取是否为bean时,加载类失败!");
return;
}

扫描的原理也就到这里结束咯。

第三步 BeanDefinition的生成

在上一步中,我们判断到了某一个类是否为一个bean,那么接下来是不是应该要开始创建实例存储了呢?
当然不急!虽然还没写之前还有所耳闻,Spring里面是单例bean;但是bean也是可以多例的。为了标识这个bean是否需要多例,需要一个@Scope注解。

1
2
3
4
5
6
7
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Scope {

String value() default "";

}

对于之前的UserService,我们加上这个注解:

1
2
3
4
@Component
@Scope("prototype")
public class UserService {
}

prototype应该不陌生吧,就是多例。
标记好了bean的单多例情况,接下来难道就要开始创建了吗?也不是,Spring在真正实例化bean的前面还加了一个态:BeanDefinition,方便处理单多例情况。
那我们就着手写一个beanDefinition吧。也就是bean的定义。需要一些什么成员变量呢?
首先肯定是类型。class类型对象,然后就是scope,也就是单多例情况。先写这两个吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class BeanDefinition {

private Class type;
private String scope;

public Class getType() {
return type;
}

public void setType(Class type) {
this.type = type;
}

public String getScope() {
return scope;
}

public void setScope(String scope) {
this.scope = scope;
}
}

这下我们就可以回头继续完成构造方法了,当判断到一个类是bean的时候,我们先对其进行定义,通过解析@Scope确定单多例和@Component确定bean的名字(如果没有,默认使用类名):

1
2
3
4
5
6
7
8
9
10
11
12
String beanName = clazz.getAnnotation(Component.class).value();
if (beanName.isEmpty()) { //这边需要判断用户是否有指定bean名字,如果没有就应该使用默认规定变为驼峰,或者不变,如URLService就无需变化
beanName = Introspector.decapitalize(clazz.getSimpleName());
}
BeanDefinition beanDefinition = new BeanDefinition();
if (clazz.isAnnotationPresent(Scope.class)) {
Scope scopeAnnotation = clazz.getAnnotation(Scope.class);
beanDefinition.setScope(scopeAnnotation.value());
} else {
beanDefinition.setScope("singleton");
}
beanDefinition.setType(clazz);

构建好了,把这个定义存起来就行了。在容器里面新定义一个线程安全哈希表,并加入:

1
2
private ConcurrentHashMap<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>();
beanDefinitionMap.put(beanName, beanDefinition);

好了,beanDefinition的定义就构建出来了。

第四步 完成getBean方法

我们现在已经有了一个存着beanDefinition的Map了,我们会发现有一些bean已经是单例的,那我们是不是可以考虑直接进行创建进行管理呢?
完全可以!继续在构造函数后面进行添加一些创建的内容。
注意:这一步我们并没有具体实现创建bean的流程,而是实现getBean的自身逻辑。
对于单例的bean,我们可以创建一个单例池来进行管理,然后我们直接在构造函数最后开始进行创建单例bean:

1
2
3
4
5
6
7
8
9
private ConcurrentHashMap<String, Object> singletonObjects = new ConcurrentHashMap<>();

for (String beanName : beanDefinitionMap.keySet()) {
BeanDefinition beanDefinition = beanDefinitionMap.get(beanName);
if (beanDefinition.getScope().equals("singleton")) {
Object bean = createBean(beanName, beanDefinition);//createBean方法将在后面进行完成
singletonObjects.put(beanName, bean);
}
}

这么一来,我们直接就有了单例的bean池。那getBean也就好说了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Object getBean(String beanName) {
BeanDefinition beanDefinition = beanDefinitionMap.get(beanName);
if (beanDefinition == null) {
throw new NullPointerException();
} else {
String scope = beanDefinition.getScope();
if (scope.equals("singleton")) {
Object bean = singletonObjects.get(beanName);
if (bean == null) {
bean = createBean(beanName, beanDefinition);
singletonObjects.put(beanName, bean);
}
return bean;
} else {
//多例
return createBean(beanName,beanDefinition);
}
}
}

getBean就是如此简单!

第五步 创建bean

终于来到创建Bean的时候了。在上面遗留的createBean方法中,我们可以用反射轻松的实现实例化一个bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private Object createBean(String beanName, BeanDefinition beanDefinition) {
Class clazz = beanDefinition.getType();

try {
Object instance = clazz.getConstructor().newInstance();

return instance;
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}

那么似乎到目前为之,一个可以使用的核心架构就出来了呢。在Test方法里面,我们开始测试:

1
2
3
4
5
6
7
8
9
10
11
public class Test {
public static void main(String[] args) {

SwissApplicationContext swissApplicationContext = new SwissApplicationContext(AppConfig.class);

System.out.println(swissApplicationContext.getBean("userService"));
System.out.println(swissApplicationContext.getBean("userService"));
System.out.println(swissApplicationContext.getBean("userService"));
System.out.println(swissApplicationContext.getBean("userService"));
}
}

看输出结果:

1
2
3
4
com.swiss.service.UserService@60e53b93
com.swiss.service.UserService@60e53b93
com.swiss.service.UserService@60e53b93
com.swiss.service.UserService@60e53b93

果然是单例!那再把Scope换成prototype呢:

1
2
3
4
com.swiss.service.UserService@5e2de80c
com.swiss.service.UserService@1d44bcfa
com.swiss.service.UserService@266474c2
com.swiss.service.UserService@6f94fa3e

是多例,成功了。

到这里,第一天的核心的内容也就结束了。

  • 标题: 手写Spring框架Day1
  • 作者: ylnxwlp
  • 创建于 : 2025-10-01 17:24:20
  • 更新于 : 2025-10-02 15:00:17
  • 链接: https://www.swissroll.today/2025/10/01/手写Spring框架day1/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论