手写Spring框架Day1
开始探究Spring啦!先从最简单最原理的部分写起。
第一步 创建Spring容器
日常使用的有根据路径拿到配置的容器类(ClassPathXmlApplicationContext),也有根据java配置类拿到配置的容器类(AnnotationConfigApplicationContext)。今天手写的是根据配置的Spring容器。
创建容器类SwissApplicationContext。里面先写上一个成员变量,也就是指定的配置类:
1 | private Class configClass; |
创建一个类用来当作配置类,命名为AppConfig。
配置类嘛,肯定有个配置类的注解,标志一下扫描的路径,也就是@ComponentScan。写出这个注解,加到配置类的头上:
1 |
|
然后我们创建一个用来当作bean的类UserService。当然,要注册成bean,最简单的一个注解就是@Component了。同样创建出来加到UserService头上。
这两个注解目前都只有一个默认的value属性,一个定义扫描路径,一个定义bean的名字。
现在可以回到容器SwissApplicationContext上了。容器容器,肯定是要拿bean的嘛,所以肯定有一个getBean方法。所以创建一个暂时返回null的getBean,后续补充。
1 | public Object getBean(String beanName) { |
新建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 |
所以首先就得获得这个配置类告诉我们的包路径了。容器里面已经有一个配置类的成员变量实例了,直接拿来用:
1 | if (configClass.isAnnotationPresent(ComponentScan.class)) {} |
存在ComponentScan注解,再获取扫描包路径:
1 | ComponentScan componentScanAnnotation = (ComponentScan) configClass.getAnnotation(ComponentScan.class); |
但是现在就有一个问题了:扫描的内容应该是编译出来的class文件,因为运行时环境只加载.class文件,并且Spring依赖Java反射机制,所以这个路径需要做一些处理才能得到真正的编译出class文件的路径。
step1:把路径里面的”.”改为”/“。
1 | path = path.replace(".", "/");//com/swiss/service |
step2:(最重要)通过类加载器得到class资源存放路径,因为JVM会通过类路径(classpath)来查找.class文件和资源文件。
1 | ClassLoader classLoader = SwissApplicationContext.class.getClassLoader(); |
如果路径中有空格等非全英文路径情况,记得URL解析一下:
1 | String decodedPath; |
如果没有的话,直接File file = new File(resource.getFile());即可,否则传入解析后的URL。
这个时候存放class文件的File文件夹就被我们获取到了。我们可以遍历文件,判断哪些是.class文件,然后进一步处理。
1 | if (file.isDirectory()) { |
接着,如果是class文件,要怎么办呢?就得开始确定这个类是否为bean了吧。
我们知道,bean是通过@Component注解指定的,所以我们只需要确定类上有没有这个注解。那又怎么拿到这个class对象呢?
找找前面,我们发现有一个classloader已经被我们定义好了。那直接把这个类的全限定名丢进去load,不就得到这个class对象了吗?
这个时候,我们就可以判断是否有注解声明啦。
1 | try { |
扫描的原理也就到这里结束咯。
第三步 BeanDefinition的生成
在上一步中,我们判断到了某一个类是否为一个bean,那么接下来是不是应该要开始创建实例存储了呢?
当然不急!虽然还没写之前还有所耳闻,Spring里面是单例bean;但是bean也是可以多例的。为了标识这个bean是否需要多例,需要一个@Scope注解。
1 |
|
对于之前的UserService,我们加上这个注解:
1 |
|
prototype应该不陌生吧,就是多例。
标记好了bean的单多例情况,接下来难道就要开始创建了吗?也不是,Spring在真正实例化bean的前面还加了一个态:BeanDefinition,方便处理单多例情况。
那我们就着手写一个beanDefinition吧。也就是bean的定义。需要一些什么成员变量呢?
首先肯定是类型。class类型对象,然后就是scope,也就是单多例情况。先写这两个吧。
1 | public class BeanDefinition { |
这下我们就可以回头继续完成构造方法了,当判断到一个类是bean的时候,我们先对其进行定义,通过解析@Scope确定单多例和@Component确定bean的名字(如果没有,默认使用类名):
1 | String beanName = clazz.getAnnotation(Component.class).value(); |
构建好了,把这个定义存起来就行了。在容器里面新定义一个线程安全哈希表,并加入:
1 | private ConcurrentHashMap<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(); |
好了,beanDefinition的定义就构建出来了。
第四步 完成getBean方法
我们现在已经有了一个存着beanDefinition的Map了,我们会发现有一些bean已经是单例的,那我们是不是可以考虑直接进行创建进行管理呢?
完全可以!继续在构造函数后面进行添加一些创建的内容。
注意:这一步我们并没有具体实现创建bean的流程,而是实现getBean的自身逻辑。
对于单例的bean,我们可以创建一个单例池来进行管理,然后我们直接在构造函数最后开始进行创建单例bean:
1 | private ConcurrentHashMap<String, Object> singletonObjects = new ConcurrentHashMap<>(); |
这么一来,我们直接就有了单例的bean池。那getBean也就好说了:
1 | public Object getBean(String beanName) { |
getBean就是如此简单!
第五步 创建bean
终于来到创建Bean的时候了。在上面遗留的createBean方法中,我们可以用反射轻松的实现实例化一个bean:
1 | private Object createBean(String beanName, BeanDefinition beanDefinition) { |
那么似乎到目前为之,一个可以使用的核心架构就出来了呢。在Test方法里面,我们开始测试:
1 | public class Test { |
看输出结果:
1 | com.swiss.service.UserService@60e53b93 |
果然是单例!那再把Scope换成prototype呢:
1 | com.swiss.service.UserService@5e2de80c |
是多例,成功了。
到这里,第一天的核心的内容也就结束了。
- 标题: 手写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 进行许可。