写点什么

Javaweb 核心之 servlet 详解

作者:楠羽
  • 2022 年 9 月 21 日
    福建
  • 本文字数:6377 字

    阅读完需:约 21 分钟

1 Servlet

1.1 Servlet 概述

Servlet 是 SUN 公司提供的一套规范,名称就叫 Servlet 规范,它也是 JavaEE 规范之一。我们可以像学习 Java 基础一样,通过 API 来学习 Servlet。这里需要注意的是,在我们之前 JDK 的 API 中是没有 Servlet 规范的相关内容,需要使用 JavaEE 的 API。目前在 Oracle 官网中的最新版本是JavaEE8,该网址中介绍了 JavaEE8 的一些新特性。当然,我们可以通过访问官方API,学习和查阅里面的内容。


打开官方 API 网址,在左上部分找到 javax.servlet 包,在左下部分找到 Servlet,如下图显示:



通过阅读 API,我们得到如下信息:


第一:Servlet 是一个运行在 web 服务端的 java 小程序


第二:它可以用于接收和响应客户端的请求


第三:要想实现 Servlet 功能,可以实现 Servlet 接口,继承 GenericServlet 或者 HttpServlet


第四:每次请求都会执行 service 方法


第五:Servlet 还支持配置


1.2 Servlet 入门

1.2.1 Servlet 编码步骤

1)编码步骤

第一步:前期准备-创建 JavaWeb 工程


第二步:编写一个普通类继承 GenericServlet 并重写 service 方法


第三步:在 web.xml 配置 Servlet

2)测试

在 Tomcat 中部署项目


在浏览器访问 Servlet


1.2.2 Servlet 执行过程分析

我们通过浏览器发送请求,请求首先到达 Tomcat 服务器,由服务器解析请求 URL,然后在部署的应用列表中找到我们的应用。接下来,在我们的应用中找应用里的 web.xml 配置文件,在 web.xml 中找到 FirstServlet 的配置,找到后执行 service 方法,最后由 FirstServlet 响应客户浏览器。整个过程如下图所示:



一句话总结执行过程:


浏览器——>Tomcat 服务器——>我们的应用——>应用中的 web.xml——>FirstServlet——>响应浏览器

1.2.3 Servlet 类视图

在《Tomcat 和 Http 协议》这天课程和刚才的入门案例中,我们都定义了自己的 Servlet,实现的方式都是选择继承 GenericServlet,在 Servlet 的 API 介绍中,它提出了我们除了继承 GenericServlet 外还可以继承 HttpServlet,通过查阅 servlet 的类视图,我们看到 GenericServlet 还有一个子类 HttpServlet。同时,在 service 方法中还有参数 ServletRequest 和 ServletResponse,它们的关系如下图所示:


1.2.4 Servlet 编写方式

1)编写方式说明

我们在实现 Servlet 功能时,可以选择以下三种方式:


第一种:实现 Servlet 接口,接口中的方法必须全部实现。


​ 使用此种方式,表示接口中的所有方法在需求方面都有重写的必要。此种方式支持最大程度的自定义。


第二种:继承 GenericServlet,service 方法必须重写,其他方可根据需求,选择性重写。


​ 使用此种方式,表示只在接收和响应客户端请求这方面有重写的需求,而其他方法可根据实际需求选择性重写,使我们的开发 Servlet 变得简单。但是,此种方式是和 HTTP 协议无关的。


第三种:继承 HttpServlet,它是 javax.servlet.http 包下的一个抽象类,是 GenericServlet 的子类。<b><font color='red'>如果我们选择继承 HttpServlet 时,只需要重写 doGet 和 doPost 方法,不要覆盖 service 方法。</font></b>


​ 使用此种方式,表示我们的请求和响应需要和 HTTP 协议相关。也就是说,我们是通过 HTTP 协议来访问的。那么每次请求和响应都符合 HTTP 协议的规范。请求的方式就是 HTTP 协议所支持的方式(目前我们只知道 GET 和 POST,而实际 HTTP 协议支持 7 种请求方式,GET POST PUT DELETE TRACE OPTIONS HEAD )。

2)HttpServlet 的使用细节

第一步:在入门案例的工程中创建一个 Servlet 继承 HttpServlet


<font color='red'>注意:不要重写任何方法</font>,如下图所示:



第二步:部署项目并测试访问


当我们在地址栏输入 ServletDemo2 的访问 URL 时,出现了访问错误,状态码是 405。提示信息是:方法不允许。


第三步:分析原因


得出 HttpServlet 的使用结论:


​ <b><font color='red'>我们继承了 HttpServlet,需要重写里面的 doGet 和 doPost 方法来接收 get 方式和 post 方式的请求。</font></b>


为了实现代码的可重用性,我们只需要在 doGet 或者 doPost 方法中一个里面提供具体功能即可,而另外的那个方法只需要调用提供了功能的方法。

1.3 Servlet 使用细节

1.3.1 Servlet 的生命周期

对象的生命周期,就是对象从生到死的过程,即:出生——活着——死亡。用更偏向 于开发的官方说法就是对象创建到销毁的过程。


出生:请求第一次到达 Servlet 时,对象就创建出来,并且初始化成功。只出生一次,就放到内存中。


活着:服务器提供服务的整个过程中,该对象一直存在,每次只是执行 service 方法。


死亡:当服务停止时,或者服务器宕机时,对象消亡。


通过分析 Servlet 的生命周期我们发现,它的实例化和初始化只会在请求第一次到达 Servlet 时执行,而销毁只会在 Tomcat 服务器停止时执行,由此我们得出一个结论,Servlet 对象只会创建一次,销毁一次。所以,Servlet 对象只有一个实例。如果一个对象实例在应用中是唯一的存在,那么我们就说它是单实例的,即运用了单例模式。

1.3.2 Servlet 的线程安全

由于 Servlet 运用了单例模式,即整个应用中只有一个实例对象,所以我们需要分析这个唯一的实例中的类成员是否线程安全。接下来,我们来看下面的的示例:


/*    Servlet线程安全 */public class ServletDemo04 extends HttpServlet{    //1.定义用户名成员变量    //private String username = null;
@Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = null; //synchronized (this) { //2.获取用户名 username = req.getParameter("username");
try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); }
//3.获取输出流对象 PrintWriter pw = resp.getWriter();
//4.响应给客户端浏览器 pw.print("welcome:" + username);
//5.关流 pw.close(); //} }
@Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req,resp); }}
复制代码


启动两个浏览器,输入不同的参数,访问之后发现输出的结果都是一样,所以出现线程安全问题



通过上面的测试我们发现,在 Servlet 中定义了类成员之后,多个浏览器都会共享类成员的数据。其实每一个浏览器端发送请求,就代表是一个线程,那么多个浏览器就是多个线程,所以测试的结果说明了多个线程会共享 Servlet 类成员中的数据,其中任何一个线程修改了数据,都会影响其他线程。因此,我们可以认为 Servlet 它不是线程安全的。


分析产生这个问题的根本原因,其实就是因为 Servlet 是单例,单例对象的类成员只会随类实例化时初始化一次,之后的操作都是改变,而不会重新初始化。


解决这个问题也非常简单,就是在 Servlet 中定义类成员要慎重。如果类成员是共用的,并且只会在初始化时赋值,其余时间都是获取的话,那么是没问题。如果类成员并非共用,或者每次使用都有可能对其赋值,那么就要考虑线程安全问题了,把它定义到 doGet 或者 doPost 方法里面去就可以了。

1.3.3 Servlet 的注意事项

1)映射 Servlet 的细节

Servlet 支持三种映射方式,以达到灵活配置的目的。


首先编写一个 Servlet,代码如下:


public class ServletDemo5 extends HttpServlet {
/** * doGet方法输出一句话 * @param req * @param resp * @throws ServletException * @throws IOException */ @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("ServletDemo5接收到了请求"); }
/** * 调用doGet方法 * @param req * @param resp * @throws ServletException * @throws IOException */ @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req,resp); }}
复制代码


第一种:指名道姓的方式


​ 此种方式,只有和映射配置一模一样时,Servlet 才会接收和响应来自客户端的请求。


​ 例如:映射为:/servletDemo5


​ 访问 URL:http://localhost:8585/servlet_demo/servletDemo5



第二种:/开头+通配符的方式


​ 此种方式,只要符合目录结构即可,不用考虑结尾是什么。


​ 例如:映射为:/servlet/*


​ 访问 URL:http://localhost:8585/servlet/itheima


​ http://localhost:8585/servlet/itcast.do


​ 这两个 URL 都可以。因为用的*,表示/servlet/后面的内容是什么都可以。



第三种:通配符+固定格式结尾


​ 此种方式,只要符合固定结尾格式即可,其前面的访问 URI 无须关心(注意协议,主机和端口必须正确)


​ 例如:映射为:*.do


​ 访问 URL:http://localhost:8585/servlet/itcast.do


​ http://localhost:8585/itheima.do


​ 这两个 URL 都可以方法。因为都是以.do 作为结尾,而前面用*号通配符配置的映射,所有无须关心。



通过测试我们发现,Servlet 支持多种配置方式,但是由此也引出了一个问题,当有两个及以上的 Servlet 映射都符合请求 URL 时,由谁来响应呢?注意:HTTP 协议的特征是一请求一响应的规则。那么有一个请求,必然有且只有一个响应。所以,我们接下来明确一下,多种映射规则的优先级。


先说结论:指名道姓的方式优先级最高,带有通配符的映射方式,有/的比没/的优先级高


所以,我们前面讲解的三种映射方式的优先级为:第一种>第二种>第三种。


演示代码如下:


public class ServletDemo6 extends HttpServlet {
/** * doGet方法输出一句话 * @param req * @param resp * @throws ServletException * @throws IOException */ @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("ServletDemo6接收到了请求"); }
/** * 调用doGet方法 * @param req * @param resp * @throws ServletException * @throws IOException */ @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req,resp); }}
复制代码


<!--配置ServletDemo6--><servlet>    <servlet-name>servletDemo6</servlet-name>    <servlet-class>com.itheima.web.servlet.ServletDemo6</servlet-class></servlet><servlet-mapping>    <servlet-name>servletDemo6</servlet-name>    <url-pattern>/*</url-pattern></servlet-mapping>
复制代码


运行结果如下:


2)多路径映射 Servlet

上一小节我们讲解了 Servlet 的多种映射方式,这一小节我们来介绍一下,一个 Servlet 的多种路径配置的支持。


它其实就是给一个 Servlet 配置多个访问映射,从而可以根据不同请求 URL 实现不同的功能。


首先,创建一个 Servlet:


public class ServletDemo7 extends HttpServlet {
/** * 根据不同的请求URL,做不同的处理规则 * @param req * @param resp * @throws ServletException * @throws IOException */ @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //1.获取当前请求的URI String uri = req.getRequestURI(); uri = uri.substring(uri.lastIndexOf("/"),uri.length()); //2.判断是1号请求还是2号请求 if("/servletDemo7".equals(uri)){ System.out.println("ServletDemo7执行1号请求的业务逻辑:商品单价7折显示"); }else if("/demo7".equals(uri)){ System.out.println("ServletDemo7执行2号请求的业务逻辑:商品单价8折显示"); }else { System.out.println("ServletDemo7执行基本业务逻辑:商品单价原价显示"); } }
/** * 调用doGet方法 * @param req * @param resp * @throws ServletException * @throws IOException */ @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req,resp); }}
复制代码


接下来,在 web.xml 配置 Servlet:


<!--配置ServletDemo7--><servlet>    <servlet-name>servletDemo7</servlet-name>    <servlet-class>com.itheima.web.servlet.ServletDemo7</servlet-class></servlet><!--映射路径1--><servlet-mapping>    <servlet-name>servletDemo7</servlet-name>    <url-pattern>/demo7</url-pattern></servlet-mapping><!--映射路径2--><servlet-mapping>    <servlet-name>servletDemo7</servlet-name>    <url-pattern>/servletDemo7</url-pattern></servlet-mapping><!--映射路径3--><servlet-mapping>    <servlet-name>servletDemo7</servlet-name>    <url-pattern>/servlet/*</url-pattern></servlet-mapping>
复制代码


最后,启动服务测试运行结果:


3)启动时创建 Servlet

我们前面讲解了 Servlet 的生命周期,Servlet 的创建默认情况下是请求第一次到达 Servlet 时创建的。但是我们都知道,Servlet 是单例的,也就是说在应用中只有唯一的一个实例,所以在 Tomcat 启动加载应用的时候就创建也是一个很好的选择。那么两者有什么区别呢?


  • 第一种:应用加载时创建 Servlet,它的优势是在服务器启动时,就把需要的对象都创建完成了,从而在使用的时候减少了创建对象的时间,提高了首次执行的效率。它的弊端也同样明显,因为在应用加载时就创建了 Servlet 对象,因此,导致内存中充斥着大量用不上的 Servlet 对象,造成了内存的浪费。

  • 第二种:请求第一次访问是创建 Servlet,它的优势就是减少了对服务器内存的浪费,因为那些一直没有被访问过的 Servlet 对象都没有创建,因此也提高了服务器的启动时间。而它的弊端就是,如果有一些要在应用加载时就做的初始化操作,它都没法完成,从而要考虑其他技术实现。


通过上面的描述,相信同学们都能分析得出何时采用第一种方式,何时采用第二种方式。就是当需要在应用加载就要完成一些工作时,就需要选择第一种方式。当有很多 Servlet 的使用时机并不确定是,就选择第二种方式。


在 web.xml 中是支持对 Servlet 的创建时机进行配置的,配置的方式如下:我们就以 ServletDemo3 为例。


<!--配置ServletDemo3--><servlet>    <servlet-name>servletDemo3</servlet-name>    <servlet-class>com.itheima.web.servlet.ServletDemo3</servlet-class>    <!--配置Servlet的创建顺序,当配置此标签时,Servlet就会改为应用加载时创建        配置项的取值只能是正整数(包括0),数值越小,表明创建的优先级越高    -->    <load-on-startup>1</load-on-startup></servlet><servlet-mapping>    <servlet-name>servletDemo3</servlet-name>    <url-pattern>/servletDemo3</url-pattern></servlet-mapping>
复制代码


4)默认 Servlet

默认 Servlet 是由服务器提供的一个 Servlet,它配置在 Tomcat 的 conf 目录下的 web.xml 中。如下图所示:



它的映射路径是<b><font color='red'><url-pattern>/<url-pattern></font></b>,我们在发送请求时,首先会在我们应用中的 web.xml 中查找映射配置,找到就执行,这块没有问题。但是当找不到对应的 Servlet 路径时,就去找默认的 Servlet,由默认 Servlet 处理。所以,一切都是 Servlet。

1.4 Servlet 关系总图


发布于: 刚刚阅读数: 6
用户头像

楠羽

关注

还未添加个人签名 2022.08.04 加入

还未添加个人简介

评论

发布
暂无评论
Javaweb核心之servlet详解_Servlet_楠羽_InfoQ写作社区