ServletServlet 详解(使用+原理)
Posted 吞吞吐吐大魔王
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ServletServlet 详解(使用+原理)相关的知识,希望对你有一定的参考价值。
文章目录
- 1. Servlet 介绍
- 2. Servlet 程序创建步骤
- 3. 使用 Smart Tomcat 进行部署
- 4. 访问出错解决方案
- 5. Servlet 运行原理
- 6. Servlet API 详解
- 7. 实现服务器版表白墙程序
- 8. Cookie 和 Session
- 9. 上传文件操作
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
: 表示组织或者公司的 IDartifactId
: 表示项目或者产品的 IDversion
: 表示版本号scope
: 用于指定依赖的作用范围,包含所在项目的测试、编译、运行、打包等声明周期。
-
如果你想找到刚刚 Maven 下载到本地的第三方库,路径如下
2.3 创建目录
Web 项目对于目录结构还有自己的要求,只有 Maven 的标准目录是不够的,需要再创建以下目录并进行配置
-
在 main 目录下,创建一个
webapp
目录webapp 目录就是用于部署到 Tomcat 中的一个重要目录,里面可以存放一些静态资源
-
在 webapp 目录下,创建一个
WEB-INF
目录 -
在 WEB-INF 目录下,创建一个
web.xml
文件Tomcat 通过找到这个 web.xml 文件才能够正确处理 webapp 中的动态资源
-
编写
web.xml
Servlet 中 web.xml 中的内容不能是空的,里面的写法是固定的(这里的写法专属于 Servlet),用到的时候可以直接拷贝下面的代码
<!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)
- packaging 标签中用于设置打包的类型(如果不修改打包类型则默认为 jar 包,jar 包是普通 Java 程序打包的结果,里面包含了一些
-
执行打包操作(打开 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.以上是关于ServletServlet 详解(使用+原理)的主要内容,如果未能解决你的问题,请参考以下文章