一、Servlet运行原理
细心的同学会发现,我们的Servlet代码中并没有写main方法,那么我们的doGet代码是如何被调用的?相应又是如何返回给浏览器的?
当浏览器给服务器发送请求的时候,Tomcat作为HTTP服务器就可以接收到这个请求。
接受请求:
我们在浏览器输入一个URL,浏览器就会构造一个HTTP请求,这个请求会经过网络协议栈逐层的进行封装直到bit流,通过物理层硬件设备转换为光/电信号传输出去,服务器收到该信号后,又通过网络协议栈逐层分用,层层解析,最终还原成HTTP请求交给Tomcat进行处理,Tomcat通过socket读取到该字符串,按照HTTP请求的格式来解析该请求,根据Context path确定一个webapp,再通过Servlet path确定一个具体的类,然后再根据HTTP请求的方法,确定该类的具体方法,我们的方法中的HttpServletRequest中就包含该HTTP请求的详细信息
计算响应:
我们在方法中,根据请求,然后计算响应
返回响应:
我们的方法执行完,Tomcat会自动的把HttpServletResponse我们设置的对象转换成一个HTTP协议的字符串,通过Socket将该响应发出去,层层封装最后浏览器获取到HTTP响应,浏览器的Socket读到该响应,按照HTTP响应的格式来解析该响应,并把body中的数据按照一定的格式显示出来
二、Servlet API
HttpServlet
核心方法:
方法 | 调用时机 |
---|---|
init | 在HttpServlet实例化之后被调用一次 |
destroy | 在HttpServelet实例不再使用时调用一次 |
service | 收到HTTP请求时调用 |
doGet | 收到GET请求时调用 |
doPost | 收到POST请求时调用 |
doPut,doDelete…也是同理,在收到对应请求时调用,由service调用
init方法: 该方法是在tomcat首次收到了该类相关联(访问/hello路径的请求)的请求时,就会调用到HelloServlet,就需要先对HelloServlet进行实例化,后续在收到请求时,不必再实例化了,直接复用之前的HelloServlet实例即可,只执行一次
destroy方法: 当HttpServlet实例不再使用时调用该方法,啥时候该实例就不再使用了?服务器只要不停止,该实例就一直被使用,只有当服务器停止后了,才会调用该方法,只执行一次
这里的destroy能否被执行到,是存在争议的:
如果是通过停止按钮,这个本质操作是通过tomcat的8005端口,主动停止,才能触发destroy
如果是直接杀死进程,此时就来不及执行destroy
所以不建议在destroy内执行有效代码
service: 收到HTTP请求就会调用
Service中根据请求的类型不同,调用不同的方法,doGet,doPost方法等等,会执行多次,每收到一次HTTP请求就执行一次
Servlet的生命周期:
1.开始的时候执行init
2.每次收到请求后,执行service
3.销毁之前执行destroy
处理请求
我们重写doGET方法,当浏览器使用GET方法访问该Servlet路径(method)时,我们在控制台打印hello GET,并给浏览器发一个hello GET响应
我们在webapp目录下创建testMethod.html,我们的Servlet程序中可以同时部署静态文件,静态文件我们放在webapp目录下即可
我们编写前端代码,一般使用VScode编写
我们打开文件路径后,直接右键打开方式VScode打开即可,然后我们开始使用ajax构造请求发送给服务器
我们首先引入jquery,我们在前面已经教过大家,在浏览器搜索 query cdn即可
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
<script>
$.ajax({
type: 'get',
url: 'method',
success: function(body,status) {
console.log(body);
}
});
</script>
然后我们启动tomcat,使用浏览器访问testMethod.html
我们打开开发者工具看一下
大家需要注意这里的访问路径,我们这里访问的是testMethod.html文件
我们这里的method其实写的是相对路径,基准路径就是Html所在的路径,大家也可以写作绝对路径
我们想要处理其他方法也是同理,在html中type类型修改,servelet方法中重写对应方法即可,我们再来演示一个
我们每次修改完代码之后都需要重启服务器
三、HttpServletRequest
当 Tomcat 通过 Socket API 读取 HTTP 请求(字符串), 并且按照 HTTP 协议的格式把字符串解析成
HttpServletRequest 对象
方法 | 描述 |
---|---|
String getProtocol() | 返回请求协议的名称和版本 |
String getMethod() | 返回请求的 HTTP 方法的名称,例如,GET、POST 或 PUT |
String getRequestURI() | 从协议名称直到 HTTP 请求的第一行的查询字符串中,返回该请求的 URL 的一部分 |
String getContextPath() | 返回指示请求上下文的请求 URI 部分 |
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() | 返回请求body的长度 |
InputStream getInputStream() | 用于读取请求的 body 内容. 返回一个 InputStream 对象 |
我们query string 是键值对结构,我们可以通过getParameter根据key获取到value
我们接下来就使用这些方法来将请求信息打印到浏览器上
@WebServlet("/showRequest")
public class ShowRequest extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//指定返回响应的格式
resp.setContentType("text/html; charset = utf8");
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(req.getProtocol());
stringBuilder.append("<br");//换行
stringBuilder.append(req.getMethod());
stringBuilder.append("<br>");
stringBuilder.append(req.getRequestURI());
stringBuilder.append("<br>");
stringBuilder.append(req.getContextPath());
stringBuilder.append("<br>");
stringBuilder.append(req.getQueryString());
stringBuilder.append("<br>");
stringBuilder.append("<br>");
Enumeration<String> headerNames = req.getHeaderNames();
while(headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
stringBuilder.append(headerName+": "+req.getHeader(headerName));
stringBuilder.append("<br>");
}
resp.getWriter().write(stringBuilder.toString());
}
}
前后端交互
1.GET通过query string
2.POST通过form
3.POST通过json
我们下来演示一下上述三种方法,前端给后端传参,我们后端获取到数据
GET query string
我们前端这里直接通过地址栏构造一个URL发送给后端,useId = 10 & classId = 001,我们后端使用getParameter()方法获取到数据然后响应给浏览器
@WebServlet("/getParameter")
public class GetParameterServelet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//使用getParameter获取前端query string的数据 useId = 10 & classId = 001
String userId = req.getParameter("useId");
String classId = req.getParameter("classId");
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write(userId +", " + classId);
}
}
这里请求中的query string键值对会被tomcat处理成形似map结构的数据,我们就可以通过key去获取value了,需要注意的是我们这里的value都是String类型的,如果我们getParameter的参数前端并没有传递,那么我们的value就是null
POST form
我们前端使用form构造POST请求,我们后端仍然是使用getParameter来获取,因为它的数据也是键值对,只不过这部分是在body中
我们在该HTML文件中构造一个form表单
<form action="getParameter" method="post">
<input type="text" name="useId">
<input type="text" name="classId">
<input type="sumbit" value="提交">
</form>
我们来提交一组数据
我们可以发现报了405错误,因为我们servlet没有重写doPOST方法
我们接下来重写doPost方法
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String userId = req.getParameter("useId");
String classId = req.getParameter("classId");
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write(userId +", " + classId);
}
此时我们就可以获取到前端数据,并且响应给浏览器,使用getParameter,既可以获取到query string中的键值对,也可以获取到form表单body中的键值对
POST json
json是目前比较主流的一种数据格式,也是键值对格式,我们可以将body按照该格式组织,我们在前端既可以使用ajax的方式构造,也可以使用postman直接构造.
我们先实现一下后端处理逻辑
@WebServlet("/postParameter2")
public class PostParameter2 extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//读取body为json的数据
//这里我们使用getInputstream 将body数据读出来
int length = req.getContentLength();
byte[] buffer = new byte[length];
InputStream inputStream = req.getInputStream();
inputStream.read(buffer);
String body = new String(buffer,0,length,"utf8");
System.out.println("body= "+body);
resp.getWriter().write(body);
}
}
然后我们在postman构造一下josn格式的请求,大家需要注意一些格式细节
我们可以看到我们读取到的body数据,我们也可以抓包来看一下。
我们这个代码的执行流程和form表单传参是类似的,只不过是传输的数据格式是不同的,但是我们当前通过Json传输数据时,我们服务器只是把整个body读出来了,并没有按照键值对的方式来处理,这里我们使用第三方库比较合适:jackson,我们通过maven引入该依赖
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.14.1</version>
</dependency>
我们在pom.xml引入该依赖,记得刷新,我们现在读取body为json格式的数据时,就可以简化了
class Student {
public int userId;
public int classId;
}
我们这里需要根据前端json中的key来设置我们类中的属性,一一对应,如果前端传的参数多了,会报500错误
ObjectMapper是我们jackson中涉及到的核心对象
这一步操作我们实际会做以下操作:
1.会将body中json格式的数据取出
2.根据第二个类参数,创建Student实例
3.根据json格式的字符串,处理成map键值对结构
4.遍历键值对,看键的名字是否与Student实例的属性名匹配,如果匹配就将value赋值给该属性
5.返回该Student对象
我们写好处理逻辑后,前端再发一次请求
四、HttpServletResponse
核心方法:
方法 | 描述 |
---|---|
void setStatus(int sc) | 给响应设置状态码 |
void setHeader(String name,String value) | 设置一个header,如果存在覆盖value |
void addHeader(String name,String value) | 添加一个带有给定的名称和值的 header. 如果 name 已经存在,不覆盖旧的值, 并列添加新的键值对 |
void setCharacterEncoding(String charset) | 设置被发送到客户端响应的字符编码 |
void sendRedirect(String location) | 使用重定向位置URL发送临时重定向给客户端 |
PrintWriter getWriter() | 往body中写入文本数据 |
OutputStream getOutputStream() | 往body写入二进制数据 |
我们来演示一下部分方法:
我们之前处理Get请求的方法,我们在写回数据时,我们加入中文会怎样?
因为我们没有指明编码方式,此时浏览器只能随便指定一个编码方式,出不出现乱码完全是运气问题,所以我们需要指明编码方法
大家需要注意这里是text/html而不是test/html
当我们指定后,浏览器就能正确识别中文相应了
resp.setContentType("text/html; charset=utf8");
我们也可以这样指定。
我们现在在来演示一下重定向方法,我们就来实现,当有访问该servlet路径时,我们跳转到百度主页
当我们访问redirect路径时,我们就会跳转到百度主页,我们来抓包看一下
这里请求就是一个正常的请求,正常的访问servlet路径
我们可以发现响应是一个302类型,临时重定向,loaction代表的是我们要定向到那个路径