迷你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
启动Tomcat命令行中用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,把类放在开发环境中
原书
编译servletwin系统
win系统编译servelt注解:-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
编译servletwin系统下
win系统编译servlet注:第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测试应用
网友评论