仿寫一個簡陋的 IOC/AOP 框架 mini-spring

講道理,感覺自己有點菜。Spring 源碼看不懂,不想強行解釋,等多積累些項目經驗之後再看吧,但是 Spring 中的控制反轉(IOC)和麪向切面編程(AOP)思想很重要,爲了更好的使用 Spring 框架,有必要理解這兩個點,爲此,我使用 JDK API 實現了一個玩具級的簡陋 IOC/AOP 框架 mini-spring,話不多說,直接開幹。

環境搭建&快速使用

全部代碼已上傳 GitHub:https://github.com/czwbig/mini-spring

  1. 將代碼弄到本地並使用 IDE 打開,這裏我們用 IDEA;
  2. 使用 Gradle 構建項目,可以使用 IDEA 提供的 GUI 操作,也可以直接使用 gradle build 命令;

  1. 如下圖,右擊 mini-spring\framework_use_test\build\libs\framework_use_test-1.0-SNAPSHOT.jar ,點擊 Run,當然也可以直接使用 java -jar jarPath.jar 命令來運行此 jar 包;

  1. 瀏覽器打開 localhost:8080/rap 即可觀察到顯示 CXK 字母,同時 IDE 控制檯會輸出:
first,singing <chicken is too beautiful>.
and the chicken monster is dancing now.
CXK rapping...
oh! Don't forget my favorite basketball.

下面開始框架的講解。

簡介

本項目使用 Java API 以及內嵌 Tomcat 服務器寫了一個玩具級 IOC/AOP web 框架。實現了 @Controller@AutoWired@Component@Pointcut@Aspect@Before@After 等 Spring 常用註解。可實現簡單的訪問 uri 映射,控制反轉以及不侵入原代碼的面向切面編程。

講解代碼實現之前,假設讀者已經掌握了基礎的項目構建、反射、註解,以及 JDK 動態代理知識,項目精簡,註釋詳細,並且總代碼 + 註釋不足 1000 行,適合用來學習。其中構建工具 Gradle 沒用過也不要緊,我也是第一次使用,當成沒有 xml 的 Maven 來看就行,下面我會詳細解讀其構建配置文件。

模塊組成

項目由兩個模塊組成,一個是框架本身的模塊,實現了框架的 IOC/AOP 等功能,如下圖:

類比較多,但是大部分都是代碼很少的,特別是註解定義接口,不要怕。

  • aop 包中是 After 等註解的定義接口,以及動態代理輔助類;
  • bean 包中是兩個註解定義,以及 BeanFactory 這個 Bean 工廠,其中包含了類掃描和 Bean 的初始化的代碼;
  • core 包是一個 ClassScanner 類掃描工具類;
  • starter 包是一個框架的啓動與初始化類;
  • web/handler 包中是 uri 請求的處理器的收集與管理,如查找 @Controller 註解修飾的類中的 @RequestMapping 註解修飾的方法,用來響應對應 uri 請求。
  • web/mvc 包定義了與 webMVC 有關的三個註解;
  • web/server 包中是一個嵌入式 Tomcat 服務器的初始化類;
  • web/servlet 包中是一個請求分發器,重寫的 service() 方法定義使用哪個請求處理器來響應瀏覽器請求;

另一個模塊是用來測試(使用)框架的模塊,如下圖:

就像我們使用 Spring 框架一樣,定義 Controller 等來響應請求,代碼很簡單,就不解釋了。

項目構建

根目錄下有 setting.gradlebuild.gradle 項目構建文件,其中 setting.gradle 指定了項目名以及模塊名。

rootProject.name = 'mini-spring'
include 'framework'
include 'framework_use_test'

build.gradle 是項目構建設置,主要代碼如下:

plugins {
    id 'java'
}

group 'com.caozhihu.spring'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8

repositories {
    repositories { maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' } }
//    mavenCentral()
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'
}

引入了 gradle 的 java 插件,因爲 gradle 不僅僅可以用於 java 項目,也可以用於其他項目,引入了 java 插件定義了項目的文件目錄結構等。

然後就是項目的版本以及 java 源代碼適配級別,這裏是 JDK 1.8,在後面是指定了依賴倉庫,gradle 可以直接使用 maven 倉庫。

最後就是引入項目具體依賴,這裏和 maven 一樣。

每個模塊也有單獨的 build.gradle 文件來指定模塊的構建設置,這裏以 framework_use_test 模塊的 build.gradle 文件來說明:

dependencies {
    // 只在單元測試時候引入此依賴
    testCompile group: 'junit', name: 'junit', version: '4.12'
    // 項目依賴
    compile(project(':framework'))
}

jar {
    manifest {
        attributes "Main-Class": "com.caozhihu.spring.Application"
    }
    // 固定打包句式
    from {
        configurations.runtime.asFileTree.files.collect { zipTree(it) }
    }
}

除去和項目根目錄下構建文件相同部分,其他的構建代碼如上,這裏的 dependencies 除了添加 Junit 單元測試依賴之外,還指定了 framework 模塊。

下面指定了 jar 包的打包設置,首先使用 manifest 設置主類,否則生成的 jar 包找不到主類清單,會無法運行。還使用了 from 語句來設置打包範圍,這是固定句式,用來收集所有的 java 類文件。

framework 實現流程

如下圖:

啓動 tomcat 服務

public void startServer() throws LifecycleException {
        tomcat = new Tomcat();
        tomcat.setPort(8080);
        tomcat.start();

        // new 一個標準的 context 容器並設置訪問路徑;
        // 同時爲 context 設置生命週期監聽器。
        Context context = new StandardContext();
        context.setPath("");
        context.addLifecycleListener(new Tomcat.FixContextListener());
        // 新建一個 DispatcherServlet 對象,這個是我們自己寫的 Servlet 接口的實現類,
        // 然後使用 `Tomcat.addServlet()` 方法爲 context 設置指定名字的 Servlet 對象,
        // 並設置爲支持異步。
        DispatcherServlet servlet = new DispatcherServlet();
        Tomcat.addServlet(context, "dispatcherServlet", servlet)
                .setAsyncSupported(true);

        // Tomcat 所有的線程都是守護線程,
        // 如果某一時刻所有的線程都是守護線程,那 JVM 會退出,
        // 因此,需要爲 tomcat 新建一個非守護線程來保持存活,
        // 避免服務到這就 shutdown 了
        context.addServletMappingDecoded("/", "dispatcherServlet");
        tomcat.getHost().addChild(context);

        Thread tomcatAwaitThread = new Thread("tomcat_await_thread") {
            @Override
            public void run() {
                TomcatServer.this.tomcat.getServer().await();
            }
        };

        tomcatAwaitThread.setDaemon(false);
        tomcatAwaitThread.start();
    }

這裏看代碼註釋,結合下面這張 tomcat 架構圖就可以理解了。

圖片來自 http://click.aliyun.com/m/1000014411/

如果暫時不理解也沒關係,不影響框架學習,我只是爲了玩一玩內嵌 tomcat,完全可以自己實現一個乞丐版的網絡服務器的。

這裏使用的是我們自定義的 Servlet 子類 DispatcherServlet 對象,該類重寫了 service() 方法,代碼如下:

@Override
    public void service(ServletRequest req, ServletResponse res) throws IOException {
        for (MappingHandler mappingHandler : HandlerManager.mappingHandlerList) {
            // 從所有的 MappingHandler 中逐一嘗試處理請求,
            // 如果某個 handler 可以處理(返回true),則返回即可
            try {
                if (mappingHandler.handle(req, res)) {
                    return;
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
        res.getWriter().println("failed!");
    }

HandlerManager 和 MappingHandler 處理器後面會講,這裏先不展開。至此,tomcat 服務器啓動完成;

掃描類

掃描類是通過這句代碼完成的:

// 掃描類
List<Class<?>> classList = ClassScanner.scannerCLasses(cls.getPackage().getName());

ClassScanner.scannerCLasses 方法實現如下:

public static List<Class<?>> scannerCLasses(String packageName)
            throws IOException, ClassNotFoundException {
        List<Class<?>> classList = new ArrayList<>();
        String path = packageName.replace(".", "/");
        // 線程上下文類加載器默認是應用類加載器,即 ClassLoader.getSystemClassLoader();
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

        // 使用類加載器對象的 getResources(ResourceName) 方法獲取資源集
        // Enumeration 是古老的迭代器版本,可當成 Iterator 使用
        Enumeration<URL> resources = classLoader.getResources(path);
        while (resources.hasMoreElements()) {
            URL url = resources.nextElement();
            // 獲取協議類型,判斷是否爲 jar 包
            if (url.getProtocol().contains("jar")) {
                // 將打開的 url 返回的 URLConnection 轉換成其子類 JarURLConnection 包連接
                JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
                String jarFilePath = jarURLConnection.getJarFile().getName();
                // getClassesFromJar 工具類獲取指定 Jar 包中指定資源名的類;
                classList.addAll(getClassesFromJar(jarFilePath, path));
            } else {
                // 簡單起見,我們暫時僅實現掃描 jar 包中的類
                // todo
            }
        }
        return classList;
    }

    private static List<Class<?>> getClassesFromJar(String jarFilePath, String path) throws IOException, ClassNotFoundException {
         // 爲減少篇幅,這裏完整代碼就不放出來了
    }

註釋很詳細,就不多廢話了。

初始化Bean工廠

這部分是最重要的,IOC 和 AOP 都在這裏實現。

代碼請到在 BeanFactory 類中查看,GitHub 在線查看 BeanFactory

註釋已經寫的非常詳細。這裏簡單說下處理邏輯。

首先通過遍歷上一步類掃描獲得類的 Class 對象集合,將被 @Aspect 註解的類保存起來,然後初始化其他被 @Component@Controller 註解的類,並處理類中被 @AutoWired 註解的屬性,將目標引用對象注入(設置屬性的值)到類中,然後將初始化好的對象保存到 Bean 工廠。到這裏,控制反轉就實現好了。

接下來是處理被 @Aspect 註解的類,並解析他們中被 @Pointcut@Before@After 註解的方法,使用 JDK 動態代理生成代理對象,並更新 Bean 工廠。

注意,在處理被 @Aspect 註解的類之前,Bean 工廠中的對象依賴已經設置過了就舊的 Bean,更新了 Bean 工廠中的對象後,需要通知依賴了被更新對象的對象重新初始化。

例如對象 A 依賴對象 B,即 A 的類中有一句

@AutoWired
B b;

同時,一個切面類中的切點 @Pointcut 的值指向了 B 類對象,然後他像 Bean 工廠更新了 B 對象,但這時 A 中引用的 B 對象,還是之前的舊 B 對象。

這裏我的解決方式是,將帶有 @AutoWired 屬性的類保存起來,處理好 AOP 關係之後,再次初始化這些類,這樣他們就能從 Bean 工廠獲得新的已經被代理過的對象了。

至於如何使用 JDK 動態代理處理 AOP 關係的,請參考 GitHub ProxyDyna 類
中代碼,總的來說是,定義一個 ProxyDyna 類實現 InvocationHandler 接口,然後實現 invoke() 方法即可,在 invoke() 方法中處理代理增強邏輯。

然後獲取對象的時候,使用 Proxy.newProxyInstance() 方法而不是直接 new,如下:

Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(), this);

初始化Handler

HandlerManager 類中調用 parseHandlerFromController() 方法來遍歷處理所有的已掃描到的類,來初始化 MappingHandler 對象,方法代碼如下:

private static void parseHandlerFromController(Class<?> aClass) {
        Method[] methods = aClass.getDeclaredMethods();
        // 只處理包含了 @RequestMapping 註解的方法
        for (Method method : methods) {
            if (method.isAnnotationPresent(RequestMapping.class)) {
                // 獲取賦值 @RequestMapping 註解的值,也就是客戶端請求的路徑,注意,不包括協議名和主機名
                String uri = method.getDeclaredAnnotation(RequestMapping.class).value();
                List<String> params = new ArrayList<>();
                for (Parameter parameter : method.getParameters()) {
                    if (parameter.isAnnotationPresent(RequestParam.class)) {
                        params.add(parameter.getAnnotation(RequestParam.class).value());
                    }
                }

                // List.toArray() 方法傳入與 List.size() 恰好一樣大的數組,可以提高效率
                String[] paramsStr = params.toArray(new String[params.size()]);
                MappingHandler mappingHandler = new MappingHandler(uri, aClass, method, paramsStr);
                HandlerManager.mappingHandlerList.add(mappingHandler);
            }
        }
    }

MappingHandler 對象表示如何處理一次請求,包括請求 uri,應該調用的類,應該調用的方法以及方法參數。

如此,在 MappingHandler 的 handle() 方法中處理請求,直接從 Bean 工廠獲取指定類對象,從 response 對象中獲取請求參數值,使用反射調用對應方法,並接收方法返回值輸出給瀏覽器即可。

再回顧我們啓動 tomcat 服務器時指定運行的 servlet:

@Override
    public void service(ServletRequest req, ServletResponse res) throws IOException {
        for (MappingHandler mappingHandler : HandlerManager.mappingHandlerList) {
            // 從所有的 MappingHandler 中逐一嘗試處理請求,
            // 如果某個 handler 可以處理(返回true),則返回即可
            try {
                if (mappingHandler.handle(req, res)) {
                    return;
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
        res.getWriter().println("failed!");
    }

一目瞭然,其 service() 方法只是遍歷所有的 MappingHandler 對象來處理請求而已。

框架使用

測試使用 IOC 和 AOP 功能。這裏以定義一個 /rap 路徑舉例,

1. 定義Controller

@Controller
public class RapController {
    @AutoWired
    private Rap rapper;

    @RequestMapping("/rap")
    public String rap() {
        rapper.rap();
        return "CXK";
    }
}

RapController 從 Bean 工廠獲取一個 Rap 對象,訪問 /rap 路徑是,會先執行該對象的 rap() 方法,然後返回 “CXK” 給瀏覽器。

2. 定義 Rap 接口及其實現類

public interface Rap {
    void rap();
}
// ----another file----
@Component
public class Rapper implements Rap {
    public void rap() {
        System.out.println("CXK rapping...");
    }
}

接口一定要定義,否則無法使用 AOP,因爲我們使用的是 JDK 動態代理,只能代理實現了接口的類(原理是生成一個該接口的增強帶向)。Spring 使用的是 JDK 動態代理和 CGLIB 兩種方式,CGLIB 可以直接使用 ASM 等字節碼生成框架,來生成一個被代理對象的增強子類。

使用瀏覽器訪問 http://localhost:8080/rap ,即可看到 IDE 控制檯輸出 CXK rapping...,可以看到,@AutoWired 註解成功注入了對象。

但如果我們想在 rap 前面先 唱、跳,並且在 rap 後面打籃球,那麼就需要定義織面類來面向切面編程。

定義一個 RapAspect 類如下:

@Aspect
@Component
public class RapAspect {

    // 定義切點,spring的實現中,
    // 此註解可以使用表達式 execution() 通配符匹配切點,
    // 簡單起見,我們先實現明確到方法的切點
    @Pointcut("com.caozhihu.spring.service.serviceImpl.Rapper.rap()")
    public void rapPoint() {
    }

    @Before("rapPoint()")
    public void singAndDance() {
        // 在 rap 之前要先唱、跳
        System.out.println("first,singing <chicken is too beautiful>.");
        System.out.println("and the chicken monster is dancing now.");
    }

    @After("rapPoint()")
    public void basketball() {
        // 在 rap 之後別忘記了籃球
        System.out.println("oh! Don't forget my favorite basketball.");
    }
}

織面類 RapAspect 定義了切入點以及前置後置通知等,這樣 RapController 中使用 @AutoWired 註解引入的 Rap 對象,會被替換爲增強的 Rap 代理對象,如此,我們無需改動 RapController 中任何一處代碼,就實現了在 rap() 方法前後執行額外的代碼(通知)。

增加 RapAspect 後,再次訪問會在 IDE 控制檯輸出:

first,singing <chicken is too beautiful>.
and the chicken monster is dancing now.
CXK rapping...
oh! Don't forget my favorite basketball.

總結與參考

沒啥好說的,該說的,都說了,你懂得,就夠了,怎麼有某一種悲哀… 哈哈哈哈

參考

tomcat 使用與框架圖:手寫一個簡化版Tomcat
gradle 配置與 DI 部分實現:慕課網
Spring 常用註解 how2j SPRING系列教材

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章