注:因为我并不完全是为了从0开始Java开发,因此,我这里先暂时跳过第二章服务器环境相关的内容,直接开始第三章的内容。

3.1、Servlet 的基本结构:

​ 下面的代码给出了一个基本的 Servlet ,它处理 GET 请求。GET 请求是浏览器请求的常见类型,用来请求 Web 页面。用户在地址栏中输入 URL 、点击 Web 页面内的连接、或提交没有指定 METHOD 或 METHOD=”GET” 的 HTML 表单时,浏览器都会生成这个请求。 Servlet 还可以容易地处理 POST 请求(提交 METHOD=”POST” 的 HTML 表单时,会生成 POST 请求)。HTML 表单的使用细节以及 GET 和 POST 之间的区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.firstweb.study01;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;

public class ServletTemplate extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

//Use "request" to read incoming HTTP headers
//(e.g.,Cookies) and query data from HTML forms .

//Use "response" to specify the HTTP response status
//code and headers (e.g.,the content type ,cookies)

PrintWriter out = resp.getWriter();
//Use "out" to send content to browser
}
}

​ Servlet 一般扩展 HttpServlet ,并依数据发送方式的不同(GET或POST),覆盖 doGet 或 doPost 方法。如果希望 Servlet 对 GET 和 POST 请求采用同样的行动,只需要让 doGet 调用 doPost ,反之,依然。

​ doGet 和 doPost 都接受两个参数:HttpServletRequest 和 HttpServletResponse 。通过 HttpServletRequest,可以得到所有的输入数据;这个类提供相应的方法,通过这些方法可以找出诸如表单(查询)数据、HTTP 请求报头和客户的主机名等信息。通过 HttpServletResponse 可以制定输出信息,比如 HTTP 状态代码(200,404 等)和响应报头(Content-Type,Set-Cookie等)。最重要的是,通过HttpServletResponse 可以获得PrintWriter ,用它可以将文档内容发送给客户。对于简单的 Servlet ,大部分工作都花在用 println 语句生成期望的页面上。

​ 由于 doGet 和 doPost 抛出两种异常(ServletException 和 IOException),所以在方法声明中包括他们。最后,还必须导入 java.io(PrintWrite 等)、javax.servlet(HttpServlet 等)和 javax.servlet.http(HttpServletResponse 和 HttpServletRequest)中的类。

3.2、生成纯文本的 Servlet :

​ 代码 3.2 列出了一个输出纯文本的简单 Servlet,图 3.2 是它的输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//代码 3.2 
package com.firstweb.study01;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/hello")
public class HelloWorld extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
PrintWriter out = resp.getWriter();
out.println("Hello World");
}
}

​ 图 3.2:

在这里插入图片描述

3.3、生成 HTML 的 Servlet:

​ 大多数的 Servlet 生成 HTML ,而非前述例子中的纯文本。要生成HTML ,需要在刚才介绍的过程中加入如下三步:

  1. 告知浏览器,即将向它发送 HTML 。

  2. 修改 println 语句,构建合法的 Web 页面。

  3. 用形式语法检验其(format syntax validator)检查生成的 HTML。

    第一步通过将 HTTP Content-Type 响应报头设为 text/html 来完成。一般而言,报头使用 HttpServletResponse 的 setHeader方法来设置,但由于设置内容的类型是一项十分常见的任务,因而,HttpServletResponse 提供特殊的 setContentType 方法,专门用于这种目的。指明 HTML 的方式是使用 text/html 类型,因此,代码应该如下:

1
resp.setContentType("text/html");

​ 尽管 HTML 是 Servlet 创建的最常见的文档类型,但是,Servlet 创建其他类型文档的情况也很常见。例如,使用 Servlet 生成的 Excel 表格(内容类型是 application/vnd.ms-excel)、JPEG 图像(内容类型是 image/jpeg )和 XML 文档(内容类型是 text/xml)的情况也是十分常见。同时,一般很少使用 Servlet 生成格式相对固定的 HTML 页面(即每次请求,页面的布局改动很小);这种情况下 JSP 常常更为方便。

​ 需要注意的是:需要在 PrintWriter 实际返回任何内容之前,设置响应报头。这是由于 HTTP 响应由状态行、一个或多个报头、一个空行和实际的文档一次次序构成。包头的出现次序并不重要,Servlet 会缓冲报头数据,将他们一次法案送到客户端,因此,即使在设定报头之后,依旧可以设置状态代码(属于返回内容的第一行)。但是,Servlet 不是一定要缓冲文档本身,因为对于篇幅较长的页面,用户或许只希望看到部分结果。Servlet 引擎可以缓冲部分输出,但并未规定缓冲区的大小。可以使用 HttpServletResponse 和 getBufferSize 方法确定这个大小,或使用 setBufferSize 指定这个大小。也可以在缓冲区填满,要发往客户时,对报头进行设置。如果不确定缓冲区是否已经发送出去,可以使用 isCommitted 方法来检查。及时如此,最佳的方案还是将 setContentType 行放在任何 PrintWrite 的行之前。

警告:必须在传送实际的文档之前设定内容的类型。

​ 在编写构建 HTML 文档的 Servlet 时,第二步时用 println 语句输出 HTML(不再是纯文本)。代码 303 列出了 HelloServlet.java ,结果如图 3.3 所示,浏览器按照 HTML 来格式化得到的结果,而非按照纯文本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//代码 3.3
package com.firstweb.study01;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
String docType =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0" +
"Transitional//EN\">\n";
out.println(docType +
"<HTML>\n" +
"<HEAD><TITLE>Hello</TITLE><HEAD>\n" +
"<BODY BGCOLOR=\"#FDF5E6\">\n" +
"<H1>Hello</H1>\n" +
"</BODY></HTML>");
}
}

​ 图 3.3 :

在这里插入图片描述

​ 最后一步时合适生成的 HTML 中,不存在有可能会在不同的浏览器上引起不可预期结果的语法错误。参考 3.5 节中有关 HTML 验证其的论述。

3.4、Servlet 的打包:

​ 在产品开发过程中,多个程序员可能为同一服务器开发 Servlet。因此,将所有的 Servlet 放到同一目录中将会生成数目庞大且难以管理的类,如果两个开发人员为 Servlet 或使用工具类名时,不经意间选择了相同的名称,还会导致命名冲突。现在,通过 Web 应用,可以将内容划分到多个单独的目录中,每个目录都有自己的一套 Servlet、实用工具类、JSP 页面和 HTML 文件,避免了这个问题。然而,由于单个 Web 应用也可能比较庞大,因此,我们依旧需要采用 Java 中用以避免命名冲突的标准解决方案:包。

​ 在将 Servlet 放到包中,需要执行下面两个额外的步骤。

  1. 将文件放到与预定的包名匹配的子目录中。
  2. 在类文件中插入包语句。

3.5、简单的 HTML 构建工具:

​ 由于 HTML 文档的结构如下:

1
2
3
4
5
<!DOCTYPE ...>
<HTML>
<HEAD><TITLE>...</TITLE>...</HEAD>
<BODY ...>...</BODY>
</HTML>

​ 在使用 Servlet 构建 HTML 时,可能会略去这个结构的某个部分,尤其是 DOCTYPE 行,因为虽然 HTML 规范需要它,但几乎所有主流的浏览器都忽略它。我们极不赞成这种做法。DOCTYPE 行的长处是,它告诉 HTML 验证器您使用的是 HTML 的哪个版本,从而验证器知道应该按照哪种规范对文档进行检查。这些验证器对调试很有价值,能够帮助您捕获那些您的路i兰奇可以推测出来,但其他浏览器在显示时可能会有困难的 HTML 语法错误。

​ 毫无疑问,使用 println 语句生成 HTML 有些笨重,尤其是那些冗长乏味的行,如 DOCTYPE 声明。有些人编写很长的 HTML 生成使用工具程序,然后,在编写 Servlet 时使用这些实用工具程序,,以此来解决这个问题。我们对这类扩展库的有效性持怀疑态度。首先且最重要的是,变成生成 HTML 的不便是 JSP 解决的主要问题之一。其次,用来生成 HTML 的例程可能十分笨重,一般并不支持所有的 HTML 属性。

​ 标准 Servlet 中,Web 页面中的两部分内容(DOCTYPE 和 HEAD)一般不会发生变化,因而可以归结到一个简单的使用工具文件中。代码 3.5 就是这样一个文件,代码 3.6 列出 HelloServlet 类的又一个变体,它使用这个实用工具类。

1
2
3
4
5
6
7
8
9
10
11
12
13
//代码 3.5
package com.firstweb.study01;

public class ServletUtilities {
public static final String DOCTYPE =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 " + "\n" +
"Transitional//EN\">";
public static String headWithTitle(String title){
return (DOCTYPE + "\n" +
"<HTML>\n" +
"<HEAD><TITLE>" + title + "</TITLE><HEAD>\n");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//代码 3.6
package com.firstweb.study01;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/hello")
public class HelloServlet3 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
String title = "hello3";
out.println(ServletUtilities.headWithTitle(title) +
"<BODY BGCOLOR=\"#FDF5E6\">\n" +
"<H1>" + title + "</H1>\n" +
"</BODY></HTML>");
}
}

​ 编译完 HelloServlet3.java 之后(会导致自动编译 ServletUtilities.java),需要将这两个类文件移动到服务器默认部署位置(…/WEB-INF/classes)中,的相应的包中。最后结果如下图所示:

在这里插入图片描述

3.6、Servlet 的生命周期:

​ 服务器只创建每个 Servlet 的单一实例,每个用户请求都会引发新的线程——将用户请求交付给相应的 doGet 或 doPost 进行处理。

​ 首次创建 Servlet 时,它的 init 方法会得到调用,因此,init 是放置一次性设置代码的地方。在这之后,针对每个用户请求,都会创造一个线程,该线程调用前面创建的实例的 Servlet 方法。多个并发请求一般会导致多个线程同时调用 service。之后由 service 方法依据接收到的 HTTP 请求的类型,调用 doGet,doPost,或其他 doXxx方法。最后,如果服务器决定卸载某个 Servlet ,它会首先调用 Servlet 的 destroy 方法。

3.6.1、service 方法:

​ 服务器每次接收到对 Servlet 的请求,都会产生一个新的线程,调用 service 方法。service 方法检查 HTTP 请求的类型(GET,POST,PUT,DELETE等)并相应地调用 doGet,doPost,doPut,doDelete 等方法。

​ 如果需要在 Servlet 中等同地处理 POST 和 GET 请求,只需要让 doPost 调用 doGet 或者反过来即可,尽量别直接覆盖 service 方法。

​ 虽然这种方法要多出几行代码,但相对于直接覆盖 service ,它有几个有点。首先,之后还可以加入 doPut,doTrace等,支持其他的 HTTP 请求方法。直接覆盖 service 则排除了这种可能性。其次,还可以通过添加 getLastModified 方法,加入对修改日期的支持,由于 getLastModified 由默认的 service 方法调用,所以覆盖 service 方法也就失去了这个选项。最后,service 提供对 HEAD,OPTION 和 TRACE 请求的自动支持。

3.6.2、doGet,doPost 和 doXxx 方法:

​ 这些部分才是 Servlet 的主体。因此,可以覆盖 doGet 和 doPost。如果愿意,也可以覆盖 DELETE 请求的 doDelete、PUT 请求的 doPut 、OPTIONS 请求的 doOptions 以及 TRACE 请求的 doTrace。然而,要记住已经拥有对 OPTION 和 TRACE 的自动支持。

​ 一般情况下,不需要实现 doHead 以处理 HEAD 请求(HEAD 请求规定,服务器应该只返回正常的 HTTP 报头,不含与之相关联的文档)。由于系统会自动调用 doGet ,并用生成的状态行和报头设定来应答 HEAD 请求,故而,一般不需要实现 doHead 。有时,为了能够更快地生成对 HEAD 请求的响应,会实现 doHead 方法。

3.6.3、init 方法:

​ 大多数时候,Servlet 只需处理单个请求的数据,doGet 或 doPost 是生命周期中唯一需要的方法。然而,有时候希望在 Servlet 首次载入时,执行复杂的初始化任务,但并不想每个请求都重复这些任务。init 方法就是专门针对这种情况设计;它在 Servlet 初次创建时被调用,之后处理每个用户的请求时,则不再调用这个方法。因此,它主要用于一次性的初始化,和 applet 的 init 方法相同。Servlet 一般在用户首次调用对应的 Servlet 的 URL 时创建,但也可以指定 Servlet 在服务器启动后载入。

​ init 方法执行两种类型的初始化:常规初始化,以及由初始化参数控制的初始化。

  1. 常规初始化:

    init 只是创建或载入在 Servlet 声明周期内用到的一些数据,或者执行某些一次性的计算。

  2. 由初始化参数控制的初始化:

3.6.4、destroy 方法:

​ 服务器可能会决定移除之前载入的 Servlet 实例,或许因为服务器的管理员要求它这样做。但是,在服务器移除 Servlet 的实例之前,它会调用 Servlet 的 destroy 方法,从而使得 Servlet 有机会关闭数据库连接、停止后台的线程、将 Cookie 列表和点击计数写入到磁盘、并执行其他清理活动。但是,要意识到 Web 服务器有可能崩溃。因此,不要将 destroy 机制作为向磁盘上保存状态的唯一机制。如果服务器执行诸如点击计数,或对 Cookie 值的列表进行累加等活动,应该主动地定期将数据写到磁盘上。

3.7、SingleThreadModel 接口:

注:由于我缺少前置知识,所以这部分内容先临时跳过,借鉴我的博客内容学习的师傅可以去支持下这本书。

3.8、Servlet 的调试:

​ 如下,书中列出了一部分的调试技巧:

  1. 使用打印语句。
  2. 使用集成在 IDE 中的调试器。
  3. 使用日志文件。
  4. 使用 Apache Log4J。
  5. 编写独立类。
  6. 预先做好数据缺失或异常的准备。
  7. 检查 HTML 源代码。
  8. 单独检查请求数据。
  9. 单独检查响应数据。
  10. 停止和重启服务器。
    注:由于我缺少前置知识,所以这部分内容先临时跳过,借鉴我的博客内容学习的师傅可以去支持下这本书。

3.8、Servlet 的调试:

​ 如下,书中列出了一部分的调试技巧:

  1. 使用打印语句。
  2. 使用集成在 IDE 中的调试器。
  3. 使用日志文件。
  4. 使用 Apache Log4J。
  5. 编写独立类。
  6. 预先做好数据缺失或异常的准备。
  7. 检查 HTML 源代码。
  8. 单独检查请求数据。
  9. 单独检查响应数据。
  10. 停止和重启服务器。