目录
- 本章重点
- Servlet创建项目流程
- Servlet常见出错响应状态码
-
- get请求
- post请求
- Servlet运行原理
-
- tomcat定位
- tomcat伪代码
- Servlet API详解
-
- HttpServlet
- HttpServletResquest
- HttpServletRespondse
本章重点
- 掌握创建
Servlet
项目的七步骤 - 了解
Servlet
运行原理 - 熟悉使用
Servlet
中的关键api
(HttpServlet/HttpServletRequest/HttpServletResponse
)的方法 - 熟悉用户层协议报文的设计
- 能够通过
Servlet
编写http
请求和响应 - 熟悉掌握在
tomcat
下部署web
项目
Servlet创建项目流程
创建
Servlet
项目七步骤:
- 创建
Maven
项目- 引入依赖(
Servlet
jar包导入到pom.xml
)- 创建目录
src/main/webapp/WEB-INF/web.xml
- 编写
servlet
代码- 打包
- 部署(这里打包和部署可以通过引入
smart tomcat
插件完成)- 验证
Servlet常见出错响应状态码
405
Method Not Allowed
方法不匹配
我们构造的doGet
就只能用get
请求处理!
doPost
需要通过post
请求处理!
如果不匹配响应就会访问这个405
状态码!
可以看到我们需要用Post
请求处理该servlet
代码,而我们却直接输入url
这样的方式就是直接通过get
请求访问服务器!
当我们没有把调用父类下的doGet
方法注释掉时,也会返回405
状态码!我们看一下源码就知道了!
这里父类的doGet
方法直接返回405
!
我们怎样区分get
和post
请求呢?
get请求
- 直接在浏览器搜索框中输入
url
- 我们
html
下的<a>
标签,img/linkscript
标签等等! form
表单指定method
属性为get
ajax
构造get请求在type
设置为get
post请求
- 通过
form
表单,method
指定为post
ajax
构造post请求,type
指定为post
500
Internal Server Error
服务器出错
这里的500状态码对我们初学者来说是很常见的,就是我们的服务器出错,也就是我们的Servlet
代码发生异常并没有处理掉!这回将异常抛到tomcat
而tomcat直接将异常返回给客户端!
这里出bug
了,但是浏览器还是将响应信息返回到浏览器上了!
如果我们将上面的响应信息给去掉,就可以看到返回的错误信息在页面上,通过这个错误可以精准找到我们的bug!
服务器未启动或者端口号被占用
出现这个错误,说明是TCP
连接出现了问题!
这个错误说明我们该Servlet
类的路径没有按指定规则编写!
我们需要加上/
Servlet运行原理
我们的Servlet
代码连一个main
方法都没有是怎么运行呢?
这里我们需要了解一下tomcat
帮我们做的工作和处理机制!
tomcat定位
我们知道tomcat就是一个http
服务器,而http
是用户层协议!
所以我们的Servlet
代码是基于tomcat
运行的!
当用户在浏览器中发送请求后,tomcat
作为应用层服务器就可以接这个请求,而http
只是一个应用层协议,需要通过其他层协议协助传输!这里的传输过程也是要经过5层协议进行封装分用,这里和之前的一样!
我们分析一下上述流程
- 接收请求
我们浏览器客户端发送一个请求,然后用户的请求参数随着查询字符串或者body构造了一个
http
请求然后到达了用户层,用户层协议就是http,然后调用操作系统内核下的,socket
api发送到网络层,网络层加上TCP
报头,到达传输层加上IP
协议报头,然后就传输到了数据链路层,加上帧头帧尾,最后到达物理层调用网卡设备将这些信息转换成光信号或者高低电平,通过网络设备传输到达服务器主机,服务器主机通过网卡接收到这一组信号解析成以太网数据帧,进行分用!层层解析最后解析成一个http
请求并交给tomcat
进程进行处理!
tomcat
拿到http
协议报(字符串)按照协议报格式进行解析,根据ContentPath
路径确定webapp
,在通过ServletPath
确定具体的类,根据请求的方法,决定调用doGET/POST
方法,此时我们的HttpServletResquest
对象就包含了这个请求的详细信息!
- 根据请求处理响应
我们通过
HttpServletRequest
中的请求信息,计算相应的响应信息,通过HttpServletResponse
这个对象,存放响应信息!比如我们可以设置一些响应的状态码,body字段等!
- 返回响应
我们的
doGet/doPost
执行结束后,就会自动把HttpServletResponse
以及我们已经设置的一些属性转换成相应的http
响应,通过socket
发送!后面的过程就是网络传输层层分用分装的过程,最后将响应中的body信息展现在浏览器上给用户!
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
初始化
我们看到这里
tomcat
其实是有main
方法的,tomcat启动就从main
方法开始!
启动后就会将@webServlet
标记的类获取到,这些类已经是.class
文件,需要通过反射机制创建好对应的实例,这些实例创建好就会调用init
方法进行初始化,这个方法在HttpServlet
类中,我们也可以重写这个方法!
这些请求处理业务完成后,就会将这些实例销毁调用其destroy
方法,这里的方法也是在HttpServlet
类中,我们也可以进行重写!
我们可以看到tomcat
的内部也是调用操作系统中的socket
进行网络通信的!
还有这里tomcat
需要处理多个htttp
请求,这里采取了多线程的方式,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 页面,表示服务器内部错误
}
}
}
我们这里的
tomcat
通过调用socketapi
然后获取到http
请求,然后将请求按照http
协议报的格式解析成HttpServlet
对象,然后通过url
中的资源目录,获取到对应的ContentPath
和Servlet
路径获取到对应的文件,如果是静态资源就直接通过socket
返回给客户端,如果是动态资源就会调用HttpServlet
下的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 API详解
Servlet API有很多,我们只需要掌握HttpServlet/HttpServletRequset/HttpServletResponse
这时个关键类中的核心方法即可!
HttpServlet
我们编写Servlet
代码第一步就是继承HttpServlet
类,并重写该类中的某些方法,处理请求!
核心方法:
方法名称 | 调用时机 |
---|---|
init | HttpServlet实例化后就调用 |
destory | HttpServlet实例不再使用就调用该方法 |
service | 收到Http请求就调用,为了匹配相应的处理请求方法 |
doGet/doPost/doPut… | 收到匹配的请求时,由service 方法调用对应的方法! |
而我们实际开发很少重写init/destory
这些tomcat会帮我们调用!
我们主要任务还是处理对应请求,对不同方法请求,重写匹配的doXxx方法,处理不同的请求,返回对应的响应即可!
这些上述方法的调用时机,又称Servlert
的生命周期!
这里的init
方法,当HttpServlet
实例化后就会通过该方法进行初始化,然后生命周期结束就是在destroy
方法调用后将HttpServlet
实例化对象销毁!期间可能要处理不同方法请求,所以可能会多次调用service
方法!
注意:
HttpServlet
实例只是在程序启动后创建一次就好了,并不是每次收到http
请求都创建实例!
上述这些方法我们都可以进行重写,从而设置某些特有的属性,当时我们很少这样做,我们最常用的就是重写处理请求的方法doXXX
!
代码示例
处理一个
Get
请求
我们分别通过ajax
和form
表单进行构造!
- 基于
ajax
<!--引入jQuery-->
<script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
<script>
//构造请求
$.ajax({
type:'get',
url:'test', //不要加/表示绝对路径!
success: function(body){
console.log(body); //在浏览器控制台打印body信息!
},
error:function(){
console.log("请求失败");
}
});
</script>
注意:
这里的
url
不用加/
和servlet
不同,而且这里的url
就和我们的@WebServlet
注释对应!
我们通过ajax
的方式用前端构造请求,记得将这个html
文件放入到webapp
目录下,通过访问这个网页就向服务器发送了一个get
方式的请求!
然后这里ajax
通过回调的方式,如果请求成功,就会在控制台打印服务器给我们放回的body
内容!
- 基于
form
表单
我们通过form
表单构造一个post
请求!
<form action="test" method="post">
<input type="text" name="name">
<input type="password" name="password">
<input type="submit" name="post请求" value="post请求">
</form>
客户端:服务器
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/test")
public class Test extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("hello world!");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String nameValue = req.getParameter("name");
String passwordValue = req.getParameter("password");
resp.getWriter().write("name:"+nameValue+" password:"+passwordValue);
}
}
将响应输入到了浏览器上!
HttpServletResquest
我们知道HttpServletResquest
类就是我们收到的请求,通过这个类将接收到了http
的请求信息然后转换了对应http
协议格式的字符串!我们通过该类提供的一些方法就可以获取到请求的报头信息还有内容了!
核心方法
上述方法,我们根据其英文意思和之前对http
协议报头的学习就可以大概得出什么功能!
我们们通过上述方法就是为了得到一个http
请求的报头和内容!
所以我们多使用上述方法就知道使用场景如何了!
代码示例
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;
@WebServlet("/request")
public class HttpServletRequest extends HttpServlet {
@Override
protected void doGet(javax.servlet.http.HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String Protocolname = req.getProtocol();//返回协议名称和版本!
String method = req.getMethod();//返回请求方法
String url = req.getRequestURI();
String QueuryString = req.getQueryString();//获取到查询字符串
Enumeration<String> headernames = req.getHeaderNames();//请求header部分!
resp.setContentType("text/html;charset=utf8");//响应的格式以及编码方式!
resp.getWriter().write(Protocolname+"<br>"+method+"<br>"+url+"<br>"+QueuryString);
while(headernames.hasMoreElements()){
String headerKey = headernames.nextElement();//获取到header中的key值
String headerVal = req.getHeader(headerKey);//通过key值找到val值!
resp.getWriter().write(headerKey+":"+headerVal+"<br>");
}
}
}
浏览器返回的结果!
fiddler
抓取的响应
这就是HttpServletRequest
类中核心方法的使用!
HttpServletRespondse
这个类就是我们服务器用来返回响应的类!
我们Servlet
处理请求的doxx
方法,我们根据请求计算出响应,我们可以根据请求将响应信息通过该类中的方法构造好,然后该类方法的对象通过http
协议格式,转化成一个字符串,并通过socket
写会给浏览器!
核心方法:
代码示例
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Created with IntelliJ IDEA.
* Description:
* User: hold on
* Date: 2022-06-30
* Time: 13:03
*/
@WebServlet("/response")
public class HttpResponse extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf8");//设置响应内容类型,和编码方式!
resp.setHeader("name","bug郭");//设置响应头部分,传入键值对信息!
resp.setHeader("password","666666");
resp.setStatus(404);//设置响应状态码!
resp.getWriter().write("收到响应!");
}
}
浏览器获取到的结果:
fiddler
抓包获取到的响应
可以看到我们自己构造的响应头部分,出现了中文乱码!!!
但是我们刚刚不是已经设置过了编码方式utf8
嘛?
为啥还这样呢?
我们看到这里的setContentType
只是设置响应内容,就是body部分的格式和字符编码!我们的响应头部分中的属性一般都是已有的属性,一般没有中文,所以并不能设置!
resp.setStatus(304);//设置响应状态码重定向!(也可以省略)
resp.sendRedirect("https://www.bilibili.com/");//重定向后跳转的网页
请求
重定向
fiddler
抓取到的响应!