一.什么是servlet
Servlet 是一种实现动态页面的技术。 是一组 Tomcat 提供给程序猿的 API, 帮助程序猿简单高效的开发一个 web app。
1.回顾 动态页面 vs 静态页面
静态页面也就是内容始终固定的页面。即使 用户不同/时间不同/输入的参数不同 , 页面内容也不会发生变化。(除非网站的开发人员修改源代码, 否则页面内容始终不变)。对应的, 动态页面指的就是 用户不同/时间不同/输入的参数不同, 页面内容会发生变化。
举个例子哔哩哔哩由于用户输入的参数或者时间不同页面是会发生变化的,所以它是一个动态页面。
构建动态页面的技术有很多, 每种语言都有一些相关的库/框架来做这件事。
Servlet 就是 Tomcat 这个 HTTP 服务器提供给 Java 的一组 API, 来完成构建动态页面这个任务。
2.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 项目。
- 菜单 -> 文件 -> 新建项目 -> Maven
一定要选择Maven!!! Maven是java中常用的构建工具,一个程序在编写过程中,往往需要涉及第三方库的依赖,另外还需要针对这个写好的程序进行打包****部署。 Maven存在的意义就是能够方便的对依赖进行管理和打包。
打开后看编译器左边:
2. 引入依赖
当前的代码要使用servlet开发,servlet并不是java标准库自带的,而是Tomcat提供的api,所以得先把servlet的依赖获取过来。
- 在中央仓库中央仓库地址中搜索 “servlet”, 找到java servlet api
点开java servlet api
选择3.1.0版本。Servlet和Tomcat是对应关系,如果Tomcat版本是8,那么Servlet版本是3.1,如果不匹配可能出现问题。
复制这段代码到pom文件
<!-- 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>
先写标签,然后标签里面把复制的代码写入。
点击Maven
点击该按钮刷新Maven,一般引入的依赖都得手动刷新
如果控制台出现这种情况说明你的环境没有配对
解决这个问题首先File->settings->Maven
点击Maven home path:
然后选择这个文件
同理选择usering settings file:
选择该文件然后点击刷新再看控制台,此时就没有报错了。
3. 创建目录
Tomcat对于Servlet项目是有一定的额外要求的,Maven是一个通用的工具,可以管理的不仅仅是Servlet项目,还可以管理其他项目,在这个基础上,在按照Servlet项目要求创建出一些特定的目录和文件。
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. 编写代码
在 java 目录中创建一个类 HelloServlet, 代码如下:
创建一个类 HelloServlet , 继承自 HttpServlet,如果HttpServlet类编译器报红说明Servlet的依赖没有正确的引入,如果颜色正常说明引入成功。
req代表的是这次HTTP请求的内容,resp代表响应的内容。doGet是重写父类的方法,该方法本身是HttpServlet的。doGet方法不需要手动调用,本质是一个回调函数,把这个方法写好后会交给Tomcat,Tomcat在收到一个合适的请求后就会调用doGet方法。调用doGet的时候,Tomcat就会解析这次的HTTP请求,生成一个HttpservletRequest对象(这个对象的属性啥的都是HTTP协议格式),同时Tomcat也会构造出一个空的HttpServletRequese对象,把这个对象传到doGet里面,doGet要做的事情就是根据请求计算响应。doGet里面的代码就会根据req里面的不同参数的细节,生成一个具体的resp对象(往空对象里面设计属性),tomcat就会根据这个响应对象转换符号http协议的响应报文返回给浏览器。
创建一个类 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 写回给浏览器。
这个代码虽然只有寥寥几行, 但是包含的信息量是巨大的。
- 我们的代码不是通过 main 方法作为入口了。main 方法已经被包含在 Tomcat 里, 我们写的代码会被 Tomcat 在合适的时机调用起来。
此时我们写的代码并不是一个完整的程序, 而是 Tomcat 这个程序的一小部分逻辑。- 我们随便写个类都能被 Tomcat 调用嘛? 满足啥样条件才能被调用呢?
主要满足三个条件:
a) 创建的类需要继承自 HttpServlet
b) 这个类需要使用 @WebServlet 注解关联上一个 HTTP 的路径
c) 这个类需要实现 doXXX 方法。
当这三个条件都满足之后, Tomcat 就可以找到这个类, 并且在合适的时机进行调用。
5. 打包程序
使用 maven 进行打包. 打开 maven 窗口 (一般在 IDEA 右侧就可以看到 Maven 窗口, 如果看不到的话,可以通过 菜单 -> View -> Tool Window -> Maven 打开),然后展开 Lifecycle , 双击 package 即可进行打包.
在打包之前pom文件中加这段代码
<packaging>war</packaging><build><finalName>java108</finalName></build>
这段代码的意思是打出来的包叫什么名字,finalname标签中写的是java108,所以包打出来的名字是java108。Tomact需要的是war包,并非jar包,所以往pom文件中写入war标签把jar包改为war包。
war 包和 jar 包的区别
jar 包是普通的 java 程序打包的结果. 里面会包含一些 .class 文件。
war 包是 java web 的程序, 里面除了会包含 .class 文件之外, 还会包含 HTML, CSS, JavaScript, 图
片, 以及其他的 jar 包。打成 war 包格式才能被 Tomcat 识别.
如果比较顺利的话, 能够看到 SUCCESS 这样的字样.
打包成功后, 可以看到在 target 目录下, 生成了一个 war 包.
然后打开这个war包的路径
点击Explorer在文件夹中找到打出来的包
把 war 包拷贝到 Tomcat 的 webapps 目录下。
当复制进来后Tomcat就能识别有新的webapp来了,对新的war包进行压缩。
## 7. 验证程序
此时通过浏览器访问 http://127.0.0.1:8080/java108/hello1就能看到结果了
8.更方便的部署方式
手动拷贝 war 包到 Tomcat 的过程比较麻烦. 我们还有更方便的办法。此处我们使用 IDEA 中的 Smart Tomcat 插件完成这个工作。
理解 “插件” (plugin)
天火 + 擎天柱 => 会飞的擎天柱。
天火在牺牲之前把自己变成了擎天柱的 “飞行插件”。在擎天柱需要起飞的时候就变成翅膀装在擎天柱身上。不需要起飞的时候就卸下来放到擎天柱的集装箱里。程序开发的时候也经常如此。像 IDEA 这样的程序虽然功能强大, 但是也无法面面俱到。对于一些特殊场景的功能, 开发者就可以开发一些 “插件”。如果需要这个插件, 就单独安装。插件就是对程序的一些特定场景, 做出一些特定的功能的扩展。
9.安装 Smart Tomcat 插件
- 菜单 -> 文件 -> Settings
- 选择 Plugins, 选择 Marketplace, 搜索 “tomcat”, 点击 “Install”.
- 安装完毕之后, 会提示 “重启 IDEA”
配置 Smart Tomcat 插件
- 点击右上角的 “Add Configuration”
- 选择左侧的 “Smart Tomcat”
- 在 Name 这一栏填写一个名字(可以随便写)在 Tomcat Server 这一栏选择 Tomcat 所在的目录. 其他的选项不必做出修改。
- 点击 OK 之后, 右上角变成了
点击run:
然后浏览器输入url查看结果。
使用 Smart Tomcat 部署的时候, 我们发现 Tomcat 的 webapps 内部并没有被拷贝一个 war 包,
也没有看到解压缩的内容。Smart Tomcat 相当于是在 Tomcat 启动的时候直接引用了项目中的 webapp 和 target 目录。
三.几种常见的错误
1.出现 404
404 表示用户访问的资源不存在。大概率是 URL 的路径写的不正确。
错误实例2: 少写了 Servlet Path通过 /eat1访问服务器。
错误实例3: Servlet Path 写的和 URL 不匹配。
错误实例4: web.xml 写错了
清除 web.xml 中的内容
在 Tomcat 启动的时候也有相关的错误提示
2.出现 405
405 表示对应的 HTTP 请求方法没有实现.
错误实例: 没有实现 doGet 方法.
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
}
在浏览器地址栏直接输入 URL , 会发送一个 HTTP GET 请求。此时就会根据 /eat1/hello1 这个路径找到 HelloServlet 这个类. 并且尝试调用HelloServlet 的 doGet 方法。但是如果没有实现 doGet 方法, 就会出现上述现象。
3.出现 500
往往是 Servlet 代码中抛出异常导致的。
错误实例:
@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());}}
重启 Tomcat 服务器。重新访问页面, 可以看到:
异常信息里已经提示了出现异常的代码是 HelloServlet.java 的第 13 。resp.getWriter().write(s.length());仔细检查这里的代码就可以看到空指针异常。
4.出现 “空白页面”
错误实例:
修改代码, 去掉 resp.getWritter().write() 操作。
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {System.out.println("hello");}}
重启服务器,访问服务器, 可以看到一个空白页面。
抓包可以看到, 响应 body 中的内容就是 “空数据”
5.小结
初学 Servlet, 遇到的这类问题会非常多。我们不光要学习 Servlet 代码的基本写法, 也要学习排查错误的思路。程序猿调试 BUG 如同医生诊病。
一个有经验的程序猿和一个新手程序猿相比, 最大的优势往往不是代码写的多好, 而是调试效率有多高.。同一个问题可能新手花了几天都无法解决的, 但是有经验的程序猿可能几分钟就搞定了。
你还觉得 “程序猿是吃青春饭” 嘛?熟悉 HTTP 协议能够让我们调试问题事半功倍。4xx 的状态码表示路径不存在, 往往需要检查 URL 是否正确, 和代码中设定的 Context Path 以及Servlet Path 是否一致。
5xx 的状态码表示服务器出现错误, 往往需要观察页面提示的内容和 Tomcat 自身的日志, 观察是否
存在报错。
出现连接失败往往意味着 Tomcat 没有正确启动, 也需要观察 Tomcat 的自身日志是否有错误提示。
空白页面这种情况则需要我们使用抓包工具来分析 HTTP 请求响应的具体交互过程。
Servlet 运行原理
在 Servlet 的代码中我们并没有写 main 方法, 那么对应的 doGet 代码是如何被调用的呢? 响应又是如何返回给浏览器的?
Tomcat 的定位
我们自己的实现是在 Tomcat 基础上运行的。
当浏览器给服务器发送请求的时候, Tomcat 作为 HTTP 服务器, 就可以接收到这个请求。HTTP 协议作为一个应用层协议, 需要底层协议栈来支持工作. 如下图所示:
更详细的交互过程可以参考下图:
- 接收请求:
用户在浏览器输入一个 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 中的数据按照一定的格式显示在浏览器的界面上。