目录
一、Servlet 运行原理
在 Servlet 的代码中我们并没有写 main 方法, 那么它是如何运行的?那么对应的 doGet 代码是如何被调用的呢?响应又是如何返回给浏览器的?
其实是Tomcat在调用Servlet,我们在重写doGet和doPost方法的时候,启动Tomcat来运行,当浏览器给服务器发送请求的时候,Tomcat作为HTTP服务器,就可以接收到这个请求。重写的doGet和doPost方法会在Tomcat内部被自动调用执行,Tomcat 程序可以理解为是一个普通的Java进程。
HTTP 协议作为一个应用层协议,需要底层协议栈来支持工作,如下图所示:
更加具体的交互过程:
二、Servlet常用API
主要就是三个重点类:
- HttpServlet(抽象类)
- HttpServletRequest
- HttpServletResponse
2.1 HttpServlet(抽象类)
我们写 Servlet 代码的时候, 首先第一步就是先创建类, 继承自 HttpServlet, 并重写其中的某些方法,每一个Servlet程序都要继承这个HttpServlet类~~~
核心方法:
方法名称 | 调用时机 |
init | 在 HttpServlet 实例化之后被调用一次 |
destory | 在 HttpServlet 实例不再使用的时候调用一次 |
service | 收到 HTTP 请求的时候调用 |
doGet | 收到 GET 请求的时候调用(由 service 方法调用) |
doPost | 收到 POST 请求的时候调用(由 service 方法调用) |
doPut/doDelete/doOptions/... | 收到其他请求的时候调用(由 service 方法调用) |
我们实际开发的时候主要重写 doXXX 方法, 很少会重写 init / destory / service,这些方法的调用时机, 就称为 "Servlet 生命周期". (也就是描述了一个 Servlet 实例从生到死的过程)
注意: HttpServlet 的实例只是在程序启动时创建一次. 而不是每次收到 HTTP 请求都重新创建实例.
2.1.1. init 方法
在初始化阶段执行, 用来初始化每一个Servlet对象, 是在 Tomcat 首次收到 Servlet 类注解相关联路径的请求时, 就会调用执行, 用户可重写该方法, 来执行一些初始化程序的逻辑, 没有重写, init 方法一般是空的, 每个 Servlet 对象只执行一次.
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
public void init() throws ServletException {
System.out.println("init");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write("这是一个 doGet 方法");
System.out.println("hello");
}
}
不论请求多少次,都只执行一次init
2.1.2 service方法
service 方法, 每次收到请求后, 就会执行, service中根据请求的类型不同, 调用不同的方法, doGet, doPost等, 会执行多次, 每收到一次 HTTP 请求就会执行一次.
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write("这是一个 doGet 方法");
System.out.println("hello");
}
记得要设置编码方法,否则会乱码!!!
- IDEA默认返回的编码方式为utf8
- 浏览器是根据系统默认的编码方式(Windows11默认gbk)
2.1.3 destroy方法
Tomcat 结束之前, 即 在 HttpServlet 实例销毁时就会执行该方法, 用来释放资源.
我们可以通过postman来访问其他的doxxx方法~~
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
public void init() throws ServletException {
System.out.println("init");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write("这是一个 doGet 方法");
System.out.println("hello");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("doPost");
}
@Override
protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("doPut");
}
@Override
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("doDelete");
}
@Override
public void destroy() {
System.out.println("destroy");
}
}
要注意的是,destory到底能否被执行,是不确定的,如果我们通过IDEA的停止按钮来关闭服务器,这个本质操作是通过Tomcat的8005管理端口,主动停止才能触发destroy
直接通过杀死进程(ctrl + f2),此时destroy执行不了,而这种暴力的关闭方式却是更方便更常用的, 所以不建议在 destroy 内执行有效代码
前面提到,Tomcat用到两个端口
- 8080业务端口
- 8005管理端口
这就类似于,一个人的两个微信
- 工作微信,(同事客户领导…)
- 生活微信,(家人朋友…)
而不同的端口,就会有不同的请求响应
- 8080就是我们的业务,我们需要通过这个端口访问服务器,以及其部署的资源
- 8005则是做一些加载配置,重新启动,调整设置项…
- 其中就包括,关闭服务器
三、HttpServletRequest
一个HTTP请求里有啥,这个对象中就有啥
- 方法
- URL(host,queryString)
- 版本号
- header
- body
- Cookie
- …
这些内容,我们都可以用对应的方法进行获取和设置~
核心方法:
方法 | 描述 |
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 对象. |
Protocol:
协议名称和版本号
URL 与 URI:
URL是唯一资源定位符,URI是唯一资源标识符,特别相似,相似到我们很多时候直接混着用
Parameter:参数
- 其实就请求中的键值对
Enumeration:列举
- 是请求中所有键值对的所有key的名称
- 用不了for each语法,因为这个集合类没继承那个集合接口,也不是数组~ 而其本身,也可以看做是自身的迭代器
Header:请求头
3.1 HttpServletRequest常用方法演示
@WebServlet("/showRequest")
public class ShowRequest extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException, IOException {
StringBuilder result = new StringBuilder();
result.append(req.getProtocol());
result.append("<br>");
result.append(req.getMethod());
result.append("<br>");
result.append(req.getRequestURI());
result.append("<br>");
result.append(req.getQueryString());
result.append("<br>");
result.append(req.getContextPath());
result.append("<br>");
result.append("=========================<br>");
Enumeration<String> headerNames = req.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
String headerValue = req.getHeader(headerName);
result.append(headerName + ": " + headerValue + "<br>");
}
// 在响应中设置上 body 的类型. 方便浏览器进行解析
resp.setContentType("text/html;charset=utf8");
resp.getWriter().write(result.toString());
}
}
四、前端给后端传输数据的三种方式
前端给后端传输数据, 是非常常见的需求, 常见的有以下三种方式:
- 发送
Get
请求通过query string
传输数据 - 发送
Post
请求通过form
提交数据 - 发送
Post
请求通过json
格式提交数据
下面来逐个讲解~
4.1 发送Get请求通过query string传输数据
我们约定前端通过URL来传递 username 和 password 这两个信息,url为:http://localhost:8080/servletDemo/getParameter?username=zhangsan&password=123
由于我们这里的 key
值是前后端交互前提前约定好的, 所以我们可以直接使用 getParameter
方法通 key
从 req
中得到 value
, 然后在后端我们就可以根据前端传来的数据构造响应返回给前端.
@WebServlet("/getParameter")
public class GetParameter extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException, IOException {
// 前端通过 url 的 query string 传递 username 和 password 两个属性.
String username = req.getParameter("username");
if (username == null) {
System.out.println("username 这个 key 在 query string 中不存在!");
}
String password = req.getParameter("password");
if (password == null) {
System.out.println("password 这个 key 在 query string 中不存在!");
}
System.out.println("username=" + username + ", password=" + password);
resp.getWriter().write("ok");
}
当然, 在不知道 query string 的 key 的情况下也是可以使用 getParameter 拿到查询字符串的各个键值对的, 可以先使用 getHeaderNames 方法获取所有的查询字符串的所有 key 值, 这个一个枚举对象, 然后再根据 getParameter 方法通过 key 值遍历枚举对象获取 value
@WebServlet("/getParameter")
public class GetParameterServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html; charset=utf-8");
StringBuilder stringBuilder = new StringBuilder();
Enumeration query = req.getParameterNames();
while(query.hasMoreElements()) {
String key = (String)query.nextElement();
stringBuilder.append(key + ": " + req.getParameter(key));
stringBuilder.append("<br>");
}
}
}
4.2 发送Post请求通过form提交数据
使用 Post 请求来传递数据, 数据此时是在 HTTP 格式中的 body 部分的, 使用 from 表单进行构造, 此时 body 中的请求内容的格式 (Content-Type) 是 application/x-www-form-urlencode 格式, 在形式上和 query string 是一样的, 后端仍然使用 getParameter 来获取.
我们先构建一个form表单
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录界面</title>
</head>
<body>
<form action="login" method="post">
<input type="text" name="username">
<input type="text" name="password">
<input type="submit" value="登录">
</form>
</body>
</html>
后端我们可以直接使用HttpServletRequest
中的 getParameter
方法依据 key
来获取 value
, 然后再将获取到的数据返回, form表单构造的请求会自动跳转页面.
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1. 先从请求中拿到用户名和密码.
// 为了保证读出来的参数也能支持中文, 要记得设置请求的编码方式是 utf8
req.setCharacterEncoding("utf8");
String username = req.getParameter("username");
String password = req.getParameter("password");
//2.验证用户名密码是否正确
if (username == null || password == null || username.equals("") || password.equals("")){
resp.setContentType("text/html;charset=utf8");
resp.getWriter().write("当前输入的用户名或密码不能为空!");
return;
}
// 此处假定用户名只能是 zhangsan 或者 lisi. 密码都是 123
// 正常的登录逻辑, 验证用户名密码都是从数据库读取的.
if (!username.equals("zhangsan") && !username.equals("lisi")){
//用户名有问题
resp.setContentType("text/html;charset=utf8");
resp.getWriter().write("用户名或密码有误");
return;
}
if (!password.equals("123")){
//密码有问题
resp.setContentType("text/html;charset=utf8");
resp.getWriter().write("用户名或密码有误");
return;
}
// 3. 用户名和密码验证 ok, 接下来就创建一个会话.
// 当前用户处于未登录的状态, 此时请求的 cookie 中没有 sessionId
// 此处的 getSession 是无法从服务器的 哈希表 中找到该 session 对象的.
// 由于此处把参数设为 true 了, 所以就允许 getSession 在查询不到的时候, 创建新的 session 对象和 sessionId
// 并且会自动的把这个 sessionId 和 session 对象存储的 哈希表 中.
// 同时返回这个 session 对象, 并且在接下来的响应中会自动把这个 sessionId 返回给客户端浏览器.
HttpSession session = req.getSession(true);
// 接下来可以让刚刚创建好的 session 对象存储咱们自定义的数据. 就可以在这个对象中存储用户的身份信息.
session.setAttribute("username",username);
// 4. 登录成功之后, 自动跳转到 主页
resp.sendRedirect("index");
}
}
@WebServlet("/index")
public class IndexServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 此处禁止创建会话. 如果没找到, 认为用户是未登录的状态!!
// 如果找到了才认为是登录状态.
HttpSession session = req.getSession(false);
if (session == null){
//未登录状态
resp.setContentType("text/html;charset=utf8");
resp.getWriter().write("当前用户未登录!");
return;
}
String username = (String) session.getAttribute("username");
if (username == null){
// 虽然有会话对象, 但是里面没有必要的属性, 也认为是登录状态异常.
resp.setContentType("text/html;charset=utf8");
resp.getWriter().write("当前用户未登录!");
return;
}
// 如果上述检查都 ok, 接下来就直接生成一个动态页面.
resp.setContentType("text/html;charset=utf8");
resp.getWriter().write("欢迎你!"+username);
}
}
这里我们要注意,需要设置请求和响应的编码格式,显示的告诉后端代码,请求使用utf8编码,要不然会解析的时候会发生乱码~尤其是请求内容中有中文存在的情况下, 也就是说, 我们在写后端代码时, 最好将请求和响应的编码格式都进行设置, 保证前后端解析的统一.
4.3 发送Post请求通过json格式提交数据
使用 Post 请求传输数据, 还可以使用当前比较主流的 json
数据格式组织 body
中的数据, 它也是键值对格式的,用getParameter方法的话,似乎是取不到json里面的键值对~
对于 json 格式, Servlet 自身是没有内置 json 的解析功能的, 如果我们自己进行手动解析并不容易
json格式:
[
{
key1: value,
key2: value
},
{
key3: value,
key4: value
},
...
]
那么我们就需要引入json依赖,json依赖有很多,用法差不多,功能相似,例如fastjson、gson、jackson…
- jackson是spring官方的指定产品(后续spring有关操作也恰好要用到jackson)
现在中央仓库里面找到依赖并且引入
Maven Repository: Central (mvnrepository.com)
我选择的是2.15.0
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.0</version>
</dependency>
我们先拿 postman 演示发送一个post请求通过json格式:
后端代码如下:
- 创建 Jackson 核心对象 ObjectMapper 对象.
- 创建用来接受 json 数据的实体类.
- 读取请求中的 body 信息, 该过程通过 ObjectMapper 对象的readValue方法实现, 这个方法的参数有两个, 第一个参数用来表示请求的来源, 可以是路径字符串, 也可以是InputSream对象, 也可以是File对象, 第二个参数表示接收 json 数据的实体类对象.
- 处理并响应请求.
class User {
public String username;
public String password;
}
@WebServlet("/json")
public class JsonServlet extends HttpServlet {
// 使用 jackson, 最核心的对象就是 ObjectMapper
// 通过这个对象, 就可以把 json 字符串解析成 java 对象; 也可以把一个 java 对象转成一个 json 格式字符串.
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException, IOException {
// 通过 post 请求的 body 传递过来一个 json 格式的字符串.
User user = objectMapper.readValue(req.getInputStream(), User.class);
System.out.println("username=" + user.username + ", password=" + user.password);
resp.getWriter().write("ok");
}
}
响应结果:
4.3.1 ObjectMapper类
// 使用 jackson, 最核心的对象就是 ObjectMapper
// 通过这个对象, 就可以把 json 字符串解析成 java 对象; 也可以把一个 java 对象转成一个 json 格式字符串.
private ObjectMapper objectMapper = new ObjectMapper();
- 通过对象内部的映射关系,制作json格式的字符串
- 通过json格式的字符串,构造对象
4.3.2 通过json构造对象
用readValue方法(readValue的话,就是传过来的body是json数组)
User user = objectMapper.readValue(req.getInputStream(), User.class);
- 读取 body 中 json 格式的数据字符串, 并解析成若干键值对.
- 根据第二个参数实体类对象, 创建 User 实例.
- 遍历解析出来的键值对, 获得 key, 并与所需传入的对象中的属性相比, 如果 key 与属性的名字相同, 则把 key 对应的 value赋值给这个属性(通过反射完成).
- 返回该 User 对象.
4.3.3 通过对象构造json字符串
一般构造json,是为了写入响应返回给客户端,所以从习惯上是客户端发送GET请求,所以重写doGet方法,所以我们使用writeValueAsString方法
4.3.4 小结
Jackson 库的核心类为 ObjectMapper. 其中的 readValue 方法把一个 JSON 字符串转成 Java 对象.其中的 writeValueAsString 方法把一个 Java 对象转成 JSON 格式字符串.
readValue 和 writeValueAsString 本质上是通过实体类的setter和getter方法来进行构造对象,所以我们必须要生成对应的getter和setter方法,才能被反射到~~
五、HttpResponse
表示一个HTTP响应,响应有什么,里面就有什么~
关键方法:
方法 | 描述 |
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 中写入二进制格式数据. |
5.1 设置响应状态码
只需要调用 httpServletResponse
对象中的 setStatus
方法就可以了, 设置不同的状态码, 只要变换 status
的值即可, 就可以看到不同的响应结果.
@WebServlet("/status")
public class StatusServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException, IOException {
resp.setStatus(404);
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write("返回 404 响应!");
}
}
也就是说, 平时我们所见到的其他的网站的 404 都是人家自定义的 404
状态响应页面
5.2 自动页面刷新
自动页面刷新只要在响应报头 (header) 中设置一下 Refresh
字段就能实现页面的定时刷新了, 对于响应 header
的设置, 可以通过 HttpServletResponse
对象中的 setHeader
方法来设置 Refresh 属性和刷新频率.
@WebServlet("/refresh")
public class RefreshServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException, IOException {
// 每隔 1s 自动刷新一次.
resp.setHeader("Refresh", "1");
resp.getWriter().write("time=" + System.currentTimeMillis());
}
}
还可以进行格式化时间输出~
@WebServlet("/refresh")
public class RefreshServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException, IOException {
// 每隔 1s 自动刷新一次.
resp.setHeader("Refresh", "1");
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
resp.getWriter().write("time=" + format.format(System.currentTimeMillis()));
}
}
5.3 重定向
返回一个重定向 HTTP 响应, 自动跳转到另外一个页面.
有两种实现方式:
- 设置状态码302,设置响应头的第一个参数为Location,第二个参数为重定向地址
- 直接使用sendRedirect
@WebServlet("/redirect")
public class RedirectServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException, IOException {
// 用户访问这个路径的时候, 自动重定向到 搜狗主页 .
// resp.setStatus(302);
// resp.setHeader("Location", "https://www.sogou.com");
resp.sendRedirect("https://www.sogou.com");
}
}