前言
學習如何使用Spring,SpringMVC是很快的,但是在往后使用的過程中難免會想探究一下框架背后的原理是什么,本文將通過講解如何手寫一個簡單版的springMVC框架,直接從代碼上看框架中請求分發,控制反轉和依賴注入是如何實現的。
建議配合示例源碼閱讀,github地址如下:
https://github.com/liuyj24/mini-spring
項目搭建
項目搭建可以參考github中的項目,先選好jar包管理工具,Maven和Gradle都行,本項目使用的是Gradle。
然后在項目下建兩個模塊,一個是framework,用於編寫框架;另外一個是test,用於應用並測試框架(注意test模塊要依賴framework模塊)。
接着在framework模塊下按照spring創建好beans,core,context,web等模塊對應的包,完成后便可以進入框架的編寫了。
請求分發
在講請求分發之前先來梳理一下整個web模型:
- 首先用戶在客戶端發送一個請求到服務器,經操作系統的TCP/IP棧解析后會交到在某個端口監聽的web服務器。
- web服務器程序監聽到請求后便會把請求分發給對應的程序進行處理。比如Tomcat就會將請求分發給對應的java程序(servlet)進行處理,web服務器本身是不進行請求處理的。
本項目的web服務器選擇Tomcat,而且為了能讓項目直接跑起來,選擇了在項目中內嵌Tomcat,這樣框架在做測試的時候就能像spring boot一樣一鍵啟動,方便測試。
Servlet
既然選擇了使用Java編寫服務端程序,那就不得不提到Servlet接口了。為了規范服務器與Java程序之間的通信方式,Java官方制定了Servlet規范,服務端的Java應用程序必須實現該接口,把Java作為處理語言的服務器也必須要根據Servlet規范進行對接。
在還沒有spring之前,人們是這么開發web程序的:一個業務邏輯對應一個Servlet,所以一個大項目中會有多個Servlet,這大量的Servlet會被配置到一個叫web.xml的配置文件中,當服務器運行的時候,tomcat會根據請求的uri到web.xml文件中尋找對應的Servlet業務類處理請求。
但是你想,每來一個請求就創建一個Servlet,而且一個Servlet實現類中我們通常只重寫一個service方法,另外四個方法都只是給個空實現,這太浪費資源了。而且編起程序來創建很多Servlet還很難管理。能不能改進一下?
Spring的DispatcherServlet
方法確實有:
從上圖可以看到,我們原來是經過web服務器把請求分發到不同的Servlet;我們可以換個思路,讓web服務器把請求都發送到一個Servlet,再由這個Servlet把請求按照uri分發給不同的方法進行處理。
這樣一來,不管收到什么請求,web服務器都會分發到同一個Servlet(DispatcherServlet),避免了多個Servlet所帶來的問題,有以下好處:
- 把分發請求這一步從web服務器移動到框架內,這樣更容易控制,也方便擴展。
- 可以把同一個業務的處理方法集中到同一個類里,把這種類起名為controller,一個controller中有多個處理方法,這樣配置分散不雜亂。
- 配置uri映射路徑的時候可以不使用配置文件,直接在處理方法上用注解配置即可,解決了配置集中,大而雜的問題。
實操
建議配合文章開頭給出的源碼進行參考
- 首先在web.mvc包中創建三個注解:Controller,RequestMapping,RequestParam,有了注解我們才能在框架啟動時動態獲得配置信息。
- 由於處理方法都是被注解的,要想解析被注解的類,首先得獲得項目中相關的所有類,對應是源碼中core包下的ClassScanner類
public class ClassScanner {
public static List<Class<?>> scanClass(String packageName) throws IOException, ClassNotFoundException {
//用於保存結果的容器
List<Class<?>> classList = new ArrayList<>();
//把文件名改為文件路徑
String path = packageName.replace(".", "/");
//獲取默認的類加載器
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
//通過文件路徑獲取該文件夾下所有資源的URL
Enumeration<URL> resources = classLoader.getResources(path);
int index = 0;//測試
while(resources.hasMoreElements()){
//拿到下一個資源
URL resource = resources.nextElement();
//先判斷是否是jar包,因為默認.class文件會被打包為jar包
if(resource.getProtocol().contains("jar")){
//把URL強轉為jar包鏈接
JarURLConnection jarURLConnection = (JarURLConnection)resource.openConnection();
//根據jar包獲取jar包的路徑名
String jarFilePath = jarURLConnection.getJarFile().getName();
//把jar包下所有的類添加的保存結果的容器中
classList.addAll(getClassFromJar(jarFilePath, path));
}else{//也有可能不是jar文件,先放下
//todo
}
}
return classList;
}
/**
* 獲取jar包中所有路徑符合的類文件
* @param jarFilePath
* @param path
* @return
*/
private static List<Class<?>> getClassFromJar(String jarFilePath, String path) throws IOException, ClassNotFoundException {
List<Class<?>> classes = new ArrayList<>();//保存結果的集合
JarFile jarFile = new JarFile(jarFilePath);//創建對應jar包的句柄
Enumeration<JarEntry> jarEntries = jarFile.entries();//拿到jar包中所有的文件
while(jarEntries.hasMoreElements()){
JarEntry jarEntry = jarEntries.nextElement();//拿到一個文件
String entryName = jarEntry.getName();//拿到文件名,大概是這樣:com/shenghao/test/Test.class
if (entryName.startsWith(path) && entryName.endsWith(".class")){//判斷是否是類文件
String classFullName = entryName.replace("/", ".")
.substring(0, entryName.length() - 6);
classes.add(Class.forName(classFullName));
}
}
return classes;
}
}
- 然后在handler包創建MappingHandler類,在將來框架運行的過程中,一個MappingHandler就對應一個業務邏輯,比如說增加一個用戶。所以一個MappingHandler中要有“請求uri,處理方法,方法的參數,方法所處的類”這四個字段,其中請求uri用於匹配請求uri,后面三個參數用於運行時通過反射調用該處理方法
public class MappingHandler {
private String uri;
private Method method;
private Class<?> controller;
private String[] args;
MappingHandler(String uri, Method method, Class<?> cls, String[] args){
this.uri = uri;
this.method = method;
this.controller = cls;
this.args = args;
}
public boolean handle(ServletRequest req, ServletResponse res) throws IllegalAccessException, InstantiationException, InvocationTargetException, IOException {
//拿到請求的uri
String requestUri = ((HttpServletRequest)req).getRequestURI();
if(!uri.equals(requestUri)){//如果和自身uri不同就跳過
return false;
}
Object[] parameters = new Object[args.length];
for(int i = 0; i < args.length; i++){
parameters[i] = req.getParameter(args[i]);
}
Object ctl = BeanFactory.getBean(controller);
Object response = method.invoke(ctl, parameters);
res.getWriter().println(response.toString());
return true;
}
}
- 接下來在handler包創建HandlerManager類,這個類擁有一個靜態的MappingHandler集合,這個類的作用是從獲得的所有類中,找到被@controller注解的類,並將controller類中每個被@ReqeustMapping注解的方法封裝成一個MappingHandler,然后把MappingHandler放入靜態集合中
public class HandlerManager {
public static List<MappingHandler> mappingHandlerList = new ArrayList<>();
/**
* 處理類文件集合,挑出MappingHandler
* @param classList
*/
public static void resolveMappingHandler(List<Class<?>> classList){
for(Class<?> cls : classList){
if(cls.isAnnotationPresent(Controller.class)){//MappingHandler會在controller里面
parseHandlerFromController(cls);//繼續從controller中分離出一個個MappingHandler
}
}
}
private static void parseHandlerFromController(Class<?> cls) {
//先獲取該controller中所有的方法
Method[] methods = cls.getDeclaredMethods();
//從中挑選出被RequestMapping注解的方法進行封裝
for(Method method : methods){
if(!method.isAnnotationPresent(RequestMapping.class)){
continue;
}
String uri = method.getDeclaredAnnotation(RequestMapping.class).value();//拿到RequestMapping定義的uri
List<String> paramNameList = new ArrayList<>();//保存方法參數的集合
for(Parameter parameter : method.getParameters()){
if(parameter.isAnnotationPresent(RequestParam.class)){//把有被RequestParam注解的參數添加入集合
paramNameList.add(parameter.getDeclaredAnnotation(RequestParam.class).value());
}
}
String[] params = paramNameList.toArray(new String[paramNameList.size()]);//把參數集合轉為數組,用於反射
MappingHandler mappingHandler = new MappingHandler(uri, method, cls, params);//反射生成MappingHandler
mappingHandlerList.add(mappingHandler);//把mappingHandler裝入集合中
}
}
}
- 完成上面四步后,我們在框架啟動的時候就獲得了一個MappingHandler集合,當請求來到時,我們只要根據請求的uri從集合中找到對應的MappingHandler,就可以通過反射調用對應的處理方法,到此也就完成了框架請求分發的功能。
控制反轉和依賴注入
完成了請求分發功能后,進一步想這么一個問題:
假設現在處理一個請求需要創建A,B,C三個對象,而
A 有個字段 D
B 有個字段 D
C 有個字段 B
如果按照順序創建ABC的話,
首先要創建一個D,然后創建一個A;
接着先創建一個D,然后創建一個B;
接着先創建一個D,然后創建一個B,才能創建出一個C
總共創建了一個A,兩個B,一個C,三個D。
上述是我們編寫程序的一方創建對象的方式,可以看到由於對象不能被重復引用,導致創建了大量重復對象。
為了解決這個問題,spring提出了bean這么個概念,你可以把一個bean理解為一個對象,但是他對比普通的對象有如下特點:
- 不像普通對象一樣朝生暮死,聲明周期較長
- 在整個虛擬機內可見,不像普通對象只在某個代碼塊中可見
- 維護成本高,以單例形式存在
為了制作出上述的bean,我們得有個bean工廠,bean工廠的原理也很簡單:在框架初始化的時候創建相關的bean(也可以在用到的時候創建),當需要使用bean的時候直接從工廠中拿。也就是我們把創建對象的權力交給框架,這就是控制反轉
有了bean工廠后按順序創建ABC的過程如下:
首先創建一個D,把D放入工廠,然后創建一個A,把A放入工廠;
接着從工廠拿出一個D,創建一個B,把B也放入工廠;
接着從工廠拿出一個B,創建一個C,把C也放入工廠;
總共創建了一個A,一個B,一個C,一個D
達到了對象重復利用的目的
至於創建出一個D,然后把D設置為A的一個字段這么個過程,叫做依賴注入
所以控制反轉和依賴注入的概念其實很好理解,控制反轉是一種思想,而依賴注入是控制反轉的一種具體實現。
實操
- 首先在bean包下創建@Bean和@AutoWired兩個注解,同樣是用於框架解析類的。
- 接着在bean包下創建BeanFactory,BeanFactory要能提供一個根據類獲取實例的功能,這就要求他要有一個靜態的getBean()方法,和一個保存Bean的映射集合。
- 為了初始化Bean,要有一個根據類文件集合解析出bean的方法。該方法會遍歷集合中所有的類,把有注解的,屬於bean的類提取出來,創建該類的對象並放到靜態集合中。
- 在這里有個有意思的點——按什么順序創建bean?在本文給出的源碼中,用了一個循環來創建bean,如果該bean沒有依賴其他的bean就直接創建,如果有依賴其他bean就看其他bean有沒被創建出來,如果沒有就跳過當前的bean,如果有就創建當前的bean。
- 在循環創建bean的過程中可能出現一種bean之間相互依賴的現象,源碼中暫時對這種現象拋出異常,沒作處理。
public class BeanFactory {
//保存Bean實例的映射集合
private static Map<Class<?>, Object> classToBean = new ConcurrentHashMap<>();
/**
* 根據class類型獲取bean
* @param cls
* @return
*/
public static Object getBean(Class<?> cls){
return classToBean.get(cls);
}
/**
* 初始化bean工廠
* @param classList 需要一個.class文件集合
* @throws Exception
*/
public static void initBean(List<Class<?>> classList) throws Exception {
//先創建一個.class文件集合的副本
List<Class<?>> toCreate = new ArrayList<>(classList);
//循環創建bean實例
while(toCreate.size() != 0){
int remainSize = toCreate.size();//記錄開始時集合大小,如果一輪結束后大小沒有變證明有相互依賴
for(int i = 0; i < toCreate.size(); i++){//遍歷創建bean,如果失敗就先跳過,等下一輪再創建
if(finishCreate(toCreate.get(i))){
toCreate.remove(i);
}
}
if(toCreate.size() == remainSize){//有相互依賴的情況先拋出異常
throw new Exception("cycle dependency!");
}
}
}
private static boolean finishCreate(Class<?> cls) throws IllegalAccessException, InstantiationException {
//創建的bean實例僅包括Bean和Controller注釋的類
if(!cls.isAnnotationPresent(Bean.class) && !cls.isAnnotationPresent(Controller.class)){
return true;
}
//先創建實例對象
Object bean = cls.newInstance();
//看看實例對象是否需要執行依賴注入,注入其他bean
for(Field field : cls.getDeclaredFields()){
if(field.isAnnotationPresent(AutoWired.class)){
Class<?> fieldType = field.getType();
Object reliantBean = BeanFactory.getBean(fieldType);
if(reliantBean == null){//如果要注入的bean還未被創建就先跳過
return false;
}
field.setAccessible(true);
field.set(bean, reliantBean);
}
}
classToBean.put(cls, bean);
return true;
}
}
- 有了bean工廠之后,凡是用到bean的地方都能直接通過bean工廠拿了
- 最后我們可以寫一個小Demo測試一下自己的框架是否能正確地處理請求完成響應。相信整個迷你框架擼下來,Spring的核心功能,以及控制反轉,依賴控制等名詞在你腦海中不再只是概念,而是一行行清晰的代碼了。
本文參考自慕課網同名課程