講道理,感覺自己有點菜。Spring 原始碼看不懂,不想強行解釋,等多積累些專案經驗之後再看吧,但是 Spring 中的控制反轉(IOC)和麵向切面程式設計(AOP)思想很重要,為了更好的使用 Spring 框架,有必要理解這兩個點,為此,我使用 JDK API 實現了一個玩具級的簡陋 IOC/AOP 框架 mini-spring,話不多說,直接開幹。
環境搭建&快速使用
全部程式碼已上傳 GitHub:https://github.com/czwbig/mini-spring
- 將程式碼弄到本地並使用 IDE 開啟,這裡我們用 IDEA;
- 使用 Gradle 構建專案,可以使用 IDEA 提供的 GUI 操作,也可以直接使用
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.
下面開始框架的講解。
簡介
本專案使用 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.gradle
、build.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系列教材