使用springmvc運行流程分析,手寫spring框架嘗試

該文章主要是分析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的類置於容器內。

 /* 創建一個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

springMVC介紹以及執行流程

什麼是SpringMVC?

SpringMVC是一個實現瞭MVC設計模式的輕量級web層框架,使用起來簡單方便。

SpringMVC的優勢是什麼?

1、清晰的角色劃分:

  • 前端控制器(DispatcherServlet)
  • 請求到處理器映射(HandlerMapping)
  • 處理器適配器(HandlerAdapter)
  • 視圖解析器(ViewResolver)
  • 處理器或頁面控制器(Controller)
  • 驗證器( Validator)
  • 命令對象(Command 請求參數綁定到的對象就叫命令對象)
  • 表單對象(Form Object 提供給表單展示和提交到的對象就叫表單對象)。

2、分工明確,而且擴展點相當靈活,可以很容易擴展,雖然幾乎不需要。

3、由於命令對象就是一個 POJO,無需繼承框架特定 API,可以使用命令對象直接作為業務對象。

4、和 Spring 其他框架無縫集成,是其它 Web 框架所不具備的。

5、可適配,通過 HandlerAdapter 可以支持任意的類作為處理器。

6、可定制性, HandlerMapping、 ViewResolver 等能夠非常簡單的定制。

7、功能強大的數據驗證、格式化、綁定機制。

8、利用 Spring 提供的 Mock 對象能夠非常簡單的進行 Web 層單元測試。

9、本地化、主題的解析的支持,使我們更容易進行國際化和主題的切換。

10、強大的 JSP 標簽庫,使 JSP 編寫更容易。

還有比如RESTful風格的支持、簡單的文件上傳、約定大於配置的契約式編程支持、基於註解的零配置支持等等。

與Struts2的對比:

共同點:都是基於MVC設計模式的表現層框架,底層實現都離不開原始的Servlet,處理請求的機制都是一個核心控制器;

區別:Spring MVC 的入口是 Servlet, 而 Struts2 是 Filter

Spring MVC 是基於方法設計的,而 Struts2 是基於類, Struts2 每次執行都會創建一個動作類。所以 Spring MVC 會稍微比 Struts2 快些。

相比來說,SpringMVC已經全面超越瞭Struts2。

執行流程:

DispatcherServlet: 是整個springmvc框架的核心。

前端控制器/核心控制器:所有的請求和響應都由此控制器進行分配。

前端控制器的所有工作都是基於組件完成的:

三大組件:

  • HandlerMapping: 它負責根據客戶端的請求尋找對應的hadler,找到以後把尋找的handler返回給DispatcherServlet;
  • HandlerAdapter:它負責執行尋找到的Handler的方法,方法執行完後將返回值給HandlerAdapter, HandlerAdapter將返回值傳給DispatcherServlet;
  • ViewResolver:它根據DispatcherServlet指定的返回結果尋找對應的頁面,找到後將結果返回給DispatcherServlet。
  • DispatcherServlet負責最終的響應,默認是轉發的操作。

執行流程圖:

在這裡插入圖片描述

圖畫的有些靈魂,但是大致的流程就是這樣,大傢可以參考著理解下。

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: