JavaWeb编程基础(Servlet)

最近在做一个SpringBoot的项目,当我真正动手写完了一个登陆功能之后,倏然觉得自己对整个JavaWeb的开发只停留在遵循规范这一层面,并未真正静下心来去思考原理。所以,基于这个原因,我准备从头开始学习JavaWeb的开发,彻底搞懂背后的细节。

2021.11.23

Servlet

Servlet 是 Server Applet 的缩写,是一种使用 Java 语言来开发动态网站的技术。

Java 是一种功能强大的通用型编程语言,可以处理 HTTP 请求,可以访问数据库,可以生成 HTML 代码,您完全可以使用原生 Java 来开发动态网站。但是,使用原生 Java 开发动态网站非常麻烦,需要自己解析 HTTP 请求的报头,需要自己分析用户的请求参数,需要自己加载数据库组件……种种原因导致使用原生 Java 开发动态网站几乎是一件不能被接受的事情。正是基于这种原因,Java 官方后来推出了 Servlet 技术,它对开发动态网站需要使用的原生 Java API 进行了封装,形成了一套新的 API,称为 Servlet API。

使用 Servlet 开发动态网站非常方便,程序员只需要集中精力处理业务逻辑,不需要再为那些基础性的、通用性的功能编写代码,这使得 Servlet 在动态网站开发领域具备了很高的实用性。

Servlet 基于 Java,可以使用几乎全部的 Java API,所以它的功能异常强大,完全可以胜任企业级开发,能够处理那些高并发、大吞吐量、业务逻辑复杂的应用场景。

您可以这样理解,Servlet 是 Sun 公司推出的一种基于 Java 的动态网站开发技术。编写 Servlet 代码需要遵循 Java 语法,一个 Servlet 程序其实就是一个按照 Servlet 规范编写的 Java 类。Servlet 程序需要先编译成字节码文件(.class文件),然后再部署到服务器运行。

Servlet 是一种规范

严格来说,Servlet 只是一套 Java Web 开发的规范,或者说是一套 Java Web 开发的技术标准。只有规范并不能做任何事情,必须要有人去实现它。所谓实现 Servlet 规范,就是真正编写代码去实现 Servlet 规范提到的各种功能,包括类、方法、属性等。

Servlet 规范是开放的,除了 Sun 公司,其它公司也可以实现 Servlet 规范,目前常见的实现了 Servlet 规范的产品包括 Tomcat、Weblogic、Jetty、Jboss、WebSphere 等,它们都被称为“Servlet 容器”。Servlet 容器用来管理程序员编写的 Servlet 类。

Servlet 接口

所有的 Servlet 功能都是通过一个名为Servlet的接口(Interface)向外暴露的,编写 Servlet 代码,可以从实现 Servlet 接口开始,就像下面这样:

1
2
3
·        public class ServletDemo implements Servlet {
· //TODO:
· }

直接实现 Servlet 接口比较麻烦,需要实现很多方法,所以 Servlet 规范又提供了两个抽象类,分别是 GenericServlet 类和 HttpServlet 类,它们都实现了 Servlet 接口的很多常用功能。和 GenericServlet 类相比,HttpServlet 类更加方便,所以实际开发中一般都继承自 HttpServlet 类。

JSP

Servlet 是第一代 Java Web 开发技术,它将 HTML 代码以字符串的形式向外输出,编写 HTML 文档就是在拼接字符串,非常麻烦,所以 Java 官方又推出了第二代 Web 开发技术——JSP。

JSP 才是现代化的 Web 开发技术,它允许 HTML 代码和 JSP 代码分离,让程序员能够在 HTML 文档中直接嵌入 JSP 代码。

现在没有人直接使用 Servlet 开发动态网站,大家都转向了 JSP 阵营。但是 JSP 依赖于 Servlet,用户访问 JSP 页面时,JSP 代码会被翻译成 Servlet 代码,最终,HTML 代码还是以字符串的形式向外输出的。您看,JSP 只是在 Servlet 的基础上做了进一步封装。

JSP 代码可以调用 Servlet 类,程序员可以将部分功能在 Servlet 中实现,然后在 JSP 中调用即可。

总之,Servlet 是 JSP 的基础,Servlet 虽然不直接面向用户,但是它依然是 JSP 的后台支撑,想玩转 JSP,必须先玩转 Servlet。

学习顺序:

· 学习 Servlet 的正确顺序是:Java –> Servlet。

· 学习 JSP 的正确顺序是:Java –> Servlet –> JSP。

Servlet容器(Web容器)是什么

Web 服务器

初学者可能认为,只要有 Web 服务器,我们编写的网站代码就可以运行了,就可以访问数据库了,就可以注册登录并发布文章了,这其实是一种误解。

我们通常所说的 Web 服务器,比如 Apache、Nginx、IIS 等,它们的功能往往都比较单一,只能提供 http(s) 服务,让用户访问静态资源(HTML 文档、图片、CSS 文件、JavaScript 文件等),它们不能执行任何编程语言,也不能访问数据库,更不能让用户注册和登录。

也就是说,如果只有 Web 服务器,那您只能部署静态网站,不能部署动态网站。要想部署动态网站,必须要有编程语言运行环境(运行时,Runtime)的和数据库管理系统的支持。

运行环境(运行时)

开发网站使用的编程语言一般都是脚本语言(比如 PHP、ASP、Python),部署网站时都是将源代码直接扔到服务器上,然而源代码自己并不能运行,必须要有解释器的支持;当用户访问动态页面时,解释器负责分析、编译和执行源代码,然后得到处理结果。

解释器是执行脚本语言的核心部件,除此以外还有一些辅助性的部件,例如:

· 垃圾回收器:负责及时释放不需要的内存,腾出资源供其它页面使用;

· 标准库:任何编程语言都会附带标准库,它们提供了很多通用性的功能,极大地提高了开发效率,避免重复造轮子。

我们习惯将以上各种支持脚本语言运行的部件统称为运行环境,或者运行时(Runtime)。

数据库

Web 服务器不带数据库,编程语言也不带数据库,数据库是一款独立的软件;要想实现用户注册、发布文章、提交评论等功能,就必须安装一款数据库,比如 MySQL、Oracle、SQL Server 等。

总结

部署动态网站一般至少需要三个组件,分别是 Web 服务器、脚本语言运行时和数据库,例如,部署 PHP 网站一般选择「Apache + PHP 运行时 + MySQL」的组合。

Web 容器

我们知道,Servlet 是基于 Java 语言的,运行 Servlet 必然少不了 JRE 的支持,它负责解析和执行字节码文件(.class文件)。然而 JRE 只包含了 Java 虚拟机(JVM)、Java 核心类库和一些辅助性性文件,它并不支持 Servlet 规范。要想运行 Servlet 代码,还需要一种额外的部件,该部件必须支持 Servlet 规范,实现了 Servlet 接口和一些基础类,这种部件就是 Servlet 容器。

Servlet 容器就是 Servlet 代码的运行环境(运行时),它除了实现 Servlet 规范定义的各种接口和类,为 Servlet 的运行提供底层支持,还需要管理由用户编写的 Servlet 类,比如实例化类(创建对象)、调用方法、销毁类等。

Servlet 中的容器和生活中的容器是类似的概念:生活中容器用来装水、装粮食,Servlet 中的容器用来装类,装对象。

读者可能会提出疑问,我们自己编写的 Servlet 类为什么需要 Servlet 容器来管理呢?这是因为我们编写的 Servlet 类没有 main() 函数,不能独立运行,只能作为一个模块被载入到 Servlet 容器,然后由 Servlet 容器来实例化,并调用其中的方法。

一个动态页面对应一个 Servlet 类,开发一个动态页面就是编写一个 Servlet 类,当用户请求到达时,Servlet 容器会根据配置文件(web.xml)来决定调用哪个类。

下图演示了 Servlet 容器在整个 HTTP 请求流程中的位置:

HTTP请求流程

您看,Web 服务器是整个动态网站的“大门”,用户的 HTTP 请求首先到达 Web 服务器,Web 服务器判断该请求是静态资源还是动态资源:如果是静态资源就直接返回,此时相当于用户下载了一个服务器上的文件;如果是动态资源将无法处理,必须将该请求转发给 Servlet 容器。

Servlet 容器接收到请求以后,会根据配置文件(web.xml)找到对应的 Servlet 类,将它加载并实例化,然后调用其中的方法来处理用户请求;处理结束后,Servlet 容器将处理结果再转交给 Web 服务器,由 Web 服务器将处理结果进行封装,以 HTTP 响应的形式发送给最终的用户。

常用的 Web 容器有 Tomcat、Jboss、Jetty、WebLogic 等,其中 Tomcat 由 Java 官方提供,是初学者最常使用的。

为了简化部署流程,Web 容器往往也会自带 Web 服务器模块,提供基本的 HTTP 服务,所以您可以不用再安装 Apache、IIS、Nginx 等传统意义上的服务器,只需要安装一款 Web 容器,就能部署 Servlet 网站了。正是由于这个原因,有的教材将 Tomcat 称为 Web 容器,有的教材又将 Tomcat 称为 Web 服务器,两者的概念已经非常模糊了。

将 Web 容器当做服务器使用后,上面的流程图就变成了下面的样子:

HTTP 请求流程

注意,Servlet 容器自带的 Web 服务器模块虽然没有传统的 Web 服务器强大,但是也足以应付大部分开发场景,对初学者来说是足够的。当然,您也可以将传统的 Web 服务器和 Servlet 容器组合起来,两者分工协作,各司其职,共同完成 HTTP 请求。

总结

Servlet 容器就是 Servlet 程序的运行环境,它主要包含以下几个功能:

· 实现 Servlet 规范定义的各种接口和类,为 Servlet 的运行提供底层支持;

· 管理用户编写的 Servlet 类,以及实例化以后的对象;

· 提供 HTTP 服务,相当于一个简化的服务器。

Tomcat目录结构

Tomcat 解压完成以后会看到如表1所示的很多目录,这些目录都有各自的用途,初学者有必要了解一下。

Tomcat 子目录及其说明
子目录 说明
bin 命令中心(启动命令,关闭命令……)
conf 配置中心(端口号,内存大小……)
lib Tomcat 的库文件。Tomcat 运行时需要的 jar 包所在的目录。
logs 存放日志文件。
temp 存储临时产生的文件,即缓存。
webapps 存放项目的文件,web 应用放置到此目录下浏览器可以直接访问。
work 编译以后的 class 文件。

1. bin 目录

bin 目录用来存放 Tomcat 命令,主要分为两大类,一类是以.sh结尾的 Linux 命令,另一类是以.bat结尾的 Windows 命令。很多环境变量都在此处设置,例如 JDK 路径、Tomcat 路径等。

bin 目录包含的内容
图1:bin 目录包含的内容

下面是几个常用的 Tomcat 命令:

· startup.sh/startup.bat:用来启动 Tomcat;

· shutdown.sh/shutdown.bat:用来关闭 Tomcat;

· catalina.bat/ catalina.bat:用来设置 Tomcat 的内存。

2. conf 目录

conf 目录主要是用来存放 Tomcat 的配置文件,如下图所示:

conf 目录包含的内容
图2:conf 目录包含的内容

下面是常用到的几个文件:

· server.xml 用来设置域名、IP、端口号、默认加载的项目、请求编码等;

· context.xml 用来配置数据源等;

· tomcat-users.xml 用来配置和管理 Tomcat 的用户与权限;

· web.xml 可以设置 Tomcat 支持的文件类型;

· 在 Catalina 目录下可以设置默认加载的项目。

3. lib 目录

lib 目录主要用来存放 Tomcat 运行需要加载的 jar 包。

lib 目录包含的内容
图3:lib 目录包含的内容

4. logs 目录

logs 目录用来存放 Tomcat 在运行过程中产生的日志文件,清空该目录中的文件不会对 Tomcat 的运行带来影响。

在 Windows 系统中,控制台的输出日志在 catalina.xxxx-xx-xx.log 文件中;在 Linux 系统中,控制台的输出日志在 catalina.out 文件中。

5. temp 目录

temp 目录用来存放 Tomcat 在运行过程中产生的临时文件,清空该目录中的文件不会对 Tomcat 的运行带来影响。

temp 目录包含的内容
图4:temp 目录包含的内容

6. webapps 目录

webapps 目录用来存放应用程序(也就是通常所说的网站),当 Tomcat 启动时会去加载 webapps 目录下的应用程序,我们编写的 Servlet 程序就可以放在这里。Tomcat 允许以文件夹、war 包、jar 包的形式发布应用。

webapps 目录包含的内容
图5:webapps 目录包含的内容

7. work 目录

work 目录用来存放 Tomcat 在运行时的编译文件(也即 class 字节码文件),例如 JSP 编译后的文件。清空 work 目录,然后重启 Tomcat,可以达到清除缓存的作用。

Servlet三种创建方式

在 Servlet 中,一个动态网页对应一个 Servlet 类,我们可以通过 web.xml 配置文件将 URL 路径和 Servlet 类对应起来。访问一个动态网页的过程,实际上是将对应的 Servlet 类加载、实例化并调用相关方法的过程;网页上显示的内容,就是通过 Servlet 类中的某些方法向浏览器输出的 HTML 语句。

所以,使用 Servlet 创建动态网页的第一步,就是创建 Servlet 类。

Servlet 规范的最顶层是一个名为 javax.servlet.Servlet 的接口,所有的 Servlet 类都要直接或者间接地实现该接口。直接实现 Servlet 接口不太方便,所以 Servlet 又内置了两个 Servlet 接口的实现类(抽象类),分别为 GenericServlet 和 HttpServlet,因此,创建 Servlet 类有如下三种方式:

  1. 实现 javax.servlet.Servlet 接口,重写其全部方法。

  2. 继承 javax.servlet.GenericServlet 抽象类,重写 service() 方法。

  3. 继承 javax.servlet.http.HttpServlet 抽象类,重写 doGet() 或 doPost() 方法。

Servlet、GenericServlet 、HttpServlet 的关系

下图展示了 Servlet、GenericServlet 以及 HttpServlet 三者之间的关系,其中 MyServlet 是我们自定义的 Servlet 类。

Servlet 关系图

由上图可知:

  1. GenericServlet 是实现了 Servlet 接口的抽象类。

  2. HttpServlet 是 GenericServlet 的子类,具有 GenericServlet 的一切特性。

  3. Servlet 程序(MyServlet 类)是一个实现了 Servlet 接口的 Java 类。

Servlet 接口

javax.servlet.Servlet 是 Servlet API 的核心接口,所有的 Servlet 类都直接或间接地实现了这一接口。

Servlet 接口中定义了 5 个方法,下面我们对他们做简单的介绍。

返回值 方法 备注
void init(ServletConfig config) Servlet 实例化之后,由 Servlet 容器调用,用来初始化 Servlet 对象。该方法只能被调用一次。 参数 config 用来向 Servlet 传递配置信息。
void service(ServletRequest req,ServletResponse res) Servlet 容器调用该方法处理客户端请求。
void destroy() 服务器关闭、重启或者 Servlet 对象被移除时,由 Servlet 容器调用,负责释放 Servlet 对象占用的资源。
ServletConfig getServletConfig() 该方法用来获取 ServletConfig 对象,该对象中包含了 Servlet 的初始化参数。
String getServletInfo() 该方法用于获取 Servlet 的信息,例如作者、版本、版权等。

示例 1

通过实现 Servlet 接口创建 Servlet,示例代码如下。

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
30
31
32
33
34
35
36
37
38
39
40
41
1.  package net.biancheng.www;
2.
3. import javax.servlet.*;
4. import java.io.IOException;
5. import java.io.PrintWriter;
6.
7. public class MyServlet implements Servlet {
8.
9. //Servlet 实例被创建后,调用 init() 方法进行初始化,该方法只能被调用一次
10. @Override
11. public void init(ServletConfig servletConfig) throws ServletException {
12. }
13.
14. //返回 ServletConfig 对象,该对象包含了 Servlet 的初始化参数
15. @Override
16. public ServletConfig getServletConfig() {
17. return null;
18. }
19.
20. //每次请求,都会调用一次 service() 方法
21. @Override
22. public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
23. //设置字符集
24. servletResponse.setContentType("text/html;charset=UTF-8");
25. //使用PrintWriter.write()方法向前台页面输出内容
26. PrintWriter writer = servletResponse.getWriter();
27. writer.write("胡小宁的博客欢迎你的到来,网址: 1905060202.github.io");
28. writer.close();
29. }
30.
31. //返回关于 Servlet 的信息,例如作者、版本、版权等
32. @Override
33. public String getServletInfo() {
34. return null;
35. }
36.
37. //Servelet 被销毁时调用
38. @Override
39. public void destroy() {
40. }
41. }

GenericServlet 抽象类

javax.servlet.GenericServlet 实现了 Servlet 接口,并提供了除 service() 方法以外的其他四个方法的简单实现。通过继承 GenericServlet 类创建 Servlet ,只需要重写 service() 方法即可,大大减少了创建 Servlet 的工作量。

GenericServlet 类中还提供了以下方法,用来获取 Servlet 的配置信息。

返回值 方法 备注
String getInitParameter(String name) 返回名字为 name 的初始化参数的值,初始化参数在 web.xml 中进行配置。如果参数不存在,则返回 null。
Enumeration getInitParameterNames() 返回 Servlet 所有初始化参数的名字的枚举集合,若 Servlet 没有初始化参数,返回一个空的枚举集合。
ServletContext getServletContext() 返回 Servlet 上下文对象的引用。
String getServletName() 返回此 Servlet 实例的名称。

示例 2

通过继承 GenericServlet 抽象类创建 Servlet,示例代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1.  package net.biancheng.www;
2.
3. import javax.servlet.*;
4. import java.io.IOException;
5. import java.io.PrintWriter;
6.
7. public class MyServlet extends GenericServlet {
8.
9. @Override
10. public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
11. //设置字符集
12. servletResponse.setContentType("text/html;charset=UTF-8");
13. //使用PrintWriter.write()方法向前台页面输出内容
14. PrintWriter writer = servletResponse.getWriter();
15. writer.write("胡小宁的博客欢迎你的到来,网址: 1905060202.github.io ");
16. writer.close();
17. }
18. }

HttpServlet 抽象类

javax.servlet.http.HttpServlet 继承了 GenericServlet 抽象类,用于开发基于 HTTP 协议的 Servlet 程序。由于 Servlet 主要用来处理 HTTP 的请求和响应,所以通常情况下,编写的 Servlet 类都继承自 HttpServlet。

在 HTTP/1.1 协议中共定义了 7 种请求方式,即 GET、POST、HEAD、PUT、DELETE、TRACE 和 OPTIONS。

HttpServlet 针对这 7 种请求方式分别定义了 7 种方法,即 doGet()、doPost()、doHead()、doPut()、doDelete()、doTrace() 和 doOptions()。

HttpServlet 重写了 service() 方法,该方法会先获取客户端的请求方式,然后根据请求方式调用对应 doXxx 方法。

示例 3

由于我们使用的请求方式主要是 GET 和 POST,所以通过继承 HttpServlet 类创建 Servlet 时,只需要重写 doGet 或者 doPost 方法,代码如下。

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
1.  package net.biancheng.www;
2.
3. import javax.servlet.*;
4. import javax.servlet.http.HttpServlet;
5. import javax.servlet.http.HttpServletRequest;
6. import javax.servlet.http.HttpServletResponse;
7. import java.io.IOException;
8. import java.io.PrintWriter;
9.
10. public class MyServlet extends HttpServlet {
11.
12. public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
13. //使用PrintWriter.write()方法向前台页面输出内容
14. resp.setContentType("text/html;charset=UTF-8");
15. PrintWriter writer = resp.getWriter();
16. writer.write("胡小宁的博客欢迎你的到来,网址: 1905060202.github.io ");
17. writer.close();
18. }
19.
20. public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
21. //使用PrintWriter.write()方法gaifang向前台页面输出内容
22. PrintWriter writer = resp.getWriter();
23. writer.write("胡小宁的博客欢迎你的到来,网址: 1905060202.github.io ");
24. writer.close();
25. doGet(req, resp);
26. }
27. }

总结

上面演示了三种创建 Servlet 的方式,那么在实际开发中,我们究竟该选择哪一种呢?下面我们就来分析和对比一下。

1) Servlet 接口

通过实现 Servlet 接口创建 Servlet 类,需要重写其全部的方法,比较繁琐,所以我们很少使用该方法创建 Servlet。

2) GenericServlet 类

GenericServlet 抽象类实现了 Servlet 接口,并对 Servlet 接口中除 service() 方法外的其它四个方法进行了简单实现。通过继承 GenericServlet 创建 Servlet,只需要重写 service() 方法即可,大大减少了创建 Servlet 的工作量。

Generic 是“通用”的意思,正如其名,GenericServlet 是一个通用的 Servlet 类,并没有针对某种场景进行特殊处理,尤其是 HTTP 协议,我们必须手动分析和封装 HTPP 协议的请求信息和响应信息。

3) HttpServlet 类

HttpServlet 是 GenericServlet 的子类,它在 GenericServlet 的基础上专门针对 HTTP 协议进行了处理。HttpServlet 为 HTTP 协议的每种请求方式都提供了对应的方法,名字为 doXxx(),例如:

· 处理 GET 请求的方法为 doGet();

· 处理 POST 请求的方法为 doPost()。

正如其名,HttpServlet 就是专为 HTTP 协议而量身打造的 Servlet 类。

在互联网上,人们都是通过 HTTP 协议来访问动态网页的,其中使用最频繁的就是 GET 方式和 POST 方式,因此,我们通常基于 HttpServlet 来创建 Servlet 类,这样就省去了处理 HTTP 请求的过程。

Servlet的部署和访问

Servlet 没有 main() 方法,不能独立运行,但它可以作为 JavaWeb 应用的一个组件被部署到 Servlet 容器中,由容器来实例化和调用 Servlet 的方法,例如:doGet() 、doPost() 等。

那么,JavaWeb 应用是什么呢?Servlet 是如何部署和访问的呢?本节我们将针对这些问题进行讲解。

JavaWeb 应用

JavaWeb 应用由一组 Servlet/JSP、HTML 文件、相关 Java 类、以及其他的资源组成,它可以在由各种供应商提供的 Servlet 容器中运行。由 JavaWeb 应用的定义可知, Servlet 是 JavaWeb 应用的一个组件。

为了让 Servlet 容器顺利地找到 JavaWeb 应用的各个组件,Servlet 规范规定,JavaWeb 应用必须采用固定的目录结构,即每种组件在 JavaWeb 应用中都有固定的存放目录。

以 Tomcat 为例,通常将 JavaWeb 应用存放到 Tomcat 的 webapps 目录下。在 webapps 下,每一个子目录都是一个独立的 Web 应用,子目录的名字就是 Web 应用的名字,也被称为 Web 应用的上下文根。用户可以通过这个上下文根来访问 JavaWeb 应用中的资源。

webapps 的目录结构如下图。

JavaWeb 结构图

下表中对 webapps 下各个目录进行了介绍。

目录 描述 是否必需
\servletDemo Web 应用的根目录,属于该 Web 应用的所有资源都存放在这个目录下。
\servletDemo\WEB-INF 存放 web.xml、lib 目录以及 classes 目录等。
\servletDemo\WEB-INF\classes 存放各种 .class 文件或者包含 .class 文件的目录,Servlet 类的 .class 文件也存放在此。
\servletDemo\WEB-INF\lib 存放应用所需的各种 jar 包,例如 JDBC 驱动程序的 jar 包。
\servletDemo\WEB-INF\web.xml web.xml 中包含应用程序的配置和部署信息。

部署

在 Tomcat 中部署 JavaWeb 应用最快捷的方式,就是直接将 JavaWeb 应用的所有文件复制到 Tomcat 的 /webapps 目录下。在默认情况下,Tomcat 会自动加载 webapps 目录下的 JavaWeb 应用,并把它发布到名为 localhost 的虚拟主机中。

Tomcat 既可以运行采用开放式目录结构(只编译不打包)的 Web 应用,也可以运行 Web 应用的打包文件(WAR 文件)。在开发阶段,为了方便程序调试,通常采用开放式的目录结构部署 JavaWeb 应用。在开发完成,进入产品发布阶段时,就应该将整个应用打包成 WAR 文件,再进行部署。

即使采用 WAR 文件的形式发布,Tomcat 启动时也会将 WAR 文件自动展开为开放式的目录结构。

在本案例中,我们将采用开放式的目录结构进行部署,具体操作步骤如下。

1. 进入 Windows DOS 命令行窗口

我们知道,编译 Java 源代码一般可以通过两种方式完成:DOS 命令行 和 IDE(集成开发环境)。使用 IDE 可以一键完成编译工作,十分的方便,但 IDE 给我们带来方便的同时,也隐藏了编译过程中的许多细节,我们无法了解其中的原理和机制。

对于初学者来说,为了更加深刻地理解 Servlet 的运行原理和机制,最好的办法就是使用 DOS 命令行进行编译。

使用快捷键 “Windows + R”,打开运行对话框,在对话框输入cmd,点击“确定”,进入 Windows 系统命令行窗口,如图。

cmd

2. 引入 javax.servlet 包

由于 Servlet 是 JavaEE 下的技术标准,不是 JDK 的组成部分,所以在编译 Servlet 前,需要先引入 servlet-api.jar 包(在 Apache Tomcat 安装目录下的 lib 文件夹中提供了此 jar 包)。

使用命令set classpath可以将 servlet-api.jar 引入,该命令的语法如下,其中 path 表示引入 jar 包的路径。

1
·        set classpath = path

为了方便操作,可以先在命令行中输入set classpath=,然后将 servlet-api.jar 文件拖进命令行内,回车执行。

set calsspath

注:**set classpath**命令用来为当前窗口设置临时环境变量,只在当前窗口下有效。**

**

3. 编译 Servlet

1)在命令提示符中使用 cd命令进入 MyServlet.java 所在的目录。

servlet cd

2)使用 javac -encoding UTF-8 -d . MyServlet.java命令进行编译,若没有报错,则编译成功。

其中:

· javac :编译命令;

· -encoding UTF-8:用于指定编译源文件时的字符编码,这里指定为 UTF-8;

· javac -d:带包编译 ;

· .:表示当前位置。

3)进入 MyServlet.java 文件所在的目录,可发现新增了编译后的目录和 .classes 字节码文件,自此我们就完成了对 Servlet 的编译工作。

4. 创建目录结构

在完成对 Servlet 的编译后,下一步依照 JavaWeb 应用的固定目录结构,在 Tomcat 中为该 Servlet 创建目录。

  1. 在 Tomcat 的 webapps 目录中创建 servletDemo(web 应用的名字,由自己定义)文件夹;

  2. 在 servletDemo 文件夹内创建 WEB-INF 文件夹;

  3. 在 WEB-INF 内创建 classes 文件夹;

  4. 在 WEB-INF 内创建配置文件 web.xml(可以复制 \webapps\ROOT\WEB-INF 中的 web.xml 使用)。

5. 将 Servlet 移动到 Tomcat 目录中

将编译好的字节码和目录移动到 Tomcat\webapps\servletDemo\WEB-INF\classes 目录下。

6. 配置 web.xml

对 webapps\servletDemo\WEB-INF 目录的 web.xml 中进行配置,具体配置代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1.  <?xml version="1.0" encoding="UTF-8"?>
2. <web-app xmlns="http://java.sun.com/xml/ns/javaee"
3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4. xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
5. http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
6. version="3.0" metadata-complete="true">
7. <servlet>
8. <servlet-name>MyServlet</servlet-name>
9. <servlet-class>io.github.1905060202.www.MyServlet</servlet-class>
10. </servlet>
11. <servlet-mapping>
12. <servlet-name>MyServlet</servlet-name>
13. <url-pattern>/MyServlet</url-pattern>
14. </servlet-mapping>
15. </web-app>

web.xml 中各元素含义及用法如下:

· : 根元素。

· :用于注册 Servlet,即给 Servlet 起一个独一无二的名字。

· 包含两个主要的子元素 ,分别用于指定 Servlet 的名称和 Servlet 的完整限定名(包名+类名)。

· :用于定义 Servlet 与 URL 之间的映射。

· 包含两个子元素 ,分别用于指定 Servlet 的名称和虚拟路径。

访问

进入 Tomcat\bin 目录下双击 startup.bat,启动 Tomcat。

tomcat 启动

Tomcat 启动成功后,在地址栏中输入“http://localhost:8080/servletDemo/MyServlet”,访问 MyServlet,结果如下图。

访问路径 http://localhost:8080/servletDemo/MyServlet 中,各部分含义如下:

· http:// 表示 HTTP 协议;

· localhost: 表示服务器 IP;

· 8080 表示端口号;

· /servletDemo 表示 Web 应用的上下文根路径;

· /MyServlet 表示资源路径,即 web.xml 中 元素的取值。

IDEA创建Servlet项目

概述

之前发现 IDEA 没法新建 Servlet 项目,正好最近要用 Servlet 写个小项目,开个文章记录一下。
本文使用的 IDEA 版本为 2020.2

准备工作

  1. 首先新建项目

  2. 选择 Java 项目,直接 Next

  3. 继续 Next
    3

  4. 修改一下 Project Name,点击 Finish

添加 Framework

  1. 在工程上右键,选择 Add Framework Support

  2. 勾选 Web Application,点击 OK

  3. 可以看到工程结构下多了个 web 目录

新建文件夹

  1. web/WEB-INF 目录下新建一个 classes 文件夹

  2. 配置 Project Structure

Modules——Path 下,修改 Compiler Output,选择 Use module compile output path,修改两个内容框为刚才新建的 classes 目录,点击时工构发生变化

导入 servlet-api.jar

  1. 下载 tomcat,我下载的是 tomcat8
    https://tomcat.apache.org/

  2. 进入 Project Structure——Modules——Dependencies,点击下面的加号

  3. 选择 JARs or directories,找到刚才下载解压的 tomcat 目录中的 lib 文件夹下的 servlet-api.jar,确定。

  4. 添加完成后选择 OK 即可

创建 Servlet 测试

  1. 在 src 下新建一个测试用的 Servlet
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
30
31
32
33
34
35
36
37
38
39
40
41
42
·        package com.wanvale.lms;
·
·
·
· import javax.servlet.ServletException;
·
· import javax.servlet.annotation.WebServlet;
·
· import javax.servlet.http.HttpServlet;
·
· import javax.servlet.http.HttpServletRequest;
·
· import javax.servlet.http.HttpServletResponse;
·
· import java.io.IOException;
·
·
·
· @WebServlet({"/test", "/get"})
·
· public class test extends HttpServlet {
·
· @Override
·
· protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
·
· resp.getWriter().write("POST Method");
·
· }
·
·
·
· @Override
·
· protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
·
· resp.getWriter().write("GET Method");
·
· }
·
· }
·
  1. 选择菜单栏中的 Run——Run

  2. 选择 Edit Configuration

  3. 添加一个 Local Tomcat Server

  4. 点击 Configure 添加 Tomcat

  5. 选择 Tomcat 路径,点击 OK

  6. 配置一下 DeploymentServer 的参数

  1. 点击 Run,等待 Server 跑起来以后访问测试一下

至此,配置运行结束。

@WebServlet注解(Servlet注解)

在 Servlet 中,web.xml 扮演的角色十分的重要,它可以将所有的 Servlet 的配置集中进行管理,但是若项目中 Servelt 数量较多时,web.xml 的配置会变得十分的冗长。这种情况下,注解(Annotation)就是一种更好的选择。

与 XML 不同,注解不需要依赖于配置文件,它可以直接在类中使用,其配置只对当前类有效,这样就避免了集中管理造成的配置冗长问题。那么 Servelt 支持注解吗?

为了简化 Servlet 的配置,Servlet 3.0 中增加了注解支持,例如:@WebServlet、@WebInitParm 、@WebFilter 和 @WebLitener 等,这使得 web.xml 从 Servlet 3.0 开始不再是必选项了。下面我们对 @WebServlet 进行介绍。

@WebServlet 注解的属性

@WebServlet 用于将一个类声明为 Servlet,该注解会在部署时被容器处理,容器根据其具体的属性配置将相应的类部署为 Servlet。该注解具有下表给出的一些常用属性。

属性名 类型 标签 描述 是否必需
name String 指定 Servlet 的 name 属性。 如果没有显式指定,则取值为该 Servlet 的完全限定名,即包名+类名。
value String[ ] 该属性等价于 urlPatterns 属性,两者不能同时指定。 如果同时指定,通常是忽略 value 的取值。
urlPatterns String[ ] 指定一组 Servlet 的 URL 匹配模式。
loadOnStartup int 指定 Servlet 的加载顺序。
initParams WebInitParam[ ] 指定一组 Servlet 初始化参数。
asyncSupported boolean 声明 Servlet 是否支持异步操作模式。
description String 指定该 Servlet 的描述信息。
displayName String 指定该 Servlet 的显示名。

@WebServlet 注解的使用

1. 启用注解支持

web.xml 的顶层标签 中有一个属性:metadata-complete,该属性用于指定当前 web.xml 是否是完全的。若该属性设置为 true,则容器在部署时将只依赖 web.xml,忽略所有的注解。若不配置该属性,或者将其设置为 false,则表示启用注解支持。

1
2
3
4
5
6
7
8
1.	<?xml version="1.0" encoding="UTF-8"?>
2. <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3. xmlns="http://xmlns.jcp.org/xml/ns/javaee"
4. xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
5. id="WebApp_ID" metadata-complete="false" version="4.0">
6. <!-- metadata-complete取值为true,表示关闭注解支持 -->
7. <!-- metadata-complete取值为false,表示启用注解支持 -->
8. </web-app>

由于 metadata-complete 属性的默认值是 false,即默认启用 Servlet 注解支持,所以默认情况下,使用该注解时,不必创建 web.xml 文件。

2. 使用 @WebServlet 注解

@WebServlet 属于类级别的注解,标注在继承了 HttpServlet 的类之上。常用的写法是将 Servlet 的相对请求路径(即 value)直接写在注解内,如下所示。

1
@WebServlet("/MyServlet") 

该写法省略了 urlPatterns 属性名,其完整的写法如下所示。

1
@WebServlet(urlPatterns = "/MyServlet")

如果 @WebServlet 中需要设置多个属性,则属性之间必须使用逗号隔开,如下所示。

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
1.	package net.biancheng.www;
2.
3. import java.io.IOException;
4. import javax.servlet.ServletException;
5. import javax.servlet.annotation.WebInitParam;
6. import javax.servlet.annotation.WebServlet;
7. import javax.servlet.http.HttpServlet;
8. import javax.servlet.http.HttpServletRequest;
9. import javax.servlet.http.HttpServletResponse;
10.
11. @WebServlet(asyncSupported = true, name = "myServlet", description = "name描述", loadOnStartup = 1, urlPatterns = {
12. "/MyServlet", "/*" }, initParams = {
13. @WebInitParam(name = "编程帮", value = "www.biancheng.net", description = "init参数1"),
14. @WebInitParam(name = "京东", value = "www.jd.com", description = "init参数2") })
15. public class MyServlet extends HttpServlet {
16. private static final long serialVersionUID = 1L;
17.
18. protected void doGet(HttpServletRequest request, HttpServletResponse response)
19. throws ServletException, IOException {
20. }
21.
22. protected void doPost(HttpServletRequest request, HttpServletResponse response)
23. throws ServletException, IOException {
24. }
25.
26. }

注意事项:

  • 通过实现 Serlvet 接口或继承 GenericServlet 创建的 Servlet 类无法使用 @WebServlet 注解。
  • 使用 @WebServlet 注解配置的 Servlet 类,不要在 web.xml 文件中再次配置该 Servlet 相关属性。若同时使用 web.xml 与 @WebServlet 配置同一 Servlet 类,则 web.xml 中 的值与注解中 name 取值不能相同,否则容器会忽略注解中的配置。

@WebServlet 注解 和 web.xml 的优缺点

使用 web.xml 或 @WebServlet 注解都可以配置 Servlet, 两者各有优缺点。 @WebServlet 注解配置 Servlet 优点:@WebServlet 直接在 Servlet 类中使用,代码量少,配置简单。每个类只关注自身业务逻辑,与其他 Servlet 类互不干扰,适合多人同时开发。

缺点:Servlet 较多时,每个 Servlet 的配置分布在各自的类中,不便于查找和修改。 web.xml 配置文件配置 Servlet 优点:集中管理 Servlet 的配置,便于查找和修改。

缺点:代码较繁琐,可读性不强,不易于理解。

Servlet生命周期(附带实例)

生命周期是指事物从创建到毁灭的过程。人的生命周期就是从出生到死亡的过程,在这个过程中,必定会有一些与生命周期息息相关的事件,如:出生、吃饭、上学、死亡等。这些事件会在生命周期中的某个特定时刻发生。

Servlet 也有生命周期,Servlet 的生命周期就是 Servlet 从创建到销毁的过程。Servlet 的生命周期由 Servlet 容器管理,主要分为以下 3 个阶段。

  1. 初始化阶段
  2. 运行时阶段
  3. 销毁阶段

在 javax.servlet.Servlet 接口中定义了 3 个方法:init()、service()、destory(),它们分别在 Servlet 生命周期的不同阶段被 Servlet 容器调用。

初始化阶段

Servlet 初始化是其生命周期的第一个阶段,也是其他阶段的基础。只有完成了初始化,Servlet 才能处理来自客户端的请求。

Servlet 初始化阶段分为 2 步: 加载和实例化 Servlet; 调用 init() 方法进行初始化。 1. 加载和实例化 Servlet Servlet 容器负责加载和实例化 Servlet。当容器启动或首次请求某个 Servlet 时,容器会读取 web.xml 或 @WebServlet 中的配置信息,对指定的 Servlet 进行加载。加载成功后,容器会通过反射对 Servlet 进行实例化。

因为 Servlet 容器是通过 Java 的反射 API 来创建 Servlet 实例的,需要调用 Servlet 的默认构造方法(default constructor,即不带参数的构造方法),所以在编写 Servlet 类时,不能只提供一个带参数的构造方法。 2. 调用 init() 方法进行初始化 加载和实例化完成后,Servlet 容器调用 init() 方法初始化 Servlet 实例。

初始化的目的:让 Servlet 实例在处理请求之前完成一些初始化工作,例如建立数据库连接,获取配置信息等。

在 Servlet 的整个生命周期内,init() 方法只能被调用一次。

初始化期间,Servlet 实例可以通过 ServletConfig 对象获取在 web.xml 或者 @WebServlet 中配置的初始化参数。

运行时阶段

运行时阶段是 Servlet 生命周期中最重要的阶段。Servlet 容器接收到来自客户端请求时,容器会针对该请求分别创建一个 ServletRequst 对象和 ServletResponse 对象,将它们以参数的形式传入 service() 方法内,并调用该方法对请求进行处理。

这里需要注意的是,执行 service() 方法前,init() 方法必须已成功执行。

在 service() 方法中,Servlet 通过 ServletRequst 对象获取客户端的相关信息和请求信息。在请求处理完成后,通过 ServletResponse 对象将响应信息进行包装,返回给客户端。当 Servlet 容器将响应信息返回给客户端后,ServletRequst 对象与 ServletResponse 对象就会被销毁。

在 Servlet 的整个生命周期内,对于 Servlet 的每一次请求,Servlet 容器都会调用一次 service() 方法,并创建新的 ServletRequest 和 ServletResponse 对象。即 service() 方法在 Servlet 的整个生命周期中会被调用多次。

销毁阶段

当 Servlet 容器关闭、重启或移除 Servlet 实例时,容器就会调用 destory() 方法,释放该实例使用的资源,例如:关闭数据库连接,关闭文件的输入流和输出流等,随后该实例被 Java 的垃圾收集器所回收。

对于每个 Servlet 实例来说,destory() 方法只能被调用一次。

Servlet 生命周期执行流程

Servlet 生命周期流程如下图所示。

Servlet 生命周期
在 Servlet 的整个生命周期中,创建 Servlet 实例、init() 方法和 destory() 方法都只执行一次。当初始化完成后,Servlet 容器会将该实例保存在内存中,通过调用它的 service() 方法,为接收到的请求服务。

示例

下面通过一个案例加深对 Servlet 生命周期的理解。

  1. 在 servletDemo 项目中,对 MyServlet.java 进行修改,代码如下:
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
1.	package net.biancheng.www;
2.
3. import java.io.IOException;
4. import java.io.PrintWriter;
5.
6. import javax.servlet.ServletException;
7. import javax.servlet.annotation.WebInitParam;
8. import javax.servlet.annotation.WebServlet;
9. import javax.servlet.http.HttpServlet;
10. import javax.servlet.http.HttpServletRequest;
11. import javax.servlet.http.HttpServletResponse;
12.
13. @WebServlet("/MyServlet")
14. public class MyServlet extends HttpServlet {
15. private static final long serialVersionUID = 1L;
16. private int initCount = 0;
17. private int httpCount = 0;
18. private int destoryCount = 0;
19.
20. @Override
21. public void destroy() {
22. destoryCount++;
23. super.destroy();
24. // 向控制台输出destory方法被调用次数
25. System.out.println(
26. "**********************************destroy方法:" + destoryCount + "*******************************");
27. }
28.
29. @Override
30. public void init() throws ServletException {
31. initCount++;
32. super.init();
33. // 向控制台输出init方法被调用次数
34. System.out.println("init方法:" + initCount);
35. }
36.
37. public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
38. httpCount++;
39. // 控制台输出doGet方法次数
40. System.out.println("doGet方法:" + httpCount);
41. // 设置返回页面格式与字符集
42. resp.setContentType("text/html;charset=UTF-8");
43. PrintWriter writer = resp.getWriter();
44. // 向页面输出
45. writer.write("初始化次数:" + initCount + "<br/>" + "处理请求次数:" + httpCount + "<br/>" + "销毁次数:" + destoryCount);
46. writer.close();
47. }
48.
49. protected void doPost(HttpServletRequest request, HttpServletResponse response)
50. throws ServletException, IOException {
51. }
52.
53. }
  1. 启动 Tomcat,在地址栏输入“http://localhost:8080/servletDemo/MyServlet”,多次访问 MyServlet,结果如下图。

Servlet生命周期

  1. 控制台输出,如下图。

servlet生命周期1

  1. 关闭 Tomcat 服务器,控制台输出如下图。

Servlet 生命周期销毁