我们写 Servlet 代码的时候, 首先第一步就是先创建类, 继承自 HttpServlet, 并重写其中的某些方法。
方法名称 | 调用时机 |
---|---|
init | 在 HttpServlet 实例化之后被调用一次 |
destroy | 在 HttpServlet 实例不再使用的时候调用一次 |
service | 收到 HTTP 请求的时候调用 |
doGet | 收到 GET 请求的时候调用(由 service 方法调用) |
doPost | 收到 POST 请求的时候调用(由 service 方法调用) |
doPut/doDelete/doOptions/… | 收到其他请求的时候调用(由 service 方法调用) |
我们实际开发的时候主要重写 doXXX 方法, 很少会重写 init / destroy / service
destroy 不一定真的能调到!分两种情况:
service:
tomcat 收到请求,实际上是先调用 service,在 service 里面再去根据方法,调用不同的 doXXX。
实际开发,很少会重写 service,就是重写 doXXX 就够了 ~~
这些方法的调用时机, 就称为 “Servlet 生命周期” (也就是描述了一个 Servlet 实例从生到死的过程)
注意: HttpServlet 的实例只是在程序启动时创建一次, 而不是每次收到 HTTP 请求都重新创建实例。
面试题: 请描述 Servlet 的生命周期是什么?
Servlet生命周期描述的是Servlet创建到销毁的过程:
- 当一个请求从HTTP服务器转发给Servlet容器时,容器检查对应的Servlet是否创建,没有创建就实例化该Servlet,并调用 init() 方法,init() 方法只调用一次,之后的请求都从第二步开始执行;
- 请求进入 service() 方法,根据请求类型转发给对应的方法处理,如doGet, doPost, 等等
- 容器停止前,调用 destory() 方法,进行清理操作,该方法只调用一次,随后JVM回收资源。
在 java 目录中创建一个类 HelloServlet,代码如下:
解释:
上面的代码虽然只有寥寥几行,但是包含的信息量是巨大的!
- 我们的代码不是通过 main 方法作为入口了。main 方法已经被包含在 Tomcat 里,我们写的代码会被 Tomcat 在合适的时机调用起来。此时我们写的代码并不是一个完整的程序,而是 Tomcat 这个程序的一小部分逻辑。
- 我们随便写个类都能被 Tomcat 调用吗?满足啥样条件才能被调用呢?主要满足三个条件:
a) 创建的类需要继承自 HttpServlet;
b) 这个类需要使用 @WebServlet 注解关联上一个 HTTP 的路径;
c) 这个类需要实现 doXXX 方法。
当这三个条件都满足之后,Tomcat 就可以找到这个类,并且在合适的时机进行调用。
当 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() | 以字节为单位返回请求主体的长度,并提供输入流,或者如果长度未知则返回 -1。 |
InputStream getInputStream() | 用于读取请求的 body 内容,返回一个 InputStream 对象。 |
通过这些方法可以获取到一个请求中的各个方面的信息。
注意: 请求对象是服务器收到的内容, 不应该修改。因此上面的方法也都只是 “读” 方法, 而不是 “写” 方法 ~~
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;
import java.util.Enumeration;
@WebServlet("/showRequest")
public class ShowRequestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// resp 是响应对象, setContentType, 给响应的 ContentType 设置了值. html
// 声明响应 body 是 html 结构的数据.
resp.setContentType("text/html");
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(req.getProtocol());
stringBuilder.append("
");
stringBuilder.append(req.getMethod());
stringBuilder.append("
");
stringBuilder.append(req.getRequestURI());
stringBuilder.append("
");
stringBuilder.append(req.getContextPath());
stringBuilder.append("
");
stringBuilder.append(req.getQueryString());
stringBuilder.append("
");
// 把请求的 header 也拼进来
Enumeration<String> headerNames = req.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
String value = req.getHeader(name);
stringBuilder.append(name + ": " + value);
stringBuilder.append("
");
}
resp.getWriter().write(stringBuilder.toString());
}
}
部署程序。在浏览器通过 URL http://127.0.0.1:8080/yyhjava/showRequest 访问, 可以看到
如果不设置 content type,此时浏览器就懵逼了,会尝试猜一猜这里的 body 是啥格式 (猜的结果不一定对) !!!所以要一定要记得设置数据格式 ~~
格式有很多种:text/plain;text/html;text/css;application/js;application/json;image/png…
GET 请求中的参数一般都是通过 query string 传递给服务器的,形如 https://v.bitedu.vip/personInf/student?userId=1111&classId=100。此时浏览器通过 query string 给服务器传递了两个参数:userId 和 classId,值分别是 1111 和 100 。在服务器端就可以通过 getParameter 来获取到参数的值。
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("/getParameter")
public class GetParameterServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取 query string 中的键值对.
// 假设浏览器的请求形如 ?studentId=10&studentName=张三
String studentId = req.getParameter("studentId");
String studentName = req.getParameter("studentName");
System.out.println(studentId);
System.out.println(studentName);
// 需要告知格式和编码方式!
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write(studentId + ", " + studentName);
}
}
如果通过 http://127.0.0.1:8080/yyhjava/getParameter?studentId=10&studentName=张三 访问,可以看到:
此时说明服务器已经获取到客户端传递过来的参数 ~~
getParameter 的返回值类型为 String,必要的时候需要手动把 String 转成 int。
这里我们没有 URL encode,为什么也正常运行了呢?
不进行 URL encode,不是 100% 就会出错的 (有的浏览器,有的版本没事儿)。
但是还是要记得 encode 一下,encode助手:http://www.urlencode.com.cn/
所以,通过 http://127.0.0.1:8080/yyhjava/getParameter?studentId=10&studentName=%E5%BC%A0%E4%B8%89 访问。
(servlet getParameter 会自动针对 URL encode 的结果进行 decode,不需要咱们手动处理)
getParameter 获取键值对的时候:
如果键不存在,得到的是 null;
如果键存在但值不存在,得到的是 " " (空字符串)
POST 请求的参数一般通过 body 传递给服务器。而 body 中的数据格式有很多种:
通过 form 表单 等方式构造 POST 请求,body 一般是 x-www-form-urlencoded 格式 ~~
这种情况与 doGet 类似,仍然可以通过 getParameter 获取参数的值:
(完全可以写在一个类中)
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("/getParameter")
public class GetParameterServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取 query string 中的键值对.
// 假设浏览器的请求形如 ?studentId=10&studentName=张三
String studentId = req.getParameter("studentId");
String studentName = req.getParameter("studentName");
System.out.println(studentId);
System.out.println(studentName);
// 需要告知格式和编码方式!
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write(studentId + ", " + studentName);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 通过 body 获取, 发个 post 请求
// 预期请求的 body 里是 studentId=10&studentName=张三
req.setCharacterEncoding("utf8");
String studentId = req.getParameter("studentId");
String studentName = req.getParameter("studentName");
System.out.println(studentId);
System.out.println(studentName);
// 响应这里设置字符集有两种写法. 但是还是建议使用 setContentType 完整写法
// 设置的字符集只是一小部分, 还需要设置格式.
resp.setContentType("text/html; charset=utf8");
// resp.setCharacterEncoding("utf8");
// 注意与上行代码的先后顺序不能颠倒!!!
resp.getWriter().write(studentId + ", " + studentName);
}
}
req.setCharacterEncoding("utf8");
这是设置 解析请求 使用的字符集!告诉 servlet (tomcat) 如何解析 ~~- 而
resp.setContentType("text/html; charset=utf8");
这里的 utf8 是告诉浏览器如何解析响应 ~~
响应这里设置字符集有两种写法 (还有resp.setCharacterEncoding("utf8");
),但是还是建议使用 setContentType 完整写法!
那么怎么构造 POST 请求呢?这里展示两种方法:
(博客链接:https://blog.csdn.net/yyhgo_/article/details/128454930?spm=1001.2014.3001.5501)
1)from 表单:
创建一个 student.html 文件,放到 webapp 目录中,形如
一个 Servlet 程序中可以同时部署静态文件,静态文件就放到 webapp 目录中即可 ~~
student.html:
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
<form action="getParameter" method="post">
<input type="text" name="studentId">
<input type="text" name="studentName">
<input type="submit" value="提交">
form>
body>
html>
重新启动 tomcat 服务器,然后在浏览器中访问页面 http://127.0.0.1:8080/yyhjava/student.html
Postman 也是 http 客户端,和浏览器是对等的 ~~
类似
{
studentld: 20,
studentName: "张三"
}
需要先读取 body 中的内容 (通过 getInputStream 读流对象);
然后进行解析。
问题来了,怎样进行解析呢?
比较麻烦,所以推荐使用一些第三方库:fastjson;Jackson;gson…
这里我们使用 Jackson (spring 御用的 json 库)!
在中央仓库 (链接:https://mvnrepository.com/) 搜索 Jackson
点进去,随便挑一个版本,复制代码:
粘贴到 pom.xml 中:
标红的话,等一会儿下载 ~~
Jackson 只需要学会其中的 一个类、两个方法 即可 ~~
代码:
import com.fasterxml.jackson.databind.ObjectMapper;
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;
class Student {
// 1. 这个类里的属性务必是 public 或者带有 public 的 getter / setter
// 否则 jackson 无法访问这个对象的属性
// 2. 这个类务必要有无参版本的构造方法. 如果不写任何构造方法, 编译器能自动生成无参构造方法
public int studentId;
public String studentName;
}
@WebServlet("/json")
public class JsonServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 此处假设请求的 body 格式为
// { studentId: 10, studentName: "张三" }
// jackson 提供的核心的类
// 一个方法叫做 readValue, 把 json 格式的数据转成 java 的对象
// 还有一个方法叫做 writeValueAsString, 把 java 对象转成 json 格式的字符串
ObjectMapper objectMapper = new ObjectMapper();
// readValue 第一个参数可以是字符串, 也可以是输入流.
// 第二个参数, 是一个类对象. 也就是要解析出来的结果的对象的类.
Student s = objectMapper.readValue(req.getInputStream(), Student.class);
System.out.println(s.studentId);
System.out.println(s.studentName);
// resp.setContentType("application/json; charset=utf8");
// resp.getWriter().write(objectMapper.writeValueAsString(s));
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write(s.studentId + ", " + s.studentName);
// objectMapper.writeValue(resp.getWriter(), s);
}
}
注意代码中的注释!!!
两个方法:
- ObjectMapper 的 readValue 方法能直接从一个 InputStream 对象读取数据
- ObjectMapper 的 writeValueAsString 方法能把一个对象数组直接转成 JSON 格式的字符串
重新启动 tomcat 服务器,然后使用 Postman 构造一个请求:
在 Postman 中写 json 格式的时候,务必要保证这里的 key 是带引号的!!!
正常的 json,key 都得带引号,而 js 里的对象是不必带的 ~~
Servlet 中的 doXXX 方法的目的就是根据请求计算得到相应, 然后把响应的数据设置到 HttpServletResponse 对象中。
然后 Tomcat 就会把这个 HttpServletResponse 对象按照 HTTP 协议的格式, 转成一个字符串, 并通过 Socket 写回给浏览器。
方法 | 描述 |
---|---|
void setStatus(int sc) | 为该响应设置状态码 |
void setHeader(String name, String value) | 设置一个带有给定的名称和值的 header。如果 name 已经存在,则覆盖旧的值 |
void addHeader(String name, String value) | 添加一个带有给定的名称和值的 header。如果 name 已经存在,不覆盖旧的值, 并列添加新的键值对 |
void setContentType(String type) | 设置被发送到客户端的响应的内容类型 |
void setCharacterEncoding(String charset) | 设置被发送到客户端的响应的字符编码(MIME 字符集)例如 UTF-8 |
void sendRedirect(String location) | 使用指定的重定向位置 URL 发送临时重定向响应到客户端 |
PrintWriter getWriter() | 用于往 body 中写入文本格式数据 |
OutputStream getOutputStream() | 用于往 body 中写入二进制格式数据 |
注意: 响应对象是服务器要返回给浏览器的内容,这里的重要信息都是程序猿设置的,因此上面的方法都是 “写” 方法 ~~
注意: 对于状态码/响应头的设置要放到 getWriter / getOutputStream 之前,否则可能设置失效!
实现一个程序,用户在浏览器通过参数指定要返回响应的状态码:
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("/status")
public class StatusServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 约定, 浏览器 query string 传个参数过来.
// 形如 type=1
// 如果 type 为 1, 返回 200; type 为 2, 返回404; type 为 3 返回一个 500;
String type = req.getParameter("type");
if (type.equals("1")) {
resp.setStatus(200);
} else if (type.equals("2")) {
resp.setStatus(404);
// sendError 效果就是返回一个 tomcat 自带的错误页面.
resp.sendError(404);
} else if (type.equals("3")) {
resp.setStatus(500);
} else {
resp.setStatus(504);
}
}
}
通过类似于 http://127.0.0.1:8080/yyhjava/status?type=2 的访问,就可以得到不同的响应页面。
实现一个程序,让浏览器每秒钟自动刷新一次,并显示当前的时间戳:
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("/autoRefresh")
public class AutoRefreshServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 直接返回响应就好.
resp.setHeader("refresh", "2");
resp.getWriter().write(System.currentTimeMillis() + "");
}
}
通过 http://127.0.0.1:8080/yyhjava/autoRefresh 访问:
时间戳不断变化 ~~
当前设置的是每 2s 刷新—次,但实际并不是精确的 2000ms,会比 2000 略多一点!?
调度要消耗时间 / 网络传输消耗时间 / 服务器响应 … 再加上对于 ms 级别的计时存在误差 ~~
实现一个程序,返回一个重定向 HTTP 响应,自动跳转到另外一个页面:
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("/redirect")
public class RedirectServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 进行重定向. 收到请求, 跳转到 搜狗主页
// resp.setStatus(302);
// resp.setHeader("Location", "https://www.sogou.com");
resp.sendRedirect("https://www.sogou.com");
}
}
通过 http://127.0.0.1:8080/yyhjava/redirect 访问页面,会直接跳转到 https://www.sogou.com ~~