Servlet 是一种实现动态页面的技术. 是一组 Tomcat 提供给程序猿的 API, 帮助程序猿简单高效的开发一个 web app
静态页面也就是内容始终固定的页面. 即使 用户不同/时间不同/输入的参数不同 , 页面内容也不会发生变化. (除非网站的开发人员修改源代码, 否则页面内容始终不变)
动态页面指的就是 用户不同/时间不同/输入的参数不同, 页面内容会发生变化
构建动态页面的技术有很多, 每种语言都有一些相关的库/框架来做这件事
Servlet 就是 Tomcat 这个 HTTP 服务器提供给 Java 的一组 API, 来完成构建动态页面这个任务
简而言之, Servlet 是一组 Tomcat 提供的 API, 让程序猿自己写的代码能很好的和 Tomcat 配合起来, 从而更简单的实现一个 web app
使用 IDEA 创建一个 Maven 项目
Maven 项目创建完毕后, 会自动生成一个 pom.xml 文件
我们需要在 pom.xml 中引入 Servlet API 依赖的 jar 包
当项目创建好了之后, IDEA 会帮我们自动创建出一些目录
在 main 目录下, 和 java 目录并列, 创建一个 webapp 目录
webapp 目录就是未来部署到 Tomcat 中的一个重要的目录. 当前我们可以往 webapp 中放一些静态资源, 比如 html , css 等
在这个目录中还有一个重要的文件 web.xml. Tomcat 找到这个文件才能正确处理 webapp 中的动态资源
然后在 webapp 目录内部创建一个 WEB-INF 目录, 并创建一个 web.xml 文件
往 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>
当前的servlet的程序没有main方法,main方法可以被视为汽车的发动机,有了发动机,才能启动;在程序中同样如此,这里的servlet程序相等于是车厢,只需要将其连接上车头tomcat即可,也就是将程序放到webapps目录下即可
tomcat只需要通过识别那些文件下存在WEB-INF/web.xml便可知道该文件是需要连接上的车厢
在 java 目录中创建一个类 HelloServlet, 代码如下
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//控制台打印
System.out.println("hello servlet");
//写入resp的body中
resp.getWriter().write("hello servlet");
}
}
使用 maven 进行打包. 打开 maven 窗口
然后展开 生命周期 , 双击 package 即可进行打包
打包成功之后, 能够看到 SUCCESS 这样的字样
打包成功后, 可以看到在 target 目录下, 生成了一个 jar 包
war包是tomcat专属的用于描述webapp的程序
把 war 包拷贝到 Tomcat 的 webapps 目录下
启动 Tomcat , Tomcat 就会自动把 war 包解压缩
此时通过浏览器访问 http://127.0.0.1:8080/hello_servlet-1.0-SNAPSHOT/hello
URL 中的 PATH 分成两个部分, 其中 HelloServlet 为 Context Path, hello 为 Servlet Path
总结
上述在浏览器地址栏中输入URL之后,浏览器立即构造了一个对应的HTTP GET请求,发给了tomcat;tomcat根据第一级路径,确定具体的webapp,根据第二级路径,确定调用哪个类;再通过GET/POST方法确定调用HelloServlet的哪个方法
我们自己的实现是在 Tomcat 基础上运行的
当浏览器给服务器发送请求的时候, Tomcat 作为 HTTP 服务器, 就可以接收到这个请求
HTTP 协议作为一个应用层协议, 需要底层协议栈来支持工作. 如下图所示
更详细的交互过程可以参考下图
方法名称 | 调用时机 |
---|---|
init | 在 HttpServlet 实例化之后被调用一次 |
destory | 在 HttpServlet 实例不再使用的时候调用一次 |
service | 收到 HTTP 请求的时候调用 |
doGet | 收到 GET 请求的时候调用(由 service 方法调用) |
doPost | 收到 POST 请求的时候调用(由 service 方法调用) |
doPut/doDelete/doOptions/… | 收到其他请求的时候调用(由 service 方法调用) |
写代码的时候主要重写 doXXX 方法, 很少会重写 init / destory / service
这些方法的调用时机, 就称为 “Servlet 生命周期”. (也就是描述了一个 Servlet 实例从生到死的过程)
HttpServlet 的实例只是在程序启动时创建一次. 而不是每次收到 HTTP 请求都重新创建实例
创建 MethodServlet.java, 创建 四种 方法
@WebServlet("/method")
public class MethodServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("hello doget");
resp.getWriter().write("doget");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("hello dopost");
resp.getWriter().write("dopost");
}
@Override
protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("hello doput");
resp.getWriter().write("doput");
}
@Override
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("hello dodelete");
resp.getWriter().write("dodelete");
}
}
通过postman进行检测
剩余方法类似,这里不再赘述
当 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 |
Enumeration getHeaderNames() | 返回一个枚举,包含在该请求中包含的所有的头名 |
String getHeader(String name) | 以字符串形式返回指定的请求头的值 |
int getContentLength() | 以字节为单位返回请求主体的长度,并提供输入流,或者如果长度未知则返回 -1 |
InputStream getInputStream() | 用于读取请求的 body 内容. 返回一个 InputStream 对象 |
通过这些方法可以获取到一个请求中的各个方面的信息
请求对象是服务器收到的内容, 不应该修改. 因此上面的方法也都只是 “读” 方法, 而不是 “写” 方法
创建 RequestServlet 类
@WebServlet("/request")
public class RequestServlet extends HelloServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 这里是设置响应的 content-type. 告诉浏览器, 响应 body 里的数据格式是啥样的.
resp.setContentType("text/html");
// 搞个 StringBuilder, 把这些 api 的结果拼起来, 统一写回到响应中.
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>");
stringBuilder.append("<br>");
stringBuilder.append("<br>");
// 获取到 header 中所有的键值对
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());
}
}
部署程序,观察结果如下
GET 请求中的参数一般都是通过 query string 传递给服务器的
创建 GetParameterServlet 类
public class GetParameterServlet extends HelloServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 预期浏览器会发一个形如 /getParameter?studentId=10&classId=20 请求.
// getParameter 方法就能拿到 query string 中的键值对内容了.
// getParameter 得到的是 String 类型的结果.
String studentId = req.getParameter("studentId");
String classId = req.getParameter("classId");
resp.setContentType("text/html; charset=utf8");
// resp.setCharacterEncoding("utf8");
resp.getWriter().write("学生id = " + studentId + " 班级id = " + classId);
}
}
重新部署程序,观察结果如下
当没有 query string的时候, getParameter 获取的值为 null
当手动添加 query string的时候
POST 请求的参数一般通过 body 传递给服务器. body 中的数据格式有很多种. 如果是采用 form 表单的形式, 仍然可以通过 getParameter 获取参数的值
创建类 PostParameterServlet
@WebServlet("/postparameter")
public class PostParameterServlet extends HelloServlet{
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String studentId = req.getParameter("studentId");
String classId = req.getParameter("classId");
resp.setContentType("text/html");
resp.getWriter().write("studentId = " + studentId + " classId = " + classId);
}
}
创建 test.html, 放到 webapp 目录中
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="postparameter" method="post">
<input type="text" name="studentId">
<input type="text" name="classId">
<input type="submit" value="提交">
</form>
</body>
</html>
重新部署程序,输入URL,填入数值观察结果如下
点击提交后,根据form表单中的请求方式和URL,页面发生跳转,最后也是获取到POST请求的参数
详细过程
如果 POST 请求中的 body 是按照 JSON 的格式来传递, 那么获取参数的代码就要发生调整
创建 PostParameter2Servlet 类
@WebServlet("/postparameter2")
public class PostParameter2Servlet extends HelloServlet{
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//在流对象中读多少个字节? 取决于 Content-Length
int length = req.getContentLength();
byte[] buffer = new byte[length];
InputStream inputStream = req.getInputStream();
inputStream.read(buffer);
// 把这个字节数组构造成 String, 打印出来.
String body = new String(buffer, 0, length, "utf8");
System.out.println("body = " + body);
resp.getWriter().write(body);
}
}
这里使用postman进行模拟
通过结果可以看出,服务器拿到的 JSON 数据仍然是一个整体的 String 类型, 如果要想获取到 userId 和classId 的具体值, 还需要搭配 JSON 库进一步解析
引入 Jackson 这个库, 进行 JSON 解析
在中央仓库中搜索 Jackson, 选择 JackSon Databind
把中央仓库中的依赖配置添加到 pom.xml 中
<!-- 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>
class Student{
public int studentId;
public int classId;
@Override
public String toString() {
return "studentId:"+studentId+" "+"classId:"+classId;
}
}
@WebServlet("/postparameter2")
public class PostParameter2Servlet extends HelloServlet{
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//在流对象中读多少个字节? 取决于 Content-Length
// int length = req.getContentLength();
// byte[] buffer = new byte[length];
// InputStream inputStream = req.getInputStream();
// inputStream.read(buffer);
// // 把这个字节数组构造成 String, 打印出来.
// String body = new String(buffer, 0, length, "utf8");
// System.out.println("body = " + body);
// resp.getWriter().write(body);
ObjectMapper objectMapper = new ObjectMapper();
// readValue 就是把一个 json 格式的字符串转成 Java 对象.
Student student = objectMapper.readValue(req.getInputStream(), Student.class);
System.out.println(student.studentId + ", " + student.classId);
resp.getWriter().write(student.toString());
}
}
重新部署,观察结果
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 中写入二进制格式数据 |
响应对象是服务器要返回给浏览器的内容, 这里的重要信息都是程序猿设置的. 因此上面的方法都是 “写” 方法
实现一个程序, 返回一个重定向 HTTP 响应, 自动跳转到另外一个页面
创建 RedirectServlet 类
@WebServlet("/redirect")
public class RedirectServlet extends HelloServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setStatus(302);
resp.setHeader("Location", "https://www.baidu.com");
}
}
重新部署,观察结果
输入URL之后,直接跳转至百度首页
抓包结果
HTTP 协议自身是属于 “无状态” 协议;默认情况下 HTTP 协议的客户端和服务器之间的这次通信, 和下次通信之间没有直接的联系
浏览器提供的持久化存储数据的机制
cookie是服务器返回给浏览器的;服务器代码中由开发者决定要什么样的信息保存到客户端(浏览器)中,通过HTTP响应的Set-Cookie字段,把键值对写回到浏览器中
cookie在后续浏览器访问服务器的时候顺带在请求的header中发送给服务器;如此操作的原因是服务器只有一个,但是客服端却有很多,服务器便可以通过cookie中的值来识别当前客户端的身份
cookie存储在浏览器(客户端)所在主机的硬盘中;浏览器根据域名和存储类似键值对
用户登录时服务器并不知道用户的身份;针对登录操作,B站会查询数据库,验证用户的用户名和密码是否正确,如果正确,则登录成功;B站会把当前用户的身份信息在内存中也保存一份,同时也会给当前用户分配一个标识身份的序号,也成为sessionId;服务器使用像hash这样的结构将序号作为key,身份信息作为value存储起来,服务器把生成的这些键值对称为session
关联:网站的登录功能中,配合使用
区别:cookie是客户端的存储机制;session是服务器的存储机制
cookie中可以存储各种键值对;session专门用来保存用户的身份信息
cookie可以单独使用,不配合session;session也可以不搭配cookie使用
cookie和浏览器是强相关的;cookie则是属于HTTP协议的一分部
实现简单的用户登陆逻辑
主要涉及两个页面:登录页面,主页面;涉及两个servlet:处理登录的loginservlet判定用户和密码,构造主页面的indexservlet
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<form action="login" method="post">
<input type="text" name="username">
<br>
<input type="password" name="password">
<br>
<input type="submit" value="提交">
</form>
</body>
</html>
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException, IOException {
String username = req.getParameter("username");
String password = req.getParameter("password");
// 验证用户名密码是否正确.
if (!username.equals("zhangsan") && !username.equals("lisi")) {
// 登陆失败!!
// 重定向到 登陆页面
System.out.println("登陆失败, 用户名错误!");
resp.sendRedirect("login.html");
return;
}
if (!password.equals("123")) {
// 登陆失败!!
System.out.println("登陆失败, 密码错误!");
resp.sendRedirect("login.html");
return;
}
// 登陆成功
// 1. 创建一个会话.
HttpSession session = req.getSession(true);
// 2. 把当前的用户名保存到会话中. 此处 HttpSession 又可以当成一个 map 使用.
session.setAttribute("username", username);
// 3. 重定向到主页
resp.sendRedirect("index");
}
}
// 1. 创建一个会话.
HttpSession session = req.getSession(true);
// 2. 把当前的用户名保存到会话中. 此处 HttpSession 又可以当成一个 map 使用.
session.setAttribute("username", username);
用户登录成功,创建一个会话,getSession(true)判定当前请求是否有对应的会话;如果sessionId不存在,创建会话,放入哈希表中
创建过程:
@WebServlet("/index")
public class IndexServlet extends HttpServlet {
// 通过 重定向, 浏览器发起的是 GET .
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException, IOException {
// 先判定用户的登陆状态.
// 如果用户还没登陆, 要求先登陆.
// 已经登陆了, 则根据 会话 中的用户名, 来显示到页面上.
// 这个操作不会触发会话的创建.
HttpSession session = req.getSession(false);
if (session == null) {
// 未登录状态
System.out.println("用户未登录!");
resp.sendRedirect("login.html");
return;
}
// 已经登陆
String username = (String) session.getAttribute("username");
// 构造页面.
resp.setContentType("text/html;charset=utf8");
resp.getWriter().write("欢迎 " + username + "回来!");
}
}
第一次交互,进行登录操作
请求
响应
第二次交互
请求
响应