跳转至

第7章 Servlet

第 7 章

Servlet

JavaWeb 应用中包括三大重要组件,即 Servlet、Listener 和 Filter。本章我们将进入 Servlet 的学习。由于 Web 开发基于 HTTP,而 Servlet 规范其实就是对 HTTP 面向对象的封装,Servlet 实现了接收客户端的请求数据,并生成响应结果最终返回给客户端的过程。同时,本章也是本书的一大重点,内容主要包括 Servlet 的生命周期、体系结构、请求与响应,以及如何应用等。

7.1 Servlet 简介

Servlet 是 Server Applet 的简称,称为小服务程序或服务连接器,是使用 Java 语言编写的服务器端程序,具有独立于平台和协议的特性,主要功能在于交互式地浏览和生成数据,生成动态 Web 内容。在整个 Web 应用中,Servlet 主要负责接收处理请求、协同调度功能,以及响应数据。因此,我们可以把 Servlet 称为 Web 应用中的控制器。

类比生活中的例子,顾客去餐馆吃饭,点了一份宫保鸡丁,需要通过服务员告知厨师,厨师再从库房中取来宫保鸡丁的原料进行制作,制作完成后通知服务员,然后服务员为顾客上菜,如图 7-1 所示。

如果把上述点菜流程对应到 Web 应用中,用户发送请求,如显示所有员工信息,则需要通过一个组件把该请求传递给后端,并通过编写 Java 代码从数据库获取所有员工信息,然后再将其信息传回给用户。要实现的效果如图 7-2 所示。

5d2bf9324a071b9617c8ee58282282fd221788ea61f5425493bea316fb4c66d7.jpg

5ea263368df2e43b0b632aca94144a1169b33cea1938bc608ba69a861f7bd0f8.jpg

而在图 7-2 中,传递用户请求到后端,以及将后端响应的结果返回给用户都需要借助一个组件,该服务器端组件就是 Servlet,如果把 Web 应用比作一个餐厅,那么 Servlet 充当的就是餐厅中服务员的角色,负责接待顾客、上菜、结账等工作。可见 Servlet 承上启下,在 Web 项目中起到至关重要的作用。

例如,用户填写注册信息,提交数据后通过 Servlet 处理,实现保存数据到数据库,如图 7-3 所示。

51bad1d077faf4dc5b1085e12fea219106de343decaa02bfdd3370326168fa9d.jpg

再例如,通过网页驱动服务器端的 Java 程序查询数据库中的数据,然后将结果展示到网页上,同样需要借助 Servlet 实现,如图 7-4 所示。

545bfd1a334047052f9c04ef718da5b038b73d6a2af03b8083de7b9d98e6e711.jpg

从广义上来讲,Servlet 规范是 Sun 公司制定的一套技术标准,包含与 Web 应用相关的一系列接口,是 Web 应用实现方式的宏观解决方案。而具体的 Servlet 容器负责提供标准的实现。

从狭义上来讲,Servlet 指的是 javax.servlet.Servlet 接口及其子接口,子接口包括 ServletConfig 接口和 ServletContext 接口,也可以指实现了 Servlet 接口的实现类。其中,GenericServlet 实现了 Servlet 接口,HttpServlet 继承了 GenericServlet。GenericServlet 和 HttpServlet 是我们接下来重点介绍的两个 Servlet 实现类。

接下来,通过一个入门案例来体验如何应用 Servlet。

7.1.1 Servlet 的入门案例

通常情况下,使用一个接口的方式是创建一个类实现该接口,并通过 new 关键字创建其实现类的对象,然后便可以调用该类中的方法实现相应的功能了。但使用 Servlet 接口的方式与普通接口有所不同,具体操作步骤如下。

HelloServlet 的案例流程如图 7-5 所示。

下面通过创建 chapter07_servlet 模块来实现上述 HelloServlet 的功能。在模块相应位置创建 HelloServlet 实现类,以及 index.html 页面,目录结构如图 7-6 所示。

67e6f0c8c79ee4b6a7f46b2dc281dcd20389e776b213d1809a943c9d3eba1078.jpg

faa41be5db93769e7742ecd09523d9b3019b252f4add88a75a81894eb05a364b.jpg

HelloServlet 实现了 Servlet 接口,需要重写 5 个方法,包括初始化方法、获取 Servlet 配置信息、处理请求和作出响应、获取 Servlet 相关信息以及销毁方法,这里重点关注处理请求和作出响应的 service () 方法,其余方法在介绍 Servlet 生命周期时再作解释。HelloServlet 实现类的具体代码如下。

在 web.xml 文件中为 HelloServlet 设置映射路径,示例代码如下。

值得注意的是,如果配置文件一旦修改,需要重启服务器端来重新部署才能生效。然后在 index.html 页面中创建超链接,并将 HelloServlet 的映射路径设置给超链接,从而实现单击超链接访问 HelloServlet 的效果,示例代码如下。

最后,启动服务器进行测试,首页显示效果如图 7-7 所示。

a9d1d4e36363181e16fa7f9aca335549cee233e016a700d46e4834cf984405ef.jpg

单击超链接,浏览器页面显示 “Hello, I am Servlet” 内容,如图 7-8 所示。

e7aa9adf2da65f2319973d329b4f5539e3130bdb833e4341faafd095b22ee8b5.jpg

查看 IDEA 控制台,如图 7-9 所示,表示成功访问 HelloServlet 实现类的 service () 方法。

52637226bc46f2f9325e70684bf95c727a4e006b1484286cbd343cbf4135c071.jpg

以上就是通过 Servlet 实现的一个简单的前后端交互案例,由此,我们可以看出 Servlet 可以接收客户端发送的请求,还可以将服务器端数据响应给客户端。

7.1.2 Servlet 的映射路径

静态资源和动态资源的访问方式是不同的。访问静态资源是静态资源在 web 文件夹中的路径,一般为 “/Web 应用名称 / 静态资源本身的路径”。访问动态资源需要借助映射路径,一般为 “/Web 应用名称 / 映射路径”。

Servlet 的映射路径是提供一个让别人能够访问该 Servlet 的路径,例如 Servlet 的映射路径是 “/hello”,那么在浏览器上访问该 Servlet 的路径就是 “http://localhost:8080 / 项目部署名 /hello”。另外,Servlet 的映射路径可以分为三类,分别为完全路径匹配、目录匹配和扩展名匹配。

1. 完全路径匹配

完全路径匹配,指访问当前 Servlet 的路径需要和配置的映射路径完全一致,例如 Servlet 配置的映射路径是 “/demo01”,那么访问该 Servlet 的路径也必须是 “http://localhost:8080 / 项目部署名 /demo01”,否则无法访问到该 Servlet。

2. 目录匹配

目录匹配指以 “/” 开始且以 “*” 结束的路径(注意,该方式在 Servlet 中很少使用,目录匹配方式更多应用于过滤器中)。

例如,配置映射路径为 “/*”,表示访问的路径可写成 “/ 任意字符串”。

例如,配置映射路径为 “/aa/*”,表示访问的路径可写成 “/aa/ 任意字符串”。

3. 扩展名匹配

扩展名匹配指以 “*” 开头且以 “. 扩展名” 结束的路径,表示能够匹配所有以 “. 相同扩展名” 结尾的请求路径。

例如,“*.action” 对应的访问路径可以是 “任意字符串.action”。对比映射路径为 “/a.action”,则只能通过 “/a.action” 来访问。

7.2 Servlet 的生命周期

前面介绍了 Servlet 简介以及如何应用,接下来我们继续学习 Servlet 的生命周期。本节内容比较重要,希望大家理解并掌握。

应用程序中的对象不仅在空间上有层次结构的关系,在时间上也会因为处于程序运行过程中的不同阶段而表现出不同状态和不同行为,因此我们将对象在容器中从开始创建到销毁的过程称为其生命周期。

Servlet 对象是 Servlet 容器创建的,生命周期方法都是由容器调用的,当 Web 应用卸载时,Servlet 容器也会自动销毁 Servlet 对象。之前学过的 Tomcat 就是 Servlet 容器,Tomcat 的启动会加载 Web 应用,Tomcat 关闭会卸载 Web 应用。当然我们也可以借助工具在不重启 Tomcat 的前提下,进行 Web 应用的加载和卸载。这和我们之前编写的代码有很大不同,但在今后的学习中我们会看到,越来越多的对象交给容器或框架来创建,越来越多的方法由容器或框架来调用。正因为如此,开发人员只需要将尽可能多的精力放在业务逻辑的实现上,大大提高了开发效率。

7.2.1 Servlet 生命周期的主要过程

Servlet 生命周期的主要过程,也就是 Servlet 对象从创建到销毁的整个过程,包括通过构造器创建对象、通过 init () 方法执行初始化操作、通过 service () 方法处理请求、通过 destroy () 方法销毁对象。下面依次对以上步骤展开介绍。

1. Servlet 对象的创建

默认情况下,Servlet 容器第一次收到 HTTP 请求时创建对应的 Servlet 对象。容器之所以能做到这一点,是由于在注册 Servlet 时提供了全类名,容器使用反射技术创建了 Servlet 的对象。

2. Servlet 对象初始化

Servlet 容器创建 Servlet 对象之后,会调用 init (ServletConfig config) 方法,执行初始化操作。例如,读取一些资源文件、配置文件,或建立某种连接(比如数据库连接)等。值得注意的是,init () 方法只在创建对象时执行一次,以后再接到请求时就不再执行了。

3. 处理请求

在有请求发送到该 Servlet 时,会调用 service (ServletRequest req, ServletResponse res) 方法,执行请求处理操作。例如,获取请求参数、调用其他 Java 类、给出响应数据等。值得注意的是,在每次接到请求后都会执行一次该方法。

4. Servlet 对象的销毁

服务器端重启、服务器端停止执行或 Web 应用卸载时会销毁 Servlet 对象,会调用 destroy () 方法。此方法用于销毁如释放缓存、关闭连接、保存内存数据持久化等操作。

综上所述,Servlet 处理不同请求时,执行的方法有所不同,如表 7-1 所示。

表 7-1 Servlet 处理不同请求的区别

执行的方法
第一次请求1调用构造器,创建对象2执行 init()方法3执行 service()方法
第一次之后的请求执行 service()方法
对象销毁前执行 destroy()方法

下面通过代码来演示 Servlet 对象从创建到销毁的过程。

修改 HelloServlet 类,分别在构造器、初始化方法 init ()、销毁方法 destroy (),以及 service () 方法中编写输出语句,示例代码如下。

启动项目,查看控制台,发现没有输出任何结果,单击页面超链接访问 HelloServlet 后,再次查看控制台,如图 7-10 所示。

09ebb032a0324b1b8c8b45c0f4887904500ce39808ad9f7b6cca08873183df9c.jpg

可以看出,Servlet 容器在第一次收到 HTTP 请求时,会创建 HelloServlet 对象,执行 init () 方法进行初始化,并调用 service () 方法处理请求。

当创建 HelloServlet 对象后,多次访问 HelloServlet,查看控制台结果,如图 7-11 所示。

3d2cee537b3233e7b95c21b22e4ebbb28b205ebadfa1fa8f473f180a3619add7.jpg

第二次及以后的每次请求都只调用了 service () 方法,使用的 HelloServlet 对象都是同一个,不会再创建新的 HelloServlet 对象。最后在 Web 项目被卸载时,Servlet 容器会销毁该 HelloServlet 对象,例如关闭服务查看控制台,如图 7-12 所示,执行了销毁方法。

3517a021e69d7dc90c4bf18788d6a1aad856831ac2fb0d4b834a8435ab1db78c.jpg

7.2.2 配置 Servlet 提前创建对象

有时候需要在 Servlet 创建对象的时候做一些资源加载等耗时操作。针对这种情况,如果 Servlet 在第一次接收请求时才创建对象的话,必然会影响用户的访问速度,因此需要提前创建 Servlet 对象,将 Servlet 创建对象提前到服务器端启动时进行。

需要在 web.xml 文件中,Servlet 标签内添加 标签,并设置该标签的值为非零整数即可。如果有多个 Servlet 对象需要设置在启动服务器端时被创建,可以设置不同的 标签值,该值越小,代表优先级越高。

例如,设置 HelloServlet 对象在启动服务器端时被创建,示例代码如下。

然后重启项目查看控制台,如图 7-13 所示,发现 HelloServlet 对象在启动服务器端时被创建并进行了初始化。需要注意的是,在后续的请求发送到该 Servlet 时,直接执行 service () 方法,不再执行对象创建和初始化操作。

cb58a6cf7f02c4f2c733a1ee078d788a5106915ed8ee13086687e7278c91a4fb.jpg

7.3 Servlet 的体系结构

为了封装不同方法,扩展了不同的 Servlet 接口,其中 GenericServlet 实现了 Servlet 接口,而 HttpServlet 又继承了 GenericServlet 类。开发人员创建属于自己的 Servlet 类,只需继承 HttpServlet 即可。Servlet 体系结构如图 7-14 所示。

下面分别介绍 GenericServlet 抽象类和 HttpServlet 抽象类。

7.3.1 GenericServlet 类

GenericServlet 是 Servlet 的实现类,对 Servlet 接口的功能进行了封装和完善,重写了 init (ServletConfig config) 方法,可以用来获取 ServletConfig 对象。

值得注意的是,如果此时 GenericServlet 的子类 (通常是自定义 Servlet) 又重写了 init (ServletConfig config) 方法,有可能导致无法获取 ServletConfig 对象,因此子类不应该重写带参数的 init () 方法。如果想要进行初始化操作,可以重写 GenericServlet 提供的无参的 init () 方法,这样不会影响 ServletConfig 对象的获取。

另外,该类中将 service (ServletRequest req, ServletResponse res) 仍然保留为抽象方法,让使用者仅关心业务实现即可。GenericServlet 类包含的方法如图 7-15 所示。

9d0653d0905cef874dd7890dc5d1d352b44895249011efb02805bf7f945ac190.jpg

c68019bba782032912ed9faf21bc0aceb33111271550d83877480e0912038125.jpg

查看 GenericServlet 部分源码,以下是 GenericServlet 对 HttpServlet 类中五个抽象方法的处理(其他方法省略)。

7.3.2 HttpServlet 类

HttpServlet 继承自 GenericServlet 是专门用来处理 HTTP 请求的 Servlet。对 GenericServlet 实现进一步的封装和扩展,在 service (ServletRequest req, ServletResponse res) 方法中,将 ServletRequest 对象和 ServletResponse 对象转换为 HttpServletRequest 对象和 HttpServletResponse 对象,根据不同 HTTP 请求类型调用专门的方法进行处理。

HttpServlet 类包含的方法如图 7-16 所示。

a5c1df34b9db2ceea561032aa637698fd0a3f219f730b2a5ec09c93029ceba9f.jpg

今后在实际应用开发中,只需继承 HttpServlet 抽象类从而创建属于开发人员自己的 Servlet 实现类,然后重写 doGet (HttpServletRequest req, HttpServletResponse resp) 和 doPost (HttpServletRequest req, HttpServletResponse resp) 方法实现具体的请求处理,不再需要重写 service (ServletRequest req, ServletResponse res) 方法,如图 7-17 所示。

cf87a8b8f09224508278f92bf7fd8b84f889a1597ac5038f62be2d1a6b2c46c5.jpg

又因为在实际业务中,对于 GET 和 POST 的处理方式都是一样的,所以我们只需要实现其中一种方法即可,另外一种方法中则直接调用写好的前一种方法(doXxx (req,resp);)。另外 web.xml 配置与之前还是一样的,无须更改。示例代码如下。

7.4 Servlet 注解开发

因为 Servlet 属于动态资源,所以我们需要为 Servlet 设置映射路径,除了在配置文件中配置 标签设置映射路径,我们还可以通过注解的方式设置映射路径。具体步骤如下。

AnnotationServlet 类的示例代码如下。

在 AnnotationServlet 类上添加 “@WebSocket (value="/annotationServlet")” 注解后,通过访问 “http://localhost:8080 / 上下文路径 /annotationServlet” 路径,便可以成功调用 AnnotationServlet 类的 doGet () 方法。由此也说明,通过注解设置映射路径和通过配置文件设置映射路径具有同等效果。需要注意的是,对于同一个 Servlet,注解方式和配置文件方式只能二选一。而且相对来说,注解方式更为方便,因此接下来对于 Servlet 类的映射路径均采用注解方式设置。

另外,IDEA 开发工具为我们提供了创建 Servlet 的模板,步骤如下。

右击 Servlet 类所在的文件夹,选择 “New”,然后再选择 “Servlet”,进入 “New Servlet” 新窗口,如图 7-18 所示。

在 “New Servlet” 新窗口,设置 Servlet 的类名、所在包,并勾选注解方式,如图 7-19 所示。

然后单击 “OK” 即可创建完成,MyServlet 类的示例代码如下。

4a83330fd9196ef500461bd4c523eb90534f78bae46bbb5bf38cf268e56e611f.jpg

d03af85df3bc4f640f49942c42e599f8ce49aede745ae92be7e0660d84185658.jpg

7.5 两个接口介绍

下面介绍与 Servlet 相关的两个重要接口,分别为 ServletConfig 接口和 ServletContext 接口。ServletConfig 封装了当前 Servlet 配置信息,而 ServletContext 代表 Web 应用。

7.5.1 ServletConfig 接口

一个 Servlet 对应唯一的 ServletConfig 对象,封装当前 Servlet 配置信息。ServletConfig 接口包含的方法如图 7-20 所示。

39427c3cddacb7250dfc6452beed08007d31c347cd6ba8d24a744fe13f1a5f63.jpg

ServletConfig 对象由 Servlet 容器(如 Tomcat)创建,并传入生命周期方法 init (ServletConfig config) 中,然后便可以直接获取使用。值得注意的是,当前 Web 应用的 ServletContext 对象也封装到了 ServletConfig 对象中,使 ServletConfig 对象成为获取 ServletContext 对象的一座桥梁。另外,通过 ServletConfig 对象还可以获取 Servlet 名称,以及 Servlet 初始化参数等。下面通过代码分别演示 ServletConfig 对象的功能。

(1)获得 Servlet 的名字,实际上获取的是 web.xml 文件中 标签内的值。

(2)获得 ServletContext 对象,ServletContext 对象相关内容将在 7.5.2 节展开介绍。

//3. 输出展示结果

(3)获得当前 Servlet 的初始化参数。在 web.xml 文件中借助 标签配置初始化参数,示例代码如下。

值得注意的是,设置初始化参数 标签的位置必须在 标签前面。通过 ServletConfig 对象的 getInitParameter () 方法,来获取初始化参数 url 和 driverClass 对应的值,示例代码如下。

7.5.2 ServletContext 接口

Web 容器在启动时,会为每个 Web 应用程序创建一个唯一对应的 ServletContext 对象,意思是 Servlet 上下文,代表当前 Web 应用。一个 Web 应用程序中的所有 Servlet 都共享同一个 ServletContext 对象,也因此 ServletContext 对象被称为 Application 对象(Web 应用程序对象)。

ServletContext 对象由 Servlet 容器在项目启动时创建,在项目卸载时销毁。它可以通过 ServletConfig 对象的 getServletContext () 方法获取,也可以通过 service () 方法的参数 ServletRequest 对象获取。ServletContext 接口包含的方法如图 7-21 所示。

dc014213b34b7866830234d72031f74171da37bd1ffe18202c79c5f9f8ac3374.jpg

ServletContext 对象的获取方式包含四种,分别是通过 ServletConfig 对象、当前 Servlet 对象、根据 Session,以及 HttpServletRequest 对象获取,示例代码如下。

需要注意的是,不管通过哪种方式获取的都是同一个 ServletContext 对象,因为一个 Web 应用只有一个 ServletContext 对象。

下面通过代码来演示 ServletContext 对象的常用功能,具体如下。

(1)获取项目的上下文路径,示例代码如下。

b9b54f3ce1fe0ebea015677a6058cf0392f59046633492494bc53f87e9d0b409.jpg

上下文路径即部署在服务器端上的项目名,可以手动设置该值,如图 7-22 所示,单击 “Edit Configurations”。

进入 “Run/Debug Configurations” 新窗口,选择 “Deployment” 选项,便可设置 “Application context”,对于 chapter07_servlet 项目的上下文路径默认值为 “/chapter07_servlet_war_exploded”,如图 7-23 所示。

另外,从图 7-23 中还可以看到 “chapter07_servlet:war exploded” WAR 包,指的是编译后的项目。对此,我们也可以手动添加或删除。

2a3af5a61f6e2a00403f78a01d2ea35b56ccfa2b6811211920f2ff08af14a3fa.jpg

(2)通过 getRealPath (String path) 方法,获取虚拟路径映射的本地真实路径。虚拟路径是指浏览器访问 Web 应用中某个资源时所使用的路径。本地路径是指资源在文件系统中的实际保存路径。

(3)获取 Web 应用程序的全局初始化参数。

首先在 web.xml 文件的根标签下设置 Web 应用初始化参数,如下所示。

然后通过 getInitParameter () 方法,根据参数名获取 Web 应用初始化参数,示例代码如下。

(4)ServletContext 对象还可以作为最大的域对象,在整个项目的不同 Web 资源内共享数据。域对象的作用是实现在一定作用域范围内,起到共享数据的目的。Servlet 包括三类域对象,分别为应用域对象、会话域对象和请求域对象。ServletContext 对象属于应用域对象,请求域对象是 HttpServletRequest 请求对象,将在 7.6 节展开介绍。而会话域对象,比如 HttpSession 对象,将在第 9 章展开介绍。

ServletContext 对象作为域对象共享数据所涉及的方法,如图 7-24 所示。

f4f3fc015089b2ad177d9fd1f2bc11632630461074715c71379f130784ac0c38.jpg

其中,setAttribute (key,value) 方法为 ServletContext 对象设置属性,该属性可以在任意位置取出并使用。getAttribute (key) 方法根据 key 值获取对应的属性值。removeAttribute (key) 方法根据 key 值移除该属性。

下面通过创建三个 Servlet,分别实现设置共享数据、获取共享数据,以及移除共享数据操作,来演示 ServletContext 应用域对象在不同 Servlet 间传递数据。

创建 SetMsgServlet 实现在 ServletContext 应用域对象中设置属性,示例代码如下。

创建 GetMsgServlet 实现从 ServletContext 应用域对象中通过属性名获取属性,示例代码如下。

创建 RemoveMsgServlet 实现从 ServletContext 应用域对象中通过属性名移除属性,示例代码如下。

在 index.html 中编写如下代码,单击超链接访问其对应的 Servlet。

启动项目测试应用域对象共享数据的首页面如图 7-25 所示。

单击 “单击访问设置共享数据的 Servlet”,然后返回首页再次单击 “单击访问获取共享数据的 Servlet”,查看控制台,如图 7-26 所示。

可以看出,首先访问 SetMsgServlet 类向 ServletContext 应用域中设置属性,并且在 GetMsgServlet 类中成功通过该域对象获取其属性值,实现了 SetMsgServlet 和 GetMsgServlet 之间的数据传递。

9c6d4ba18a5c4cce6ef70d63abfd0b6e2a27cb950c5d426597f1f98970df044a.jpg

a805975f969fbb833714be16d089cb9969160707ef981b8809ff5f5c11b80c17.jpg

如果没有提前在 SetMsgServlet 类中设置数据,就直接获取 ServletContext 应用域中数据的话,肯定是获取不到的。例如,直接单击 “单击访问获取共享数据的 Servlet”,然后查看控制台,如图 7-27 所示。

b51c6a09c8b492364f43bbc9f5d9bc5439d1f446ea5184ea12573912e5f85a95.jpg

从图 7-27 中可知,获取的结果为 “null”,表示该值不存在,获取失败。

最后,使用完数据后,为了节省空间资源,我们还可以手动从 ServletContext 应用域中移除该值。

单击移除原来设置的数据后,再次单击 “单击访问获取共享数据的 Servlet”,并查看控制台,如图 7-28 所示。

4d489eb17c077e9c86e78c8744feb510e5ec150162182321f16e7236d71e391a.jpg

从图中可知,在移除数据后,再次获取数据的结果为 “null”,表示原来的数据已经不存在了。

7.6 请求与响应

前面介绍了 Servlet 的体系结构、生命周期等内容,让我们对 Servlet 有了一定的了解,接下来继续学习 Servlet 是如何处理请求和做出响应的,主要用到两个重要接口,一个是 HttpServletRequest,用来处理请求;另一个是 HttpServletResponse,用来处理响应。

7.6.1 HttpServletRequest 处理请求

<|box_end|><|ref_start|> 中华民族 HttpServletResponse 接口是 ServletRequest 接口的子接口,封装了 HTTP 请求的相关信息。浏览器请求服务器端时,会封装请求报文交给服务器端,服务器端接收到请求后,将请求报文解析生成 HttpServletRequest 接口的实现类对象,简称 HttpServletResponse 对象。该对象由 Servlet 容器创建,同时将传入 HttpServletResponse 类的 doGet (HttpServletRequest req, HttpServletResponse res) 或者 doPost (HttpServletRequest req, HttpServletResponse res) 方法中。中华民族接口包含的方法,如图 7-29 所示。

通过 HttpServletRequest 可以从请求报文中获取数据,比如获取 URL 地址参数,包括 IP 地址、端口号、协议、上下文路径等,以及获取请求头信息、请求参数等。

0b8f3ad40eb7184178ba0dae46107b8ce86e71651d587a9b9dd02b2d930d567e.jpg

(1)获取 URL 地址参数,比如获取主机名、端口号、协议,以及上下文路径,示例代码如下。

// 获取上下文路径(重要)

(2)获取请求头信息,比如获取 User-Agent 和 Referer 信息,示例代码如下。

// 获取 User-Agent 信息

(3)获取请求方式,目前请求方式只有 GET 和 POST,示例代码如下。

(4)获取请求参数。

请求参数就是浏览器向服务器端提交的数据。那么,浏览器如何向服务器端发送数据呢?

对于 GET 请求,请求参数通常拼接在 URL 后面。而对于 POST 请求,请求参数通常会放到请求体中。例如,使用表单进行请求参数提交。

使用 HttpServletRequest 对象获取请求参数,借助 getParameter () 和 getParameterValues (String name) 方法实现,示例代码如下。

(5) 请求的转发。

转发是进行页面跳转的一种方式,可以从一个 Servlet 跳转至另一个 Servlet,也可以跳转至其他页面。

例如,创建 FirstServlet 类实现将请求转发至 SecondServlet 类,示例代码如下。

例如,创建 SecondServlet 类将请求转发至 success.html 页面,示例代码如下。

(6) HttpServletRequest 对象作为请求域对象共享数据。

<|box_end|><|ref_start|> 中华民族网络请求对象可以作为域对象,称为请求域对象,它的作用范围为一次请求,所有数据在本次请求内有效,一旦本次请求结束,请求中的所有数据也就消失了。

值得注意的是,请求的转发发生在一次请求内。例如,FirstServlet 共享 HttpServletRequest 对象中的数据到 SecondServlet 或者 success.html,必须是转发的关系。操作域对象中数据的常用方法如下。

示例代码如下。

需要注意的是,如果是 Servlet 转发到网页时请求域中携带了共享数据,想要在网页上展示共享数据内容,暂时还不能实现,学完第 8 章才可以实现该需求。

7.6.2 案例:表单提交

下面通过一个表单提交功能来练习 HttpServletRequest 对象的使用。

创建一个表单,内容包括 username、password、email 和 hobby 等信息,实现单击 “提交” 按钮,将表单数据提交到后端 Servlet 进行处理,并封装所有数据到一个 user 对象。

首先在 index.html 页面中,编写代码填写如下表单信息。

创建 User 类对应表单信息,示例代码如下。

创建 RequestTestServlet 类处理表单,实现将其数据加入 user 对象中,示例代码如下。

启动项目填写表单如图 7-30 所示,然后提交数据后查看控制台,如图 7-31 所示。

2e32339b4683a3ec2fafea566bbe4a6c18ec6c5c22a2d14cbc5e8eab45281a49.jpg

8b63a21eea56513227ae9691d9d4697cfea48a57a65a4c3569342d76999a3f2e.jpg

由于 id 没有传值,结果为 null,其余参数都成功赋值给 user 对象。

另外,获取参数还有一种更简便的方式,即借助 getParameterMap () 方法,将所有请求参数存储在 map 集合中。

传统方式下,map 集合中的数据是按照上述代码依次遍历的,但相对来说过于烦琐,为此引入第三方工具类 BeanUtils,自动将 map 集合中的数据映射到对应的 JavaBean 对象中。不过前提条件是,必须保证 map 集合的 key 值和 JavaBean 对象的属性名一一对应。

使用第三方工具类 BeanUtils,首先需要引入其 JAR 包,并放入到 web/WEB-INF/lib 目录下,如图 7-32 所示。单击右键,将所有 JAR 包添加为 “Add as Library”,这样才能保证 JAR 包起作用,如图 7-33 所示。

29eb7a530929d00d8982ab6da8ddcd3648ba09aa2d66ddd7aaf636dfdaacbb4c.jpg

23dd77a718df99ed6c47cd5eb8b5238acc2803f391f688e05ca10890b15d38fb.jpg

然后修改 RequestTestServlet 的 doGet () 方法,借助 BeanUtils 工具类重新实现将请求参数保存到 user 对象中,示例代码如下。

启动项目,再次填写表单后查看控制台,如图 7-34 所示。

8f26f81794f5b6cda887e35f252be39c52b0205c81748c8d6dea25aefeff991b.jpg

发现,同样实现将请求参数成功保存到 user 对象中。

7.6.3 HttpServletResponse 处理响应

HttpServletResponse 接口是 ServletResponse 接口的子接口,封装了服务器端针对 HTTP 响应的相关信息。HttpServletResponse 接口的实体类对象,简称 HttpServletResponse 对象,同样是由 Servlet 容器创建,并传入 HttpServlet 类的 service (HttpServletRequest req, HttpServletResponse res) 方法中。HttpServletResponse 接口包含的方法如图 7-35 所示。

HttpServletResponse 对象的主要功能,具体如下。

688cad251c58b7ccb9eab29dc1ea9df5aae7f347b6ded500eea000104cd895b4.jpg

(1)通过输出流方式向浏览器输出数据。

通过输出流方式向浏览器输出数据,一般情况下,请求来自哪里就在哪里输出数据。值得注意的是,如果浏览器作为客户端,不会使用这种方式作为响应结果,而是采用转发或者重定向(本节将会对重定向展开介绍)。

其中,写出的数据可以是页面、页面片段、字符串等。当写出的数据包含中文时,浏览器接收到的响应数据就可能有乱码。为了避免乱码,可以使用 HttpServletRequest 对象在向浏览器输出数据前设置响应头。响应头就是浏览器解析页面的配置。

(2) 设置响应头信息。

例如,告诉浏览器使用哪种编码和文件格式解析响应体内容,示例代码如下。

设置好以后,在浏览器的响应报文中可以查看到设置的响应头中的信息。如图 7-36 所示。

(3) 重定向。

重定向和转发类似,也是进行页面跳转的一种方式,使用重定向同样可以实现从一个 Servlet 跳转到另一个 Servlet,也可以跳转到其他页面。

44813f6df5e79f2b218bb8304c9aaa24b46ced9af88793211ddf131a1720676f.jpg

例如,将请求重定向到 SecondServlet 页面,示例代码如下。

例如,将请求重定向到 success.html 页面,示例代码如下。

注意路径问题,加上 “/” 会导致失败,因为转发以 “/” 开始表示项目根路径,重定向以 “/” 开始表示主机地址,而重定向一般需要加上项目名。另外,在 7.8 节将会详细介绍 “/” 的含义。

7.6.4 转发和重定向的区别

请求的转发与重定向都是 Web 应用页面跳转的主要手段,在 Web 应用中使用非常广泛。我们一定要

57ebdb3e8832dfc8e03d1e0a865132adc2cb16a663ae87189999b29215a1d4e7.jpg

搞清楚两者的区别,如图 7-37 所示。

对于转发来说,图 7-37 中第一个 Servlet 接收到了浏览器端的请求,进行了一定的处理,然后没有立即对请求进行响应,而是将请求 “交给下一个 Servlet” 继续处理,下一个 Servlet 处理完成后,对浏览器进行了响应。

在服务器端内部将请求 “交给” 其他组件继续处理称为请求的转发。对浏览器来说,一共只发了一次请求,服务器端内部进行的 “转发” 浏览器感觉不到,同时浏览器地址栏中的地址不会变成 “下一个 Servlet” 的虚拟路径。请求转发的过程如图 7-38 所示。

综上所述,在转发的情况下,两个 Servlet 可以共享同一个

<|box_end|><|ref_start|> 中华民族对象中保存的数据,还可以直接访问 WEB-INF 下的资源。

而对于重定向而言,图 7-37 中 Servlet1 接收到了浏览器端的请求,进行了一定的处理,然后给浏览器一个特殊的响应消息,这个特殊的响应消息会通知浏览器去访问另外一个资源,这个动作是服务器和浏览器自动完成的。整个过程中浏览器端会发出两次请求,且在浏览器地址栏里面能够看到地址的改变,改变为下一个资源的地址。请求重定向的过程如图 7-39 所示。

ad6501d7f0bca87bf81259a49bb4435f78c100ab9e374751e721d70ae589b667.jpg

dcb3b49d26a2d2b7c8503ed73915fdb58137383e8ecbdb72d0304aad383b2ffb.jpg

重定向的情况下,原 Servlet 和目标资源之间就不能共享请求域数据了。请求转发和请求重定向的区别如表 7-2 所示。

表 7-2 请求转发和请求重定向的区别

转发重定向
浏览器感知在服务器端内部完成,浏览器感知不到服务器端以302状态码通知浏览器访问新地址,浏览器有感知
浏览器地址栏不改变改变
整个过程发送请求次数一次两次
能否共享HttpServletRequest对象数据不能
WEB-INF下的资源能够访问不能访问
目标资源必须是当前Web应用中的资源不局限于当前Web应用

值得重点关注的是,浏览器不能访问服务器端 WEB-INF 下的资源,而服务器端是可以访问的。因此转发可以转发到 WEB-INF 下的资源,但重定向是不能重定向到 WEB-INF 下的资源的。

7.7 字符编码问题

Web 程序在接收请求并处理过程中,如果不注意编码格式及解码格式,很容易导致中文乱码,引起这个问题的原因到底在哪里?又该如何解决呢?本节将带领大家讨论此问题。

提到中文乱码问题,我们先来说一说什么是字符集,字符集是指各种字符的集合,包括汉字,英文,标点符号等。各国都有不同的文字、符号,这些文字符号的集合就叫字符集。现有的字符集包括 ASCII、GB2312、BIG5、GB18030、Unicode、ISO-8859-1 等。这些字符集,集合了很多的字符,然而,字符要以二进制的形式存储在计算机中,我们就需要对其进行编码,将编码后的二进制存入。取出时我们就要对其解码,将二进制解码成我们之前的字符。这时我们就需要制定一套编码解码标准,否则就会导致出现混乱,也就是前面所提到的乱码问题。

编码指将字符转换为二进制数,不同的编码方式对应不同的二进制结果,如表 7-3 所示。

表 7-3 汉字 “中” 的不同编码

汉字编码方式编码二进制
'中'GB2312D6D01101 0110-1101 0000
'中'UTF-164E2D0100 1110-0010 1101
'中'UTF-8E4B8AD1110 0100-1011 1000-1010 1101

解码则正好和编码相反,即将二进制数转换为字符的过程,如将二进制 “1110 0100-1011 1000-1010 1101” 根据 E4B8AD 方式解码,转换为汉字 “中”。

如果一段文本,使用 A 字符集编码,并使用 B 字符集解码,就会产生乱码。如图 7-40 所示,汉字 “中” 使用 “UTF-8” 字符集编码,而后使用 GBK 方式解码就出现了中文乱码问题。

1ff311ef6ad358bebda05a0cc48d0ac1cb753e82e0c29c10a21f9262d1b0227b.jpg

解决乱码问题的根本方法就是统一编码和解码的字符集,如图 7-41 所示。

乱码问题分为请求乱码和响应乱码,下面分别介绍这两种情况。

1. 请求乱码问题

首先需要声明一下,对于 GET 请求使用 Tomcat8 之前的版本可能会出现中文乱码问题,本书基于 Tomcat8.5.27, 输入中文时并不会出现乱码问题。而对于 POST 请求,输入中文都会出现乱码问题。下面通过代码进行演示。

在 index.html 文件中,编写代码创建一个表单,并且设置请求方式为 GET 请求,示例代码如下。

创建 CharSetTestServlet 类编写代码,测试中文乱码问题,示例代码如下。

084b06f15a0091c86c588e1600ab5ed91c21ee968a2fa29e2bd99e0067002606.jpg

运行代码查看效果,并输入中文进行测试,如图 7-42 所示。

b7df2c2710935e02ec53769f2efa7c0b9b13bb8620f075b251a7bdb9bbd8d711.jpg

查看控制台,如图 7-43 所示。修改提交表单的请求方式为 “POST”,再次运行代码,输入中文,查看控制台,如图 7-44 所示,发现中文乱码。

a3e2037d738d7c3b6ab249e8f60c9fef0f3fe7374944891db31831b808882737.jpg

832e27ae2bf0cb7c222d93ca7b9bfb50f2d07f068743b050bb18cfa34a990144.jpg

另外,对于 GET 请求和 POST 请求,两种请求的乱码解决方式也不同。

对于 GET 请求,针对 Tomcat7 及以下版本需要手动处理,GET 请求参数是链接在地址后面的,我们需要修改 Tomcat 的配置文件进行设置。需要在 server.xml 文件修改 标签,添加 “URIEncoding="UTF-8"”。

/ >

配置好 URIEncoding 属性后,可以解决当前工作空间中所有的 GET 请求的乱码问题。

对于 POST 请求,如果提交了携带中文的请求体,服务器端解析时可能会出现乱码问题。解决方法就是在获取参数值之前,设置请求的解码格式,使其和页面保持一致。

值得注意的是,POST 请求乱码问题的解决,只适用于当前操作所在的类。不能类似于 GET 请求一样,可以统一解决。因为请求体有可能会上传文件,文件内容不一定都是中文字符,所以不同字符需要分别设置其解码格式。

2. 通过输出流输出中文乱码问题

修改 CharSetTestServlet 类,通过输出流方式响应结果,示例代码如下。

运行代码,提交表单后查看页面,如图 7-45 所示,响应的中文是乱码的状态。

f7aed8f3f515195429842a0bcdb6171459676ab48978676426b7533c1233a9e9.jpg

向浏览器发送响应时,要告诉浏览器使用的是哪个字符集,浏览器就会按照这种方式来解码。那么如何告诉浏览器响应内容的字符编码方式呢?其实操作起来很简单,示例代码如下。

再次修改 CharSetTestServlet 类,设置浏览器字符编码为 “UTF-8”,然后运行代码,提交表单后,查看浏览器响应的结果为正常中文,如图 7-46 所示。

bedccfa3919e3de8990a51f18fbebb376f4ed3312d0da6e1985a4066ea2b42e0.jpg

7.8 Web 项目的路径问题

通过前面的学习,可以发现 Web 项目中很多地方都用到了不同的路径。主要出现在以下四个位置,比如 web.xml 文件中 标签设置 Servlet 访问路径或 @WebSocket 注解设置 Servlet 访问路径、HTML 网页中超链接或表单等通过路径方式访问服务器端,以及请求的转发和重定向中通过路径方式查找下一个资源等。不同场景书写路径时也存在一些注意事项,接下来本节将对 Web 项目中路径问题展开介绍。

首先了解两个概念,URL 和 URI。URL,统一资源定位符,表示从网络环境中定位一个资源,例如 “http://localhost:8888/MyServlet/index.html”。URI,统一资源标识符,表示从服务器端定位一个资源,例如 “/MyServlet/index.html”。

路径的使用可以分为绝对路径和相对路径。如果路径前带有 “/”,表示为绝对路径,否则,则为相对路径。建议 Web 项目内所有使用路径的位置都采用绝对路径,因为如果使用相对路径的话,一旦发现文件位置变化,网页中的路径必须全部跟着改变,比较烦琐,不利于后期项目的扩展性。

这里的绝对路径其实就是前面所提到的 URL,而相对路径也是相对于 URL 而言的。而且 “/” 的含义也有两个,服务器和浏览器解析的结果不同,例如,对于 “http://localhost:8888/MyServlet/index.html” 路径,由服务器端解析,“/” 可以代表当前项目下,即 “http://localhost:8888/MyServlet/”。而由浏览器解析,“/” 可以代表当前服务器端下,即 “http://localhost:8888/”。一般情况下,web.xml 文件中或 @WebSocket 注解 value 属性中的带 “/” 的路径由服务器端解析,而网页中的带 “/” 的路径需要通过浏览器解析。

例如,在 MyServlet 项目中配置 UrlTestServlet 类的映射路径为 “/urlTest”,实际上完整的路径为 “http://localhost:8888/MyServlet/urlTest”。

在 index.html 页面中,编写代码使用超链接访问 UrlTestServlet,示例代码如下。

代码中使用的是相对路径,超链接中的 urlTest 路径前省略了 “./”。表示 url 中 index.html 的所在路径为 “http://localhost:8888/MyServlet/”。因此完整路径即 “http://localhost:8888/MyServlet/urlTest”,保证与 web.xml 文件中设置的映射路径完全一致。否则,访问失败。

例如,在 web 目录下创建一个 pages 目录,将 index.html 移动到 pages 目录下,index.html 页面中代码不变的情况下,再次单击 “访问 UrlTestServlet” 超链接,将访问失败。

因为,此时 “./” 代表的路径为 “http://localhost:8888/MyServlet/pages/”,拼接后的完整路径为 “http://localhost:8888/MyServlet/pages/urlTest”,但 web.xml 文件中设置的映射路径为 “http://localhost:8888/

MyServlet/urlTest”,两者不一致,所以访问失败。

那么此时,我们需要修改 index.html 页面中的访问路径为 “../urlTest”,才能够访问成功。

首先,根据 “./” 的含义可知 “../” 代表的含义是 url 中 index.html 的所在路径的上一级目录,即 “http://localhost:8888/MyServlet/”。因此拼接上之后得到的完成路径为 “http://localhost:8888/MyServlet/urlTest”,与 web.xml 中设置的映射路径相同,则访问成功。

网页中使用绝对路径访问 UrlTestServlet 类,如下代码所示,需要在 “/” 后添加上下文路径。

而且,无论 index.html 网页的位置是否发生变化,都不会影响使用该绝对路径访问 UrlTestServlet 类。

前面分别介绍了在 web.xml 中和网页中路径的应用,转发和重定向中路径的使用与上述所讲大同小异,下面依次展开介绍。

由于转发是服务器的行为,所以转发中路径出现的 “/” 由服务器解析,例如,转发到 ResonseTestServlet, 示例代码如下。

上述代码中,“/resonseTestServlet” 中的 “/” 表示当前项目下,如果该转发发生在 MyServlet 项目中,则完整路径为 “http://localhost:8888/MyServlet/resonseTestServlet”。转发到另一个页面,操作也是类似的,示例代码如下。

如果采用的是相对路径,示例代码如下。

“resonseTestServlet” 前省略了 “./”,由于 ResonseTestServlet 的映射路径为 “/resonseTestServlet”,所以完整路径仍为 “http://localhost:8888/MyServlet/resonseTestServlet”,可以访问成功。

重定向中使用相对路径访问页面与转发类似,示例代码如下。

但是,重定向如果使用绝对路径,路径中的 “/” 由浏览器解析,表示当前服务器端下。如果想要成功重定向到 ResponseTestServlet 类,需要在 “/” 后添加上下文路径,示例代码如下。

值得注意的是,为了保险起见在 Java 代码中最好动态获取上下文路径,不然一旦上下文路径发生改变,代码中则需要多处修改,示例代码如下。

7.9 JDBC 简介

相信读到这里,我们已经学习了 JavaSE,编写了 Java 程序,Java 代码能够实现将数据保存在变量、数组、集合等内存空间中,但这只是暂时保存,如果想要长期保存该怎么办呢?方法一,借助 IO 流将数据写入文件,不足之处是不方便管理数据以及维护数据的关系。方法二,借助数据库管理软件,比如 MySQL,相对来说它可以更方便地管理数据。

既然如此,我们是否可以集二者之所长,将其结合起来呢?即 Java 程序操作 MySQL,实现数据的存储和处理。也就是本节要介绍的内容,使用 JDBC 技术实现数据持久化,后期可以使用 MyBatis 等持久层框架,其底层仍然使用了 JDBC 技术。

JDBC 代表一组独立于任何数据库管理系统(DBMS)的 API,声明在 java.sql 与 javax.sql 包中,是 Sun(现 Oracle)公司提供的一组接口规范。它由各个数据库厂商来提供实现类,这些实现类的集合构成了数据库驱动 jar 包。

Java 程序调用 JDBC 技术,然后通过 JDBC 技术调用数据库实现数据持久化的全过程,如图 7-47 所示。

2bc1175082aa70bb88fa7b92427cf74873765fd4ac227c853160bc30d14a5d33.jpg

通过 Java 程序对 MySQL 进行操作,主要分为两部分:一是创建 Java 程序,并建立与 MySQL 数据库的连接;二是通过 JDBC 对 MySQL 数据库进行增删改查的操作。下面分别对其进行介绍。

7.9.1 Java 程序连接 MySQL 数据库

接下来为大家介绍如何借助 Java 程序连接 MySQL 数据库,具体步骤如下。

a32e5b191ad2b946098516e1648000b54d41cad5e176b2ab300c0e700212a407.jpg

- 在 MySQL 内创建 atguigu 数据库,通过编写 Java 代码实现与该数据库的连接。

chapter07_JdbcTest 项目的目录结构,如图 7-48 所示。

创建 JdbcTest 类,编写如下代码连接 atguigu 数据库,示例代码如下。

综上可知,连接数据库具体分为三步,首先加载驱动,然后设置参数,即指明 url、用户名和密码。其中 url 包括需要连接的数据库的类型 “mysql”、对应的 IP 地址 “localhost”、端口号 3306,以及数据库名字 “atguigu”。最后传递上述参数进行连接即可。

运行上述代码查看控制台,如图 7-49 所示,表示连接成功。

3ee936b4ad54f9eaeb820351a8ed206182ef1ba79c5e325d5324591336e25721.jpg

7.9.2 JDBC 进行增删改查

成功连接数据库并获取数据库连接对象后,下面继续介绍如何实现对 MySQL 数据库的增删改查操作。首先,在 atguigu 数据库中创建一个表,命名为 users,users 表结构如表 7-4 所示。

表 7-4 users 表结构

字段名称数据类型Key
用户编号idINTPRI
用户名称usernameVARCHAR(20)
密码passwordVARCHAR(50)
邮箱emailVARCHAR(50)

下面编写 Java 代码,借助 JDBC 技术实现对该表的数据进行增删改查操作。

1. 新增数据

创建 InsertTest 类,实现新增数据的操作,示例代码如下。

运行代码查看控制台,如图 7-50 所示。查看数据库的 users 表,发现表中多了一条记录,如图 7-51 所示。

599b7c0dfdfb45ee3029d800b608455f696ab4f75f8326dbbdc1bb0ce317e027.jpg

6e3cfaa5bdc592dc40ef7228f7e30be994702eae87ab47a1d746d4ab63881655.jpg

2. 更新数据

创建 UpdateTest 类,实现修改数据的操作,示例代码如下。

运行代码查看控制台,如图 7-52 所示。查看数据库 users 表,发现用户名由 “xiaoshang” 修改为

“shangguigu”,如图 7-53 所示。

b6fd3438150c38ba032b3d7541b37f75538ebdd4b1a54ef75ee8ff09fa39fa43.jpg

f3427c1201c6bf481ea037e42133a672f40b000e50d3fd9f16abfdaf6b067428.jpg

3. 查询数据

创建 QueryTest 类实现查询数据的操作,示例代码如下。

运行代码查看控制台,如图 7-54 所示,成功查询到 users 表中的数据。

9b519ca239c7be3ac5fad6e45fe955c4d259b6555215c78e34fc77c4fab623f7.jpg

4. 删除数据

创建 DeleteTest 类,实现删除数据的操作,示例代码如下。

运行代码,查看控制台,如图 7-55 所示。

查看数据库 users 表,发现表中的数据被删除了。再次启动 QueryTest 类进行查询,查看控制台,如图 7-56 所示,结果为空。

e7867ffc80e6e2c9b7515484a7652d99a6fbdb2c6eb306997cba2e27596fe1c2.jpg

efb999f9cbfaec942b6f1f56ca7669672ab9def4d1e67ca76c25d17ccd53f1da.jpg

7.9.3 Druid 数据库连接池

通过前面 JDBC 进行增删改查,我们可以发现一个现象,每次连接数据库都需要通过 DriverManager 获取新连接,而且用完就要抛弃断开释放资源。这样一来,连接的利用率较低,浪费比较严重。对于数据库服务器来说压力较大,数据库服务器和 Java 程序对连接数无法控制,很容易导致数据库服务器崩溃。本节所讲的数据库连接池是数据库连接对象的缓冲区,负责申请、分配管理,以及释放连接的操作。

首先,建立一个连接池,这个池中能容纳一定数量的连接对象。一开始,我们可以先替用户创建一些连接对象。当用户需要使用连接对象时就直接从池中获取,无须重新建立连接,这样也可以节省时间。用户使用完,再把连接对象放回连接池中,下次别人还可以接着使用。这样一来大大提高了连接的使用率。

如果当连接池中的现有连接对象都用完了,那么连接池可以向服务器申请新的连接对象放到池中,直到池中的连接数量达到 “最大连接数” 就不能再申请了。这时,如果有用户来获取池中的连接对象,没有拿到连接只能等待。

JDBC 的数据库连接池使用 “javax.sql.DataSource” 来表示,DataSource 只是一个接口,通常被称为数据源。该接口通常由服务器(如 Weblogic、WebSphere、Tomcat)提供实现,也有一些开源组织提供实现,

例如 DBCP、C3P0、BoneCP、Druid 等。

下面对 Druid 数据库连接池的具体使用展开介绍。

Druid 数据库连接池的使用很简单,具体步骤如下。

例如,在 chapter07_JdbcTest 项目中,引入 Druid 的依赖包,并创建 druid.properties 文件设置数据库信息。目录结构如图 7-57 所示。

9782b5032db7da497cbfaa5998d864ec8c280b7790d4f48f6629205ea98361b5.jpg

druid.properties 文件的示例代码如下。

创建 TestPool 类,编写代码实现从数据库连接池中获取数据库连接对象,示例代码如下。

运行代码后测试连接,查看控制台,如图 7-58 所示。从图中可知,成功通过 Druid 数据库连接池获取连接对象。

3d39b9482e5d3a12cc60a03002be1e62babdf8a69f315705337e6328f2fc1461.jpg

7.9.4 JDBCTools 的封装

经过 JDBC 的学习发现,所有对数据库的操作(增、删、改、查)都需要获取数据库连接,为了代码的简洁以及编写方便,我们可以封装一个工具类,专门提供数据库连接的获取和释放。

考虑后期多线程的问题,将 ThreadLocal 也一并封装进来,JDK 1.2 中提供了 java.lang.ThreadLocal,为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以简洁地编写出优美的多线程程序,通常用来在多线程中管理共享数据库连接、Session 等。

ThreadLocal 主要用于保存某个线程的共享变量。在 Java 中,每一个线程对象中都对应一个 ThreadLocalMap,其 key 值就是一个 ThreadLocal 对象,而 value 值是一个 Object 对象,即为该线程的共享变量,并且该 ThreadLocalMap 对象是通过 ThreadLocal 的 set 和 get 方法操作的。对于同一个 ThreadLocal,只能对其对应的变量进行 get、set 或 remove 操作,而不会影响其他线程的变量。

创建 JDBCTools 类,封装代码实现数据库连接的获取和释放,示例代码如下。

7.9.5 DBUtils 的使用

Apache 组织提供的一个开源 JDBC 工具类库 “commons-dbutils”,它是对 JDBC 的简单封装,学习成本极低,并且使用 DBUtils 工具能极大简化 JDBC 编码的工作量,同时也不会影响程序的性能。

其中 DBUtils 类库中有一个重要的类,即 QueryRunner 类,它封装了 SQL 的执行,可以实现增删改查

和批处理等操作,并且是线程安全的。我们需要掌握 QueryRunner 类的两个方法: update () 和 query () 方法。对于增删改操作使用 update () 方法,而查询操作使用 query () 方法。

下面,在 chapter07_JdbcTest 项目中引入 “commons-dbutils-1.4.jar”,如图 7-59 所示,演示 DBUtils 工具的使用,此处只介绍最基本的增删改查操作。

3772ce54a819b6d135e73432f29ca4637fbb3e0f29a4e15f27d344f248b6f0e5.jpg

新建 dbutils 目录,在该目录下创建 InsertTest 类,实现向数据库新增数据操作,示例代码如下。

运行代码后,查看控制台发现新增成功。

同理,在 dbutils 目录下创建 UpdateTest 类,实现修改数据操作,关键代码如下。

在 dbutils 目录下创建 DeleteTest 类,实现删除数据操作,关键代码如下。

查询数据比较复杂,需要使用 query () 方法,并且 DBUtils 工具可以将查询结果直接映射到实体类中,DBUtils 为我们提供了丰富的结果集处理器,接下来为大家演示常用的三种结果集处理器。

首先创建 User 实体类,要求该实体类的属性名与 users 表的列名保持一致,示例代码如下。

创建 QueryTest 类,分别测试以上三种不同的查询结果。

如果 sql 语句查询结果确定只有一条数据,则可以使用 BeanHandler,示例代码如下。

如果 sql 语句查询结果有多条数据,则可以使用 BeanListHandler,关键代码如下。

如果 sql 语句查询结果只有一个值,则可以使用 ScalarHandler,关键代码如下。

7.9.6 BaseDao 的封装

虽然 DBUtils 对数据库操作省略了大部分代码,但是我们依然可以对其进行再次封装,让代码的复用性再提升一个级别。

例如,创建 BaseDao 类,将最基础的增删改查方法封装进去,后期对数据的增删改查操作直接使用 BaseDao 即可。示例代码如下。

7.10 案例:用户注册和登录

下面通过实现用户登录和注册的过程,来演示 Servlet 和 DBUtils 的使用。

该案例借助尚好房项目的前端资料,可以根据前言提示自行下载,在此基础上进行如下代码的实现,前端页面可以直接从资料中复制。

创建 chapter07_login_register 项目,首页 index.html 关键代码如下,在首页创建超链接,实现单击 “登录” 跳转登录页面;单击 “注册” 跳转注册页面。

登录页 login.html 关键代码如下,创建表单,输入用户名和密码进行登录,提交表单后跳转后台 Servlet 进行处理,如果用户名未注册,单击 “立即注册” 跳转注册页面进行注册。

注册页 register.html 关键代码如下,创建表单,输入用户名、密码,以及昵称进行注册,提交表单后跳转后台 Servlet 进行处理,如果用户名已注册,单击 “立即登录” 跳转登录页面进行登录。

创建用户类 User,包含用户 ID、手机号码、密码和用户昵称四个属性,示例代码如下。

在 MySQL 数据库创建与 User 类属性一一对应的 users 表,然后创建 db.properties 文件,放在 resources 文件夹下,提供数据库的 url、用户名和密码等信息。示例代码如下。

导入连接数据库所需的 jar 包,如图 7-60 所示,以及工具类 JDBCTools 类、BaseDao 类和 MD5Util 类。

7963375550469600d9590bcad2955dfef4b3a41d1a9e2ac74a0820a73008410b.jpg

创建 UserDaoImpl 类继承 BaseDao 类实现 UserDao 接口,实现向数据库插入数据,以及根据手机号查询数据方法。示例代码如下。

创建 UserServiceImpl 类实现 UserService 接口,实现对登录和注册的校验,例如判断用户名是否重复,以及对密码进行加密等。

创建 LoginServlet 类,处理提交登录表单后的操作,实现登录成功跳转首页,登录失败则留在原页面。示例代码如下。

创建 RegisterServlet 类,处理提交注册表单后的操作,实现注册成功挑战登录页面,注册失败仍留在

原页面。示例代码如下。

创建完之后,chapter07_login_register 项目的目录结构如图 7-61 所示。

b8c4221cd62a76c8e099f24bdad9d1bf346a2fbaa47703e7363724662f114b81.jpg

启动项目,进入尚好房首页,如图 7-62 所示。

aa376262f5b8ca9ac00c1ab88818514a5175f1a4227ad4021324fe8081f8b06d.jpg

单击右上角的 “注册” 按钮,进入注册页面,输入如下信息进行注册,如图 7-63 所示。

1748430af2ab5f382f7a6c1f1c8b23d139b0e8e5d954680aa626b1caed055956.jpg

单击 “立即注册” 按钮跳转至登录页面,表示注册成功,查看数据库如图 7-64 所示,增添了一条记录。然后输入手机号码和密码进行登录,如图 7-65 所示。

1e37aef946afd56862e6cdd5223165079bb3cef753cd6f87185dd33232f54230.jpg

如果未注册直接登录,发现仍跳回至登录页面,就表示登录失败。至此实现了用户的登录和注册功能,即从前端页面输入信息,然后经过后端代码处理,根据处理结果跳转对应页面的全过程。

e6c7f89a73162cac3cbc23c5755f81014384320d7b085989edb1e7efda27561b.jpg

7.11 本章小结

本章通过入门案例介绍了 Servlet 的操作步骤,并结合 IDEA 工具讲解如何应用,以及 Servlet 注解开发。介绍了 Servlet 的生命周期,即 Servlet 对象从创建到销毁的过程。介绍了 Servlet 的体系结构,主要包括 GenericServlet 类和 HttpServlet 类,对 Servlet 接口的功能进行了不同的封装和完善。还介绍了 Servlet 的两个相关接口:ServletConfig 和 ServletContext。最后介绍了 Servlet 最主要的功能 —— 请求和响应,并了解到针对 HTTP 专门提供了两个接口处理其请求和响应,其中 HttpServletRequest 用来处理请求,HttpServletResponse 用来处理响应。另外,在 Web 项目编写过程中,还要注意路径问题和中文乱码问题等。希望大家理解、掌握 Servlet 相关知识,并能够灵活运用。