Spring原始碼系列:初探底層,手寫Spring

努力的小雨發表於2023-04-12

前言

在學習Spring框架原始碼時,記住一句話:原始碼並不難,只需要給你各種業務場景或者專案經理,你也能實現自己的Spring。雖然你的實現可能無法與開源團隊相媲美,但是你肯定可以實現一個0.0.1版本。因此,初次閱讀原始碼時,不要陷入太深的細節中。先了解大體邏輯,再仔細研讀。

實現功能

本文將帶領大家實現一個簡易版的Spring框架,並介紹以下功能點:

  1. 瞭解Spring的底層原始碼啟動過程
  2. 瞭解BeanDefinition的概念
  3. 瞭解Spring解析配置類等底層原始碼工作流程
  4. 瞭解依賴注入,Aware回撥等底層原始碼工作流程
  5. 瞭解Spring AOP的底層原始碼工作流程

以上功能點將使我們對Spring框架的實現有所瞭解,但我們並不會一下子實現整個Spring框架的業務。我們將從上述功能點入手,透過手寫模擬Spring框架來實現這些功能。

首先,我們像使用Spring一樣,傳入配置類獲取applicationContext,再透過getBean方法獲取具體物件。最後,我們呼叫方法並列印日誌。如果你對這些基本流程不熟悉,可以檢視我的入門系列文章:Spring入門系列:淺析知識點

詳細流程如下:

  1. 解析配置類上的ComponentScan註解,獲取掃描的基本路徑
  2. 開始解析各個被掃描到的檔案,是否是需要被Spring管理,如果是則暫存到list集合中
  3. 開始遍歷被Spring管理list集合,解析各個類上的註解,比如是否是懶載入,然後將這些屬性都封裝到applicationContext中的以beanName為key的BeanDefineMap中
  4. 針對已經解析好的bean定義進行建立物件並例項化,並將其放入以beanName為key的singletonMap例項化快取池中。
  5. 在例項化時,如果發現有依賴注入的物件,則將例項化快取池中的物件存入。如果快取池中沒有該物件,則進行建立後再注入。
  6. 判斷物件是否實現了各個Aware介面,如果實現,則進行回撥。
  7. 判斷物件是否屬於增強類。在這裡,我們模擬了事務註解。如果有事務註解,則建立一個代理物件,併為所有方法增加攔截器。然後將該代理物件存入單例快取池中。

詳細解析

專案結構

基本路徑:com.user目錄

各個註解及上下文類:config目錄

需要被管理的Bean:service目錄

啟動類:com.user根目錄

截圖

原始碼分析

@Component
public class UserService implements ApplicationContextAware, BeanNameAware {

    @AutoWired
    ServiceDemo serviceDemo;

    private XiaoyuApplicationContext applicationContext;
    private String beanName;

    public void test() {
        serviceDemo.say();
//        System.out.println(serviceDemo);
        System.out.println("userService:"+applicationContext.getBean("userService"));
        System.out.println("beanName:"+beanName);
    }

    @Override
    public void setApplicationContext(XiaoyuApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

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

UserService類主要用於測試是否Spring已經管理了相關物件並生成了代理物件,是我們的日常業務類,我們仔細看下XiaoyuApplicationContext類,主要的流程在這邊:

public class XiaoyuApplicationContext {


    //配置類
    private Class config;
    //初始的bean定義
    private Map<String,BeanDefinition> beanDefineMap = new HashMap<>();
    //單例快取池
    private Map<String,Object> singleBean = new HashMap<>();

    public XiaoyuApplicationContext(Class myDemoConfigClass) {
        config = myDemoConfigClass;
        //解析配置類
        scan();

    }

    public void scan(){
        ComponentScan declaredAnnotation = (ComponentScan) config.getDeclaredAnnotation(ComponentScan.class);
        String value = declaredAnnotation.basePackages();
        doScan(value);
        //將bean定義Map生成具體的Bean物件
        beanDefineMap.entrySet().stream().forEach(item->{
            String beanName = item.getKey();
            BeanDefinition beanDefinition = item.getValue();
            if (!beanDefinition.isLazy() && "singleton".equals(beanDefinition.getScope())) {
                Object bean = createBean(beanName);
                singleBean.put(beanName,bean);
            }
        });
    }

    /**
     *     解析配置類
     */
    private void doScan(String value) {
        String path = value.replace(".","/");
        //正常走檔案解析
        ClassLoader classLoader = this.getClass().getClassLoader();
        URL resource = classLoader.getResource(path);
        File file = new File(resource.getFile());

        List<File> classFile = new ArrayList<>();
        //簡單點直接雙層解析即可
        if (file.isDirectory()) {
            for (File f : file.listFiles()) {
                if (f.isDirectory()) {
                    for (File f1 : f.listFiles()) {
                        if (!f1.isDirectory()) {
                            classFile.add(f1);
                        }
                    }
                } else {
                    classFile.add(f);
                }
            }
        }
        //遍歷所有解析檔案
        for (File cFile : classFile) {
            String absolutePath = cFile.getAbsolutePath();
            String className = absolutePath.substring(absolutePath.indexOf("com"), absolutePath.indexOf(".class"))
                    .replace("\\", ".");

            try {
                Class<?> clazz = classLoader.loadClass(className);
                //是否需要被Spring管理
                if (clazz.isAnnotationPresent(Component.class)) {
                    //將bean上的註解封裝到bean定義中
                    BeanDefinition beanDefinition = new BeanDefinition();
                    beanDefinition.setType(clazz);
                    beanDefinition.setLazy(clazz.isAnnotationPresent(Lazy.class));
                    if (clazz.isAnnotationPresent(Scope.class)) {
                        beanDefinition.setScope(clazz.getAnnotation(Scope.class).value());
                    } else {
                        beanDefinition.setScope("singleton");
                    }

                    String beanName = clazz.getAnnotation(Component.class).value();
                    if (beanName.isEmpty()) {
                        //如果不設定beanName會預設生產唯一一個name,Spring底層也是這樣做的
                        beanName = Introspector.decapitalize(clazz.getSimpleName());
                    }

                    beanDefineMap.put(beanName, beanDefinition);

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

        }
    }

    public Object createBean(String beanName){

        BeanDefinition beanDefinition = beanDefineMap.get(beanName);
        Class type = beanDefinition.getType();

        try {
            Object instance = type.newInstance();
            //屬性填充,依賴注入
            populateBean(instance);

            if (instance instanceof ApplicationContextAware){
                ((ApplicationContextAware) instance).setApplicationContext(this);
            }
            if (instance instanceof BeanNameAware) {
                ((BeanNameAware) instance).setBeanName(beanName);
            }
            //是否需要AOP增強
            if (type.isAnnotationPresent(Transaction.class)) {
                Enhancer enhancer = new Enhancer();
                enhancer.setSuperclass(type);
                //簡單的方法切面
                enhancer.setCallback(new MethodInterceptor() {
                    @Override
                    public Object intercept(Object proxy, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                        //開啟事務,關閉自動提交
                        System.out.println("事務已開啟");
                        Object res = method.invoke(instance, objects);
                        //提交事務
                        System.out.println("事務已提交");
                        return res;
                    }
                });
                return enhancer.create();
            }
            return instance;
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

        return null;
    }

    private void populateBean(Object instance) {
        Field[] declaredFields = instance.getClass().getDeclaredFields();

        Arrays.stream(declaredFields).forEach(item->{
            //尋找注入點
            if (item.isAnnotationPresent(AutoWired.class)) {
                Object bean = getBean(item.getName());
                item.setAccessible(true);
                try {
                    item.set(instance,bean);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        });

    }


    public Object getBean(String beanName){
        if (!beanDefineMap.containsKey(beanName)) {
           throw new NullPointerException();
        }
        if ("singleton".equals(beanDefineMap.get(beanName).getScope())) {
            if (singleBean.containsKey(beanName)) {
                return singleBean.get(beanName);
            } else {
                Object bean = createBean(beanName);
                singleBean.put(beanName,bean);
                return bean;
            }
        }
        return createBean(beanName);
    }
}

以上即為整個流程的基本梳理。我們在實現過程中沒有涉及Bean迴圈依賴以及其他各種建立快取,但Spring在實現Bean的建立過程中確實用到了各種本地快取和同步鎖(synchronized)。在學習原始碼時,不要太關注這些細節。首先要理解整個流程,再深入研究。

結語

最後,我們在gitee上提供了專案原始碼。如果需要,可以檢視spring-xiaoyu。雖然我認為寫一遍自己的程式碼更好,因為這是最簡單的流程,有助於理解Spring原始碼。
公眾號

相關文章