javaEE&從servlet到SpringMVC

javaWeb的開發經歷了多個階段;
剛開始時,由於框架等不夠成熟,主要使用serlvet,jsp等技術實現,如果需求較爲簡單,當然可以勝任,不過隨着項目複雜度的增加,這種略顯"混亂"的方式便有些力所不逮了。
之後SpringMVC的出現,使得開發可以將更多的工作量放在覈心任務之上,雖然還是需要做一定的配置,但總的來說,已經很方便了。

這裏對比JSP的開發方式,來看一下SpringMVC的代碼邏輯。

出於易懂的初衷,一般使用很簡單的代碼來查看整個流程,暫時不考慮邏輯的合理性

什麼是servlet編程?

在古老的servlet時代,後臺都是用java代碼將html數據一行行打印生成的,代碼量極爲龐大,這裏以一個簡單的servlet來看:

TestAServlet.java:

public class TestAServlet extends HttpServlet implements CommonI {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");
        PrintWriter out = resp.getWriter();
        out.println("<html>");
        out.println("<head>");
        out.println("<title>Hello World!</title>");
        out.println("</head>");
        out.println("<body>");
        out.println("<h1>Hello World!</h1>");
        out.println("</body>");
        out.println("</html>");
        out.close();
        /*var a =req.getRequestDispatcher("/gc/abc");ing
        a.forward(req,resp );*/
    }
}

該類繼承自 HttpServletCommonI 是自定義的用於獲取 Logger 的接口,不必過多考慮,Servlet的繼承結構圖如下:

在這裏插入圖片描述

這也是Servlet經典繼承關係圖:

  • Servlet 接口定義了模版方法:

    public interface Servlet {
      void init(ServletConfig var1) throws ServletException;
    
      ServletConfig getServletConfig();
    
      void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
    
      String getServletInfo();
    
      void destroy();
    }
    

    出於簡單的原則,我們只看個大概就好:

    • init方法用於初始化操作,這個從方法名字也可以看得出來,一般整個生命週期內只調用一次。
    • destroy方法在容器銷燬時纔會調用。
    • service方法在每次有客戶端請求到來時都會調用,用於處理主要的業務邏輯。
  • GenericServlet是個抽象類,主要是使用代理模式在類中保持一個ServletConfig對象,用於獲取各種參數或者配置信息(還提供了log方法)

  • HttpServlet抽象類主要是根據Method的不同,將請求路由到不同的方法中:例如**將 GET 請求路由到 doGet 方法 **,至於其他功能,處於簡單的情況考慮,都不重要

因此,我們在TestAServlet中重載了doGet方法,就可以正確的處理GET請求,然後按照一貫的做法,在web.xml中,我們配置一下攔截路徑就可以了:

<!--servlet:TestAServlet 攔截-->
<servlet>
    <servlet-name>test</servlet-name>
    <servlet-class>com.knowledge.mnlin.znd.TestAServlet</servlet-class>

    <!-- 支持異步請求 -->
    <async-supported>true</async-supported>
</servlet>
<servlet-mapping>
    <servlet-name>test</servlet-name>
    <url-pattern>/servlet</url-pattern>
</servlet-mapping>

這也是之前Servlet開發的基本流程,此時在瀏覽器中訪問:

http://localhost:8080/servlet

就可以獲取返回信息:

<html>
    <head>
        <title>Hello World!</title>
    </head>
    <body>
        <h1>Hello World!</h1>
    </body>
</html>

從這裏看到,html顯示的所有內容,包括<html>一類的標籤,都是通過 out.println() 直接寫出來

但這種方式的弊端很容易發現,代碼太過繁瑣,可視性等都很差,如果再加上對於<style>或者<script>的處理,那更是不堪重負,對於簡單請求還好,其他的就算了。

不過通過Servlet,更容易去把控web框架是如何處理請求信息的。

結合上面的例子,即便我們沒有去查看tomcat的源碼,也很容易反向推測出被成爲Web容器的tomcat,在這個過程中的作用:監聽某個端口;解析處理Http協議
解釋的說:建立socket連接,因爲http只是協議,無法直接監聽某個端口(如8080),獲取網絡數據,因此需要在服務端建立socket-server,當有客戶端發送請求時,其實是向 8080 端口 發送了一段數據,該端數據滿足 http 協議,經過 tomcat 處理後,轉化爲可以使用的對象,然後在 servlet 中進行處理。

我們拋卻其中可能複雜的部分,以簡單的邏輯示圖:

在這裏插入圖片描述

什麼是JSP內置對象?

JSP有九大內置對象,這個做開發時,應該都有了解,但對於JSP文件生成的代碼(class類),可能不會做太多的關注,這裏以一個例子查看jsp到底是怎麼被jvm使用的。

當前有一JSP文件:start.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    String path = request.getContextPath();
    String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
%>
<html>
<head>
    <title><%=basePath.substring(0, 1)%>
    </title>
</head>
<body>
靜止的首頁信息:<br><br>
<%=basePath%>
</body>
</html>

功能很簡單:**獲取請求的路徑信息,並進行顯示,比如請求路徑爲:

http://localhost:8080/start.jsp

那麼,客戶端將收到服務端返回的信息(假設路徑能正確響應):

<html>
    <head>
        <title>h
    </title>
    </head>
    <body>
靜止的首頁信息:
        <br>
        <br>
http://localhost:8080/

    </body>
</html>

從上面的html代碼可以看到,框架對於jsp的處理,就是將其中有效的以 <% .* %> 聲明的部分,或者一些已定義好的標籤,用真實的數據進行填充,然後將結果返回給客戶端瀏覽器進行顯示。

我們可以查看該jsp對應生成的java/class文件,該文件一般存放在tomcat目錄下,類似這樣:

E:\apache-tomcat-9.0.12\work\Catalina\localhost\war_create\org\apache\jsp\start_jsp.java
E:\apache-tomcat-9.0.12\work\Catalina\localhost\war_create\org\apache\jsp\start_jsp.class

即:

tomcat安裝目錄\work\Catalina\localhost\項目名\org\apache\jsp\*

先查看 java 文件:start_jsp.java

public final class start_jsp extends org.apache.jasper.runtime.HttpJspBase
    implements org.apache.jasper.runtime.JspSourceDependent,
                 org.apache.jasper.runtime.JspSourceImports {

  private static final javax.servlet.jsp.JspFactory _jspxFactory =
          javax.servlet.jsp.JspFactory.getDefaultFactory();

  private static java.util.Map<java.lang.String,java.lang.Long> _jspx_dependants;

  private static final java.util.Set<java.lang.String> _jspx_imports_packages;

  private static final java.util.Set<java.lang.String> _jspx_imports_classes;

  static {
    _jspx_imports_packages = new java.util.HashSet<>();
    _jspx_imports_packages.add("javax.servlet");
    _jspx_imports_packages.add("javax.servlet.http");
    _jspx_imports_packages.add("javax.servlet.jsp");
    _jspx_imports_classes = null;
  }

  private volatile javax.el.ExpressionFactory _el_expressionfactory;
  private volatile org.apache.tomcat.InstanceManager _jsp_instancemanager;

  public java.util.Map<java.lang.String,java.lang.Long> getDependants() {
    return _jspx_dependants;
  }

  public java.util.Set<java.lang.String> getPackageImports() {
    return _jspx_imports_packages;
  }

  public java.util.Set<java.lang.String> getClassImports() {
    return _jspx_imports_classes;
  }

  public javax.el.ExpressionFactory _jsp_getExpressionFactory() {
    if (_el_expressionfactory == null) {
      synchronized (this) {
        if (_el_expressionfactory == null) {
          _el_expressionfactory = _jspxFactory.getJspApplicationContext(getServletConfig().getServletContext()).getExpressionFactory();
        }
      }
    }
    return _el_expressionfactory;
  }

  public org.apache.tomcat.InstanceManager _jsp_getInstanceManager() {
    if (_jsp_instancemanager == null) {
      synchronized (this) {
        if (_jsp_instancemanager == null) {
          _jsp_instancemanager = org.apache.jasper.runtime.InstanceManagerFactory.getInstanceManager(getServletConfig());
        }
      }
    }
    return _jsp_instancemanager;
  }

  public void _jspInit() {
  }

  public void _jspDestroy() {
  }

  public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)
      throws java.io.IOException, javax.servlet.ServletException {
     // 主代碼部分,下面會詳細介紹
  }
}

這裏粘貼出該類所有的代碼方便解析。我們先從繼承關係來看該jsp文件的結構

在這裏插入圖片描述

相比較於第一部分介紹Servlet時的 TestAServletJspPageHttpJspPageHttpJspBase所做的事情,只是提供了initdestroy生命週期時的模版方法,再就是將之前依據Method分開的處理邏輯,統一又交還給了_jspService方法。

_jspService方法裏面的邏輯層次很清晰:

  • 首先判斷請求的Method,jsp 只支持四種方式的請求:GET, HEAD, POST, OPTIONS,如果不是這四種,則直接拋出 405 錯誤碼:METHOD_NOT_ALLOWED

     if (!javax.servlet.DispatcherType.ERROR.equals(request.getDispatcherType())) {
      final java.lang.String _jspx_method = request.getMethod();
      if ("OPTIONS".equals(_jspx_method)) {
        response.setHeader("Allow","GET, HEAD, POST, OPTIONS");
        return;
      }
      if (!"GET".equals(_jspx_method) && !"POST".equals(_jspx_method) && !"HEAD".equals(_jspx_method)) {
        response.setHeader("Allow","GET, HEAD, POST, OPTIONS");
        response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "JSPs only permit GET, POST or HEAD. Jasper also permits OPTIONS");
        return;
      }
    }
    
  • 然後是對於 JSP 內置對象的處理:

    final javax.servlet.jsp.PageContext pageContext;
    javax.servlet.http.HttpSession session = null;
    final javax.servlet.ServletContext application;
    final javax.servlet.ServletConfig config;
    javax.servlet.jsp.JspWriter out = null;
    final java.lang.Object page = this;
    javax.servlet.jsp.JspWriter _jspx_out = null;
    javax.servlet.jsp.PageContext _jspx_page_context = null;
    

    接下來是對於這些對象的賦值操作

      response.setContentType("text/html;charset=UTF-8");
      pageContext = _jspxFactory.getPageContext(this, request, response,
      			null, true, 8192, true);
      _jspx_page_context = pageContext;
      application = pageContext.getServletContext();
      config = pageContext.getServletConfig();
      session = pageContext.getSession();
      out = pageContext.getOut();
      _jspx_out = out;
    

    除了不經常使用的 exception 對象,剩餘的八個都在這裏定義好了:

    1. pageContext屬於 JSP 加入的,可以進行一些數據的保存讀取等等。具體的功能可以參照實現類:org.apache.jasper.runtime.PageContextImpl
    2. page 對應了該class類自身,也就是this
    3. session自不必說,項目開發中使用的很多,詳細 創建過程可以參照源碼:org.apache.catalina.connector.Requestorg.apache.catalina.connector.RequestFacade
    4. applicationconfig包含的信息比較多,詳細功能可參照接口參數。
    5. requestresponse,使用的也很多,_jspService方法參數傳入。
    6. out對象其實是從response獲取到的輸出流對象,具體代碼可參照org.apache.jasper.runtime.JspWriterImpl
      this.out = this.response.getWriter();
      

因此,在JSP中,我們纔可以直接去使用這些對象,而不用去顯式的創建,更根本的說:JSP開發和Servlet開發並無區別,只是開發效率上有了很大的提升。

好了,簡單剖析了一下 JSP 和 Servlet ,可以看到,javaEE中最爲麻煩的部分,其實 tomcat 已經幫我們處理好了,對於簡單的業務需求,完成起來並會特別耗費力氣。

但其中路徑攔截處理是在很麻煩,需要一直去配置,更麻煩的是,隨着業務邏輯的複雜性提高以及需求的變更,使用jsp來進行開發,着實不太方便。

SpringMVC 到底做了什麼?

使用SpringMVC進行開發時,會發現配置文件很少,對於web.xml,基本上不需要做什麼大的配置,如果不進行國際化或者主題切換(已當前來看,這種需求前端可以自行完成),那麼只需要完成兩個文件的處理就好:

  1. web.xml 這個配置是不可缺少的,畢竟tomcat容器依據的就是此文件;在該文件中,我們只需要配置一個Servlet,然後攔截所有的網絡請求即可;該Servlet即爲org.springframework.web.servlet.DispatcherServlet
    <!--spring mvc 配置-->
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    
    <!-- Spring MVC配置文件的位置-->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/contextConfigLocation.xml</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    
  2. contextConfigLocation.xml,該文件用於配置 SpringMVC
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xmlns:p="http://www.springframework.org/schema/p"
          xmlns:context="http://www.springframework.org/schema/context"
          xmlns:mvc="http://www.springframework.org/schema/mvc"
          xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc
    http://www.springframework.org/schema/mvc/spring-mvc.xsd">
       <mvc:annotation-driven/>
       <!--掃描固定註解內容-->
       <context:component-scan base-package="com.knowledge.mnlin" use-default-filters="true"/>
    </beans>
    

默認的配置信息很少,SpringMVC 本質就是單Servlet應用,在一個Servlet中完成所有的路由選擇,請求處理等信息。

之後爲了簡寫,使用 mvc 代替 SpringMVC

跟之前兩部分一樣,我們根據org.springframework.web.servlet.DispatcherServlet的繼承結構,來簡單說明 mvc 如何工作的,不過限於篇幅以及mvck框架結構的複雜性,只做大概性的說明。

org.springframework.web.servlet.DispatcherServlet繼承關係圖:

在這裏插入圖片描述

這裏只看中間主線的繼承關係,相比於Servlet開發模式,這裏又多了三層:

  1. HttpServletBean 主要是把xml中配置的servlet參數信息設置到類的屬性中。

  2. FrameworkServlet初始化了WebApplicationContext,WebApplicationContext類的功能可以從其註釋得到:

    Interface to provide configuration for a web application. This is read-only while
    the application is running, but may be reloaded if the implementation supports this.
    

    FrameworkServlet還使用LocaleContextHolder保存了當前請求的Locale信息,使用RequestContextHolder保存了此次請求的request信息,並在本次請求處理結束後進行了還原。

  3. DispatcherServlet做的事情雖然比較多,但層次很清晰;

    1. 在容器init階段,進行一些全局信息的初始化,主要是初始化 mvc 的幾個內置對象

      protected void initStrategies(ApplicationContext context) {
      	//處理包含文件上傳的請求
      	initMultipartResolver(context);
      	//處理Locale,對於一些需要做國際化等等的項目,可能需要進行環境處理
      	initLocaleResolver(context);
      	//處理 theme,同locale一樣,暫時不考慮這個
      	initThemeResolver(context);
      	//handler-mapping,根據request來確定使用哪個處理器處理網絡請求
      	initHandlerMappings(context);
      	//當確定了用來處理本地請求的處理器時,需要找到對應的adapter,adapter用於指定處理器如何處理該請求
      	initHandlerAdapters(context);
      	// 當處理器模塊出錯時,會調用這個配置的對象去獲取默認的視圖信息
      	initHandlerExceptionResolvers(context);
      	//當View層沒有值時,通過該方法獲取一個ViewName
      	initRequestToViewNameTranslator(context);
      	//view 層獲取到值時,通過解析來獲取真正的 渲染的視圖對象
      	initViewResolvers(context);
      	//在 重定向 時,獲取第一次請求的參數信息
      	initFlashMapManager(context);
      }
      

      這些內置對象在下面還會做出解釋,這裏先看一個大概。

    2. 在 請求到來時,每次都會調用doService方法,該方法會在真正處理請求前,將一些locale,flashMap,theme等信息添加到request對象中,但這些對象一般我們並不用,所以直接略過,直接查看“真正”的方法doDispatch

    3. doDispatch會先檢查是否爲文件上傳請求,是的話調用initMultipartResolver方法配置的內置對象先處理一次;然後,根據initHandlerMappings配置的 一些 hanlder-mapping,找到適合的處理器:handler;然後根據initHandlerAdapters配置好的數據,找到適合該處理器的適配器:adapter;然後結合adapterhandler從本次request得到 一個ModelAndView,看這個名字就很清楚,包含了viewmodel兩個模塊,一個視圖,一個數據,進行數據填充後就可以得到最終的返回數據(當然,這個填充數據的過程需要使用initViewResolvers配置好的view-resovler來獲取可用代碼處理的view層)。

    4. 正常的邏輯基本就像上面所述,當然如果中間出現了異常,或者說view層爲null,那就需要使用到initHandlerExceptionResolvers以及initRequestToViewNameTranslator配置的內置對象了。

好了,目前爲止,已經淺析了 servlet 與 mvc 模式以及部分源碼,關於兩者之間可使用的“內置對象”,也做了簡單的說明,不過,僅僅從源碼,我們無法開出springmvc 開發的簡便之處,這裏可以使用一個例子來進行說明:

@Controller
@RequestMapping(value = "/gc")
public class GoController implements EnvironmentAware, CommonI {
    /**
     * 處理GET類型的"/index"和”/ind”請求
     * <p>
     * 匹配 @RequestMapping 註解的是 RequestMappingHandlerMapping
     */
    @RequestMapping(value = {"/index", "/ind"}, method = {RequestMethod.GET})
    public String index(Model model) throws Exception {
        getL().info("get請求:訪問index或者ind");
        model.addAttribute("msg", "Go Go Go!");
        return "index.jsp";
    }
}

相比於 servlet 的需要在 xml 中進行路徑配置,mvc只需要使用註解就可以實現同樣的需求,並且結構更爲簡單。

這些註解是由 mvc 框架自動解析配置的,無需進行“人工”干涉;之前 servlet 開發好像將訪問路徑攔截到了具體的HttpServlet子類,mvc 像是根據訪問路徑先攔截到具體的標註了@Controller的類,然後又攔截到了類中具體的方法(事實上並非這麼簡單,只是在理解上可能會比較方便一些);

如果查看 mvc 具體負責請求處理的類源碼,會很複雜,如果有興趣可以參照看透springmvc源代碼分析與實踐;框架可能會在更迭中有所升級,不過核心原理一般不會變更。這裏僅以 debug 模式來查看一下,mvc 自帶的 “內置對象” 是哪些類:

在這裏插入圖片描述

按照前面initStrategies方法中內置對象出現的順序,在初始化之後,我們可以查看到各個內置對象的值(有些是列表,有些是單個對象):

  • multipart-resolver :mvc 框架內置默認沒有處理文件上傳,需要的話可以自己定義。
  • locale-resolver:語言環境處理,默認有實現,不過用的比較少。
  • theme-resolver:主題切換處理,默認有實現,用的較少
  • flash-map:重定向時使用,用session實現,知道使用就好

然後看剩餘的幾個:

在這裏插入圖片描述

  • request-to-view-name-translator:默認實現是DefaultRequestToViewNameTranslator類,當沒有明確指定 view 時,會從 request 中取得請求路徑,然後加上前後綴,這裏前後綴都是空串:"",因此取值就是路徑
    @Override
    public String getViewName(HttpServletRequest request) {
       String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
       return (this.prefix + transformPath(lookupPath) + this.suffix);
    }
    
  • view-resolver:當返回的 ModelAndView 中 view 是字符串時,我們需要解析對應的真正可使用的 view 層對象,view-resolver是個 list ,因此可以存儲多個實現類,它們之間的優先級是根據實現的 Ordered 接口來的,如果前面的view-resolver不想進行解析,可以在resolveViewName中直接返回null,這樣框架會調用第二個解析器去進行處理。這裏有個默認的實現類:InternalResourceViewResolver,可以返回InternalResourceView ,該 view 對應的就是 jsp 類型的視圖。

然後還剩下了最後三個對象,這是 mvc 框架最核心的部分,也是我們之前註解真正起作用的地方,這裏會說明每個對象大概完成的功能,具體類的實現如果有興趣可以參照源碼進行比對:

  • handler-mappinglist類型,有三個元素,用於尋找請求對應的處理器(controller):

    1. RequestMappingHandlerMapping:根據請求,查找到@Controller註解的類中的某個處理器(也就是單個的方法),該處理器是HandlerMethod類型。
    2. BeanNameUrlHandlerMapping,根據 url 找到 bean,參照網上一張圖片就知道其功效:
      在這裏插入圖片描述
    3. SimpleUrlHandlerMapping:與BeanNameUrlHandlerMapping差不多,不過是根據 key - value 結構 來查找 處理器的,同樣使用一張圖例進行說明:
      在這裏插入圖片描述
  • handler-adapterlist類型;之前找到了處理器,還需要知道如何使用該處理器去處理請求;系統同樣默認了三個實例:

    1. RequestMappingHandlerAdatper:配合RequestMappingHandlerMapping,mvc 框架中最核心的類,規範如何調用處理器,在處理器調用前做的初始化操作,參數賦值等等。
    2. HttpRequestHandlerAdapter:處理實現了HttpRequestHandler接口的處理器,框架會自動調用其handleRequest方法處理本次請求。
    3. SimpleControllerHandlerAdapter,處理實現了Controller接口的處理器。
  • handler-exception-resolver :也是一個列表,這裏默認實現有三個:

    1. ExceptionHandlerExceptionResolver:處理器爲HandlerMethod類型時,如果出現異常,則使用該對象進行處理;該對象一般會選擇合適的註解了@ExceptionHandler的方法。
    2. ResponseStatusExceptionResolver:從名字便可以看出,主要處理@ResponseStatus註解標註的方法產生的異常。
    3. DefaultHanlderExceptionResolver:主要處理一些通用異常,如“METHOD”不支持handler未找到等等。

ok,經過上面的分析,應該有了一個 mvc 大概的處理流程,現在我們不妨徹底的“自定義”一把,把上面的九大組件都自己實現一次,然後查看效果;當然,這些組件需要先到contextConfigLocation.xml中配置好;

在這裏插入圖片描述

自定義的組件都是以First***開頭,如果組件是單個對象,則會被替換,如果是List列表,則會添加一條數據,這樣,我們就可以針對整個系統流程進行攔截。

以上就是對 mvc 架構的一些淺析,當然,設計到具體處理器的環境,肯定不止於此,不過了解這些至少可以在有需求時進行部分的定製。


這裏附上:GITHUB:mvc-demo,可以查看文中出現的項目代碼部分。


注:文中部分圖片引用自他人blog:
BeanNameUrlHandlerMappingSimpleUrlHandlerMapping解釋圖引用自:SpringMVC 配置式開發-BeanNameUrlHandlerMapping(七)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章