Servlet 是什么
回顾 动态页面 vs 静态页面
Servlet 主要做的工作
- 允许程序猿注册一个类, 在 Tomcat 收到某个特定的 HTTP 请求的时候, 执行这个类中的一些代码.
- 帮助程序猿解析 HTTP 请求, 把 HTTP 请求从一个字符串解析成一个 HttpRequest 对象.
- 帮助程序猿构造 HTTP 响应. 程序猿只要给指定的 HttpResponse 对象填写一些属性字段, Servlet就会自动的安装 HTTP 协议的方式构造出一个 HTTP 响应字符串, 并通过 Socket 写回给客户端
第一个 Servlet 程序
1. 创建项目
![](https://i-blog.csdnimg.cn/blog_migrate/2666af98b400852368e09c526b386428.png)
2) 选择项目要存放的目录
3) 项目创建完毕后, 一般右下角会弹出以下对话框. 选择 Enable Auto-Import
2. 引入依赖
2) 选择版本. 一般我们使用 3.1.0 版本
![](https://i-blog.csdnimg.cn/blog_migrate/b526edfea6db70f403d2a0b212158437.png)
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.0http://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. 创建目录
![](https://i-blog.csdnimg.cn/blog_migrate/ca0c4b953b2f8cfbf9f4828fd26b93f8.png)
- src 表示源代码所在的目录
- main/java 表示源代码的根目录. 后续创建 .java 文件就放到这个目录中.
- main/resources 表示项目的一些资源文件所在的目录. 此处暂时不关注.
- test/java 表示测试代码的根目录. 此处暂时不关注.
这些目录还不够, 我们还需要创建一些新的目录/文件.
1) 创建 webapp 目录
![](https://i-blog.csdnimg.cn/blog_migrate/c3255630b66198bac03ae8091e1f9a13.png)
2) 创建 web.xml
3) 编写 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>
4. 编写代码
@WebServlet ( "/hello" )public class HelloServlet extends HttpServlet {@Overrideprotected 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 写回给浏览器
5. 打包程序
![](https://i-blog.csdnimg.cn/blog_migrate/a592857949acf24739b72efbec3c67ab.png)
如果比较顺利的话, 能够看到 SUCCESS 这样的字样
打包成功后, 可以看到在 target 目录下, 生成了一个 jar 包
war 包和 jar 包的区别
<packaging> war </packaging>
<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.0http://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. 部署程序
看到这个日志说明 Tomcat 已经正确识别了 ServletHelloWorld 这个 webapp
7. 验证程序
![](https://i-blog.csdnimg.cn/blog_migrate/7a5b489e8dc1c05746aa6d497685515a.png)
注意: URL 中的 PATH 分成两个部分, 其中 HelloServlet 为 Context Path, hello 为 Servlet Path
更方便的部署方式
安装 Smart Tomcat 插件
![](https://i-blog.csdnimg.cn/blog_migrate/9df9abb3c24ec392cf6d5c765b237b75.png)
2) 选择 Plugins, 选择 Marketplace, 搜索 "tomcat", 点击 "Install".
3) 安装完毕之后, 会提示 "重启 IDEA"
![](https://i-blog.csdnimg.cn/blog_migrate/0b6c14f89d2bfb88538857385d25c92e.png)
2) 选择左侧的 "Smart Tomcat"
![](https://i-blog.csdnimg.cn/blog_migrate/62742a3d43ea1999aa0dc033eb33873d.png)
其中 Context Path 默认填写的值是项目名称.
这会影响到后面咱们的访问页面.
4) 点击 OK 之后, 右上角变成了
点击绿色的三角号, IDEA 就会自动进行编译, 部署, 启动 Tomcat 的过程
![](https://i-blog.csdnimg.cn/blog_migrate/cacf943af8eb359202a4ff3055dcf15d.png)
注意路径的对应关系.
![](https://i-blog.csdnimg.cn/blog_migrate/4bc221ecf9e9e8fe19c3ac9bf3c7267c.png)
访问出错怎么办?
出现 404
![](https://i-blog.csdnimg.cn/blog_migrate/a2e0f2fb468df62f448129cd2c9c8965.png)
![](https://i-blog.csdnimg.cn/blog_migrate/36884a4833cf9c9719fbe377569451b0.png)
在 Tomcat 启动的时候也有相关的错误提示
出现 405
@WebServlet ( "/hello" )public class HelloServlet extends HttpServlet {}
![](https://i-blog.csdnimg.cn/blog_migrate/3de0b95d754d247212a43e3c0bab3a59.png)
出现 500
@WebServlet ( "/hello" )public class HelloServlet extends HttpServlet {@Overrideprotected void doGet ( HttpServletRequest req , HttpServletResponse resp )throws ServletException , IOException {String s = null ;resp . getWriter (). write ( s . length ());}}
出现 "空白页面"
@WebServlet ( "/hello" )public class HelloServlet extends HttpServlet {@Overrideprotected void doGet ( HttpServletRequest req , HttpServletResponse resp )throws ServletException , IOException {System . out . println ( "hello" );}}
![](https://i-blog.csdnimg.cn/blog_migrate/cd5845f7a5ab72425fe86f505c52d083.png)
抓包可以看到, 响应 body 中的内容就是 "空数据"
![](https://i-blog.csdnimg.cn/blog_migrate/04bec8fdb25528761b7217531cbb64ae.png)
![](https://i-blog.csdnimg.cn/blog_migrate/8f4376032d19f57b99ab4663ef1cf9ae.png)
看到的现象:
Servlet 运行原理
Tomcat 的定位
![](https://i-blog.csdnimg.cn/blog_migrate/c5789d5ae660d5be9238886562c6e9e4.png)
- 用户在浏览器输入一个 URL, 此时浏览器就会构造一个 HTTP 请求.
- 这个 HTTP 请求会经过网络协议栈逐层进行 封装 成二进制的 bit 流, 最终通过物理层的硬件设备转换成光信号/电信号传输出去.
- 这些承载信息的光信号/电信号通过互联网上的一系列网络设备, 最终到达目标主机(这个过程也需要网络层和数据链路层参与).
- 服务器主机收到这些光信号/电信号, 又会通过网络协议栈逐层进行 分用, 层层解析, 最终还原成HTTP 请求. 并交给 Tomcat 进程进行处理(根据端口号确定进程)
- Tomcat 通过 Socket 读取到这个请求(一个字符串), 并按照 HTTP 请求的格式来解析这个请求, 根据请求中的 Context Path 确定一个 webapp, 再通过 Servlet Path 确定一个具体的 类. 再根据当前请求的方法 (GET/POST/...), 决定调用这个类的 doGet 或者 doPost 等方法. 此时我们的代码中的doGet / doPost 方法的第一个参数 HttpServletRequest 就包含了这个 HTTP 请求的详细信息.
- 在我们的 doGet / doPost 方法中, 就执行到了我们自己的代码. 我们自己的代码会根据请求中的一些信息, 来给 HttpServletResponse 对象设置一些属性. 例如状态码, header, body 等.
- 我们的 doGet / doPost 执行完毕后, Tomcat 就会自动把 HttpServletResponse 这个我们刚设置好的对象转换成一个符合 HTTP 协议的字符串, 通过 Socket 把这个响应发送出去.
- 此时响应数据在服务器的主机上通过网络协议栈层层 封装, 最终又得到一个二进制的 bit 流, 通过物理层硬件设备转换成光信号/电信号传输出去.
- 这些承载信息的光信号/电信号通过互联网上的一系列网络设备, 最终到达浏览器所在的主机(这个过程也需要网络层和数据链路层参与).
- 浏览器主机收到这些光信号/电信号, 又会通过网络协议栈逐层进行 分用, 层层解析, 最终还原成HTTP 响应, 并交给浏览器处理.
- 浏览器也通过 Socket 读到这个响应(一个字符串), 按照 HTTP 响应的格式来解析这个响应. 并且把body 中的数据按照一定的格式显示在浏览器的界面上.
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 服务器// 并用线程池的方式分别处理每一个 RequestServerSocket 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 是运行在 多线程环境 下的.
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 页面,表示服务器内部错误}}}
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 ins = new HelloServlet ();ins . doGet ( req , resp );
Servlet API 详解
HttpServlet
我们实际开发的时候主要重写 doXXX 方法, 很少会重写 init / destory / service .
![](https://i-blog.csdnimg.cn/blog_migrate/676b1dd54dd0028143471459649925d3.png)
注意: HttpServlet 的实例只是在程序启动时创建一次. 而不是每次收到 HTTP 请求都重新创建实例.
代码示例: 处理 GET 请求
@WebServlet ( "/method" )public class MethodServlet extends HttpServlet {@Overrideprotected 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>
![](https://i-blog.csdnimg.cn/blog_migrate/fdcf47a449697820845efd7554302734.png)
点击 "发送 GET 请求" 按钮, 即可在控制台看到响应内容
![](https://i-blog.csdnimg.cn/blog_migrate/43b0dd2514920d3353aa07cb2382c4b3.png)
当点击 "发送 GET 请求" 按钮, 浏览器又通过 ajax 给服务器发送了一个 HTTP GET 请求
resp.getWriter().write("GET 响应");
此时在浏览器访问的时候, 会看到 "乱码" 的情况
代码示例: 处理 POST 请求
@Overrideprotected void doPost ( HttpServletRequest req , HttpServletResponse resp ) throwsServletException , 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>
![](https://i-blog.csdnimg.cn/blog_migrate/e21e5e4c7f363644b82246b01dd063bc.png)
点击 "发送 POST 请求" 按钮, 可以在控制台中看到结果
HttpServletRequest
![](https://i-blog.csdnimg.cn/blog_migrate/3bfb3b82ae0fc603fe43d2b6e37283e8.png)
通过这些方法可以获取到一个请求中的各个方面的信息
@WebServlet ( "/showRequest" )public class ShowRequest extends HttpServlet {@Overrideprotected 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 ());}}
@WebServlet ( "/getParameter" )public class GetParameter extends HttpServlet {@Overrideprotected 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 );}}
![](https://i-blog.csdnimg.cn/blog_migrate/4812dd264783643e3a2af0f48fe07494.png)
![](https://i-blog.csdnimg.cn/blog_migrate/c70f96ecae0ec26b95eb8e425e9ebc13.png)
@WebServlet ( "/postParameter" )public class PostParameter extends HttpServlet {@Overrideprotected 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>
![](https://i-blog.csdnimg.cn/blog_migrate/2665b16a6f00545d4a7df9280c17ec94.png)
在输入框中输入内容, 点击提交
可以看到跳转到了新的页面, 并显示出了刚刚传入的数据
此时通过抓包可以看到, form 表单构造的 body 数据的格式为:
@WebServlet ( "/postParameterJson" )public class PostParameterJson extends HttpServlet {@Overrideprotected 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
@WebServlet ( "/statusServlet" )public class StatusServlet extends HttpServlet {@Overrideprotected 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 );}}
抓包结果:
变换不同的 status 的值, 就可以看到不同的响应结果
@WebServlet ( "/autoRefreshServlet" )public class AutoRefreshServlet extends HttpServlet {@Overrideprotected void doGet ( HttpServletRequest req , HttpServletResponse resp )throws ServletException , IOException {resp . setHeader ( "Refresh" , "1" );long timeStamp = new Date (). getTime ();resp . getWriter (). write ( "timeStamp: " + timeStamp );}}
![](https://i-blog.csdnimg.cn/blog_migrate/760b51fbfd92a2a416d7574972ba149a.png)
抓包结果
@WebServlet ( "/redirectServlet" )public class RedirectServlet extends HttpServlet {@Overrideprotected void doGet ( HttpServletRequest req , HttpServletResponse resp )throws ServletException , IOException {resp . sendRedirect ( "http://www.sogou.com" );}}
![](https://i-blog.csdnimg.cn/blog_migrate/5d496dd9e41d83a13cb9c157e08279b5.png)