从 Servlet 的这几篇文章,其实我们就正式的迈入与 Web 交互的开发模式了,配合前面学习的 JDBC MySQL 等可以动态的实现很多功能。同时 Servlet 其实是后面 Spring 等框架的基础,所以不要跳过这部分去。

1. Setvlet基本概述

1.1 什么是Servlet ?

Servlet(Server Applet)是JavaServlet的简称,称为小服务程序或服务连接器,用Java编写的服务器端程序,具有独立于平台和协议的特性,主要功能在于交互式地浏览和生成数据,生成动态Web内容。

JavaWeb中,我们将会接触到三大组件(Servlet、Filter、Listener),Servlet 由服务器调用,处理服务器接收到的请求,即完成,接受请求数据 —> 处理请求 —> 完成响应,其本质就是一个实现了 Servlet 接口的java类
Servlet类由我们来写,但对象由服务器来创建,并且由服务器来调用相应的方法

1.2 Servlet用来做什么?

网络中比较常见的一些功能,例如登录,注册,这样存在交互的功能,而 Servlet 就可以帮助我们处理这些请求,可以说 Servlet 是 JavaWeb 知识中重要的知识点之一。
实现Servlet的方式

2. 实现Servlet有三种方式:

  • 实现 javax.servlet.Servlet 接口;
  • 继承 javax.servlet.GenericServlet类;
  • 继承 javax.servlet.http.HttpServlet类;

实际开发中,我们通常会选择继承 HttpServlet 类来完成我们的 Servlet,但认识 Servlet 接口这种方式也是很重要的,是我们入门知识中不可或缺的部分(不用不代表不需要了解)。

2.1 创建我们的第一个Servelt

我们创建一个web项目,选择对应的参数,我们所装的 JDK 为 1.8 版本,可以选择到 JavaEE8 的版本,对应versions也就是4.0,不过我们在这里选择市面上用的还是比较多的7版本
创建.png
创建一个Demo类实现Servlet接口,然后我们快速生成这个接口中未实现的方法,我们先暂时忽略Servlet中其他四个方法,只关心service()方法,因为它是用来处理请求的方法,我们在该方法内给出一条输出语句

  1. package cn.ideal.web.servlet;
  2. import javax.servlet.*;
  3. import java.io.IOException;
  4. public class ServeltDemo1 implements Servlet {
  5. // 初始化方法
  6. @Override
  7. public void init(ServletConfig servletConfig) throws ServletException {
  8. }
  9. // Servlet配置方法
  10. @Override
  11. public ServletConfig getServletConfig() {
  12. return null;
  13. }
  14. // 提供服务方法
  15. @Override
  16. public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
  17. System.out.println("理想二旬不止");
  18. }
  19. // Servlet信息方法
  20. @Override
  21. public String getServletInfo() {
  22. return null;
  23. }
  24. // 销毁方法
  25. @Override
  26. public void destroy() {
  27. }
  28. }

写完了一个最简单Servlet代码,但是如何在浏览器中可以访问到呢?我们就需要对 web/WEB-INF 下的 web.xml 进行配置,我们在<web-app></web-app>中加入以下代码(虽然后期有优化的方法,但是很推荐大家记忆下来)

  1. <servlet>
  2. <!--给这个Servlet起一个名字,一般与类名相同-->
  3. <servlet-name>ServletDemo1</servlet-name>
  4. <!--全类名-->
  5. <servlet-class>cn.ideal.web.servlet.ServeltDemo1</servlet-class>
  6. </servlet>
  7. <!--配置映射路径-->
  8. <servlet-mapping>
  9. <servlet-name>ServletDemo1</servlet-name>
  10. <!--外界访问的路径-->
  11. <url-pattern>/Demo1</url-pattern>
  12. </servlet-mapping>

现在我们根据我们在url-pattern中配置的路径来访问一下,在控制台中果然输出了,理想二旬不止这个字符串

2.2 web.xml的作用

趁热打铁,我们来简单分析一下这个 web.xml 的因由,其实在 web.xml 中配置 Servlet 的目的,就是把在浏览器中的访问路径与对应 Servlet 绑到一起,上面的例子中就是把访问路径:“/Demo1” 与 “cn.ideal.web.servlet.ServeltDemo1” 绑定到了一起

  1. <servlet></servlet> :指定 ServletDemo1 这个 Servlet 的名字为 ServletDemo1,一般此处与对应类同名
  2. <servlet-mapping></servlet=mapping> :设定访问的具体路径

而这两者又通过**<servlet-name></servlet-name>** 关联在一起

执行过程:

  1. 当服务器中接受到了浏览器的请求,解析URL路径,获取到Servlet的资源路径
  2. 寻找web.xml文件,找到 <url-pattern> 标签,寻找对应的全类名<servlet-class>
  3. Tomcat将字节码文件加载进内存,并且创建对象,调用其中的方法

所以我们需要知道:Servlet中的大多数方法均不由我们来创建和调用,均由Tomcat完成

3. Servlet 接口

3.1 生命周期简单概述

我将生命周期简单理解为这样几个过程:
生前——出生——服务——死亡——埋葬

  1. 生前:当 Tomcat 第一次访问 Servlet,Tomcat 会创建 Servlet 的实例
  2. 出生:Tomcat 会调用 init() 方法初始化这个对象
  3. 服务: 客户端访问 Servlet 的时候,service() 方法会被调用
  4. 死亡: 当 Tomcat 被关闭或者Servlet长时间不被使用,destroy() 方法会被调用
  5. 埋葬: destroy() 方法被调用后,Servlet 就等待垃圾回收(不轻易),有需要则用 init() 方法重新初始化

    3.2 生命周期详解

    3.2.1 生前

    服务器会在Servlet第一次被访问时,或者是在服务器启动时创建 Servlet。如果服务器启动时就创建 Servlet,那么还需要在 web.xml 文件中进行配置,也就是说默认情况下,Servlet是在第一次访问时由服务器创建的。

一个Servlet类型,服务器只创建一个实例对象:例如我们第一次访问<http://localhost:8080/Demo1>,服务器通过/Demo1就找到了cn.ideal.web.servlet.ServeltDemo1 ,服务器就会判断这个类型的Servlet是否创建过,若没有才通过反射来创建ServletDmoe1的实例,否则就直接使用已经存在的实例。

3.2.2 出生

在 Servlet 被创建后,服务器会立即调用 Servlet 的 void init(ServletConfig) 方法,而且一个 Servlet 的一生,这个方法只会被调用一次,我们可以把一些对 Servlet 的初始化工作放到方法中!

3.2.3 服务

当服务器每次接收到请求时,都会去调用 Servlet 的 service() 方法来处理请求。service() 方法是会被调用多次的,服务器接收到一次请求,就会调用 service() 方法一次,也正因为如此,所以我们才需要把处理请求的代码写到 service()方法中!

3.2.4 死亡及埋葬

当服务器关闭时 Servlet 也需要被销毁了,但是销毁之前,服务器会先调用 Servlet 中的 destroy() 方法,我们可以把一些释放资源的代码放到此处。

3.3 Servlet接口的三个类型

在这五个方法中,我们可以在参数中看到三个我们没有接触过的类型

  1. public void init(ServletConfig servletConfig) throws ServletException {}
  2. public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {}

也就是这个三个类型:ServletConfig、ServletRequest、ServletResponse

3.3.1 ServletConfig

ServletConfig是服务器创建的一个对象,然后传递到 Servlet 的 init() 方法中
下述方法中我们简单使用一下第一个 getServletName() 就可以了,后面的方法等我们学写了Context以及其他知识才能更好的理解

  1. // 获取Servlet在web.xml文件中的配置名称,即<servlet-name>指定的名称
  2. String getServletName()
  3. // 用来获取ServletContext对象
  4. ServletContext getServletContext()
  5. // 用来获取在web.xml中配置的初始化参数,通过参数名来获取参数值;
  6. String getInitParameter(String name)
  7. // 用来获取在web.xml中配置的所有初始化参数名称
  8. Enumeration getInitParameterNames()

3.3.2 ServletRequest & ServletResponse(重要!!!)

这两个类型出现在Servlet的service()方法中,分别代表着请求响应对象,并且两者的实例也都是由服务器创建的
但是我们想要做一个web应用,归根结底要和HTTP相挂钩,如果我们希望在service() 方法中使用HTTP相关的功能,可以把 ServletRequest 和 ServletResponse 强转成 HttpServletRequest 和HttpServletResponse
HttpServletRequest方法:

  1. // 获取指定请求参数的值;
  2. String getParameter(String paramName)
  3. // 获取请求方法,例如GET或POST
  4. String getMethod()
  5. // 获取指定请求头的值;
  6. String getHeader(String name)
  7. // 设置请求体的编码!
  8. /*
  9. GET没有请求体,所以这个方法只只对POST请求有效当调用
  10. 这个方法必须在调用getParameter()方法之前调用!
  11. 使用request.setCharacterEncoding(“utf-8”)之后,再通过getParameter()方法获取参数
  12. 时,参数值都已经通过了转码,即转换成了UTF-8编码
  13. */
  14. void setCharacterEncoding(String encoding)

HttpServletResponse方法:

  1. // 获取字符响应流,使用该流可以向客户端输出响应信息。
  2. PrintWriter getWriter()
  3. Egresponse.getWriter().print(“<h1>Just for test</h1>”);
  4. // 获取字节响应流,例如可实现向客户端响应一张图片
  5. ServletOutputStream getOutputStream()
  6. // 用来设置字符响应流的编码
  7. void setCharacterEncoding(String encoding)
  8. // 向客户端添加响应头信息
  9. void setHeader(String name, String value)
  10. EgsetHeader(“Refresh”, 3;url=http://www.xxx.com”) 表示三秒后自动刷新到该网址
  11. // 该方法是setHeader(“content-type”, “xxx”)的简便方法,即用来添加名为content-type响应头的方法
  12. /*
  13. content-type响应头用来设置响应数据的MIME类型,例如要向客户端响应jpg的图片,那么
  14. 可以setContentType(“image/jepg”),如果响应数据为文本类型,那么还要台同时设置编
  15. 码,例如setContentType(“text/html;chartset=utf-8”)表示响应数据类型为文本类型
  16. 中的html类型,并且该方法会调用setCharacterEncoding(“utf-8”)方法;
  17. */
  18. void setContentType(String contentType)
  19. //向客户端发送状态码,以及错误消息
  20. void sendError(int code, String errorMsg)

4. GenericServlet 类

A:通过查看这个类的源码可以知道,该类中只有一个方法需要实现,其他的方法已经均在源码中有了定义

  1. public abstract void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;

B:还需要提一提的两个方法就是 GenericServlet的init()方法

  1. public void init(ServletConfig config) throws ServletException {
  2. this.config = config;
  3. this.init();
  4. }
  5. public void init() throws ServletException {
  6. }

GenericServlet 类实现了Servlet 的 init (ServletConfig)方法,把参数 config 赋给了本类的成员 config,然后再调用本类自己的无参的 init() 方法
这个方法是 GenericServlet 自己的方法,而不是从 Servlet 继承下来的。当我们自定义 Servlet 时,如果想完成初始化作用就不要再重复 init(ServletConfig) 方法了,而是应该去重写init()方法。因为在 GenericServlet中的 init(ServletConfig) 方法中保存了 ServletConfig 对象,如果覆盖了保存 ServletConfig 的代码,那么就不能再使用 ServletConfig 了

C:实现了ServletConfig接口
GenericServlet还实现了ServletConfig接口,所以可以直接调用getInitParameter()、getServletContext()等ServletConfig的方法。

但是这个类我们仍然不是我们要讲的重点,我们接着看一下下一个类。

5. HttpServlet 类

5.1 概述

在上面我们实现 Servlet 接口,需要实现5个方法,十分麻烦,而HttpServlet类已经实现了Servlet接口的所有方法,编写Servlet时,只需要继承HttpServlet,重写你需要的方法即可,并且它提供了对HTTP请求的特殊支持,更加强大

5.2 service()方法

在 HttpServlet 的 service(ServletRequest,ServletResponse)方法中会把 ServletRequestServletResponse 强转成 HttpServletRequestHttpServletResponse

  1. // HttpServlet 源码节选
  2. public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
  3. HttpServletRequest request;
  4. HttpServletResponse response;
  5. try {
  6. request = (HttpServletRequest)req;
  7. response = (HttpServletResponse)res;
  8. } catch (ClassCastException var6) {
  9. throw new ServletException("non-HTTP request or response");
  10. }
  11. this.service(request, response);
  12. }

强转过后,然后调用 HttpServlet 类中提供的 service(HttpServletRequest,HttpServletResponse)方法,这是这个类本身的方法,而不是继承而来的,这说明我们在使用的时候,只需要覆盖 service(HttpServletRequest,HttpServletResponse) 就可以了,不需要再进行强转这个两个对象了
注意:其实还有更一步简化的步骤,也不必使用 service(HttpServletRequest,HttpServletResponse)

5.3 doGet() 和 doPost()

在 HttpServlet 的service(HttpServletRequest,HttpServletResponse) 方法会去判断这个请求是GET 还是 POST,如果是 GET 请求,就去调用类中的 doGet() 方法,如果是 POST 请求,就去调用 doPost() 方法,这说明我们在子类中去覆盖 doGet() 或 doPost() 方法就可以了

6. Servlet细节

6.1 线程安全问题

Servlet 只会被服务器创建一个实例对象,很多情况下,一个 Servlet 需要处理多个请求,显然,Servlet 虽然效率高,但也不是线程安全的。
所以我们不应该在 Servlet 中轻易创建成员变量,因为可能会存在多个线程同时对这个成员变量进行不同的操作
结论:不要在Servlet中创建成员!创建局部变量即可,可以创建无状态成员量,或者状态只为可读的成员

6.2 服务器启动时就创建Servlet

之前我们将生命周期的时候有说过,Servlet是在第一次访问时由服务器创建的,但我们可以通过在web.xml中对Servlet进行配置,使服务器启动时就创建Servlet

  1. <servlet>
  2. <servlet-name>ServletDemo1</servlet-name>
  3. <servlet-class>cn.ideal.web.ServletDemo1</servlet-class>
  4. <!--在<servlet>中配置<load-on-startup>,其中给出一个非负整数!-->
  5. <load-on-startup>0</load-on-startup>
  6. </servlet>

它的作用是确定服务器启动时创建Servlet的顺序

6.3 一个Servlet可以绑定多个URL

  1. <servlet-mapping>
  2. <servlet-name>Servlet</servlet-name>
  3. <url-pattern>/AServlet</url-pattern>
  4. <url-pattern>/BServlet</url-pattern>
  5. </servlet-mapping>

这样配置后无论访问/AServlet还是/BServlet,访问的都是AServlet

6.4 通配符匹配问题

<url-pattern>中可以使用通配符,也就是 “ * ” ,它可以匹配任何前缀或者后缀

  1. <!--路径匹配-->
  2. <url-pattern>/servlet/*<url-patter>:/servlet/a、/servlet/b,都匹配/servlet/*;
  3. <!--扩展名匹配-->
  4. <url-pattern>*.xx</url-pattern>:/abc/de.xx、/a.xx,都匹配*.xx;
  5. <!--什么都匹配-->
  6. <url-pattern>/*<url-pattern>:匹配所有URL;

通配符要么为前缀,要么为后缀,不能出现在URL中间位置,并且一个URL中最多只能出现一个通配符,如果存在更具体的地址,会优先访问具体的地址

7. ServletContext

7.1 概述

服务器会为每个web应用创建一个 ServletContext 对象,可以说它就代表着这个web站点,并且这个对象,在Tomcat启动时就创建,在Tomcat关闭时才会销毁

7.2 功能

所有Servlet都共享着一个ServletContext对象,所以ServletContext对象的作用是在整个Web应用的动态资源之间共享数据,也就是说不同的Servlet之间可以通过ServletContext进行通讯,从而共享数据

7.3 获取ServletContext对象

GenericServlet类有getServletContext()方法,所以可以直接使用this.getServletContext()来获取

  1. public class MyServlet implements Servlet {
  2. public void init(ServletConfig config) {
  3. ServletContext context = config.getServletContext();
  4. }
  5. }
  1. public class MyServlet extends HttpServlet {
  2. public void doGet(HttpServletRequest request, HttpServletResponse response) {
  3. ServletContext context = this.getServletContext();
  4. }
  5. }

7.4 域对象的功能

所有域对象都有存取数据的功能,可以将这种存储数据的方式看做,Map的方式
我们来看几个常见的用来操作数据的方法
存储

  1. // 用来存储一个对象,也可以称之为存储一个域属性
  2. void setAttribute(String name, Object value)
  3. EgservletContext.setAttribute(“xxx”, XXX”)
  4. // 在ServletContext中保存了一个域属性,域属性名称为xxx,域属性的值为XXX

获取

  1. // 用来获取ServletContext中的数据
  2. Object getAttribute(String name)
  3. // 获取名为xx的域属性
  4. EgString value = (String)servletContext.getAttribute(“xxx”);
  5. // 获取所有域属性的名称;
  6. Enumeration getAttributeNames()

移除

  1. // 用来移除ServletContext中的域属性
  2. void removeAttribute(String name)

访问量统计的小案例

  1. package cn.ideal.web.servlet;
  2. import javax.servlet.*;
  3. import java.io.IOException;
  4. public class ServletDemo2 extends GenericServlet {
  5. @Override
  6. public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
  7. // 获取ServletContext对象
  8. ServletContext servletContext = this.getServletContext();
  9. // 获取ServletContext对象中的count属性
  10. Integer count = (Integer) servletContext.getAttribute("count");
  11. if (count == null) {
  12. // 如果在ServletContext中不存在count属性,name设置为count的值为1,表示第一次访问
  13. count = 1;
  14. } else {
  15. // 如果在Servlet中存在count属性,说明以前被访问过,name让count在原来的基础上加1
  16. count++;
  17. }
  18. servletResponse.setContentType("text/html;charset=UTF-8");
  19. // 向客户端响应本页面被访问的次数
  20. servletResponse.getWriter().print("<h1>本页面一共访问" + count + "次</h1>");
  21. // 保存count的值到ServletContext对象中
  22. servletContext.setAttribute("count", count);
  23. }
  24. }

8. 获取资源相关方法

8.1 获取路径

使用 ServletContext 对象可以用来获取 Web 应用下的资源,例如在一个 web 应用的根目录下创建 aaa.txt 文件,WEB-INF 目录下创建 bbb.txt 文件,如果我们想要通过 Servlet 获取这两者的路径就可以这样来写

  1. //获取aaa.txt的路径
  2. String realPath = servletContext.getRealPath(“/aaa.txt”)
  3. //获取bbb.txt的路径
  4. String realPath = servletContext.getRealPath(“/WEB-INF/b.txt”)

获取单个文件路径是这样,我们还有一种方式,可以获取到指定目录下所有的资源路径,例如获取 /WEB-INF 下的所有资源路径

  1. Set set = context.getResourcePaths("/WEB-INF");
  2. System.out.println(set);

8.2 获取资源流

不仅我们可以使用ServletContext获取路径,我们还可以获取资源流,以上面假设的两个文件为例

  1. // 获取aaa.txt
  2. InputStream in = servletContext.getResourceAsStream(“/aaa.txt”);
  3. // 获取bbb.txt
  4. InputStream in = servletContext.getResourceAsStream(“/WEB-INF/b.txt”);

8.3 获取类路径下资源

  1. InputStream in = this.getClass().getClassLoader().getResourceAsStream("xxx.txt");
  2. System.out.println(IOUtils.toString(in));

9.使用注解,不再配置web.xml(正确打开方式)

每创建一个Servlet我们就需要在web.xml中配置,但是如果我们的Servlet版本在3.0以上,就可以选择不创建web.xml,而使用注解来解决,十分简单方便
例如我们创建一个Servlet,配置web.xml如下

  1. <servlet>
  2. <servlet-name>ServletDemo2</servlet-name>
  3. <servlet-class>cn.ideal.web.servlet.ServletDemo2</servlet-class>
  4. </servlet>
  5. <servlet-mapping>
  6. <servlet-name>ServletDemo2</servlet-name>
  7. <url-pattern>/Demo2</url-pattern>
  8. </servlet-mapping>
  1. //在类名的上方写入这样一句代码,引号内为外部访问路径
  2. @WebServlet("/Demo2")

是不是很简单方便,我们看一下其中的原理:

  1. //WebServlet 源码节选
  2. @Target({ElementType.TYPE})
  3. @Retention(RetentionPolicy.RUNTIME)
  4. @Documented
  5. public @interface WebServlet {
  6. String name() default "";
  7. String[] value() default {};
  8. String[] urlPatterns() default {};
  9. int loadOnStartup() default -1;

这个注解可以看到,@Target({ElementType.TYPE})作用范围为类上,@Retention(RetentionPolicy.RUNTIME)保留在运行期,name()方法反而在这里没有那么重要,因为在web.xml中,name主要起一个关联的作用,其中我们最重要的就是这个String[] urlPatterns() default {};配置一个地址,它的定义为一个数组,当然配置一个也是可以的,即urlPatterns = "/Demo2"而其中value所代表的最重要的值,其实也就代表这个地址,所以可以写为 Value = "/Demo2" ,而 Value又可以省略,所以可以写成 "/Demo2"