登录 |  注册
首页 >  编程语言 >  Spring开发实战学习笔记 >  从零开始-手写模拟Spring框架 (附教程源码)

从零开始-手写模拟Spring框架 (附教程源码)

首先就是创建一个普通的Java工程,起名TrySpring, 最终项目如下图:

tryspring.png

然后在 创建两个包 分别为service、spring。service用来存放bean、配置类等,spring包用来存放我们手撸的spring注解等

在service中创建UserService 类,再创建Test类,在Test类中创建我们的spring容器,此时以配置类的形式去创建容器,在service包中创建AppConfig配置类,在spring包里创建CsApplicationContext类,并提供一个参数与构造方法。

CsApplicationContext类

package com.cs.spring;

public class CsApplicationContext {

    private Class configClass;

    public CsApplicationContext(Class configClass) {
        this.configClass = configClass;
    }
}

Test类

package com.cs.service;

import com.cs.spring.CsApplicationContext;

public class Test {

    public static void main(String[] args) {

        //创建spring容器
        CsApplicationContext context = new CsApplicationContext(AppConfig.class);
    }
}

配置类APPConfig启动要去扫描路径,所以在spring包中定义ComponentScan注解,并提供属性等

package com.cs.spring;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)//表示注解生效的时间
@Target(ElementType.TYPE)//指定注解写的位置
public @interface ComponentScan {

    String value() default "";//指定扫描路径
}
package com.cs.service;


import com.cs.spring.ComponentScan;

@ComponentScan("com.cs.service")//指定扫描路径
public class AppConfig {

}

而UserService类一般会在类上加@Component注解表示把这个类定义为一个bean,所以此时再去spring包下写一个Component注解

package com.cs.spring;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)//表示注解生效的时间
@Target(ElementType.TYPE)//指定注解写的位置
public @interface Component {

    String value() default "";//给我们当前定义的bean取一个名字。
}
package com.cs.service;


import com.cs.spring.Component;

@Component("userService")//给当前bean指定一个名字
public class UserService {

}

此时我们就有了一个spring容器了,但是没啥用处,spring容器中有个getBean()方法,所以此时再去CsApplicationContext中创建一个getBean方法

package com.cs.spring;

public class CsApplicationContext {

    private Class configClass;

    public CsApplicationContext(Class configClass) {
        this.configClass = configClass;
    }

    public Object getBean(String beanName){


        return null;//暂时这样处理
    }
}

此时需要思考下,spring容器里的构造方法和getBean方法需要干什么 ?

我们在外面new一个Spring容器对象,就相当于我们去创建一个spring容器,或者启动一个spring容器,那么在启动spring容器的过程中,spring应该去干什么,他会去干什么?

1.spring扫描底层实现

spring启动首先去扫描,那spring是怎么进行扫描的呢?——通过类上面的@ComponentScan注解指定的路径

通过判断接收到的类,上面有没有ComponentScan注解,

package com.cs.spring;

import java.io.File;
import java.lang.annotation.Annotation;
import java.net.URL;

public class CsApplicationContext {

    private Class configClass;

    public CsApplicationContext(Class configClass) {
        this.configClass = configClass;

        //首先去判断你给我的这个类上面有没有ComponentScan这个注解
        if (configClass.isAnnotationPresent(ComponentScan.class)) {

            ComponentScan componentScanAnnotation
                    = (ComponentScan) configClass.getAnnotation(ComponentScan.class);

            String path = componentScanAnnotation.value();//拿到我们的扫描路径com.cs.service
//            System.out.println(path);

            path = path.replace(".", "/");//将包改写成路径形式com/cs/service
            //通过类加载器的一个getResource方法我们传进去一个相对路径获取对应的 资源

            ClassLoader classLoader = CsApplicationContext.class.getClassLoader();
            URL resource = classLoader.getResource(path);
            File file = new File(resource.getFile());

//            System.out.println(file);
            //D:\workspace\EveryDayFuck\TryagainSpring\out\production\TryagainSpring\com\cs\service
            if (file.isDirectory()) {
                //拿到文件夹里面所有的内容,所有文件
                File[] files = file.listFiles();

                //判断查找.class结尾的文件
                for (File f : files) {
                    //拿到文件的绝对路径,包含文件名
                    String fileName = f.getAbsolutePath();
//                    System.out.println(fileName);


                    String className = fileName.substring(fileName.indexOf("com"), fileName.indexOf(".class"));

//                    System.out.println(className);//com\cs\service\AppConfig
                    //com.cs.service.userSrevice
                    className = className.replace("\\", ".");

//                    System.out.println(className);
                    /*
                      com.cs.service.AppConfig
                      com.cs.service.Test
                      com.cs.service.UserService
                     */

                    if (fileName.endsWith(".class")) {
                        //此时我们要去判断这个.class结尾的文件是不是一个bean
                        //而判断是不是bean,我们去看看这个类上面有没有@Component注解
                        //此时用反射来判断是否有@Component注解,要用反射就得拿到Class对象,
                        //通过类加载器的loadClass方法传入类的全限定名到方法里去拿到对应的Class对象


                        try {
                            Class<?> clazz = classLoader.loadClass(className);

                            if (clazz.isAnnotationPresent(Component.class)) {
                                //存在@Component注解,是Bean

                                
                            }
                        } catch (ClassNotFoundException e) {
                            e.printStackTrace();
                        }


                    }
                }
            }
        }
    }

    public Object getBean(String beanName) {


        return null;//暂时这样处理
    }
}

2. 模拟BeanDefinition的生成

此时已经扫描到了有哪些bean,此时还不能直接给bean创建出来,因为还要分单例bean,还是多例bean,多例bean是在我们去获取的时候才会去创建,此时看向getBean方法,会根据传给我的beanName去找到对应的是哪个类,但是怎么根据名字去找到对应的类呢?又怎么根据名字判断是单例还是多例?如果是单例从单例池中去拿,如果是多例直接创建一个。

此时再去spring包下创建一个Scope注解

spring是首先会先生成一个BeanDefinition对象,在spring包下创建BeanDefinition类,提供两个属性,类型、作用域

package com.cs.spring;

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;
    }
}

总结:我们在扫描的过程间,会拿到很多的类,然后去加载这个类去判断这个类上面有没有Component注解,如果有就相当于定义了一个bean,那么我就去生成一个BeanDefinition对象,因为这个是空的BeanDefinition对象,那么这个BeanDefinition对象的类型是什么,我们可以先设置下,然后再去判断是单例的还是多例的,可以通过去判断这个类上面有没有@Scope注解,如果有就去看看Scope注解里定义的值是什么,直接设置给BeanDefinition对象,如果没有@Scope注解,就默认是单例。此时就真正生成了一个BeanDefinition对象。——然后去创建一个Map给BeanDefinition对象存进去。

3. 手写模拟getBean方法的底层实现

package com.cs.spring;

import java.io.File;
import java.lang.annotation.Annotation;
import java.net.URL;
import java.util.concurrent.ConcurrentHashMap;

public class CsApplicationContext {

    private Class configClass;

    private ConcurrentHashMap<String,BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>();

    private ConcurrentHashMap<String,Object> singletonObjects = new ConcurrentHashMap<>();

    public CsApplicationContext(Class configClass) {
        this.configClass = configClass;

        //1.扫描--->BeanDefinition-->beanDefinitionMap。首先去判断你给我的这个类上面有没有ComponentScan这个注解
        if (configClass.isAnnotationPresent(ComponentScan.class)) {

            ComponentScan componentScanAnnotation
                    = (ComponentScan) configClass.getAnnotation(ComponentScan.class);

            String path = componentScanAnnotation.value();//拿到我们的扫描路径com.cs.service
//            System.out.println(path);

            path = path.replace(".", "/");//将包改写成路径形式com/cs/service
            //通过类加载器的一个getResource方法我们传进去一个相对路径获取对应的 资源

            ClassLoader classLoader = CsApplicationContext.class.getClassLoader();
            URL resource = classLoader.getResource(path);
            File file = new File(resource.getFile());

//            System.out.println(file);
            //D:\workspace\EveryDayFuck\TryagainSpring\out\production\TryagainSpring\com\cs\service
            if (file.isDirectory()) {
                //拿到文件夹里面所有的内容,所有文件
                File[] files = file.listFiles();

                //判断查找.class结尾的文件
                for (File f : files) {
                    //拿到文件的绝对路径,包含文件名
                    String fileName = f.getAbsolutePath();
//                    System.out.println(fileName);


                    String className = fileName.substring(fileName.indexOf("com"), fileName.indexOf(".class"));

//                    System.out.println(className);//com\cs\service\AppConfig
                    //com.cs.service.userSrevice
                    className = className.replace("\\", ".");

//                    System.out.println(className);
                    /*
                      com.cs.service.AppConfig
                      com.cs.service.Test
                      com.cs.service.UserService
                     */

                    if (fileName.endsWith(".class")) {
                        //此时我们要去判断这个.class结尾的文件是不是一个bean
                        //而判断是不是bean,我们去看看这个类上面有没有@Component注解
                        //此时用反射来判断是否有@Component注解,要用反射就得拿到Class对象,
                        //通过类加载器的loadClass方法传入类的全限定名到方法里去拿到对应的Class对象


                        try {
                            Class<?> clazz = classLoader.loadClass(className);

                            if (clazz.isAnnotationPresent(Component.class)) {

                                Component component = clazz.getAnnotation(Component.class);
                                String beanName = component.value();
                                //存在@Component注解,是Bean,去生成BeanDefinition对象
                                BeanDefinition beanDefinition = new BeanDefinition();
                                beanDefinition.setType(clazz);

                                //在去判断这个类上面有没有@Scope注解;
                                if (clazz.isAnnotationPresent(Scope.class)) {
                                    //有scope注解
                                    Scope scopeAnnotaion = clazz.getAnnotation(Scope.class);
                                    String scope = scopeAnnotaion.value();
                                    beanDefinition.setScope(scope);
                                }else {
                                    //没有scope注解就是单例的
                                    beanDefinition.setScope("Singleton");
                                }

                                beanDefinitionMap.put(beanName,beanDefinition);

                            }
                        } catch (ClassNotFoundException e) {
                            e.printStackTrace();
                        }


                    }
                }
            }
        }

        //2.实例化单例Bean,拿到所有的单例bean对象,判断是否有单例bean,有的话容器启动时候直接创建,存入单例池
        for (String beanName : beanDefinitionMap.keySet()) {
            BeanDefinition beanDefinition = beanDefinitionMap.get(beanName);

            if (beanDefinition.getScope().equals("Singleton")){
                //如果是单例的汲取创建bean
                Object bean = createBean(beanName, beanDefinition);
                //通过单例池来保证单例
                singletonObjects.put(beanName,bean);
            }
        }
    }

    private Object createBean(String beanName,BeanDefinition beanDefinition){


        return null;
    }
    public Object getBean(String beanName) {
        
        BeanDefinition beanDefinition = beanDefinitionMap.get(beanName);

        if (beanDefinition == null){
            throw new NullPointerException();
        }else {
            String scope = beanDefinition.getScope();

            if ("Singleton".equals(scope)){
                //单例,在spring容器启动的时候就给单例bean生成出来
                //此时直接从单例池中获取单例bean
                Object bean = singletonObjects.get(beanName);
                //某些情况下导致单例池中没有就再去创建
                if (bean == null){
                    Object bean1 = createBean(beanName, beanDefinition);
                    //放入单例池中
                    singletonObjects.put(beanName,bean1);
                }
                return bean;//找到了,直接返回

            }else {
                //多例,每次额外去创建BeanDefinition对象
                return createBean(beanName,beanDefinition);
            }
        }

    }
}

4. 手写bean的创建流程

下面去考虑怎么去创建bean————creatBean()

在createBean(String beanName,BeanDefinition beandefinition),中有两个参数,创建bean,bean是一个对象我们得知道是哪个类,我们可以通过参数beanDefinition对象的getType方法获得类对象,再利用反射通过构造方法再去newInstance创建对象

private Object createBean(String beanName,BeanDefinition beanDefinition){

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

        return instance;
    } catch (InstantiationException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    }

    return null;
}

此时去测试单例,

userService.png

5. 手写依赖注入

bean的默认初始名

//对Bean的名字进行处理,默认是首字母小写
if (beanName.equals("")){
    //首字母小写,如果第一第二个字母大写就不管他
    beanName = Introspector.decapitalize(clazz.getSimpleName());
}

看下decapitalize方法的源码

public static String decapitalize(String name) {
        if (name == null || name.length() == 0) {
            return name;
        }
        if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) &&
                        Character.isUpperCase(name.charAt(0))){
            return name;
        }
        char chars[] = name.toCharArray();
        chars[0] = Character.toLowerCase(chars[0]);
        return new String(chars);
    }
}

此时可以不用在@Component后面加bean的名字,默认就是类名首字母小写

去测试一下

去service包下新建一个OrderService类,加上@Component注解

package com.cs.service;

import com.cs.spring.Component;

@Component
public class OrderService {

}

要实现依赖注入,此时在UserService类中,新增一个属性OrderService,去注入,此时需要再去新建一个@Autowired注解

package com.cs.spring;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)//表示写在字段上
public @interface Autowired {

}

此时就可以UserService中使用@Autowired注解,但此时OrderService是否有值呢?我们去测试一下

package com.cs.service;


import com.cs.spring.Autowired;
import com.cs.spring.Component;
import com.cs.spring.Scope;

@Component("userService")//给当前bean指定一个名字
@Scope("Prototype")
public class UserService {


    @Autowired
    private OrderService orderService;

    public void test(){
        System.out.println(orderService);
    }
}

可以发现,此时依赖还是没有注入的,那么依赖注入是在哪里完成的呢?

应该在创建bean的时候就去给他添加属性,也就是在createBean方法中,此处也就是在模拟bean的生命周期,bean的创建,第一个步骤就是先实例化得到一个对象,第二个步骤就是给对象进行依赖注入,

一个对象里面,或者其对应的类里面有很多的属性,我们只需要去给那些加了@Autowired的属性赋值,

awrited.png

此时OrderService就有值了,

6.BeanName 的回调

可能不知道某个bean的名字,或者名字呗其他机制给更改了,那么怎么能最终知道这个bean的名字呢?其实这个bean的名字是由spring生成出来的,最终需要spring来告诉我们当前bean的名字是什么,这就需要一种回调,Aware回调,这里需要一个接口

package com.cs.spring;

public interface BeanNameAware {

    public void setBeanName(String beanName);
}

我们的bean要去实现这个接口

package com.cs.service;


import com.cs.spring.Autowired;
import com.cs.spring.BeanNameAware;
import com.cs.spring.Component;
import com.cs.spring.Scope;

@Component("userService")//给当前bean指定一个名字
public class UserService implements BeanNameAware {


    @Autowired
    private OrderService orderService;

    private String beanName;


    public void test(){
        System.out.println(orderService);
    }

    @Override
    public void setBeanName(String beanName) {
        this.beanName = beanName;
    }
}

那么spring在什么时候来调用这个setBeanName方法呢?也就是我们刚刚的依赖注入完成后,spring会去判断 之前生成的对象是否实现了BeanNameAware这个接口,如果实现了 就给对象强制转换成这个接口类型,然后直接去调用你的方法

beanname.png

还有更多的实现,建议大家可以查看源码了。

如:模拟Spring初始化机制实现、BeanPostProcessor模拟实现、AOP模拟实现等

trySpring源码:

TrySpring_src.zip

上一篇: -XX:PretenureSizeThreshold的默认值和作用浅析
下一篇: 运用Mybatis动态查询字段及表名
推荐文章
  • 在HTML中,如果你想让一个输入框(input元素)不可编辑,你可以通过设置其readonly属性来实现。示例如下:input type="text" value="此处内容不可编辑" readonly在上述代码中,readonly属性使得用户无法修改输入框中的内容。另外,如果你希望输入框完全不可交
  • ASP.NET教程ASP.NET又称为ASP+,基于.NETFramework的Web开发平台,是微软公司推出的新一代脚本语言。ASP.NET是一个使用HTML、CSS、JavaScript和服务器脚本创建网页和网站的开发框架。ASP.NET支持三种不一样的开发模式:WebPages(Web页面)、
  • C# 判断判断结构要求程序员指定一个或多个要评估或测试的条件,以及条件为真时要执行的语句(必需的)和条件为假时要执行的语句(可选的)。下面是大多数编程语言中典型的判断结构的通常形式:判断语句C#提供了以下类型的判断语句。点击链接查看每个语句的细节。语句描述if语句一个 if语句 由一个布尔表达式后跟
  • C#循环有的时候,可能需要多次执行同一块代码。通常情况下,语句是顺序执行的:函数中的第一个语句先执行,接着是第二个语句,依此类推。编程语言提供了允许更为复杂的执行路径的多种控制结构。循环语句允许我们多次执行一个语句或语句组,下面是大多数编程语言中循环语句的通常形式:循环类型C#提供了以下几种循环类型
  • C#数组(Array)数组是一个存储相同类型元素的固定大小的顺序集合。数组是用来存储数据的集合,一般认为数组是一个同一类型变量的集合。声明数组变量并不是声明number0、number1、...、number99一个个单独的变量,而是声明一个就像numbers这样的变量,然后使用numbers[0]
  • ASP.NET是一个由微软公司开发的用于构建Web应用程序的框架,它是.NETFramework的一部分。它提供了一种模型-视图-控制器(MVC)架构、Web表单以及最新的ASP.NETCore中的RazorPages等多种开发模式,可以用来创建动态网页和Web服务。以下是一些基础的ASP.NET编
学习大纲