Servlet

Servlet 是什么

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

回顾 动态页面 vs 静态页面

静态页面也就是内容始终固定的页面 . 即使 用户不同 / 时间不同 / 输入的参数不同 , 页面内容也不会发生变化. ( 除非网站的开发人员修改源代码 , 否则页面内容始终不变 ).
对应的 , 动态页面指的就是 用户不同 / 时间不同 / 输入的参数不同 , 页面内容会发生变化 .
构建动态页面的技术有很多 , 每种语言都有一些相关的库 / 框架来做这件事 .
Servlet 就是 Tomcat 这个 HTTP 服务器提供给 Java 的一组 API, 来完成构建动态页面这个任务 .

Servlet 主要做的工作

  • 允许程序猿注册一个类, Tomcat 收到某个特定的 HTTP 请求的时候, 执行这个类中的一些代码.
  • 帮助程序猿解析 HTTP 请求, HTTP 请求从一个字符串解析成一个 HttpRequest 对象.
  • 帮助程序猿构造 HTTP 响应. 程序猿只要给指定的 HttpResponse 对象填写一些属性字段, Servlet就会自动的安装 HTTP 协议的方式构造出一个 HTTP 响应字符串, 并通过 Socket 写回给客户端
简而言之 , Servlet 是一组 Tomcat 提供的 API, 让程序猿自己写的代码能很好的和 Tomcat 配合起来 , 从而更简单的实现一个 web app.
而不必关注 Socket, HTTP 协议格式 , 多线程并发等技术细节 , 降低了 web app 的开发门槛 , 提高了开发效率.

第一个 Servlet 程序

1. 创建项目

使用 IDEA 创建一个 Maven 项目 .
1) 菜单 -> 文件 -> 新建项目 -> Maven

2) 选择项目要存放的目录

3) 项目创建完毕后, 一般右下角会弹出以下对话框. 选择 Enable Auto-Import 

 

2. 引入依赖

Maven 项目创建完毕后 , 会自动生成一个 pom.xml 文件 .
我们需要在 pom.xml 中引入 Servlet API 依赖的 jar .
1) 在中央仓库 https://mvnrepository.com/ 中搜索 "servlet", 一般第一个结果就是 .

 

2) 选择版本. 一般我们使用 3.1.0 版本  

Servlet 的版本要和 Tomcat 匹配 .
如果我们使用 Tomcat 8.5, 那么就需要使用 Servlet 3.1.0
可以在 http://tomcat.apache.org/whichversion.html 查询版本对应关系 .

3) 把中央仓库中提供的 xml 复制到项目的 pom.xml

 修改后的 pom.xml 形如

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns = "http://maven.apache.org/POM/4.0.0"
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation = "http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd" >
<modelVersion> 4.0.0 </modelVersion>
<groupId> org.example </groupId>
<artifactId> ServletHelloWorld </artifactId>
<version> 1.0-SNAPSHOT </version>
<dependencies>
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api
-->
<dependency>
<groupId> javax.servlet </groupId>
<artifactId> javax.servlet-api </artifactId>
<version> 3.1.0 </version>
<scope> provided </scope>
</dependency>
</dependencies>
</project>

 <dependencies> 标签内部放置项目依赖的 jar . maven 会自动下载依赖到本地.

3. 创建目录

当项目创建好了之后 , IDEA 会帮我们自动创建出一些目录 . 形如
这些目录中 :
  • src 表示源代码所在的目录
  • main/java 表示源代码的根目录. 后续创建 .java 文件就放到这个目录中.
  • main/resources 表示项目的一些资源文件所在的目录. 此处暂时不关注.
  • test/java 表示测试代码的根目录. 此处暂时不关注.

这些目录还不够, 我们还需要创建一些新的目录/文件.

1) 创建 webapp 目录

main 目录下 , java 目录并列 , 创建一个 webapp 目录 ( 注意 , 不是 webapps).

2) 创建 web.xml

然后在 webapp 目录内部创建一个 WEB - INF 目录 , 并创建一个 web.xml 文件

3) 编写 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>
webapp 目录就是未来部署到 Tomcat 中的一个重要的目录 . 当前我们可以往 webapp 中放一些静
态资源 , 比如 html , css .
在这个目录中还有一个重要的文件 web.xml. Tomcat 找到这个文件才能正确处理 webapp 中的动
态资源 .

4. 编写代码

java 目录中创建一个类 HelloServlet, 代码如下 :
@WebServlet ( "/hello" )
public class HelloServlet extends HttpServlet {
@Override
protected void doGet ( HttpServletRequest req , HttpServletResponse resp )
throws ServletException , IOException {
System . out . println ( "hello" );
resp . getWriter (). write ( "hello" );
}
}
  • 创建一个类 HelloServlet , 继承自 HttpServlet
  • 在这个类上方加上 @WebServlet("/hello") 注解, 表示 Tomcat 收到的请求中, 路径为 /hello 的请求才会调用 HelloServlet 这个类的代码. (这个路径未包含 Context Path)
  • 重写 doGet 方法. doGet 的参数有两个, 分别表示收到的 HTTP 请求和要构造的 HTTP 响应. 这个方法会在 Tomcat 收到 GET 请求时触发
  • HttpServletRequest 表示 HTTP 请求. Tomcat 按照 HTTP 请求的格式把 字符串 格式的请求转成了一个 HttpServletRequest 对象. 后续想获取请求中的信息(方法, url, header, body ) 都是通过这个对象来获取.
  • HttpServletResponse 表示 HTTP 响应. 代码中把响应对象构造好(构造响应的状态码, header, body )
  • resp.getWriter() 会获取到一个流对象, 通过这个流对象就可以写入一些数据, 写入的数据会被 构造成一个 HTTP 响应的 body 部分, Tomcat 会把整个响应转成字符串, 通过 socket 写回给浏览 
1) 我们的代码不是通过 main 方法作为入口了 . main 方法已经被包含在 Tomcat , 我们写的代码会被Tomcat 在合适的时机调用起来 .
此时我们写的代码并不是一个完整的程序 , 而是 Tomcat 这个程序的一小部分逻辑 .
2) 我们随便写个类都能被 Tomcat 调用嘛 ? 满足啥样条件才能被调用呢 ?
主要满足三个条件 :
a) 创建的类需要继承自 HttpServlet
b) 这个类需要使用 @WebServlet 注解关联上一个 HTTP 的路径
c) 这个类需要实现 doXXX 方法 .
当这三个条件都满足之后 , Tomcat 就可以找到这个类 , 并且在合适的时机进行调用 .

5. 打包程序

使用 maven 进行打包 . 打开 maven 窗口 ( 一般在 IDEA 右侧就可以看到 Maven 窗口 , 如果看不到的话 ,可以通过 菜单 -> View -> Tool Window -> Maven 打开 )
然后展开 Lifecycle , 双击 package 即可进行打包 .

 如果比较顺利的话, 能够看到 SUCCESS 这样的字样

 打包成功后, 可以看到在 target 目录下, 生成了一个 jar

这样的 jar 包并不是我们需要的 , Tomcat 需要识别的是另外一种 war 包格式 .
另外这个 jar 包的名字太复杂了 , 我们也希望这个名字能更简单一点 .

war 包和 jar 包的区别

jar 包是普通的 java 程序打包的结果 . 里面会包含一些 .class 文件 .
war 包是 java web 的程序 , 里面除了会包含 .class 文件之外 , 还会包含 HTML, CSS, JavaScript,
, 以及其他的 jar . 打成 war 包格式才能被 Tomcat 识别 .
ServletHelloWorld - 1.0 - SNAPSHOT.jar 的由来
这个名字来源于

 

相当于把 artifactId version 拼接起来了 .
pom.xml 中新增一个 packing 标签 , 表示打包的方式是打一个 war .
<packaging> war </packaging>
pom.xml 中再新增一个 build 标签 , 内置一个 finalName 标签 , 表示打出的 war 包的名字是
HelloServlet
<build>
<finalName> ServletHelloWorld </finalName>
</build>

完整的 pom.xml 形如

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns = "http://maven.apache.org/POM/4.0.0"
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation = "http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd" >
<modelVersion> 4.0.0 </modelVersion>
<groupId> org.example </groupId>
<artifactId> ServletHelloWorld </artifactId>
<version> 1.0-SNAPSHOT </version>
<dependencies>
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api
-->
<dependency>
<groupId> javax.servlet </groupId>
<artifactId> javax.servlet-api </artifactId>
<version> 3.1.0 </version>
<scope> provided </scope>
</dependency>
</dependencies>
<packaging> war </packaging>
<build>
<finalName> ServletHelloWorld </finalName>
</build>
</project>

重新使用 maven 打包, 可以看到生成的新的 war 包的结果.

6. 部署程序

war 包拷贝到 Tomcat webapps 目录下 .
启动 Tomcat , Tomcat 就会自动把 war 包解压缩 .

 

看到这个日志说明 Tomcat 已经正确识别了 ServletHelloWorld 这个 webapp

7. 验证程序

此时通过浏览器访问 http://127.0.0.1:8080/ServletHelloWorld/hello
就可以看到结果了 .

注意: URL 中的 PATH 分成两个部分, 其中 HelloServlet Context Path, hello Servlet Path 

 

更方便的部署方式

手动拷贝 war 包到 Tomcat 的过程比较麻烦 . 我们还有更方便的办法 .
此处我们使用 IDEA 中的 Smart Tomcat 插件完成这个工作
插件就是对程序的一些特定场景 , 做出一些特定的功能的扩展

安装 Smart Tomcat 插件

1) 菜单 -> 文件 -> Settings

2) 选择 Plugins, 选择 Marketplace, 搜索 "tomcat", 点击 "Install".

3) 安装完毕之后, 会提示 "重启 IDEA"  

 

配置 Smart Tomcat 插件
1) 点击右上角的 "Add Configuration"

2) 选择左侧的 "Smart Tomcat"

3) Name 这一栏填写一个名字 ( 可以随便写 )
Tomcat Server 这一栏选择 Tomcat 所在的目录 . 其他的选项不必做出修改 .

 其中 Context Path 默认填写的值是项目名称.

这会影响到后面咱们的访问页面.

4) 点击 OK 之后, 右上角变成了

 点击绿色的三角号, IDEA 就会自动进行编译, 部署, 启动 Tomcat 的过程

5) 访问页面 .
在浏览器中使用 http://127.0.0.1:8080/ServletHelloWorld/hello 访问页面 .

注意路径的对应关系.

使用 Smart Tomcat 部署的时候 , 我们发现 Tomcat webapps 内部并没有被拷贝一个 war ,
也没有看到解压缩的内容 .
Smart Tomcat 相当于是在 Tomcat 启动的时候直接引用了项目中的 webapp target 目录

访问出错怎么办?

出现 404

404 表示用户访问的资源不存在 . 大概率是 URL 的路径写的不正确 .
错误实例 1 : 少写了 Context Path
通过 /hello 访问服务器

 

错误实例 2 : 少写了 Servlet Path
通过 /ServletHelloWorld 访问服务器

 

错误实例 3: Servlet Path 写的和 URL 不匹配
修改 @WebServlet 注解的路径
重启 Tomcat 服务器 .
URL 中的路径写作 "/hello" , 而代码中写作的 Servlet Path "/helloServlet", 两者不匹配

 

错误实例 4: web.xml 写错了
清除 web.xml 中的内容

重启 Tomcat 服务器 .
通过浏览器访问 URL, 可以看到 :

  Tomcat 启动的时候也有相关的错误提示

 

出现 405

405 表示对应的 HTTP 请求方法没有实现 .
错误实例 : 没有实现 doGet 方法 .
@WebServlet ( "/hello" )
public class HelloServlet extends HttpServlet {
}
重启 Tomcat 服务器 .
在浏览器中访问 , 可以看到
在浏览器地址栏直接输入 URL , 会发送一个 HTTP GET 请求 .
此时就会根据 /ServletHelloWorld/hello 这个路径找到 HelloServlet 这个类 . 并且尝试调用
HelloServlet doGet 方法 .
但是如果没有实现 doGet 方法 , 就会出现上述现象

出现 500

往往是 Servlet 代码中抛出异常导致的 .
错误实例 :
修改代码
@WebServlet ( "/hello" )
public class HelloServlet extends HttpServlet {
@Override
protected void doGet ( HttpServletRequest req , HttpServletResponse resp )
throws ServletException , IOException {
String s = null ;
resp . getWriter (). write ( s . length ());
}
}
重启 Tomcat 服务器 .
重新访问页面 , 可以看到 :

 

出现 "空白页面"

错误实例 :
修改代码 , 去掉 resp.getWritter().write() 操作 .
@WebServlet ( "/hello" )
public class HelloServlet extends HttpServlet {
@Override
protected void doGet ( HttpServletRequest req , HttpServletResponse resp )
throws ServletException , IOException {
System . out . println ( "hello" );
}
}
重启服务器 ,
访问服务器 , 可以看到一个空白页面 :

抓包可以看到, 响应 body 中的内容就是 "空数据"  

出现 " 无法访问此网站 "
一般是 Tomcat 启动就失败了 .
错误实例 : Servlet Path 写错了
应该写作 "/hello", Tomcat 在启动的时候已经提示了相关的错误 .
Tomcat 启动的日志里面报错信息可能比较多 , 需要耐心观察 , 找到关键的提示

看到的现象:  

熟悉 HTTP 协议能够让我们调试问题事半功倍 .
4xx 的状态码表示路径不存在 , 往往需要检查 URL 是否正确 , 和代码中设定的 Context Path 以及
Servlet Path 是否一致 .
5xx 的状态码表示服务器出现错误 , 往往需要观察页面提示的内容和 Tomcat 自身的日志 , 观察是否
存在报错 .
出现连接失败往往意味着 Tomcat 没有正确启动 , 也需要观察 Tomcat 的自身日志是否有错误提示 .
空白页面这种情况则需要我们使用抓包工具来分析 HTTP 请求响应的具体交互过程 .

观察日志是调试程序的重要途径 . Tomcat 的日志往往很多 , 需要耐心阅读 , 经常阅读 , 熟练了
就能更快速的找到问题了 .

Servlet 运行原理

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

Tomcat 的定位

我们自己的实现是在 Tomcat 基础上运行的。
当浏览器给服务器发送请求的时候 , Tomcat 作为 HTTP 服务器 , 就可以 接收到这个请求 .
HTTP 协议作为一个应用层协议 , 需要底层协议栈来支持工作 . 如下图所示 :

 

1) 接收请求 :
  • 用户在浏览器输入一个 URL, 此时浏览器就会构造一个 HTTP 请求.
  • 这个 HTTP 请求会经过网络协议栈逐层进行 封装 成二进制的 bit , 最终通过物理层的硬件设备转换成光信号/电信号传输出去.
  • 这些承载信息的光信号/电信号通过互联网上的一系列网络设备, 最终到达目标主机(这个过程也需要网络层和数据链路层参与).
  • 服务器主机收到这些光信号/电信号, 又会通过网络协议栈逐层进行 分用, 层层解析, 最终还原成HTTP 请求. 并交给 Tomcat 进程进行处理(根据端口号确定进程)
  • Tomcat 通过 Socket 读取到这个请求(一个字符串), 并按照 HTTP 请求的格式来解析这个请求, 根据请求中的 Context Path 确定一个 webapp, 再通过 Servlet Path 确定一个具体的 类. 再根据当前请求的方法 (GET/POST/...), 决定调用这个类的 doGet 或者 doPost 等方法. 此时我们的代码中的doGet / doPost 方法的第一个参数 HttpServletRequest 就包含了这个 HTTP 请求的详细信息.
2) 根据请求计算响应 :
  • 在我们的 doGet / doPost 方法中, 就执行到了我们自己的代码. 我们自己的代码会根据请求中的一些信息, 来给 HttpServletResponse 对象设置一些属性. 例如状态码, header, body .
3) 返回响应 :
  • 我们的 doGet / doPost 执行完毕后, Tomcat 就会自动把 HttpServletResponse 这个我们刚设置好的对象转换成一个符合 HTTP 协议的字符串, 通过 Socket 把这个响应发送出去.
  • 此时响应数据在服务器的主机上通过网络协议栈层层 封装, 最终又得到一个二进制的 bit , 通过物理层硬件设备转换成光信号/电信号传输出去.
  • 这些承载信息的光信号/电信号通过互联网上的一系列网络设备, 最终到达浏览器所在的主机(这个过程也需要网络层和数据链路层参与).
  • 浏览器主机收到这些光信号/电信号, 又会通过网络协议栈逐层进行 分用, 层层解析, 最终还原成HTTP 响应, 并交给浏览器处理.
  • 浏览器也通过 Socket 读到这个响应(一个字符串), 按照 HTTP 响应的格式来解析这个响应. 并且把body 中的数据按照一定的格式显示在浏览器的界面上.

Tomcat 的伪代码

下面的代码通过 " 伪代码 " 的形式描述了 Tomcat 初始化 / 处理请求 两部分核心逻辑 .
所谓 " 伪代码 ", 并不是一些语法严谨 , 功能完备的代码 , 只是通过这种形式来大概表达某种逻辑 .
1) 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 方法进行初始化. (这个方法是 HttpServlet 自带的,我们自己写的类可以重写 init)
  • 这些实例被销毁之前, 会调用其中的 destory 方法进行收尾工作. (这个方法是 HttpServlet 自带的,我们自己写的类可以重写 destory)
  • Tomcat 内部也是通过 Socket API 进行网络通信.
  • Tomcat 为了能同时相应多个 HTTP 请求, 采取了多线程的方式实现. 因此 Servlet 是运行在 多线程环境 下的.
2) 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 ;
}
// 走到这里的逻辑都是动态内容了
// 根据我们在配置中说的,按照 URL -> servlet-name -> Servlet 对象的链条
// 最终找到要处理本次请求的 Servlet 对象
Servlet ins = findInstance ( req . getURL ());
// 调用 Servlet 对象的 service 方法
// 这里就会最终调用到我们自己写的 HttpServlet 的子类里的方法了
try {
ins . service ( req , resp );
} catch ( Exception e ) {
// 返回 500 页面,表示服务器内部错误
}
}
}
小结
Tomcat Socket 中读到的 HTTP 请求是一个字符串 , 然后会按照 HTTP 协议的格式解析成一个
HttpServletRequest 对象 .
Tomcat 会根据 URL 中的 path 判定这个请求是请求一个静态资源还是动态资源 . 如果是静态资源 ,
直接找到对应的文件把文件的内容通过 Socket 返回 . 如果是动态资源 , 才会执行到 Servlet 的相关
逻辑 .
Tomcat 会根据 URL 中的 Context Path Servlet Path 确定要调用哪个 Servlet 实例的 service
.
通过 service 方法 , 就会进一步调用到我们之前写的 doGet 或者 doPost
3) 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 方法 .
理解此处的 多态
我们前面自己写的 HelloServlet , 继承自 HttpServlet . HttpServlet 又继承自 Servlet. 相当
HelloServlet 就是 Servlet 的子类 .
接下来 , Tomcat 启动阶段 , Tomcat 已经根据注解的描述 , 创建了 HelloServlet 的实例 , 然后把这
个实例放到了 Servlet 数组中 .
后面我们根据请求的 URL 从数组中获取到了该 HelloServlet 实例 , 但是我们是通过 Servlet ins
这样的父类引用来获取到 HelloServlet 实例的 .
最后 , 我们通过 ins.doGet() 这样的代码调用 doGet 的时候 , 正是 " 父类引用指向子类对象 ", 此时
就会触发多态机制 , 从而调用到我们之前在 HelloServlet 中所实现的 doGet 方法
等价代码 :
Servlet ins = new HelloServlet ();
ins . doGet ( req , resp );

Servlet API 详解

HttpServlet

我们写 Servlet 代码的时候 , 首先第一步就是先创建类 , 继承自 HttpServlet, 并重写其中的某些方法 .
核心方法

 

我们实际开发的时候主要重写 doXXX 方法, 很少会重写 init / destory / service .  

这些方法的调用时机 , 就称为 "Servlet 生命周期 ". ( 也就是描述了一个 Servlet 实例从生到死的过
)

注意: HttpServlet 的实例只是在程序启动时创建一次. 而不是每次收到 HTTP 请求都重新创建实例. 

代码示例: 处理 GET 请求

创建 MethodServlet.java, 创建 doGet 方法
@WebServlet ( "/method" )
public class MethodServlet extends HttpServlet {
@Override
protected void doGet ( HttpServletRequest req , HttpServletResponse resp )
throws ServletException , IOException {
resp . getWriter (). write ( "GET response" );
}
}

创建 testMethod.html, 放到 webapp 目录中, 形如 

一个 Servlet 程序中可以同时部署静态文件. 静态文件就放到 webapp 目录中即可

<button onclick = "sendGet()" > 发送 GET 请求 </button>
<script>
function sendGet () {
ajax ({
method : 'GET' ,
url : 'method' ,
callback : function ( body , status ) {
console . log ( body );
}
});
}
// 把之前封装的 ajax 函数拷贝过来
function ajax ( args ) {
var xhr = new XMLHttpRequest ();
xhr . onreadystatechange = function () {
// 0: 请求未初始化
// 1: 服务器连接已建立
// 2: 请求已接收
// 3: 请求处理中
// 4: 请求已完成,且响应已就绪
if ( xhr . readyState == 4 ) {
args . callback ( xhr . responseText , xhr . status )
}
}
xhr . open ( args . method , args . url );
if ( args . contentType ) {
xhr . setRequestHeader ( 'Content-type' , args . contentType );
}
if ( args . body ) {
xhr . send ( args . body );
} else {
xhr . send ();
}
}
</script>
重新部署程序 , 使用 URL http://127.0.0.1:8080/ServletHelloWorld/testMethod.html 访问页

点击 "发送 GET 请求" 按钮, 即可在控制台看到响应内容

通过 Fiddler 抓包 , 可以看到 ,当浏览器中输入 URL 之后 , 浏览器先给服务器发送了一个 HTTP GET 请求

当点击 "发送 GET 请求" 按钮, 浏览器又通过 ajax 给服务器发送了一个 HTTP GET 请求 

 

注意这个 ajax 请求的 URL 路径 . 代码中写的 URL url: 'method', 为一个相对路径 , 最终真实发
送的请求的 URL 路径为 /ServletHelloWorld/method
关于乱码问题
如果我们在响应代码中写入中文 , 例如

 resp.getWriter().write("GET 响应");

此时在浏览器访问的时候, 会看到 "乱码" 的情况

关于 " 乱码 ":
中文的编码方式有很多种 . 其中最常见的就是 utf-8 .
如果没有显式的指定编码方式 , 则浏览器不能正确识别编码 , 就会出现乱码的情况

此时通过抓包可以看到 , 当加上了 resp.setContentType("text/html ; charset=utf - 8") ; 代码之后 ,
响应中多了 Content-Type 字段 , 内部指定了编码方式 . 浏览器看到这个字段就能够正确解析中文了

代码示例: 处理 POST 请求

MethodServlet.java , 新增 doPost 方法 .
@Override
protected void doPost ( HttpServletRequest req , HttpServletResponse resp ) throws
ServletException , IOException {
resp . setContentType ( "text/html; charset=utf-8" );
resp . getWriter (). write ( "POST 响应 " );
}

testMethod.html , 新增一个按钮, 和对应的点击事件处理函数 

<button onclick = "sendPost()" > 发送 POST 请求 </button>
<script>
function sendPost () {
ajax ({
method : 'POST' ,
url : 'method' ,
callback : function ( body , status ) {
console . log ( body );
}
})
}
</script>
重新部署程序 , 使用 URL http://127.0.0.1:8080/ServletHelloWorld/testMethod.html 访问页

点击 "发送 POST 请求" 按钮, 可以在控制台中看到结果

HttpServletRequest

Tomcat 通过 Socket API 读取 HTTP 请求 ( 字符串 ), 并且按照 HTTP 协议的格式把字符串解析成
HttpServletRequest 对象 .
核心方法

 通过这些方法可以获取到一个请求中的各个方面的信息

注意 : 请求对象是服务器收到的内容 , 不应该修改 . 因此上面的方法也都只是 " " 方法 , 而不是 " "
方法 .

代码示例 : 打印请求信息
创建 ShowRequest
@WebServlet ( "/showRequest" )
public class ShowRequest extends HttpServlet {
@Override
protected void doGet ( HttpServletRequest req , HttpServletResponse resp )
throws ServletException , IOException {
resp . setContentType ( "text/html; charset=utf-8" );
StringBuilder respBody = new StringBuilder ();
respBody . append ( req . getProtocol ());
respBody . append ( "<br>" );
respBody . append ( req . getMethod ());
respBody . append ( "<br>" );
respBody . append ( req . getRequestURI ());
respBody . append ( "<br>" );
respBody . append ( req . getContextPath ());
respBody . append ( "<br>" );
respBody . append ( req . getQueryString ());
respBody . append ( "<br>" );
respBody . append ( "<h3>headers:</h3>" );
Enumeration < String > headerNames = req . getHeaderNames ();
while ( headerNames . hasMoreElements ()) {
String headerName = headerNames . nextElement ();
respBody . append ( headerName + " " );
respBody . append ( req . getHeader ( headerName ));
respBody . append ( "<br>" );
}
resp . getWriter (). write ( respBody . toString ());
}
}
部署程序 .
在浏览器通过 URL http://127.0.0.1:8080/ServletHelloWorld/showRequest 访问 , 可以看到

 

代码示例 : 获取 GET 请求中的参数
GET 请求中的参数一般都是通过 query string 传递给服务器的 . 形如
https://v.bitedu.vip/personInf/student?userId=1111&classId=100
此时浏览器通过 query string 给服务器传递了两个参数 , userId classId, 值分别是 1111 100
在服务器端就可以通过 getParameter 来获取到参数的值 .
创建 GetParameter
@WebServlet ( "/getParameter" )
public class GetParameter extends HttpServlet {
@Override
protected void doGet ( HttpServletRequest req , HttpServletResponse resp )
throws ServletException , IOException {
resp . setContentType ( "text/html; charset=utf-8" );
String userId = req . getParameter ( "userId" );
String classId = req . getParameter ( "classId" );
resp . getWriter (). write ( "userId: " + userId + ", " + "classId: " +
classId );
}
}
重新部署程序 , 在浏览器中通过 http://127.0.0.1:8080/ServletHelloWorld/getParameter 访问 ,
可以看到
当没有 query string 的时候 , getParameter 获取的值为 null.
如果通过 http://127.0.0.1:8080/ServletHelloWorld/getParameter?userId=123&classId=456
访问 , 可以看到
此时说明服务器已经获取到客户端传递过来的参数 .
getParameter 的返回值类型为 String. 必要的时候需要手动把 String 转成 int.
代码示例 : 获取 POST 请求中的参数 (1)
POST 请求的参数一般通过 body 传递给服务器 . body 中的数据格式有很多种 . 如果是采用 form 表单的形式, 仍然可以通过 getParameter 获取参数的值 .
创建类 PostParameter

@WebServlet ( "/postParameter" )
public class PostParameter extends HttpServlet {
@Override
protected void doPost ( HttpServletRequest req , HttpServletResponse resp )
throws ServletException , IOException {
resp . setContentType ( "text/html; charset=utf-8" );
String userId = req . getParameter ( "userId" );
String classId = req . getParameter ( "classId" );
resp . getWriter (). write ( "userId: " + userId + ", " + "classId: " +
classId );
}
}

创建 testPost.html, 放到 webapp 目录中

<form action = "postParameter" method = "POST" >
<input type = "text" name = "userId" >
<input type = "text" name = "classId" >
<input type = "submit" value = " 提交 " >
</form>
重新部署程序 , 通过 URL http://127.0.0.1:8080/ServletHelloWorld/testPost.html 访问 , 可以
看到 HTML

在输入框中输入内容, 点击提交

 可以看到跳转到了新的页面, 并显示出了刚刚传入的数据

此时通过抓包可以看到, form 表单构造的 body 数据的格式为:

 

代码示例 : 获取 POST 请求中的参数 (2)
如果 POST 请求中的 body 是按照 JSON 的格式来传递 , 那么获取参数的代码就要发生调整 .
创建 PostParameterJson
@WebServlet ( "/postParameterJson" )
public class PostParameterJson extends HttpServlet {
@Override
protected void doPost ( HttpServletRequest req , HttpServletResponse resp )
throws ServletException , IOException {
resp . setContentType ( "application/json;charset=utf-8" );
String body = readBody ( req );
resp . getWriter (). write ( body );
}
private String readBody ( HttpServletRequest req ) throws IOException {
int contentLength = req . getContentLength ();
byte [] buffer = new byte [ contentLength ];
InputStream inputStream = req . getInputStream ();
inputStream . read ( buffer );
return new String ( buffer , "utf-8" );
}
}

 创建 testPostJson.html

<button onclick = "sendJson()" > 发送 JSON 格式 POST 请求 </button>
<script>
function sendJson () {
ajax ({
url : 'postParameterJson' ,
method : 'POST' ,
contentType : 'application/json; charset=utf-8' ,
body : JSON . stringify ({ userId : 123 , classId : 456 }),
callback : function ( body , status ) {
console . log ( body );
}
});
}
function ajax ( args ) {
// 函数体略 .... 参考之前封装的版本 .
}
</script>

 在浏览器中通过 http://127.0.0.1:8080/ServletHelloWorld/testPostJson.html 访问, 可以看到

 

点击按钮, 则浏览器就会给服务器发送一个 POST 请求, body 中带有 JSON 格式.

服务器收到这个结果之后, 又把数据返回了回去, 浏览器中看到了响应结果.  

 

HttpServletResponse

Servlet 中的 doXXX 方法的目的就是根据请求计算得到相应 , 然后把响应的数据设置到
HttpServletResponse 对象中 .
然后 Tomcat 就会把这个 HttpServletResponse 对象按照 HTTP 协议的格式 , 转成一个字符串 , 并通过
Socket 写回给浏览器 .
核心方法

 

注意 : 响应对象是服务器要返回给浏览器的内容 , 这里的重要信息都是程序猿设置的 . 因此上面的方
法都是 " " 方法 .
注意 : 对于状态码 / 响应头的设置要放到 getWriter / getOutputStream 之前 . 否则可能设置失效 .

代码示例 : 设置状态码
实现一个程序 , 用户在浏览器通过参数指定要返回响应的状态码 .
创建 StatusServlet
@WebServlet ( "/statusServlet" )
public class StatusServlet extends HttpServlet {
@Override
protected void doGet ( HttpServletRequest req , HttpServletResponse resp )
throws ServletException , IOException {
String statusString = req . getParameter ( "status" );
if ( statusString != null ) {
resp . setStatus ( Integer . parseInt ( statusString ));
}
resp . getWriter (). write ( "status: " + statusString );
}
}
部署程序 , 在浏览器中通过 URL http://127.0.0.1:8080/ServletHelloWorld/statusServlet?
status=200 访问 , 可以看到

 抓包结果:

变换不同的 status 的值, 就可以看到不同的响应结果

代码示例 : 自动刷新
实现一个程序 , 让浏览器每秒钟自动刷新一次 . 并显示当前的时间戳 .
创建 AutoRefreshServlet
@WebServlet ( "/autoRefreshServlet" )
public class AutoRefreshServlet extends HttpServlet {
@Override
protected void doGet ( HttpServletRequest req , HttpServletResponse resp )
throws ServletException , IOException {
resp . setHeader ( "Refresh" , "1" );
long timeStamp = new Date (). getTime ();
resp . getWriter (). write ( "timeStamp: " + timeStamp );
}
}

通过 HTTP 响应报头中的 Refresh 字段 , 可以控制浏览器自动刷新的时机 .
通过 Date 类的 getTime 方法可以获取到当前时刻的毫秒级时间戳 .
部署程序 , 通过 URL http://127.0.0.1:8080/ServletHelloWorld/autoRefreshServlet 访问 , 可以
看到浏览器每秒钟自动刷新一次 .

抓包结果

代码示例 : 重定向
实现一个程序 , 返回一个重定向 HTTP 响应 , 自动跳转到另外一个页面 .
创建 RedirectServlet
@WebServlet ( "/redirectServlet" )
public class RedirectServlet extends HttpServlet {
@Override
protected void doGet ( HttpServletRequest req , HttpServletResponse resp )
throws ServletException , IOException {
resp . sendRedirect ( "http://www.sogou.com" );
}
}
部署程序 , 通过 URL http://127.0.0.1:8080/ServletHelloWorld/redirectServlet 访问 , 可以看
, 页面自动跳转到 搜狗主页 了 .
抓包结果

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值