前言 開心一刻 有個同學去非洲援建,剛到工地接待他的施工員是個黑人,他就用英語跟人家交流,黑人沒做聲。 然後他又用法語,黑人還是沒說話。 然後他用手去比劃。黑人終於開口了:瞎比劃嘎哈,整個工地都中國人 前提背景 在利用maven/eclipse搭建ssm(spring+spring mvc+myba ...
前言
開心一刻
有個同學去非洲援建,剛到工地接待他的施工員是個黑人,他就用英語跟人家交流,黑人沒做聲。 然後他又用法語,黑人還是沒說話。 然後他用手去比劃。黑人終於開口了:瞎比劃嘎哈,整個工地都中國人
前提背景
在利用maven/eclipse搭建ssm(spring+spring mvc+mybatis)一文的問題反饋中,大體分兩個:404和頁面無數據;至於500,個人認為比較好解決,按照提示進行處理就好,本文就不討論500了
404
主要也是兩種
1、webapp未發佈
相關資源未部署,例如webapp未發佈部署,類似如下
不只是webapp,main下的java、resources、webapp,maven依賴都是需要部署到tomcat,不然就不完整,就會存在各種各樣的少內容的問題;
2、請求URL不對
這個確實是很多新入行的小伙伴容易出現的問題
如果工程正常部署,請求URL出現404,很有可能是我們請求的URL不對;我們到tomcat的home目錄下看看工程是否正常部署,類似如下
還可以看看工程發佈的內容(問題1中需要發佈的內容)是否都在;如果工程部署正常,而請求的URL又出現404,那不用想,就是你的URL寫錯了
404的解決方案就是:確認工程是否正確部署到tomcat,確認請求的URL是否正確,基本只要確認這兩點也就能找到問題了;後文不會再詳細的講404,我們將重點放到下麵這個問題上
頁面無數據
具體的問題應該是這樣的:當我們請求:http://localhost:埠/工程名/personController/showPerson時,數據正常顯示如下
當我們直接請求jsp時,只有title沒有數據,如下
這是為什麼?
對於這個問題一開始確實沒太在意,只是提示小伙伴去看servlet的四大作用域和jsp的九大內置對象,後面陸陸續續很多小伙伴都問了我,包括評論區留言、站內消息、QQ私聊等
站內信
評論區
我發現這個問題好像不是個別小伙伴的問題,很多新入門的小伙伴都存在這樣的疑問,下麵我們就對此次問題就行一個詳細的探究;後續篇幅較長,基礎鋪墊較多,希望大家耐心看完!
問題探究
servlet與servlet容器
狹義上來講,servlet指的就是介面:javax.servlet.Servlet,廣義上來講,servlet指的是servlet規範:Java Servlet API 標準;javax.servlet.Servlet與servlet容器都是servlet規範下的產物。Java Servlet API是Servlet容器和Servlet之間的介面,它定義了Servlet的各種方法,還定義了Servlet容器傳送給Servlet的對象類,其中最重要的是請求對象ServletRequest和響應對象ServletResponseo這兩個對象都是由Servlet容器在客戶端調用Servlet時產生的,Servlet容器把客戶請求信息封裝在ServletRequest對象中,然後把這兩個對象都傳送給要調用的Servlet,Servlet處理完後把響應結果寫入ServletResponse,然後由Servlet容器把響應結果發送到客戶端。
Servlet與Servlet容器的關係有點像槍和子彈的關係,槍是為子彈而生,而子彈又讓槍有了殺傷力。雖然它們是彼此依存的,但是又相互獨立發展,這一切都是為了適應工業化生產的結果。從技術角度來說是為瞭解耦,通過標準化介面來相互協作。Servlet 容器作為一個獨立發展的標準化產品,目前它的種類很多,包括Jetty、tomcat、resin、JBoss、WebSphere、Weblogic等,這些都是成熟的產品,有專門的公司或者組織進行維護,我們直接拿來用就好。
我們約定下,下文中的servet指的都是servlet介面:javax.servlet.Servlet,servlet容器指的是:Tomcat,Web伺服器與Servlet容器是同一個內容(實際是有區別的,具體區別大家自行去查閱)
Tomcat容器模型如下
Tomcat響應客戶請求過程
其中,①處表示Web伺服器接收到客戶端發出的HTTP請求後,轉發給Servlet容器,再由Servlet容器轉發給具體的Servlet實例進行請求的處理;②處表示Servlet實例將處理結果封裝進ServletResponse中,再由Servlet容器把ServletResponse發給Web伺服器,通知Web伺服器以HTTP響應的方式把結果發送到客戶端。也就是說,與客戶端直接打交道的是tomcat(servlet容器),而不是我們的Servlet實例,而真正處理請求的才是我們的Servlet實例。
說的簡單點,我們自定義的Servlet,其實是對servlet容器在業務層面的拓展,相當於業務定製一樣;我們可以這樣理解,servlet容器對servlet提供技術支持,而servlet對servlet容器提供業務拓展,兩者缺一不可,缺了技術支持,業務拓展實施不起來,缺了業務拓展,技術支持沒有現實意義。Servlet容器封裝了底層複雜的技術實現,使我們可以專註於業務實現,而Servlet容器與業務實現之間的紐帶就是Servlet介面,它是我們對Servlet容器進行業務拓展的標準,所以我們的業務需要實現Servlet介面。套用阿基米德的杠桿原理:給Servlet容器多個servlet實例,Servlet容器還你豐富的web服務。
JSP
示例代碼:our-servlet
我們先來看看在jsp出現之前,servlet如何輸出頁面,HelloServlet如下
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.lee.servlet; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { this.doPost(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String businessDate = "業務數據...."; resp.setCharacterEncoding("utf-8"); resp.setContentType("text/html"); PrintWriter out = resp.getWriter(); out.write("<html>"); out.write("<head>"); out.write("<title>Hello World</title>"); out.write("</head>"); out.write("<body>"); out.write("<h1>Hello World!</h1>"); out.write("<div><span><strong>"); out.write(businessDate); out.write("</strong></span></div>"); out.write("</body>"); out.write("</html>"); out.flush(); out.close(); } }View Code
不僅僅是業務數據,還包括靜態頁面的內容,通通在servlet返回,如果頁面簡單,這麼處理也能接受,但是如果頁面像淘寶、京東那樣非常複雜,你能想象嗎?太容易出錯了,一旦靜態頁面的元素少了或者多了內容,都不知道如何排查,面對茫茫多的out.write,就只有哭的份了。所以jsp就應運而生了。
JSP全稱:Java Server Pages,允許在傳統靜態網頁HTML中插入Java代碼片段(Scriptlet)和JSP標簽,以簡化頁面靜態內容的開發。但需要註意的是,JSP文件的本質還是Servlet,只不過與Servlet不同的是,JSP是專門用於進行數據展示的Servlet;JSP最終會被Tomcat解析成Servlet,在Tomcat內置了一個JSP解析引擎,當第一次訪問該JSP頁面時,解析引擎會將JSP頁面解析成Servlet,然後再由Servlet將動態數據、靜態內容全部輸出到瀏覽器供展示。我們來看看jsp解析後的文件在哪裡、內容是什麼,以示例中的index.jsp為例。路徑如下圖
index_jsp.java
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
/* * Generated by the Jasper component of Apache Tomcat * Version: Apache Tomcat/7.0.47 * Generated at: 2019-04-08 13:14:31 UTC * Note: The last modified time of this file was set to * the last modified time of the source file after * generation to assist with modification tracking. */ package org.apache.jsp; import javax.servlet.*; import javax.servlet.http.*; import javax.servlet.jsp.*; public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase implements org.apache.jasper.runtime.JspSourceDependent { 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 javax.el.ExpressionFactory _el_expressionfactory; private org.apache.tomcat.InstanceManager _jsp_instancemanager; public java.util.Map<java.lang.String,java.lang.Long> getDependants() { return _jspx_dependants; } public void _jspInit() { _el_expressionfactory = _jspxFactory.getJspApplicationContext(getServletConfig().getServletContext()).getExpressionFactory(); _jsp_instancemanager = org.apache.jasper.runtime.InstanceManagerFactory.getInstanceManager(getServletConfig()); } 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 { 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; try { 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; out.write("\n"); out.write("<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">\n"); out.write("<html>\n"); out.write("<head>\n"); out.write("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n"); out.write("<title>index</title>\n"); out.write("</head>\n"); out.write("<body>\n"); out.write(" <div>\n"); out.write(" Welcome, my friend!\n"); out.write(" </div>\n"); out.write("</body>\n"); out.write("</html>"); } catch (java.lang.Throwable t) { if (!(t instanceof javax.servlet.jsp.SkipPageException)){ out = _jspx_out; if (out != null && out.getBufferSize() != 0) try { out.clearBuffer(); } catch (java.io.IOException e) {} if (_jspx_page_context != null) _jspx_page_context.handlePageException(t); else throw new ServletException(t); } } finally { _jspxFactory.releasePageContext(_jspx_page_context); } } }View Code
發現熟悉的out.write又回來了,只是此時的out.write不是我們手動寫的,而是Tomcat解析jsp後生成的;如果jsp沒變動,jsp只會在第一次被調用時解析、編譯一次,後續的請求都會由編譯後的servlet處理,我們來驗證下,如何驗證了? 不變index.jsp內容再請求index.jsp,看看上圖中文件的修改時間會不會變
發現文件的修改時間沒有變動,也就是說上面的的結論:如果jsp沒變動,jsp只會在第一次被調用時解析、編譯一次是對的。感興趣的朋友可以去看下Tomcat的源碼,看看具體的實現細節。
有人可能會問:為什麼不將jsp的內容直接返回給瀏覽器?我們要明白一點:瀏覽器只能解析html、css、js,除此之外的內容它解析不了,那麼我們能直接將jsp的內容返回給瀏覽器嗎?所以中間有處理過程,最終由servlet將靜態內容返回給瀏覽器。有些愛問的小伙伴可能又會問了:瀏覽器為什麼只能解析:html、css、js,這涉及到瀏覽器規範的問題,除非你有能力改變這個規範,讓瀏覽器支持你想要的內容,這個問題不做過深的討論,我們姑且認為這是瀏覽器的限制,既然我們改變不了這個限制,那就適應這個限制。
Servlet四大作用域與JSP九大內置對象
Servlet四大作用域包括:page域、request域、session域、application域,作用域指的是變數的有效期限,具體如下
當變數的作用域是page,它的有效範圍只在當前jsp頁面里有效;
當變數的作用域是request,它的有效範圍是當前請求周期,所謂請求周期,就是指從http請求發起,到伺服器處理結束,返迴響應的整個過程,在這個過程中可能使用forward的方式跳轉了多個jsp頁面,在這些頁面里你都可以使用這個變數;
當變數的作用域是session,它的有效範圍是當前會話,何為當前會話,就是指從用戶打開瀏覽器開始,到用戶關閉瀏覽器的整個過程,這個過程可能包含多個請求響應;
當變數的作用域是application,它的有效範圍是整個應用,何為整個應用,就是指從應用啟動,到應用結束;
JSP九大內置對象包括:page、request 、response、pageContext、session、application、out、config、exception,內置對象指的是Servlet容器創建的一組對象,不需使用new關鍵字就可以直接使用的內置對象。
四大作用域與九大內置對象對應關係如下
更多詳情需要大家自己去查閱資料了
EL表達式與JSTL標簽
我們知道jsp中可以插入Java代碼片段,類似如下
<%pageContext.setAttribute("sex", "男"); %> <!-- 設置值,作用域是當前jsp頁面 -->
<div>
<%=pageContext.getAttribute("sex") %> <!-- 註意去看解析後的el_jsp.java,被解析成了out.print(pageContext.getAttribute("sex") ); -->
Welcome, my friend!
</div>
其中<% %>包裹的就是java片段,<%= %>輸出表達式值到頁面;可以看到不夠簡潔,閱讀性也不太友好,所以EL表達式就應運而生了,上述代碼可以替換成如下代碼
<%pageContext.setAttribute("sex", "男"); %> <!-- 設置值,作用域是當前jsp頁面 -->
<div>
${sex} Welcome, my friend! <!-- ${expression} EL的語法結構 -->
</div>
EL能夠訪問頁面的上下文以及不同作用域中的對象 ,取得對象屬性的值,或執行簡單的運算或判斷操作,用來簡化JSP中的java代碼。EL表達式是JSP1.2之後內置支持的,可以直接在JSP中使用,它從servlet四大作用域(範圍servletContext > session > request > pageContext)中取值,這四個域都有setAttribute("",object)方法和getAttribute("")方法, EL表達式會自動按作用範圍從小到大的順序從四大作用域中尋找對應名字的值,找到了就立即返回不再繼續尋找,其內部調用的就是pageContext的findAttribute("")方法。
EL固然能簡化JSP中的java代碼,但是它功能非常簡單,不能滿足一些複雜的代碼邏輯,所以就誕生了JSTL。JSP標準標簽庫(JSTL)是一個JSP標簽集合,它封裝了JSP應用的通用核心功能,支持通用的、結構化的任務,比如迭代,條件判斷,XML文檔操作,國際化標簽,SQL標簽,另外還支持自定義標簽,它實現了JSP頁面中的代碼復用、簡化了代碼的書寫,同時也保證了JSP的可讀性更強。JSTL功能比較豐富,但它不是JSP內置支持的,所以需要導入標簽庫到JSP頁面(還要添加jstl的jar包依賴)。JSTL往往會集合EL表達式來使用,簡單示例如下
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <!-- 引入JSTL標簽庫,c表示標簽庫別名,可以任意命名,一般而言用c --> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>index</title> </head> <body> <c:set var="sex" value="女" scope="page"></c:set> <!-- scope指定作用域,page/request/session/application --> <c:if test="${sex == '男' }"> Hello, ${sex} </c:if> <c:if test="${sex != '男' }"> Hi, girl </c:if> </body> </html>
這代碼看起來就清爽多了,沒有java代碼,前端開發者也很容易看懂;關於EL表達式與JSTL標簽更詳細信息,需要大家自行去查閱資料了,本文篇幅有限,不做過多的講解了。
重定向與請求轉發
那麼可想而知,重定向的request作用域的變數是會失效的,而轉發則不會
Spring MVC
還記得我們是如何配置Spring MVC的嗎, 我們會在web.xml中配置如下代碼
<servlet> <servlet-name>springDispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springDispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>
這樣就配置了Spring MVC;大家可以留意下DispatcherServlet,去看他的類圖會發現,他就是一個Servlet的實現,也就是說Sprinv MVC就是基於Servlet的拓展。
我們在Spring MVC基礎上進行開發的時候,將數據綁定到作用域的時候,一般用的是SpringMVC的數據模型:Model或者ModelMap,例如這樣
@RequestMapping("/showPerson") public String showPersons(Model model){ List<Person> persons = personService.loadPersons(); model.addAttribute("persons", persons); // 綁定數據到視圖 return "showperson"; }
而不是顯示的直接綁定到Servlet四大作用域,數據難道沒有綁定到四大作用域? 我們說過,EL表達式只能在四大作用域中取值,否則取不到,所以SpringMVC中的數據綁定最終還是會到四大作用域的某一個中,至於是何時、何地、如何將Model中的屬性綁定到哪個作用域,這個不是本文要說的了,篇幅太大了,有機會再重開一篇博客給大家講下,給大家點提示:spring3.2之前去查看AnnotationMethodHandlerAdapter,spring3.2及之後去查看:RequestMappingHandlerAdapter。這裡給個結論:在預設情況下,Model中的屬性作用域是request級別。
問題解答
有些小伙伴會抱怨了:上面嗶嗶了那麼多,怎麼就是不講答案,凈說一些沒用的
如果大家堅持看到這了,再堅持會,答案馬上揭曉,上面鋪墊了那麼多,絕對是有用的。
我們回到問題:當我們請求http://localhost:埠/工程名/personController/showPerson時,數據正常顯示,而當我們直接請求jsp時,只有title卻沒有數據,這是為什麼?title是靜態頁面內容,這個不用管,那為什麼直接請求jsp為什麼沒有資料庫的person列表呢? jsp源代碼如下
<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>person list</title>
</head>
<body>
<table>
<tr>
<th>姓名</th>
<th>年齡</th>
</tr>
<c:forEach items="${persons}" var="person">
<tr>
<td>${person.name }</td>
<td>${person.age }</td>
</tr>
</c:forEach>
</table>
</body>
</html>
裡面用到了<c:forEach>和EL表達式,解釋下這個流程:EL表達式先從四大作用域獲取名為persons的集合,然後<c:forEach>遍歷該集合,每次遍歷的結果放到page作用域,並取名叫person,最後通過EL表達式輸出person的name和age到頁面。那麼請問:直接訪問JSP,四大作用域中有名叫persons的屬性嗎?很顯然沒有,persons不存在,遍歷它會有結果輸出嗎?這就是為什麼直接訪問jsp沒有數據的答案。
我們再回到Controller層
@RequestMapping("/showPerson") public String showPersons(Model model){ List<Person> persons = personService.loadPersons(); // 從數據獲取person列表,並存放到了persons集合中 model.addAttribute("persons", persons); // 將persons集合添加到model的persons屬性中 return "showperson"; // 轉發到showperson.jsp }
代碼也非常簡單,先從資料庫獲取person集合,然後將該集合設置到了model的屬性persons中,我們知道model的屬性預設情況下會設置到request作用域;然後將請求轉發到showperson.jsp,轉發過程中,request作用域的變數仍然有效,所以jsp中EL表達式能夠讀取到persons變數,所以就有數據輸出到頁面了。
總結
1、Servlet與Servlet容器的關係比較曖昧,兩者相互作用,實現web服務;簡單點說,我們自定義的Servlet就是對Servlet容器的業務拓展,而Servlet容器是對Servlet的支撐;
2、JSP的出現時為了簡化靜態頁面的開發,EL表達式與JSTL的出現則是為了簡化JSP頁面的Java代碼;JSP本質還是Servlet,在第一次被訪問的時候會被Servlet容器解析成Servlet、編譯Servlet,最終還是有Servlet將頁面內容out.write到瀏覽器;
3、Spring MVC本質還是Servlet,它的出現是為了簡化web開發,同時可以與spring無縫對接,享受spring帶來的好處;Spring MVC的數據綁定,依托的還是Servlet的的四大作用域,只是中間存在轉換過程;
4、EL表達式的取值必須存在於四大作用域中,在jsp中用EL表達式時,一定要保證數據正確地添加到了四大作用域中,不然,EL表達式會取不到值;
參考
《深入分析JavaWeb技術內幕》
《Tomcat 系統架構與模式設計分析》