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 );*/
}
}
該類繼承自 HttpServlet
,CommonI
是自定義的用於獲取 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時的 TestAServlet
:JspPage
、HttpJspPage
、HttpJspBase
所做的事情,只是提供了init
和destroy
生命週期時的模版方法,再就是將之前依據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
對象,剩餘的八個都在這裏定義好了:pageContext
屬於 JSP 加入的,可以進行一些數據的保存讀取等等。具體的功能可以參照實現類:org.apache.jasper.runtime.PageContextImpl
page
對應了該class類自身,也就是this
session
自不必說,項目開發中使用的很多,詳細 創建過程可以參照源碼:org.apache.catalina.connector.Request
與org.apache.catalina.connector.RequestFacade
application
和config
包含的信息比較多,詳細功能可參照接口參數。request
和response
,使用的也很多,_jspService
方法參數傳入。out
對象其實是從response
獲取到的輸出流對象,具體代碼可參照org.apache.jasper.runtime.JspWriterImpl
;this.out = this.response.getWriter();
因此,在JSP中,我們纔可以直接去使用這些對象,而不用去顯式的創建,更根本的說:JSP開發和Servlet開發並無區別,只是開發效率上有了很大的提升。
好了,簡單剖析了一下 JSP 和 Servlet ,可以看到,javaEE中最爲麻煩的部分,其實 tomcat 已經幫我們處理好了,對於簡單的業務需求,完成起來並會特別耗費力氣。
但其中路徑攔截處理是在很麻煩,需要一直去配置,更麻煩的是,隨着業務邏輯的複雜性提高以及需求的變更,使用jsp來進行開發,着實不太方便。
SpringMVC 到底做了什麼?
使用SpringMVC
進行開發時,會發現配置文件很少,對於web.xml
,基本上不需要做什麼大的配置,如果不進行國際化或者主題切換(已當前來看,這種需求前端可以自行完成),那麼只需要完成兩個文件的處理就好:
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>
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開發模式,這裏又多了三層:
-
HttpServletBean
主要是把xml中配置的servlet參數信息設置到類的屬性中。 -
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
信息,並在本次請求處理結束後進行了還原。 -
DispatcherServlet
做的事情雖然比較多,但層次很清晰;-
在容器
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); }
這些內置對象在下面還會做出解釋,這裏先看一個大概。
-
在 請求到來時,每次都會調用
doService
方法,該方法會在真正處理請求前,將一些locale,flashMap,theme
等信息添加到request對象中,但這些對象一般我們並不用,所以直接略過,直接查看“真正”的方法doDispatch
-
doDispatch
會先檢查是否爲文件上傳請求,是的話調用initMultipartResolver
方法配置的內置對象先處理一次;然後,根據initHandlerMappings
配置的 一些 hanlder-mapping,找到適合的處理器:handler
;然後根據initHandlerAdapters
配置好的數據,找到適合該處理器的適配器:adapter
;然後結合adapter
和handler
從本次request
得到 一個ModelAndView
,看這個名字就很清楚,包含了view
和model
兩個模塊,一個視圖,一個數據,進行數據填充後就可以得到最終的返回數據(當然,這個填充數據的過程需要使用initViewResolvers
配置好的view-resovler
來獲取可用代碼處理的view
層)。 -
正常的邏輯基本就像上面所述,當然如果中間出現了異常,或者說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-mapping
:list類型,有三個元素,用於尋找請求對應的處理器(controller):RequestMappingHandlerMapping
:根據請求,查找到@Controller
註解的類中的某個處理器(也就是單個的方法),該處理器是HandlerMethod
類型。BeanNameUrlHandlerMapping
,根據 url 找到 bean,參照網上一張圖片就知道其功效:
SimpleUrlHandlerMapping
:與BeanNameUrlHandlerMapping
差不多,不過是根據key - value
結構 來查找 處理器的,同樣使用一張圖例進行說明:
-
handler-adapter
:list類型;之前找到了處理器,還需要知道如何使用該處理器去處理請求;系統同樣默認了三個實例:RequestMappingHandlerAdatper
:配合RequestMappingHandlerMapping
,mvc 框架中最核心的類,規範如何調用處理器,在處理器調用前做的初始化操作,參數賦值等等。HttpRequestHandlerAdapter
:處理實現了HttpRequestHandler
接口的處理器,框架會自動調用其handleRequest
方法處理本次請求。SimpleControllerHandlerAdapter
,處理實現了Controller
接口的處理器。
-
handler-exception-resolver
:也是一個列表,這裏默認實現有三個:ExceptionHandlerExceptionResolver
:處理器爲HandlerMethod
類型時,如果出現異常,則使用該對象進行處理;該對象一般會選擇合適的註解了@ExceptionHandler
的方法。ResponseStatusExceptionResolver
:從名字便可以看出,主要處理@ResponseStatus
註解標註的方法產生的異常。DefaultHanlderExceptionResolver
:主要處理一些通用異常,如“METHOD”不支持
,handler未找到
等等。
ok,經過上面的分析,應該有了一個 mvc 大概的處理流程,現在我們不妨徹底的“自定義”一把,把上面的九大組件都自己實現一次,然後查看效果;當然,這些組件需要先到contextConfigLocation.xml
中配置好;
自定義的組件都是以First***
開頭,如果組件是單個對象,則會被替換,如果是List
列表,則會添加一條數據
,這樣,我們就可以針對整個系統流程進行攔截。
以上就是對 mvc 架構的一些淺析,當然,設計到具體處理器的環境,肯定不止於此,不過了解這些至少可以在有需求時進行部分的定製。
這裏附上:GITHUB:mvc-demo,可以查看文中出現的項目代碼部分。
注:文中部分圖片引用自他人blog:
BeanNameUrlHandlerMapping
與SimpleUrlHandlerMapping
解釋圖引用自:SpringMVC 配置式開發-BeanNameUrlHandlerMapping(七)