該文章主要是分析Springmvc啓動的流程(配置階段、初始化階段和運行階段),可以讓自己對spring框架有更深一層的理解。對框架比較感興趣的朋友都可以瞭解閱讀下,對於我所描述的內容有錯誤的還望能不吝指出。
對於springmvc中的整個流程我個人把他分爲這幾個階段,包括個人手寫的spring也是參照此按階段實現:
1.配置階段
根據web.xml ,先定義DispatcherServlet並且定義該sevlet傳入的參數和路徑。
2.初始化階段
初始化階段中又可以分爲IOC、DI和MVC階段:
(1)IOC:初始化配置文件和IOC容器,掃描配置的包下的類,通過反射機制將需要實例化的類放入IOC容器,既將帶有spring註解的類進行實例化後存放到 IOC 容器中。IOC容器的實質就是一個集合;
(2)DI:DI階段(其實就是依賴注入)。對需要賦值的實例屬性進行賦值(一般較多都是處理帶有註解的@Autowrized的屬性)
(3)MVC:構造出HandlerMapping集合,主要作用就是用於存放對外公開的API和Method之間的關係,一個API一般會對應一個可執行的Method.
3.運行階段
運行階段中,當接受到一個url後,會到HandleMapping集合中,找到對應Method、通過反射機制去執行invoker,再返回結果給調用方。
這樣就大體完成了springmvc整個運行階段,所描述的都僅爲個人觀點,如果有誤請在評論中指出。
其整體流程可以參照下圖:
接下來就來嘗試手寫一個類似springmvc的框架了,這個手寫的過程還是相當有成就感的!
1.創建一個空的JavaWeb工程,引入依賴,其實因爲我們是要手寫spring,所以基本不需要什麼外部的依賴工具,只需要導入servlet-api即可,如下:
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
2.根據上述的流程描述,接下來就是對web.xml進行配置:
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>com.wangcw.cwframework.sevlet.CwDispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>application.properties</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
對於配置中的CwDispatcherServlet其實就是個人自定義一個作用與spring中DispatcherServlet相同的Servlet,此處先創建一個空的CwDispatcherServlet,繼承 javax.servlet.http.HttpServlet即可,具體實現後面會描述。
此處因爲是手寫spring的部分功能,所以配置也不用寫太多,此處僅拿一個包掃描的配置(scanPackage),各位少俠可自行拓展。
CwDispatcherServlet中初始化的配置文件application.properties內容如下:
scanPackage=com.wangcw
3.相信spring中又一部分註解都是大家比較熟悉的,接下來我們先從這幾個註解着手吧。(此處就不指出各個註解的作用了,相信百度上已經很多了)
spring註解 | 自定義註解 |
@Controller | @CwController |
@Autowired | @CwAutowired |
@RequestMapping | @CwRequestMapping |
@RequestParam | @CwRequestParam |
@Service | @CwService |
然後實現下各個自定義的註解,直接貼代碼:
/*
* 創建一個類似@Controller作用的註解類
*/
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CwController {
String value() default "";
}
/*
* 創建一個類似@Autowried作用的註解類
*/
import java.lang.annotation.*;
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CwAutowried {
String value() default "";
}
/*
* 創建一個類似@RequestMapping作用的註解類
*/
import java.lang.annotation.*;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CwRequestMapping {
String value() default "";
}
/*
* 創建一個類似@RequsetParam作用的註解類
*/
import java.lang.annotation.*;
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CwRequestParam {
String value() default "";
}
/*
* 創建一個類似@Service作用的註解類
*/
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CwService {
String value() default "";
}
4.創建一個簡單的控制層和業務層交互 Demo,加上自定的註解,具體註解的功能,後面贅述。
Controller.java
@CwController
@CwRequestMapping("/demo")
public class Controller {
@CwAutowried
private Service service;
@CwRequestMapping("/query")
public void query(HttpServletRequest req, HttpServletResponse resp,@CwRequestParam("name") String name) throws IOException {
resp.getWriter().write(service.query(name));
}
@CwRequestMapping("/add")
public void add(HttpServletRequest req, HttpServletResponse resp, @CwRequestParam("a") Integer a, @CwRequestParam("b") Integer b) throws IOException {
resp.getWriter().write("a+b="+(a+b));
}
}
Service.java
public interface Service {
String query(String name);
}
ServiceImpl.java
@CwService
public class ServiceImpl implements Service{
@Override
public String query(String name) {
return "I am "+name;
}
}
5.上面的controller層和service層已經把我們上述的自定註解都使用上去了,接下來我們開始手寫spring的核心功能了,也就是實現CwDispatcherServlet.java這個HttpServlet的子類。
首先需要重寫父類中的init方法,因爲我們要在Init過程中實現出跟spring一樣的效果。
理一理init()過程中都需要做哪些事情呢?整理了一下init()中主要需要以下幾步操作
@Override
public void init(ServletConfig config) {
/* 1.加載配置文件*/
doLoadConfig(config.getInitParameter("contextConfigLocation"));
/* 2.掃描scanPackage配置的路徑下所有相關的類*/
doScanner(contextConfig.getProperty("scanPackage"));
/* 3.初始化所有相關聯的實例,放入IOC容器中*/
doInstance();
/*4.實現自動依賴注入 DI*/
doAutowired();
/*5.初始化HandlerMapping */
initHandlerMapping();
}
第一步很簡單,在類中定義一個Properties實例,用於存放Servlet初始化的配置文件。導入配置代碼略過,IO常規讀寫即可。
private Properties contextConfig = new Properties();
第二步通過上面獲取到的配置,取到需要掃描的包路徑,然後在根據路徑找到對應文件夾,做一個遞歸掃描即可。將掃描到的文件名去除後綴,保存到一個集合中,那麼該集合就存放了包下所有類的類名。
String scanFileDir = contextConfig.getProperty("scanPackage");
/* 用於存放掃描到的類 */
private List<String> classNames = new ArrayList<String>();
/*掃描獲取到對應的class名,便於後面反射使用*/
String className = scanPackage + "." + file.getName().replace(".class", "");
classNames.add(className);
第三步就是IOC階段,簡而言之就是對上面集合中所有的類進行遍歷,並且創建一個IOC容器,將帶有@CwController和@CwService的類置於容器內。(爲了防止篇幅過長,所以沒有上傳所有代碼,僅以思想爲主,後續會把完整代碼上傳到CSDN資源和我的github)
/* 創建一個IOC容器 */
private Map<String, Object> IOC = new HashMap<String, Object>();
for (String classNme : classNames){
if( 對加了 @CwController 註解的類進行初始化){
/* 對於初始化的類還需要放入IOC容器,
對於存入IOC的實例,key值是有一定規則的,默認類名首字母小寫;*/
/* toLowerFirstCase是自定義的一個工具方法,用於將傳入的字符串首字母小寫 */
String beanName = toLowerFirstCase(clazz.getSimpleName());
IOC.put(beanName, clazz.newInstance());
} else if (對加了 @CwService 註解的類進行初始化){
/* 對於存入IOC的實例,key值是有一定規則的,而Service層的規則相對上面更復雜一些,因爲註解可以有自定義實例名,並且可能是接口實現類 */
IOC.put(beanName, instance);
} else {
//對於掃描到的沒有註解的類,忽略初始化行爲
continue;
}
}
第四步是DI操作,將IOC容器中需要賦值的實例屬性進行賦值,即帶有Autowired註解的實例屬性。僞代碼如下:
/*遍歷IOC中的所有實例*/
for(Map.Entry<String, Object> entry : IOC.entrySet()){
/* 使用getDeclaredFields暴力反射 */
Field [] fields = entry.getValue().getClass().getDeclaredFields();
for (Field field : fields){
/*1.判斷屬性是否有加註解@CwAutowried.對於有註解的屬性才需要賦值*/
....
/*屬性授權*/
field.setAccessible(true);
field.set(entry.getValue(), IOC.get(beanName));
}
}
第五步要處理Controller層的Method與請求url的匹配關係,讓請求能準確的請求到對應的url。篇幅問題,此處還是上傳僞代碼。
/* 創建HandlerMapping存放url,method的匹配關係
其中類Handler是我自己定義的一個利用正則去匹配url和method,
只要用戶傳入url,Handler就可以響應出其對應的method*/
private List<Handler> handlerMapping = new ArrayList<Handler>();
/* 遍歷IOC容器 */
for (Map.Entry<String, Object> entry : IOC.entrySet()){
Class<?> clazz = entry.getValue().getClass();
/* 只對帶有CwController註解的類進行處理 */
定義一個url,由帶有CwController的實例類上的@CwRequestMapping註解的值和Method上@CwRequestMapping註解的值組成
/* (1).判斷類上是否有CwRequestMapping註解 ,進行拼接 url*/
/* (2).遍歷實例下每個Method,並且需要判斷該方法是否有【 @CwRequestMapping 】註解,拼接url*/
/* 最後將匹配關係以正則的形式,放到HandlerMapping集合中 */
String regex = (url);
Pattern pattern = Pattern.compile(regex);
handlerMapping.add(new Handler(pattern,method));
}
到這裏就基本完成了springmvc的初始化階段,之後的工作就是重寫一下CwDispatcherServlet.java父類的doGet()/doPost()方法。根據request中的URI和參數來執行對應的Method,並且響應結果。
/* 利用反射執行其所匹配的方法 */
handler.method.invoke(handler.controller, paramValues);
到此整個步驟就完成了,此時可以愉快的啓動項目,並訪問對應的url進行測試了。
根據上面Controller定義的方法可以知道其匹配的url爲 : /demo/query 和 /demo/add,並且有使用@CwRequestParam註解定義了其各個參數的名稱。
測試結果如下:
http://localhost:8080/spring/demo/query?name=James
http://localhost:8080/spring/demo/add?a=222222&b=444444
再來測試個url,是controller中沒有聲明出@CwRequestMapping註解的,看看結果。
http://localhost:8080/spring/demo/testNoUrl
注:文章中很多內容都是使用僞代碼進行實現的,主要原因是怕文章太長,看着太枯燥。手寫的spring整個工程已經在整理了,後續會發布到github,到時候會把鏈接補上。需要的也可以留下郵箱。歡迎大佬們指出有誤的地方。