迷你MVC教程——命令行方式創建第一個Servelt小程序
本文節選自《Head First Servlets and JSP》第3章 MVC實戰,根據個人配置和實操情況有所刪改。本人注解部分用斜體表示。圖片截取自原書,部分為個人操作截圖。本人電腦配置win10x64, tomcat7, jdk10。本教程的特色處在于全程采用手工編寫和命令行結合,不借助IDE開發環境,適合初次接觸Servlet者當作練習的小項目。任何問題請在下方留言。
構建小型Web應用步驟
- 分析用戶的視圖以及高層體系結構
- 創建用于開發這個項目的開發環境
- 創建用于部署這個項目的部署環境
- 對Web應用的各個組件完成迭代式的開發和測試
(基于Beer Advisor的案例。根據用戶選擇的酒色提供酒的品牌建議的servlet小程序)
用戶視圖
- 用戶最初請求的HTML表單,生成HTTP POST請求
- 處理請求返回的result.jsp
體系結構
- 客戶請求得到form.html頁面
- 容器獲得form.html頁面
- 容器把這個頁面返回給瀏覽器,用戶再在瀏覽器上回答表單上的問題
- 瀏覽器把請求數據發送給容器
- 容器根據URL查找正確的servlet,并把請求傳遞給這個servlet
- servlet調用BeerExpert尋求幫助
- BeerExpert類返回一個回答,servlet把這個回答增加到請求對象
- servlet把請求轉發給JSP
- JSP從請求對象得到回答
- JSP為容器生成一個頁面
- 容器將jsp頁面返回給用戶
開發環境
IDE項目的目錄結構
部署環境
將Web項目部署到容器中
迭代式的開發和測試
- 構建和測試用戶最初請求的HTML表單
- 構建控制器servlet的第一個版本,并用HTML表單測試這個控制器。這個版本通過HTML表單來調用,并打印出它接收到的參數
- 為模型類構建一個測試類,然后構建并測試模型類本身
- 把servlet升級到第2版。這個版本增加了一個功能,可以調用模型類來得到啤酒建議。
- 構建JSP, 把servlet升級到第3版本(增加一個功能,可以把表示流分派到JSP完成),然后再測試整個應用。
第一個表單頁面的HTML
form.html包含標題文本,一個下拉列表,還有一個提交按鈕(原書代碼)
<html>
<body>
<h1 align="center">
Beer Selection Page
</h1>
<!--為什么選擇POST而不是GET? HTML認為這就是要調用servlet。在你的目錄結構里沒有一個叫“SelectBeer.do”的東西。這只是一個邏輯名-->
<form method="POST" action="SelectBeer.do">
<p>
Select beer characteristics
</p>
Color:
<select name="color" size="1">
<!--我們就是這樣拆個那就下拉菜單的,你可以有自己不同的選項-->
<option value="light">light</option>
<option value="amber">amber</option>
<option value="brown">brown</option>
<option value="dark">dark</option>
</select>
<br><br>
<center>
<input type="SUBMIT">
</center>
</form>
</body>
</html>
部署和測試開始頁面
- 在開發環境中創建HTML
創建這個HTML文件,取名為form.html,然后保存在開發環境的/beerV1/web/目錄下
- 把這個文件復制到部署環境
把form.html文件的一個副本放在tomcat/webapps/Beer-vl/中
- 在開發環境中創建DD
創建XML文檔,取名為web.xml, 把它保存在開發環境的/beer/etc/目錄下(原書代碼)
<!--沒有必要知道這是什么意思,只需要照著輸入就行-->
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
<servlet>
<!--這是一個虛構的名字,只能在DD的其他部分使用-->
<servlet-name>Ch3 Beer</servlet-name>
<!--servlet類文件的完全限定名-->
<servlet-class>com.example.web.BeerSelect</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Ch3 Beer</servlet-name>
<!--不要忘記最前面有一個斜線。我們希望用戶這樣引用“servlet.do”只是一個約定-->
<url-pattern>/SelectBeer.do</url-pattern>
</servlet-mapping>
</web-app>
由于原書代碼是tomcat5, 版本有所變化,可以到tomcat/webapps/ROOT/WEB-INF自帶目錄下拷貝了一份web-xml文件,并添加相應的映射語句。
- 把這個文件復制到部署環境
把web.xml文件的一個副本放在tomcat/webapps/Beer-v1/WEB-INF/
- 啟動Tomcat
Tomcat既作為Web服務器,又作為Web容器。要啟動Tomcat, 先用cd命令切換到tomcat主目錄,再運行bin/startup.sh
命令行中用cd切換到tomcat/bin目錄下(tomcat根據版本名稱不同),win系統運行startup.bat
- 測試頁面
在瀏覽器中打開這個HTML頁面,為此鍵入:
邏輯名映射到servlet類文件
以下部分為xml文件中配置的詳解
- 用戶填寫表單,然后點擊submit。瀏覽器生成了以下請求URL:
在用戶發送的http post請求中,“/Beer-v1”不是路徑的一部分。在form.html中,它只說:<form method="POST" action="SelectBeer.do>"
但瀏覽器為請求追加了“/Beer-v1/”,因為用戶請求就來自這里。換句話說,form.html中的“SelectBeer.do”相對于其所在頁面的URL。在這里,就是相對于Web應用的根:“/Beer-v1”
- 容器搜索DD, 找到
<url-pattern>
與/SelectBeer.do匹配的一個<servlet-mapping>
, 這里的斜線{/}表示Web應用的上下文根,SelectBeer.do就是資源的邏輯名 - 容器看到對應這個
<url-pattern>
的<servlet-name>
是“Ch3 Beer”。但是這并不是實際servlet類文件的名字。“Ch3 Beer”是servlet名,而不是servlet類的名字。
對容器來說,servlet只是在DD中<servlet>
標記下的一個東西。servlet名只在DD中使用,以便DD的其他部分建立與該servlet的映射
- 容器查找
<servlet-name>
為“Ch3 Beer”的<servlet>
標記 - 根據
<servlet>
標記中的<servlet-class>
,容器可以知道有哪個servlet類負責處理這個請求。如果這個servlet還沒有初始化,就會加載類,并初始化servlet - 容器開始一個新線程來處理這個請求,并把請求傳遞給這個線程(傳遞給servelt的service()方法)
- 容器把響應(通過Web服務器)發回給用戶
控制器servlet的第1版
確保HTML頁面能適當地調用servlet, 而且servlet能正確地接收HTML參數。
(原書代碼)
//確保與前面創建的開發結構和部署結構匹配
package com.example.web;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
//HttpServlet擴展了GenericServlet,GenericServlet則實現了Servlet接口
public class BeerSelect extends HttpServlet{
//我們使用doPOST來處理HTTP請求,因為HTML表單指出,method=POST
public void doPost(HttpServletRequest request,HttpServletResponse response) throws IOException, ServletException{
//這個方法來自ServletResponse接口
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println("Beer Selection Advice<br>");
//這個方法來自ServletRequest接口。注意這個參數與HTML<select>標記中"name"屬性的值匹配
String c = request.getParameter("color");
//這里我們沒有返回建議,只是把測試信息顯示出來
out.println("<br>Got beer color "+c);
}
}
編譯、部署和測試控制器servlet
編譯servlet
用-d標志編譯servlet,把類放在開發環境中
原書
win系統
注解:-classpath用于設置臨時環境變量,指定查找用戶類文件和注釋處理程序的位置;本例中由于編譯servlet需要用到額外類庫,tomcat提供了這些jar包,故選定tomcat/lib/servlet-api.jar為環境變量。書中還添加了classes和"."(代表當前目錄)作為路徑,其實可以略去。
-d用于指定編譯生成的class文件存放目錄,本例中存放于classes路徑中,由于servlet的package語句,會自動生成com.example.web目錄(若不存在)。
最后,添上servlet類的存放路徑,若存放在當前目錄則直接輸入文件名。
部署servlet
要部署servlet, 建立.class文件的一個副本,并把它移到部署結構的/Beer-v1/WEB-INF/classes/com/example/web/目錄下
測試
- 重啟tomcat
直接在tomcat/bin目錄下輸入startup.bat命令即可重啟,無須關閉
- 啟動瀏覽器,訪問:
- 選擇一種啤酒顏色,點擊“Submit”
- 如果你的servlet能正常運行,就能在瀏覽器上看到servlet的響應顯示為:
Beer Selection Advice
Got beer color brown
構建和測試模型類
模型規范
- 包應當是com.example.model
- 其目錄結構應當是/WEB-INF/classes/com/model
- 提供一個方法getBrands(), 取一個喜歡的啤酒顏色(String)作為參數,并返回一個ArrayList, 其中包含推薦的啤酒品牌(String)
為模型構建測試類
為模型創建測試類(在構建模型本身之前先創建測試類)。剛開始測試模型時,模型還在開發環境中,與其他Java類一樣,此時無需啟動Tomcat也能測試
作者代碼(我是在創建模型類后再創建測試類)
package com.example.model;
import java.util.*;
class BeerExpertTest{
public static void main(String[] args){
BeerExpert be=new BeerExpert();
List<String> testBrands1=be.getBrands("amber");
Iterator it1=testBrands1.iterator();
while(it1.hasNext()){
System.out.println("try1: "+it1.next());
}
System.out.println("----------------------");
List<String> testBrands2=be.getBrands("");
Iterator it2=testBrands2.iterator();
while(it2.hasNext()){
System.out.println("try2: "+it2.next());
}
}
}
命令行中運行測試類(請在構建模型類后進行此步)
運行此步后,開發環境項目中的/classes/com/example/model中會生成兩個.class文件(原來BeerExpert編譯的.class文件被更新)。-classpath中的./src為測試類所依賴的模型類(com.example.model.BeerExpert)的查找路徑。原教材中無此步,可略過。
可以切換至測試類所在包的基目錄,然后運行java命令
也可以直接在beerV1/下設置環境變量為classes目錄
構建和測試模型
(原書代碼,增加了List泛型為String類)
package com.example.model;
import java.util.*;
public class BeerExpert{
public List<String> getBrands(String color){
List<String> brands = new ArrayList()<String>;
if (color.equals("amber")){
brands.add("Jack Amber");
brands.add("Red Moose");
}else{
brands.add("Jail Pale Ale");
brands.add("Gout Stout");
}
return(brands);
}
}
win系統與上圖操作命令一致,因為模型類中沒有導入其他外部類,所以無須設置classpath變量
改進servlet,調用模型得到真正的建議
第2版的servlet中,通過改進doPost()方法,調用模型來得到建議(第3版還會向JSP提供建議)
改進servlet, 第2版
先把servlet放在一邊,只考慮Java
- 改進doPost()方法來調用模型
- 編譯servlet
- 部署和測試更新后的Web應用
(作者代碼,增加了List泛型部分,作者改用增強for循環遍歷集合,也可采用原書中的iterator迭代器遍歷)
import com.example.model;
public class BeerSelect2 extends HttpServlet{
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException{
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println("Beer selection Advice<br>");
String c = request.getParameter("color");
BeerExpert be = new BeerExpert();
List<String> advisedBrands= be.getBrands(c);
for(String ad:advisedBrands){
out.print("<br>try:"+ad);
}
}
}
注:原書中每次修改都替換原始的servlet, 作者采用創建新的servlet方式,以便區分不同版本的servlet——此方式需要在xml配置文件中相應修改servlet類完全限定名
第2版Servlet代碼
package com.example.web;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import com.example.model.*;
import java.util.*;
public class BeerSelect2 extends HttpServlet{
public void doPost(HttpServletRequest request,HttpServletResponse response) throws IOException, ServletException{
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println("Beer selection Advice<br>");
String c = request.getParameter("color");
BeerExpert be = new BeerExpert();
List<String> advisedBrands= be.getBrands(c);
for(String ad:advisedBrands){
out.print("<br>try:"+ad);
}
}
}
第2版servlet的關鍵步驟
主要有兩件事要做:重新編譯servlet和部署模型類
編譯servlet
win系統下
注:第2版servlet與前一版不同之處在于調用了模型,故在classpath環境變量中添加模型類的路徑
部署和測試Web應用
- 把servlet.class文件的一個副本移到以下位置:
../Beer-v1/WEB-INF/classes/com/example/web/
這會替換第1版的servlet類文件
- 把模型的.class文件副本移到:
../Beer-v1/WEB-INF/classes/com/example/model/
- 關閉并重啟tomcat
- 通過form.html測試這個應用
創建提供建議的JSP”視圖”
<!--這是一個“頁面指令”-->
<%@ page import="java.util.*" %>
<html>
<body>
<!--一些標準HTML模板-->
<h1 align="center">
Beer Recommendations JSP
</h1>
<br>
<!--以下稱為scriptlet代碼,用于輸入java代碼-->
<%
//這里從請求對象得到一個屬性
List styles=(List)request.getAttribute("styles");
Iterator it = styles.iterator();
while(it.hasNext()){
out.print("<br>try:"+it.next());
}
%>
</body>
</html>
部署JSP
不用編譯JSP(這個工作會在第一個請求到達容器時由容器完成)
- 把它命名為“result.jsp"
- 保存再開發環境的/web/中
- 將其副本移到部署環境的/Beer-v1/中
改進這個servlet,讓它”調用“JSP(第3版)
這一步,我們要把servlet修改為”調用“JSP來生成輸出(視圖)。容器提供了一種稱為”請求分派“的機制,允許容器管理一個組件調用另一個組件。我們通過使用這種機制,servlet從模型中得到信息,把它保存在請求對象中,然后把請求分派給JSP。
必須對這個servlet做的重要修改:
- 把模型組件的回答增加到請求對象,以便JSP訪問
- 要求容器把請求轉發給”result.jsp"
第3版servlet的代碼
如下修改servlet, 將模型組件的回答增加到請求對象(以便JSP獲取),并要求容器把請求分派給JSP
(作者代碼,修改了類名和泛型,需要在xml文件修改配置)
package com.example.web;
import com.example.model.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
public class BeerSelect3 extends HttpServlet{
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException{
String c = request.getParameter("color");
BeerExpert be = new BeerExpert();
List<String> result = be.getBrands(c);
//為請求對象增加一個屬性,供JSP使用。注意,JSP要尋找"styles"
request.setAttribute("styles", result);
//為JSP實例化一個請求分派器
RequestDispatcher view = request.getRequestDispatcher("result.jsp");
//使用請求分派器要求容器準備好JSP,并向JSP發送請求和響應
view.forward(request,response);
}
}
編譯、部署和測試最后的應用
編譯servlet
部署和測試Web應用
- 把servlet的.class文件副本移到../Beer-v1/WEB-INF/classes/com/example/web/
- 關閉并重啟tomcat
- 通過form.html測試應用