一也榄、簡介
空閑之余手撕一把springMVC,以加深對spring的理解司志,盡可能寫的全面甜紫,源碼中注釋也會很詳細。話不多說開搞骂远!
二囚霸、項目搭建
在IDEA上用MAVEN創(chuàng)建一個webApp項目:
原來的springMVC中,最重要的一個類就是DispatchServlet即前端請求控制器吧史,我們自定義自己的DispatchServlet邮辽,繼承HttpServlet唠雕。
因為要繼承HttpServlet,利用pom引入servlet-api的jar包:
...
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>3.0-alpha-1</version>
</dependency>
...
繼承HttpServelet新建DispatchServlet類吨述,重寫doGet岩睁、doPost、和init方法揣云。在init方法中實現(xiàn)包掃描捕儒、IOC容器初始化等一系列操作。這里面操作會在tomcat加載項目后初始化完成邓夕。
同時在web.xml配置DispatchServlet:
...
<servlet>
<servlet-name>DispatchServlet</servlet-name>
<servlet-class>com.zjx.myspringmvc.servelet.DispatchServlet</servlet-class>
<load-on-startup>0</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatchServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
...
三刘莹、流程解讀
3.1.自定義注解
常用的@Controller、@Service焚刚、@RequestMapping点弯、@RequestParam、@Qualifier等矿咕。我們對應(yīng)定義自己的注解@MyController抢肛、@MyService、@MyRequestMapping碳柱、@MyRequestParam捡絮、@MyQualifier。
自定義注解首先了解四種元注解: @Retention @Target @Document @Inherited莲镣。
@Retention:注解的保留位置
@Retention(RetentionPolicy.SOURCE)//注解僅存在于源碼中福稳,在class字節(jié)碼文件中不包含
@Retention(RetentionPolicy.CLASS)//默認的保留策略,注解會在class字節(jié)碼文件中存在瑞侮,但運行時無法獲得
@Retention(RetentionPolicy.RUNTIME)//注解會在class字節(jié)碼文件中存在的圆,在運行時可以通過反射獲取到
@Target:注解的作用目標
@Target(ElementType.TYPE)//接口、類区岗、枚舉略板、注解
@Target(ElementType.FIELD)//字段、枚舉的常量
@Target(ElementType.METHOD)//方法
*@Target(ElementType.PARAMETER)//方法參數(shù)*
*@Target(ElementType.CONSTRUCTOR)//構(gòu)造函數(shù)*
*@Target(ElementType.LOCAL_VARIABLE)//局部變量*
*@Target(ElementType.ANNOTATION_TYPE)//注解*
*@Target(ElementType.PACKAGE)//包*
@Document:說明該注解將被包含在javadoc中*
@Inherited:說明子類可以繼承父類中的該注解*
我們自定義的注解用到前面三個慈缔,其中@Retention(RetentionPolicy.RUNTIME)叮称、@Document是相同的。使用@Target不同的注解類各不相同藐鹤。具體定義代碼如下:
...
@Target({ElementType.TYPE})//可以用在接口瓤檐、類、枚舉
@Retention(RetentionPolicy.RUNTIME)//可以通過反射得到
@Documented//該注解將被包含在javadoc中
public @interface MyController {
String value() default " ";//
}
...
@Target({ElementType.FIELD})//用于字段娱节、枚舉的常量
@Retention(RetentionPolicy.RUNTIME)//可以通過反射得到
@Documented//該注解將被包含在javadoc中
public @interface MyQualifier {
String value() default " ";
}
...
@Target({ElementType.METHOD,ElementType.TYPE})//用于方法,類
@Retention(RetentionPolicy.RUNTIME)//可以通過反射得到
@Documented//該注解將被包含在javadoc中
public @interface MyRequestMapping {
String value() default " ";
}
...
@Target({ElementType.PARAMETER})//用于方法參數(shù)
@Retention(RetentionPolicy.RUNTIME)//可以通過反射得到
@Documented//該注解將被包含在javadoc中
public @interface MyRequestParam {
String value() default " ";
}
...
@Target({ElementType.TYPE})//可以用在接口挠蛉、類、枚舉
@Retention(RetentionPolicy.RUNTIME)//可以通過反射得到
@Documented//該注解將被包含在javadoc中
public @interface MyService {
String value() default " ";
}
...
3.2.init方法
其實這里就是整個項目的核心肄满,首先重寫init方法谴古。在這個方法里按序?qū)崿F(xiàn)包掃描质涛、實例化(即初始化IOC容器)、依賴注入以及建立url與方法的映射關(guān)系掰担,init方法如下汇陆。
...
@Override
public void init(ServletConfig config) throws ServletException {
//1、掃描哪些需要被實例化 class(包及包以下的class)
scanPackage("com.zjx");
//2带饱、掃描出來的類 進實例化
instance();
//依賴注入 這里項目簡單 只有service層注入controller 后續(xù)讀者可以自實現(xiàn)dao->service
iocInject();
// 4毡代、建立一個path與method的映射關(guān)系
HandlerMapping();
}
...
下面分別說明一下每個方法的流程,具體實現(xiàn)代碼就不貼出來了勺疼,因為這幾個方法在實現(xiàn)的過程中有許多相似的地方教寂,都是基于反射得到實例,再根據(jù)不同的注解進行處理执庐,具體可以參考最后的源碼酪耕。
scanPackage("com.zjx")
根據(jù)初始傳入的包名進行掃描,這里會用到遞歸耕肩,掃描已經(jīng)編譯好的項目下所有的類因妇,最后把所有的類名存放到一個List集合中问潭。-
instance()
遍歷掃描到的class文件猿诸,利用反射Class.forName()方法得到class對象,這里注意上一步得到的類名會有.class后綴狡忙,需要去掉梳虽。得到class類后先判斷是否有注解,這里為了簡單我們只關(guān)注了@MyService 灾茁、@MyController類注解窜觉,如果有該類注解,則繼續(xù)利用反射方法創(chuàng)建實例,該實例作為值北专,同時得到對應(yīng)注解上的參數(shù)值作為key(springMVC是將類名首字母小寫作為key禀挫,這里我們簡單起見),保存到一個Map容器中拓颓。
關(guān)鍵代碼如下:
image.png -
iocInject()
進行依賴注入语婴,上一步中IOC容器中已經(jīng)存放所有我們所關(guān)心的實例。遍歷容器驶睦,首先得到當(dāng)前遍歷到的類的實例和類class,根據(jù)類class來獲取注解信息砰左,如果當(dāng)前類有@MyController注解,先獲取類里面的所有屬性场航,遍歷所有屬性缠导,判斷類屬性上是否有自動裝配(依賴注入)的注解@Autowired或Qualifier,如果有獲得該注解的value溉痢,即IOC容器中的key僻造,根據(jù)該key獲得對應(yīng)實例憋他,最后給該屬性設(shè)值(即注入)。注意點:類中屬性一般是private髓削,所以設(shè)值前需要field.setAccessible(true)獲得許可举瑰,不然無法設(shè)值。
關(guān)鍵代碼如下:
image.png -
HandlerMapping
請求路徑url與方法的映射關(guān)系,主要是獲得Controller上注解的值和方法上注解的值。同樣還是遍歷IOC容器郊愧,得到類class背苦,判斷是否有@MyController注解,如果有得到注解上的值拟逮,同時獲取該類下所有的方法,遍歷判斷哪些方法有@MyRequestMapping注解,得到該注解上的值坎怪,和@MyController注解的值拼接成了請求路徑url,最后將拼接成的url作為key廓握,方法作為值存放到一個Map中搅窿。
關(guān)鍵代碼:
image.png
3.3.處理請求
DispatchServlet處理請求的兩個方法doGet、doPost隙券,這里我們只需實現(xiàn)doPost男应,doGet直接在方法里面調(diào)用doPost方法。
處理請求的實現(xiàn)的基本流程是:
- 獲取到請求路徑
- 根據(jù)請求路徑獲得對應(yīng)要執(zhí)行的方法(請求路徑和方法的映射我們在init方法中已經(jīng)得到)
- 取得控制類(controller)的實例
- 獲得方法執(zhí)行的參數(shù)值
- 調(diào)用方法的invoke娱仔,方法執(zhí)行完成
這里最重要的一步就是解析執(zhí)行方法上的參數(shù),因為參數(shù)類型可能很多牲迫,直接if..else依次判斷的話耐朴,會很繁瑣,且代碼不夠優(yōu)雅盹憎。這里我們采用策略模式筛峭,每一種類型的參數(shù),都對應(yīng)一種解析器陪每,然后通過處理器執(zhí)行得到我們想要測參數(shù)影晓。關(guān)于什么是策略模式,可以參考:策略模式奶稠。
這里我們定義了三種參數(shù)類型的解析器類:HttpServletRequestParamResolver俯艰、HttpServletResponsetParamResolver、 MyRequestParamResolver锌订,由類名可以知道對應(yīng)那種參數(shù)類型竹握,這里不再贅述。這三個處理器都實現(xiàn)同一個解析器接口ParamResolver辆飘。解析器中都實現(xiàn)了兩個方法:
-
boolean support(Class<?> type, int paramIndex, Method method)
該方法是判斷當(dāng)前傳進來的方法參數(shù)是否是該解析器對應(yīng)的類型啦辐。 -
Object paramResolver(HttpServletRequest request, HttpServletResponse response, Class<?> type, int paramIndex, Method method)
返回對應(yīng)方法參數(shù)的值谓传。
下面具體介紹一下這三個解析器類:
-
HttpServletRequestParamResolver
判斷參數(shù)是否是HttpServletRequest類型,就看是否實現(xiàn)了ServletRequest接口芹关。paramResolver方法返回的還是原來的HttpServletRequest续挟。
關(guān)鍵代碼:
image.png - HttpServletResponsetParamResolver
和上面的一樣,判斷參數(shù)是否是HttpServletResponse類型,看是否實現(xiàn)了ServletResponse接口侥衬,也是原參數(shù)返回诗祸。
關(guān)鍵代碼:
image.png - MyRequestParamResolver
這里判斷和上面就不同了,因為這是加在參數(shù)上的@MyRequestParam注解轴总,可能有多個直颅,所以每次先獲得方法注解的參數(shù)類型,由方法method.getParameterAnnotations()獲得怀樟,該方法獲得的是一個二維數(shù)組功偿,再根據(jù)傳進來的參數(shù)下標,可以取得當(dāng)前參數(shù)的注解類型往堡,判斷是否是@MyRequestParam的注解類型械荷。是該注解,取出注解的值虑灰,執(zhí)行request.getParameter(value)可以獲得對應(yīng)參數(shù)的值吨瞎。
關(guān)鍵代碼:
image.png
image.png
最后定義一個處理器類,來執(zhí)行這些策略瘩缆,返回方法執(zhí)行的參數(shù)值數(shù)組关拒。流程如下: - 拿到當(dāng)前待執(zhí)行的方法有哪些參數(shù)
- 拿到所有的解析器類
- 遍歷所有參數(shù)應(yīng)用對應(yīng)解析器,得到參數(shù)值庸娱。
-
返回最終結(jié)果數(shù)組
關(guān)鍵代碼:
image.png
image.png
四、總結(jié)和一些坑
具體的流程就是上面所寫的谐算,關(guān)鍵代碼也都貼出來了熟尉,另外還有一些坑和不完善的地方做一下說明:
- 瀏覽器會發(fā)起這么一個請求:/favicon.ico,瀏覽器會請求網(wǎng)站根目錄的這個圖標洲脂,如果網(wǎng)站根目錄也沒有這圖標會產(chǎn)生404斤儿,因為我們沒有對應(yīng)的controller類來進行處理,這里遇到這個請求就直接return了恐锦。
- 目前只參數(shù)類型只能是String往果,因為通過request.getParameter(value)獲得的就是String,還沒有做到再根據(jù)參數(shù)類型強轉(zhuǎn)一铅。
這只是小小的實現(xiàn)了一把陕贮,很多細節(jié)還不完善,希望大家多多指正潘飘。
附上源碼:mySpringMvc