Featured image of post 手把手教你寫SprintBoot框架

手把手教你寫SprintBoot框架

連假太無聊,來手寫一個SpringBoot框架吧!

前言

連假實在有點無聊,打算來手寫一個SprintBoot專案,來加深自己對這種IoC框架的理解,我打算把這個框架叫做Winter,象徵我每況愈下的人生,唉

SprintBoot 單例池

本章所用到的程式碼存放在這

https://github.com/Hoxton019030/Winter/tree/bean-scan-and-beandefinition

一切開始的地方,Main

啟動類的寫法如下

1
2
3
4
5
public class Main {
    public static void main(String[] args) {
        WinterApplicationContext applicationContext = new WinterApplicationContext(AppConfig.class);
    }
}

這邊看到兩個陌生的Class,WinterApplicationContext,以及AppConfig。先講AppConfig是什麼,它是Winter的配置類,我們會在這邊去配置我們Winter Bean的位置在哪邊,所以我們要加上@ComponentScan這個註解,讓我們的Winter框架知道它要去什麼package底下找到Bean。這邊我們就先創這個Class就好,目前先不會寫到它。

AppConfig

1
2
3
4
5
6
7
/**
 * Winter的配置文件
 */
@ComponentScan("org.hoxton.service")
public class AppConfig {

}

@ComponentScan

1
2
3
4
5
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ComponentScan {
    String value() default "" ;
}

用來示意我們的Winter框架,要去哪邊找到我們的Bean,我們的Bean都會寫在org.hoxton.service這個路徑下面。

image-20230401195655052

@Component

這個大家熟的吧,將當前類變成一個Spring Bean的註解

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

    String value() default "";
}

@Scope

標示當前類所要創建的Bean是一個單例還是一個Prototype的Bean

1
2
3
4
5
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Scope {
    String value();
}

UserService

用來假設MVC架構中的Service層

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

WinterApplicationContext

這個類就是SpringBoot框架中的容器類,我們接下來要來手寫一個容器類,首先先創這個類出來,並且它要吃一個Class參數作為Constructor

1
2
3
4
5
6
7
public class WinterApplicationContext {
    private Class configClass;
    
	public WinterApplicationContext(Class configClass){
    	this.configClass=configClass;
    }
}

接下來我們寫一個方法,叫做Scan,這個Scan方法會去得到@ComponentScan這個註解的值,取得要掃描的Package路徑,取得路徑之後,會去掃描路徑底下有哪些類有被@Component這個註解所修飾,並且會去找尋@Scope這個註解,得知哪些Bean是Singleton,哪些Bean是prototype,將這些資訊放入

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

這個ConcurrentHashMap中

步驟

  1. 從Config類中取得ComponentScan註解的值,其值即為路徑
1
2
3
4
5
6
private void scan(Class configClass) {
    // 從Config類中取得ComponetScan註解的值,其值即為路徑
        ComponentScan componentScanAnnotation = (ComponentScan) configClass.getAnnotation(ComponentScan.class);
        String path = componentScanAnnotation.value();
       
    }

  1. 使用類加載,讀出Class檔案的資訊
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
private void scan(Class configClass) {
    // 從Config類中取得ComponentScan註解的值,其值即為路徑
        ComponentScan componentScanAnnotation = (ComponentScan) configClass.getAnnotation(ComponentScan.class);
        String path = componentScanAnnotation.value();
       //掃描 org.hoxton.service
        // 類加載器 (Class Loader)
        // Java中有三種類加載器,以及對應的加載路徑
        // BootStrap ---> jre/lib
        // Ext ---> jre/ext/lib
        // App ---> classpath --->  "C:\Program Files\Java\jdk-11\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2022.2.2\lib\idea_rt.jar=5445:C:\Program Files\JetBrains\IntelliJ IDEA 2022.2.2\bin" -Dfile.encoding=UTF-8 -classpath C:\Users\hoxton\Desktop\Winter\target\classes org.hoxton.Test 編譯器顯示的資訊
    }

image-20230401202057765

  1. BeanDefinition

我們先來討論一個東西叫做BeanDefinition,這個類用來描述一個Bean的資訊,我們目前讓這個類盡量簡單一點,我們只需要紀錄一個Bean它的類型以及它的作用範圍

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class BeanDefinition {

    /**
     * 當前Bean類型
     */
    private Class clazz;

    /**
     * 當前Bean作用範圍
     */
    private String scope;

    public BeanDefinition() {
    }

    public BeanDefinition(Class clazz, String scope) {
        this.clazz = clazz;
        this.scope = scope;
    }

    public Class getClazz() {
        return clazz;
    }

    public void setClazz(Class clazz) {
        this.clazz = clazz;
    }

    public String getScope() {
        return scope;
    }

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

  1. 使用類加載器,依照@ComponentScan的值,去掃描底下的Bean,
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
    private void scan(Class configClass) {
        ComponentScan componentScanAnnotation = (ComponentScan) configClass.getAnnotation(ComponentScan.class);
        String path = componentScanAnnotation.value(); //掃描路徑

        //掃描 org.hoxton.service
        // 類加載器 (Class Loader)
        // Java中有三種類加載器,以及對應的加載路徑
        // BootStrap ---> jre/lib
        // Ext ---> jre/ext/lib
        // App ---> classpath --->  "C:\Program Files\Java\jdk-11\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2022.2.2\lib\idea_rt.jar=5445:C:\Program Files\JetBrains\IntelliJ IDEA 2022.2.2\bin" -Dfile.encoding=UTF-8 -classpath C:\Users\za546\Desktop\Winter\target\classes org.hoxton.Test 編譯器顯示的資訊

        ClassLoader classLoader = WinterApplicationContext.class.getClassLoader(); //app加載器
        URL resource = classLoader.getResource("org/hoxton/service"); //使用類加載,掃苗檔案下的.class檔
        File file = new File(resource.getFile());
        if (file.isDirectory()) {
            File[] files = file.listFiles();
            for (File f : files) {
                String fileName = f.getAbsolutePath();
                if (fileName.endsWith(".class")) { //若檔案以.class結尾即為.class檔,則進入流程判斷
                    String className = fileName.substring(fileName.indexOf("org"), fileName.indexOf(".class"));
                    className = className.replace("\\", ".");
                    Class<?> clazz = null;
                    try {
                        clazz = classLoader.loadClass(className);
                    } catch (ClassNotFoundException e) {
                        throw new RuntimeException(e);
                    }
                    if (clazz.isAnnotationPresent(Component.class)) { //類上若有@Component註解,代表為一個Bean
                        //表示這個類是個Bean
                        // ...? Class -- > bean ?
                        // 解析類,判斷當前Bean是單例Bean還是Prototype的Bean,生成BeanDefinition

                        Component componentAnnotation = clazz.getDeclaredAnnotation(Component.class);
                        String beanName = componentAnnotation.value(); //取得@Component的值(bean名)

                        //BeanDefinition- Bean定義
                        BeanDefinition beanDefinition = new BeanDefinition();  //取得一個BeanDefinition物件
                        beanDefinition.setClazz(clazz);  
                        if (clazz.isAnnotationPresent(Scope.class)) {
                            Scope scopeAnnotation = clazz.getDeclaredAnnotation(Scope.class);
                            beanDefinition.setScope(scopeAnnotation.value()); 
                        } else {
                            beanDefinition.setScope(SINGLETON); //沒加Scope註解,預設為單例
                        }

                        beanDefinitionMap.put(beanName, beanDefinition); // 將beanDefinition放進ConcurrentHashMap中

                    }

                }
            }

        }
    }
  1. 我們先來稍微預覽一下整個Class完成後會長什麼樣子,讓大家更有一個概念
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
public class WinterApplicationContext {

    final String SINGLETON ="singleton";
    private Class configClass;

    /**
     * 一個單例池,存放Spring Bean
     */
    private ConcurrentHashMap<String, Object> singletonPool = new ConcurrentHashMap<>();
    /**
     * 存放所有Spring Bean的定義
     */
    private ConcurrentHashMap<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>();

    /**
     * @param configClass Winter的配置文件
     */
    public WinterApplicationContext(Class configClass) {
        this.configClass = configClass;
        //解析配置類
        // ComponentScan註解解析 -> 掃描路徑 -> 掃描 ---> BeanDefinition ---> BeanDefinitionMap
        scan(configClass);

        for (String beanName : beanDefinitionMap.keySet()) {
            BeanDefinition beanDefinition = beanDefinitionMap.get(beanName);
            //如果bean scope是單例,則放進單例池中
            if(beanDefinition.getScope().equals(SINGLETON)){
                Object bean = createBean(beanDefinition);
                singletonPool.put(beanName, bean);
            }

        }


    }

    public Object createBean(BeanDefinition beanDefinition){
        Class clazz = beanDefinition.getClazz();
        try {
            Object instance = clazz.getDeclaredConstructor().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);
        }
    }

    private void scan(Class configClass) {
        ComponentScan componentScanAnnotation = (ComponentScan) configClass.getAnnotation(ComponentScan.class);
        String path = componentScanAnnotation.value(); //掃描路徑

        //掃描 org.hoxton.service
        // 類加載器 (Class Loader)
        // Java中有三種類加載器,以及對應的加載路徑
        // BootStrap ---> jre/lib
        // Ext ---> jre/ext/lib
        // App ---> classpath --->  "C:\Program Files\Java\jdk-11\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2022.2.2\lib\idea_rt.jar=5445:C:\Program Files\JetBrains\IntelliJ IDEA 2022.2.2\bin" -Dfile.encoding=UTF-8 -classpath C:\Users\za546\Desktop\Winter\target\classes org.hoxton.Test 編譯器顯示的資訊

        ClassLoader classLoader = WinterApplicationContext.class.getClassLoader(); //app加載器
        URL resource = classLoader.getResource("org/hoxton/service"); //使用類加載,掃苗檔案下的.class檔
        File file = new File(resource.getFile());
        if (file.isDirectory()) {
            File[] files = file.listFiles();
            for (File f : files) {
                String fileName = f.getAbsolutePath();
                if (fileName.endsWith(".class")) {
                    String className = fileName.substring(fileName.indexOf("org"), fileName.indexOf(".class"));
                    className = className.replace("\\", ".");
                    Class<?> clazz = null;
                    try {
                        clazz = classLoader.loadClass(className);
                    } catch (ClassNotFoundException e) {
                        throw new RuntimeException(e);
                    }
                    if (clazz.isAnnotationPresent(Component.class)) {
                        //表示這個類是個Bean
                        // ...? Class -- > bean ?
                        // 解析類,判斷當前Bean是單例Bean還是Prototype的Bean,生成BeanDefinition

                        Component componentAnnotation = clazz.getDeclaredAnnotation(Component.class);
                        String beanName = componentAnnotation.value();

                        //BeanDefinition- Bean定義
                        BeanDefinition beanDefinition = new BeanDefinition();
                        beanDefinition.setClazz(clazz);
                        if (clazz.isAnnotationPresent(Scope.class)) {
                            Scope scopeAnnotation = clazz.getDeclaredAnnotation(Scope.class);
                            beanDefinition.setScope(scopeAnnotation.value());
                        } else {
                            beanDefinition.setScope(SINGLETON);
                        }

                        beanDefinitionMap.put(beanName, beanDefinition);

                    }

                }
//

            }

        }
    }

    public Object getBean(String beanName) {
        // 依照beanName去判斷是單例Bean還是Prototype Bean
        if (beanDefinitionMap.containsKey(beanName)) {
            BeanDefinition beanDefinition = beanDefinitionMap.get(beanName);
            if (beanDefinition.getScope().equals(SINGLETON)) {
                Object bean = singletonPool.get(beanName);
                return bean;
            } else {
            //創建bean對象嗎?
                Object bean = createBean(beanDefinition);
                return bean;
            }
        } else {
            throw new NullPointerException("沒有這個Bean");
        }
    }
}
  1. 接下來我們繼續回到WinterApplicationContext這個Constructor,我們已經把我們Package底下的Bean掃描完,放進beanDefinitionMap中,那我們接下來要依照這個Map,創建SINGLETON的bean,接著把SINGLETON的Bean放進singletonPool中,這個singletonPool也就是Spring中的單例池
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public WinterApplicationContext(Class configClass) {
        this.configClass = configClass;
        //解析配置類
        // ComponentScan註解解析 -> 掃描路徑 -> 掃描 ---> BeanDefinition ---> BeanDefinitionMap
        scan(configClass);

        for (String beanName : beanDefinitionMap.keySet()) { //遍整個beanDefinitionMap,找出單例的Bean,放進singletonPool中
            BeanDefinition beanDefinition = beanDefinitionMap.get(beanName);
            //如果bean scope是單例,則放進單例池中
            if(beanDefinition.getScope().equals(SINGLETON)){
                Object bean = createBean(beanDefinition);
                singletonPool.put(beanName, bean);
            }

        }


    }
  1. 撰寫createBean方法會吃一個BeanDefinition為參數,這個方法會用BeanDefinition的Clazz創建一個Class,接著調用class底下的getDeclaredConstructor()取得建構子,並用newInstance()方法創造出一個那個bean的物件出來
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
    public Object createBean(BeanDefinition beanDefinition){
        Class clazz = beanDefinition.getClazz();
        try {
            Object instance = clazz.getDeclaredConstructor().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);
        }
    }
  1. 到Main中,使用getBean來取得singletonpool中的Bean
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Main {
    public static void main(String[] args) {

        WinterApplicationContext applicationContext = new WinterApplicationContext(AppConfig.class);
        System.out.println("可以看到這三個的值是一模一樣的,代表這幾個物件都是同一個");
        System.out.println(applicationContext.getBean("userService"));
        System.out.println(applicationContext.getBean("userService"));
        System.out.println(applicationContext.getBean("userService"));
    }
}

image-20230401205329971

demo

這樣就是一個基礎的SprintBoot 單例池的創建

Autowired之實現原理

To Be Continued …