ServletServlet 详解(使用+原理)

Posted 吞吞吐吐大魔王

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ServletServlet 详解(使用+原理)相关的知识,希望对你有一定的参考价值。

文章目录

1. Servlet 介绍

1.1 什么是 Servlet

  • Servlet(Server Applet 的缩写,全称 Java Servlet):用 Java 编写的服务器端程序。其主要功能在于交互式地浏览和修改数据,生成动态 Web 内容。狭义的 Servlet 是指 Java 语言实现的一个接口,广义的 Servlet 是指任何实现了这个 Servlet 接口的类,一般情况下,人们将 Servlet 理解为后者。

  • Servlet 运行于支持 Java 的应用服务器中。从原理上讲,Servlet 可以响应任何类型的请求,但绝大多数情况下 Servlet 只用来扩展基于 HTTP 协议的 Web 服务器

  • Servlet 是一种实现动态页面的技术,是一组由 Tomcat 提供给程序员的 API,帮助程序员简单高效的开发一个 web app

1.2 Servlet 的主要工作

  • 允许程序员注册一个类,在 Tomcat 收到的某个特定的 HTTP 请求的时候,执行这个类中的一些代码
  • 帮助程序员解析 HTTP 请求,把 HTTP 请求从一个字符串解析成一个 HttpRequest 对象
  • 帮助程序员构造 HTTP 响应,程序员只要给指定的 HttpResponse 对象填写一些属性字段,Servlet 就会自动的按照 HTTP 协议的方式构造出一个 HTTP 响应字符串,并通过 Socket 编写返回给客户端

2. Servlet 程序创建步骤

2.1 创建项目

以下使用 IDEA 带大家编写一个简单的 Servlet 程序,主要是让大家了解一个大致的流程

  • 首先使用 IDEA 创建一个 Maven 项目

  • 创建好的项目如下

  • 通过上图我们可以看到创建好的项目中有一些目录结构,这是 Maven 项目的标准结构,其中

    • src 用于存放源代码和测试代码的根目录
    • main 用于存放源代码的目录
    • test 用于存放测试代码的目录
    • java 用于存放 Java 代码的目录
    • resources 用于存放依赖的资源文件
    • pom.xml 是 Maven 项目的核心配置文件,关于这个 Maven 项目的相关属性,都是在这个 xml 中进行配置

2.2 引入依赖

Maven 项目创建完成后,会自动生成一个 pom.xml 文件,我们需要在这个文件中引入 Servlet API 依赖的 jar 包

  • 打开中央仓库,搜索 Servlet,点击 Java Servlet API

  • 选择对应 Tomcat 版本的 Servlet(由于我当前使用的是 Tomcat 8 系列,所以选择 Servlet 3.1.0 即可)

  • 将中央仓库提供的该版本的 xml 复制到项目的 pom.xml

  • 修改后的 pom.xml 文件如下

    一个项目中可以有多个依赖,每个依赖都是一个 <dependency> 标签。引入的依赖都要放在一个 <dependencies> 的标签中,该标签用于放置项目依赖的 jar 包,Maven 会自动下载该依赖到本地

  • 在拷贝的依赖中有几个参数,分别具有如下含义:

    • groupId 表示组织或者公司的 ID
    • artifactId 表示项目或者产品的 ID
    • version 表示版本号
    • scope 用于指定依赖的作用范围,包含所在项目的测试、编译、运行、打包等声明周期。
  • 如果你想找到刚刚 Maven 下载到本地的第三方库,路径如下

2.3 创建目录

Web 项目对于目录结构还有自己的要求,只有 Maven 的标准目录是不够的,需要再创建以下目录并进行配置

  • 在 main 目录下,创建一个 webapp 目录

    webapp 目录就是用于部署到 Tomcat 中的一个重要目录,里面可以存放一些静态资源

  • 在 webapp 目录下,创建一个 WEB-INF 目录

  • 在 WEB-INF 目录下,创建一个 web.xml 文件

    Tomcat 通过找到这个 web.xml 文件才能够正确处理 webapp 中的动态资源

  • 编写 web.xml

    web.xml 中的内容不能是空的,里面的写法是固定的,用到的时候可以直接拷贝下面的代码

    <!DOCTYPE web-app PUBLIC
    		"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    		"http://java.sun.com/dtd/web-app_2_3.dtd" >
    <web-app>
    	<display-name>Archetype Created Web Application</display-name>
    </web-app>
    

2.4 编写代码

以下编写一个让响应返回一个自定义字符换的简单代码

  • 创建一个 TestServlet 类,并且让它继承于 HttpServlet

    HttpServlet 这个类来自于 pom.xml 中引入的 Servlet API 依赖的 jar 包

  • 在 TestServlet 类中重写 doGet 方法

    doGet 是 HttpServlet 类中的方法,此处是在子类中重写了父类的 doGet

  • 为了了解 doGet 方法的作用,我们可以看看它的源码

    • HttpServletRequest 表示 HTTP 请求,Tomcat 按照 HTTP 请求的的格式把字符串格式的请求转换成了一个 HttpServletRequest 对象,通过这个对象就可以获取请求中的信息
    • HttpServletResponse 表示 HTTP 响应,通过代码可以把响应的对象构造好,然后 Tomcat 将响应返回给浏览器
    • 通过 doGet 的源码我们可以大致了解,它的作用是根据收到的请求通过响应返回一个 405 或者 400,那么我们可以重写这个方法,根据收到的请求执行自己的业务逻辑,把结果构造成响应对象
  • 在 doGet 方法中,通过 HttpServletResponse 类的 getWriter() 方法往响应的 body 中写入文本格式数据

    resp.getWriter() 会获取到一个流对象,通过这个流对象就可以写入一些数据,写入的数会被构造成一个 HTTP 响应的 body 部分,Tomcat 会把整个响应转成字符串,通过 Socket 写回给浏览器

  • 需要给 TestServlet 加上一个特定的注解 @WebServlet("/test")

    上述助解表示 Tomcat 收到的请求中,URL 的 Servlet Path 路径为 /test 的请求才会调用 TestServlet 这个类的代码,注解中的字符串表示着 URL 的 Servlet Path

  • 到这里程序的编写已经完成了!但是你可能会疑惑上述代码不是通过 main 方法作为入口的,这是因为 main 方法已经被包含在 Tomcat 中了,我们写的程序并不能单独执行,而是需要搭配 Tomcat 才能执行起来(在 Tomcat 的伪代码中我们具体分析了这个问题)

2.5 打包程序

在程序编写好之后,就可以使用 Maven 进行打包

  • 首先修改 pom.xml,加入一些必要的配置(打包的类型和打包后的包名

    • packaging 标签中用于设置打包的类型(如果不修改打包类型则默认为 jar 包,jar 包是普通 Java 程序打包的结果,里面包含了一些 .class 文件;而部署在 Tomcat 中的压缩包一般为 war 包,war 包里面是 Java Web 程序,里面除了 .class 文件之外,还包含 html、CSS、javascript、图片等等)
    • finalName 标签中用于设置打包后的名字(包名很重要,它对应着请求中 URL 的 Context Path
  • 执行打包操作(打开 Maven 窗口,展开 Lifecycle,双击 package 进行打包)

  • 打包成功后,可以发现多了个 target 目录,该目录下有一个 testServlet.war 的压缩包

2.6 部署程序

接下来我们就可以进行程序的部署

  • 首先将打好的 war 包拷贝到 Tomcat 的 webapps 目录下

  • 启动 Tomcat(在 Tomcat 的 bin 目录中点击 startup.bat

2.7 验证程序

此时通过浏览器访问 http://127.0.0.1:8080/testServlet/test 就可以看到程序实现的结果了

注意:URL 中的路径分成了两个部分 Context Path 和 Servlet Path

  • Context Path 这个路径表示一个 webapp,来源于打包的包名
  • Servlet Path 这个路径表示一个 webapp 中的一个页面,来源于对应的 Servlet 类 @WebServlet 注解中的内容

3. 使用 Smart Tomcat 进行部署

为了简化上述操作流程,其实是有一些更简单的方式

  • 对于创建项目、引入依赖、创建目录这三个步骤,其实可以使用项目模板来快速生成,但是由于项目模板加载速度很慢,因此这里并不推荐
  • 对于打包程序和部署程序这两个步骤,其实可以使用 Smart Tomcat 插件来快速实现,以下将介绍它的使用方式

3.1 安装 Smart Tomcat

  • 点击 File → Settings

  • 点击 Plugins,在搜索栏搜索 Smart Tomcat,然后进行安装即可

3.2 配置 Smart Tomcat

  • 点击 Add Configuration

  • 点击左上角的+号,并选择 Smart Tomcat

  • 主要修改这三个参数

    • Name:这一栏其实可以随便填
    • Tomcat Server:表示 Tomcat 所在的目录
    • Deployment Directory:表示项目发布目录
    • Context Path:表示项目路径,默认值是项目名称
    • Servlet Port:表示服务端口
    • Admin Port:表示管理端口
    • VM options:表示 JVM 参数
  • 配置好 Smart Tomcat 之后,Add Configuration 就会显示成 Name 的名字,并且右边多了个三角形运行的符号

3.3 使用 Smart Tomcat

  • 点击三角形运行 Smart Tomcat,出现如下信息表示程序启动成功

  • 点击蓝色的连接,跳转到项目路径,再增加 Servlet Path 就可以显示出该程序的结果

4. 访问出错解决方案

4.1 出现 404

出现 404 原因: 用户访问的资源不存在,大概率是 URL 的路径写的不正确

错误实例1: 少写了 Context Path 或者 Context Path 写错了

错误实例2: 少写了 Servlet Path 或者 Servlet Path 写错了


错误实例3: web.xml 写错了(如清空 web.xml 中的内容)

4.2 出现 405

出现 405 原因: 访问的服务器不能支持请求中的方法或者不能使用该请求中的方法

错误实例1: 没有重写 doGet 方法

错误实例2: 重写了 doGet 方法,但是没有删除父类的 doGet 方法

4.3 出现 500

出现 500 原因: 服务器出现内部错误,往往是 Servlet 代码中抛出异常导致的

错误实例: 代码中出现空指针异常

4.4 出现“空白页面”

出现空白页原因: 响应的 body 中并没有内容

错误实例:resp.getWriter().write() 操作删除

4.5 出现“无法访问此网站”

出现“无法访问此网站”原因: 一般是不能正确访问到 Tomcat(可能是 Tomcat 没启动,也可能是 IP/端口号写错了)

错误实例: 注解 @WebServlet 中少写了 /

4.6 出现中文乱码问题

响应出现中文乱码问题原因: 使用的编译器的编码方式(一般是 utf-8)和浏览器的编码方式不同,浏览器默认跟随系统编码方式,win10 系统默认是 GBK 编码

解决方式: 通过响应对象的 setContentType() 方法来修改浏览器对于响应正文的编码格式

@WebServlet("/test")
public class TestServlet extends HttpServlet 
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException 
        resp.setContentType("text/html;charset=utf-8");
        resp.getWriter().write("吞吞吐吐大魔王");
    

5. Servlet 运行原理

在 Servlet 的代码中,我们并没有写 main 方法,那么对应的 doGet 代码是如何被调用呢?响应又是如何返回给浏览器的呢?

5.1 Servlet 的架构

我们自己实现的 Servlet 是在 Tomcat 基础上运行的,下图显示了 Servlet 在 Web 应用程序中的位置

当浏览器给服务器发送请求时,Tomcat 作为 HTTP 服务器,就可以接收到这个请求。Tomcat 的工作就是解析 HTTP 请求,并把请求交给 Servlet 的代码来进行进一步的处理。Servlet 的代码根据请求计算生成响应对象,Tomcat 再把这个响应对象构造成 HTTP 响应,返回给浏览器。并且 Servlet 的代码也经常会和数据库进行数据的传递。

5.2 Tomcat 的伪代码

下面通过 Tomcat 的伪代码的形式来描述 Tomcat 初始化和处理请求两部分核心逻辑

  • Tomcat 的初始化流程

    class Tomcat 
    
        // 用来存储所有的 Servlet 对象
        private List<Servlet> instanceList = new ArrayList<>();
        
        public void start() 
            // 根据约定,读取 WEB-INF/web.xml 配置文件
            // 并解析被 @WebServlet 注解修饰的类
            
            // 假定这个数组里就包含了我们解析到的所有被 @WebServlet 注解修饰的类.
            Class<Servlet>[] allServletClasses = ...;
            
            // 这里要做的的是实例化出所有的 Servlet 对象出来;
            for (Class<Servlet> cls : allServletClasses) 
                // 这里是利用 java 中的反射特性做的
                // 实际上还得涉及一个类的加载问题,因为我们的类字节码文件,是按照约定的
                // 方式全部在 WEB-INF/classes 文件夹下存放的,所以 tomcat 内部是
                // 实现了一个自定义的类加载器(ClassLoader),用来负责这部分工作。
                
                Servlet ins = cls.newInstance();
                instanceList.add(ins);
            
            
            // 调用每个 Servlet 对象的 init() 方法,这个方法在对象的生命中只会被调用这一次
            for (Servlet ins : instanceList) 
                ins.init();
            
            
            // 启动一个 HTTP 服务器,并用线程池的方式分别处理每一个 Request
            ServerSocket serverSocket = new ServerSocket(8080);
            // 实际上 tomcat 不是用的固定线程池,这里只是为了说明情况
            ExecuteService pool = Executors.newFixedThreadPool(100);
            
            while (true) 
                Socket socket = ServerSocket.accept();
                // 每个请求都是用一个线程独立支持,这里体现了 Servlet 是运行在多线程环境下的
                pool.execute(new Runnable() 
                    doHttpRequest(socket);
                );
            
            // 调用每个 Servlet 对象的 destroy() 方法,这个方法在对象的生命中只会被调用这一次
            for (Servlet ins : instanceList) 
                ins.destroy();
            
        
        
        public static void main(String[] args) 
        	new Tomcat().start();
        
    
    
    • Tomcat 的代码内置了 main 方法,当我们启动 Tomcat 的时候,就是从 Tomcat 的 main 方法开始执行的
    • 被 @WebServlet 注解修饰的类会在 Tomcat 启动的时候就被获取到,并集中管理
    • Tomcat 通过反射这样的语法机制来创建被 @WebServlet 注解修饰的类的实例
    • 这些实例被创建完之后,就会调用其中的 init 方法进行初始化
    • 这些实例被销毁之前,就会调用其中的 destory 方法进行收尾工作
    • Tomcat 内部也是通过 Socket API 进行网络通信
    • Tomcat 为了能够同时处理多个 HTTP 请求,采取了多线程的方式实现,因此 Servlet 是运行在多线程环境下的
  • Tomcat 处理请求流程

    class Tomcat 
        
        void doHttpRequest(Socket socket) 
            // 参照我们之前学习的 HTTP 服务器类似的原理,进行 HTTP 协议的请求解析和响应构建
            HttpServletRequest req = HttpServletRequest.parse(socket);
            HttpServletRequest resp = HttpServletRequest.build(socket);
            
            // 判断 URL 对应的文件是否可以直接在我们的根路径上找到对应的文件,如果找到,就是静态内容
                
            // 直接使用 IO 进行内容输出
            if (file.exists()) 
                // 返回静态内容
                return;
            
            
            // 走到这里的逻辑都是动态内容了
            // 找到要处理本次请求的 Servlet 对象
            Servlet ins = findInstance(req.getURL());
            
            // 调用 Servlet 对象的 service 方法
            // 这里就会最终调用到我们自己写的 HttpServlet 的子类里的方法了
            try 
            	ins.service(req, resp);
             catch (Exception e) 
            	// 返回 500 页面,表示服务器内部错误
            
        
    
    
    • Tomcat 从 Socket 中读到的 HTTP 请求是一个字符串,然后 Tomcat 会按照 HTTP 协议的格式解析成一个 HttpServletRequest 对象
    • Tomcat 会根据 URL 中的 Path 判定这个请求是请求一个静态资源还是动态资源。如果是静态资源,直接找到对应的文件,把文件的内容通过 Socket 返回;如果是动态资源,才会执行到 Servlet 的相关逻辑
    • Tomcat 会根据 URL 中的 Context Path 和 Servlet Path 确定要调用哪个 Servlet 实例的 service 方法
    • 通过 service 方法,就会进一步调用我们重写的 doGet 或者 doPost 方法等等
  • Servlet 的 service 方法的实现

    class Servlet 
        public void service(HttpServletRequest req, HttpServletResponse resp) 
            String method = req.getMethod();
            if (method.equals("GET")) 
            	doGet(req, resp);
             else if (method.equals("POST")) 
            	doPost(req, resp);
             else if (method.equals("PUT")) 
            	doPut(req, resp);
             else if (method.equals("DELETE")) 
            	doDelete(req, resp);
            
            ......
        
    
    
    • Servlet 的 service 方法内部会根据当前请求的方式,决定调用其中的某个 doXXX 方法
    • 在调用 doXXX 方法的时候,会触发多态机制,从而执行到我们自己写的子类的 doXXX 方法

6. Servlet API 详解

对于 Servlet 主要介绍三个类,分别是 HttpServlet、HttpServletRequest 和 HttpServletResponse。

其中 HttpServletRequest 和 HttpServletResponse 是 Servlet 规范中规定的两个接口,HttpServlet 中并没有实现这两个接口的成员变量,它们只是 HttpServlet 的 service 和 doXXX 等方法的参数。这两个接口类的实例化是在 Servlet 容器中实现的。

6.1 HttpServlet

核心方法

方法名称调用时机
init在 HttpServlet 实例化之后被调用一次
destory在 HttpServlet 实例不再使用的时候调用一次
service收到 HTTP 请求的时候调用
doGet收到 GET 请求的时候调用(由 service 方法调用)
doPost收到 POST 请求的时候调用(由 service 方法调用)
doPut/doDelete/doOptions/...收到其它对应请求的时候调用(由 service 方法调用)

Servlet 的生命周期: Servlet 的生命周期就是 Servlet 对象从创建到销毁的过程,下面来介绍其生命周期的过程

  • Servlet 对象是由 Tomcat 来进行实例化的,并且在实例化完毕之后调用 init 方法(只调用一次
  • Tomcat 对于收到的请求,都会通过对应的 Servlet 的 service 方法来进行处理(可以调用多次
  • Tomcat 在结束之前,会调用 Servlet 的 destory 方法来进行回收资源(最多调用一次

注意: init 和 service 能够保证在各自的合适时机被 Tomcat 调用,但是 destory 不一定,它是否能够被调用取决于 Tomcat 是如何结束的

  • 如果直接杀死进程,那么就来不及调用 destory
  • 如果通过 Tomcat 的管理端口(默认 8005)进行关闭,就能够调用 destory

处理 GET 请求示例:

直接通过浏览器 URL 发送一个 GET 方法的请求,来对这个请求进行处理

@WebServlet("/get")
public class TestServlet extends HttpServlet 
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException 
        resp.getWriter().write("get");
    

处理 POST 请求示例:

由于通过浏览器 URL 发送的请求是 GET 方法的请求,因此我们需要通过其它方式来发送一个 POST 请求然后用于处理。发送 POST 请求的方式有通过 Ajax、form 表单或者 socket api 进行构造,如果单纯的用于测试就比较麻烦,这里推荐使用软件 postman,这是一个很强大的 API 调试、Http 请求的工具。

@WebServlet("/post")
public class TestServlet extends HttpServlet 
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException 
        resp.getWriter().write("post");
    

6.2 HttpServletRequest

核心方法

方法描述
String getProtocol()返回协议的名称和版本号
String getMethod()返回请求的 HTTP 方法的名称
String getRequestURL()返回请求的 URL,不带查询字符串
String getRequestURI()返回该请求的 URL 的一部分,不带协议名、端口号、查询字符串
String getContextPath()返回指示请求 URL 中 Context Path 部分
String getServletPath()返回指示请求 URL 中 ServletPath 部分
String getQueryString()返回请求首行中 URL 后面的查询字符串
Enumeration getParameterNames()返回一个 String 对象的枚举,包括在该请求中的参数的名称
String getParameter(String name)以字符串形式返回请求参数的值,如果参数不存在则返回 null
String[] getParameterValues(String name)返回一个字符串对象的数组,包括所有给定的请求的参数,如果参数不存在则返回 null
Enumeration getHeaderNames()返回一个枚举,包括该请求中所有的头名
String getHeader(String name)以字符串形式返回指定的请求头的值
String getCharacterEncoding()返回请求正文中使用的字符编码的名称
String getContentType()返回请求正文的 MIME 类型,如果不知道类型则返回 null
int getContentLength()以字节为单位返回请求正文的长度,并提供输入流,如果长度未知则返回-1
InputStream getInputStream()用于读取请求的正文内容,返回一个 InputStream 对象

示例1: 通过上述方法返回一个页面是该请求的具体 HTTP 请求格式

@WebServlet("/showRequest")
public class ShowRequestServlet extends HttpServlet 
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException 
        // 此处返回一个 HTML,在 HTML 中显示 HttpRequestServlet 类中的一些核心方法
        // 把这些 API 的返回结果通过 StringBuilder 进行拼接
        resp.setContentType("text/html;charset=utf-8");
        StringBuilder html = new StringBuilder();
        html.append(req.getMethod());
        html.append(" ");
        html.append(req.getRequestURL());
        html.append("?");
        html.append(req.getQueryString());
        html.append(" ");
        html.append(req.getProtocol());
        html.append("</br>");
        Enumeration<String> headerNames = req.getHeaderNames();
        while(headerNames.hasMoreElements())
            String headName = headerNames.nextElementServletServlet简介Servlet底层原理Servlet实现方式Servlet生命周期

ServletServlet容器等内容讲解

ServletServlet学习之基础篇(HTTP)

ServletServlet学习之基础篇(HTTP)

ServletServlet学习之基础篇(HTTP)

Servlet学习1