講道理,感受本身有點菜。Spring 源碼看不懂,不想強行解釋,等多積累些項目經驗以後再看吧,可是 Spring 中的控制反轉(IOC)和麪向切面編程(AOP)思想很重要,爲了更好的使用 Spring 框架,有必要理解這兩個點,爲此,我使用 JDK API 實現了一個玩具級的簡陋 IOC/AOP 框架 mini-spring,話很少說,直接開幹。html
所有代碼已上傳 GitHub:https://github.com/czwbig/mini-springjava
gradle build
命令;mini-spring\framework_use_test\build\libs\framework_use_test-1.0-SNAPSHOT.jar
,點擊 Run,固然也能夠直接使用 java -jar jarPath.jar
命令來運行此 jar 包;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.
下面開始框架的講解。git
本項目使用 Java API 以及內嵌 Tomcat 服務器寫了一個玩具級 IOC/AOP web 框架。實現了 @Controller
、@AutoWired
、@Component
、@Pointcut
、@Aspect
、@Before
、@After
等 Spring 經常使用註解。可實現簡單的訪問 uri 映射,控制反轉以及不侵入原代碼的面向切面編程。github
講解代碼實現以前,假設讀者已經掌握了基礎的項目構建、反射、註解,以及 JDK 動態代理知識,項目精簡,註釋詳細,而且總代碼 + 註釋不足 1000 行,適合用來學習。其中構建工具 Gradle 沒用過也沒關係,我也是第一次使用,當成沒有 xml 的 Maven 來看就行,下面我會詳細解讀其構建配置文件。web
項目由兩個模塊組成,一個是框架自己的模塊,實現了框架的 IOC/AOP 等功能,以下圖:spring
類比較多,可是大部分都是代碼不多的,特別是註解定義接口,不要怕。編程
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.gradle
、build.gradle
項目構建文件,其中 setting.gradle
指定了項目名以及模塊名。tomcat
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 類文件。
以下圖:
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 { // 爲減小篇幅,這裏完整代碼就不放出來了 }
註釋很詳細,就很少廢話了。
這部分是最重要的,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);
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系列教材