文章目录
1. Servlet 介绍
1.1 什么是 Servlet
-
Servlet(Server Applet 的缩写,全称 Java Servlet): 是用 Java 编写的服务器端程序。其主要功能在于交互式地浏览和修改数据,生成动态 Web 内容。狭义的 Servlet 是指 Java 语言实现的一个接口,广义的 Servlet 是指任何实现了这个 Servlet 接口的类,一般情况下,人们将 Servlet 理解为后者。
-
Servlet 运行于支持 Java 的应用服务器中。从原理上讲,Servlet 可以响应任何类型的请求,但绝大多数情况下 Servlet 只用来扩展基于 HTTP 协议的 Web 服务器。
-
Servlet 是一种实现动态页面的技术,是一组由 Tomcat 提供给程序员的 API,帮助程序员简单高效的开发一个 web app
1.2 Servlet 的主要工作
- 允许程序员注册一个类,在 Tomcat 收到的某个特定的 HTTP 请求的时候,执行这个类中的一些代码
- 帮助程序员解析 HTTP 请求,把 HTTP 请求从一个字符串解析成一个 HttpRequest 对象
- 帮助程序员构造 HTTP 响应,程序员只要给指定的 HttpResponse 对象填写一些属性字段,Servlet 就会自动的按照 HTTP 协议的方式构造出一个 HTTP 响应字符串,并通过 Socket 编写返回给客户端
2. Servlet 程序创建步骤
2.1 创建项目
以下使用 IDEA 带大家编写一个简单的 Servlet 程序,主要是让大家了解一个大致的流程
2.2 引入依赖
Maven 项目创建完成后,会自动生成一个 pom.xml
文件,我们需要在这个文件中引入 Servlet API 依赖的 jar 包
-
选择对应 Tomcat 版本的 Servlet(由于我当前使用的是 Tomcat 8 系列,所以选择 Servlet 3.1.0 即可)
-
一个项目中可以有多个依赖,每个依赖都是一个
<dependency>
标签。引入的依赖都要放在一个<dependencies>
的标签中,该标签用于放置项目依赖的 jar 包,Maven 会自动下载该依赖到本地
2.3 创建目录
Web 项目对于目录结构还有自己的要求,只有 Maven 的标准目录是不够的,需要再创建以下目录并进行配置
-
-
-
Servlet 中 web.xml 中的内容不能是空的,里面的写法是固定的(这里的写法专属于 Servlet),用到的时候可以直接拷贝下面的代码
<!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>
2.4 编写代码
-
创建一个 TestServlet 类,并且让它继承于
HttpServlet
-
-
-
在 doGet 方法中,通过
HttpServletResponse
类的getWriter()
方法往响应的 body 中写入文本格式数据resp.getWriter()
会获取到一个流对象,通过这个流对象就可以写入一些数据,写入的数会被构造成一个 HTTP 响应的 body 部分,Tomcat 会把整个响应转成字符串,通过 Socket 写回给浏览器 -
需要给 TestServlet 加上一个特定的注解
@WebServlet("/test")
上述助解表示 Tomcat 收到的请求中,URL 的 Servlet Path 路径为
/test
的请求才会调用 TestServlet 这个类的代码,注解中的字符串表示着 URL 的 Servlet Path -
到这里程序的编写已经完成了!但是你可能会疑惑上述代码不是通过 main 方法作为入口的,这是因为 main 方法已经被包含在 Tomcat 中了,我们写的程序并不能单独执行,而是需要搭配 Tomcat 才能执行起来(在 Tomcat 的伪代码中我们具体分析了这个问题)
2.5 打包程序
在程序编写好之后,就可以使用 Maven 进行打包
2.6 部署程序
接下来我们就可以进行程序的部署
2.7 验证程序
此时通过浏览器访问 http://127.0.0.1:8080/testServlet/test 就可以看到程序实现的结果了
注意:URL 中的路径分成了两个部分 Context Path 和 Servlet Path
- Context Path 这个路径表示一个 webapp,来源于打包的包名
- Servlet Path 这个路径表示一个 webapp 中的一个页面,来源于对应的 Servlet 类
@WebServlet
注解中的内容
3. 使用 Smart Tomcat 进行部署
- 对于创建项目、引入依赖、创建目录这三个步骤,其实可以使用项目模板来快速生成,但是由于项目模板加载速度很慢,因此这里并不推荐
- 对于打包程序和部署程序这两个步骤,其实可以使用 Smart Tomcat 插件来快速实现,以下将介绍它的使用方式
3.1 安装 Smart Tomcat
3.2 配置 Smart Tomcat
3.3 使用 Smart Tomcat
4. 访问出错解决方案
4.1 出现 404
出现 404 原因: 用户访问的资源不存在,大概率是 URL 的路径写的不正确
错误实例1: 少写了 Context Path 或者 Context Path 写错了
错误实例2: 少写了 Servlet Path 或者 Servlet Path 写错了
错误实例3: web.xml 写错了(如清空 web.xml 中的内容)
4.2 出现 405
出现 405 原因: 访问的服务器不能支持请求中的方法或者不能使用该请求中的方法
错误实例2: 重写了 doGet 方法,但是没有删除父类的 doGet 方法
4.3 出现 500
出现 500 原因: 服务器出现内部错误,往往是 Servlet 代码中抛出异常导致的
4.4 出现“空白页面”
错误实例: 将 resp.getWriter().write()
操作删除
4.5 出现“无法访问此网站”
出现“无法访问此网站”原因: 一般是不能正确访问到 Tomcat(可能是 Tomcat 没启动,也可能是 IP/端口号写错了)
错误实例: 注解 @WebServlet
中少写了 /
4.6 出现中文乱码问题
响应出现中文乱码问题原因: 使用的编译器的编码方式(一般是 utf-8)和浏览器的编码方式不同,浏览器默认跟随系统编码方式,win10 系统默认是 GBK 编码
解决方式: 通过响应对象的 setContentType()
方法来修改浏览器对于响应正文的编码格式
@WebServlet("/test")
public class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
resp.getWriter().write("吞吞吐吐大魔王");
}
}
5. Servlet 运行原理
在 Servlet 的代码中,我们并没有写 main 方法,那么对应的 doGet 代码是如何被调用呢?响应又是如何返回给浏览器的呢?
5.1 Servlet 的架构
我们自己实现的 Servlet 是在 Tomcat 基础上运行的,下图显示了 Servlet 在 Web 应用程序中的位置
当浏览器给服务器发送请求时,Tomcat 作为 HTTP 服务器,就可以接收到这个请求。Tomcat 的工作就是解析 HTTP 请求,并把请求交给 Servlet 的代码来进行进一步的处理。Servlet 的代码根据请求计算生成响应对象,Tomcat 再把这个响应对象构造成 HTTP 响应,返回给浏览器。并且 Servlet 的代码也经常会和数据库进行数据的传递。
5.2 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 的代码内置了 main 方法,当我们启动 Tomcat 的时候,就是从 Tomcat 的 main 方法开始执行的
- 被 @WebServlet 注解修饰的类会在 Tomcat 启动的时候就被获取到,并集中管理
- Tomcat 通过反射这样的语法机制来创建被 @WebServlet 注解修饰的类的实例
- 这些实例被创建完之后,就会调用其中的 init 方法进行初始化
- 这些实例被销毁之前,就会调用其中的 destory 方法进行收尾工作
- Tomcat 内部也是通过 Socket API 进行网络通信
- Tomcat 为了能够同时处理多个 HTTP 请求,采取了多线程的方式实现,因此 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; } // 走到这里的逻辑都是动态内容了 // 找到要处理本次请求的 Servlet 对象 Servlet ins = findInstance(req.getURL()); // 调用 Servlet 对象的 service 方法 // 这里就会最终调用到我们自己写的 HttpServlet 的子类里的方法了 try { ins.service(req, resp); } catch (Exception e) { // 返回 500 页面,表示服务器内部错误 } } }
- Tomcat 从 Socket 中读到的 HTTP 请求是一个字符串,然后 Tomcat 会按照 HTTP 协议的格式解析成一个 HttpServletRequest 对象
- Tomcat 会根据 URL 中的 Path 判定这个请求是请求一个静态资源还是动态资源。如果是静态资源,直接找到对应的文件,把文件的内容通过 Socket 返回;如果是动态资源,才会执行到 Servlet 的相关逻辑
- Tomcat 会根据 URL 中的 Context Path 和 Servlet Path 确定要调用哪个 Servlet 实例的 service 方法
- 通过 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); } ...... } }
6. Servlet API 详解
对于 Servlet 主要介绍三个类,分别是 HttpServlet、HttpServletRequest 和 HttpServletResponse。
其中 HttpServletRequest 和 HttpServletResponse 是 Servlet 规范中规定的两个接口,HttpServlet 中并没有实现这两个接口的成员变量,它们只是 HttpServlet 的 service 和 doXXX 等方法的参数。这两个接口类的实例化是在 Servlet 容器中实现的。
6.1 HttpServlet
核心方法
方法名称 | 调用时机 |
---|---|
init |
在 HttpServlet 实例化之后被调用一次 |
destory |
在 HttpServlet 实例不再使用的时候调用一次 |
service |
收到 HTTP 请求的时候调用 |
doGet |
收到 GET 请求的时候调用(由 service 方法调用) |
doPost |
收到 POST 请求的时候调用(由 service 方法调用) |
doPut/doDelete/doOptions/... |
收到其它对应请求的时候调用(由 service 方法调用) |
Servlet 的生命周期: Servlet 的生命周期就是 Servlet 对象从创建到销毁的过程,下面来介绍其生命周期的过程
- Servlet 对象是由 Tomcat 来进行实例化的,并且在实例化完毕之后调用 init 方法(只调用一次)
- Tomcat 对于收到的请求,都会通过对应的 Servlet 的 service 方法来进行处理(可以调用多次)
- Tomcat 在结束之前,会调用 Servlet 的 destory 方法来进行回收资源(最多调用一次)
注意: init 和 service 能够保证在各自的合适时机被 Tomcat 调用,但是 destory 不一定,它是否能够被调用取决于 Tomcat 是如何结束的
直接通过浏览器 URL 发送一个 GET 方法的请求,来对这个请求进行处理
@WebServlet("/get")
public class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("get");
}
}
处理 POST 请求示例:
由于通过浏览器 URL 发送的请求是 GET 方法的请求,因此我们需要通过其它方式来发送一个 POST 请求然后用于处理。发送 POST 请求的方式有通过 Ajax、form 表单或者 socket api 进行构造,如果单纯的用于测试就比较麻烦,这里推荐使用软件 postman,这是一个很强大的 API 调试、Http 请求的工具。
@WebServlet("/post")
public class TestServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("post");
}
}
6.2 HttpServletRequest
核心方法
方法 | 描述 |
---|---|
String getProtocol() |
返回协议的名称和版本号 |
String getMethod() |
返回请求的 HTTP 方法的名称 |
String getRequestURL() |
返回请求的 URL,不带查询字符串 |
String getRequestURI() |
返回该请求的 URL 的一部分,不带协议名、端口号、查询字符串 |
String getContextPath() |
返回指示请求 URL 中 Context Path 部分 |
String getServletPath() |
返回指示请求 URL 中 ServletPath 部分 |
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() |
用于读取请求的正文内容,返回一个 InputStream 对象 |
示例1: 通过上述方法返回一个页面是该请求的具体 HTTP 请求格式
@WebServlet("/showRequest")
public class ShowRequestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 此处返回一个 HTML,在 HTML 中显示 HttpRequestServlet 类中的一些核心方法
// 把这些 API 的返回结果通过 StringBuilder 进行拼接
resp.setContentType("text/html;charset=utf-8");
StringBuilder html = new StringBuilder();
html.append(req.getMethod());
html.append(" ");
html.append(req.getRequestURL());
html.append("?");
html.append(req.getQueryString());
html.append(" ");
html.append(req.getProtocol());
html.append("</br>");
Enumeration<String> headerNames = req.getHeaderNames();
while(headerNames.hasMoreElements()){
String headName = headerNames.nextElement();
String header = req.getHeader(headName);
html.append(headName);
html.append(": ");
html.append(header);
html.append("</br>");
}
html.append("</br>");
//InputStream body = req.getInputStream();
resp.getWriter().write(html.toString());
}
}
示例2: 处理 HTTP 请求的 body 中的数据格式
-
如果 body 的内容格式是
x-www-form-urlencoded
,使用getParameter
进行处理@WebServlet("/postParameter") public class PostParameterServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf-8"); String username = req.getParameter("username"); String passwd = req.getParameter("passwd"); resp.getWriter().write("username=" + username + "</br>" +"passwd=" + passwd); } }
-
如果 body 的内容格式是
json
,首先将整个 body 都读取出来,再借助第三方库的方法按照 json 的格式来进行解析,Java 标准库没有内置对于 json 解析的方法)- 此处是要获取 body 的数据,由于 GET 方法一般没有 body,这里使用 POST 方法演示
- 约定 body 的数据格式为:json
- 约定 body 的数据内容为:
{
username=123,
passwd=456
} - 此处使用 jackson 第三方库,使用之前需要去 Maven 的中央仓库将 jackson 的依赖引入 pom.xml 中
- jackson 中的核心类是
ObjectMapper
,通过这个类的readValue(String content, Class<T> valueType)
方法,就可以将 json 字符串转化为一个类的对象(第一个参数是 json 字符串,第二个参数是类对象),ObjectMapper 会遍历定义的类中的每个成员的名称,去 json 字符串的 key 中查找,如果找到了就将对应的值返回给该成员
// 自定义的将 json 字符串转化的类 class UserInfo { public String username; public String passwd; } @WebServlet("/jsonParameter") public class JsonParameterServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf-8"); // 1. 先将整个 body 读取出来 String body = readBody(req); // 2. 按照 json 格式进行解析 ObjectMapper objectMapper = new ObjectMapper(); UserInfo userInfo = objectMapper.readValue(body, UserInfo.class); resp.getWriter().write("username=" + userInfo.username + "</br>" + "passwd=" + userInfo.passwd); } // 定义一个方法来读取请求中的全部 body private String readBody(HttpServletRequest req) throws IOException { // 1. 先拿到 body 的长度,单位是字节 int contentLength = req.getContentLength(); // 2. 准备一个字节数组,来存放 body 内容 byte[] buffer = new byte[contentLength]; // 3. 获取到 InputStream 对象 InputStream inputStream = req.getInputStream(); // 4. 从 InputStream 对象中读取到数据,将数据放到字节数组中 inputStream.read(buffer); // 5. 将存放 body 内容的字节数组转换成字符串 return new String(buffer, "utf-8"); } }
6.3 HttpServletResponse
核心方法
方法 | 描述 |
---|---|
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) |
设置被发送到客户端的响应的字符编码,例如 utf-8 |
void sendRedirect(String location) |
设置 Location 字段,实现重定向 |
PrintWriter getWriter() |
用于往 body 中写入文本格式数据 |
OutputStream getOutputStream() |
用于往 body 中写入二进制格式数据 |
示例1: 通过代码,构造出不同的响应状态码
@WebServlet("/status")
public class StatusServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
int status = 404;
resp.setStatus(status);
resp.getWriter().write("status=" + status);
}
}
示例2: 在响应报头设置一个 Refresh 字段,实现字段刷新程序
@WebServlet("/autoRefresh")
public class AutoRefreshServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 给响应设置一个 Refresh 的 header,每隔 1s 钟刷新一次
resp.setHeader("Refresh", "1");
// 返回一个当前的时间,用来显示刷新的效果
resp.getWriter().write("timestamp=" + System.currentTimeMillis());
}
}
-
方法一:在响应报头设置状态码和 Location 来实现重定向
@WebServlet("/redirect") public class RedirectServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 将状态码设置为 3XX resp.setStatus(302); // 设置一个 Location,重定向到 CSDN 博客主页 resp.setHeader("Location", "https://blog.csdn.net/weixin_51367845?spm=1000.2115.3001.5343"); } }
-
方法二:直接使用 sendRedirect() 方法来实现重定向
@WebServlet("/redirect") public class RedirectServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.sendRedirect("https://blog.csdn.net/weixin_51367845?spm=1000.2115.3001.5343"); } }
7. 实现服务器版表白墙程序
7.1 基本介绍
在之前的文章《【Web 三件套】 JavaScript WebAPI》中实现过了一个纯前端的表白墙代码,实现后的效果如下。这次将会结合上述的知识,实现一个服务器版的表白墙程序
7.2 准备操作
-
创建好一个 Servlet 项目
-
约定好前后端交互的接口,该程序只需约定两个接口
-
创建一个
MessageServlet
类,@WebServlet
注解为/message
,对应着约定的请求路径,通过上方的约定完成服务器段的代码
7.3 代码实现
后端代码实现:
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;
import java.net.HttpRetryException;
import java.util.ArrayList;
import java.util.List;
// 这个类表示一条消息的详细情况
class Message{
public String from;
public String to;
public String message;
}
@WebServlet("/message")
public class MessageServlet extends HttpServlet {
// 通过这个数组来表示所有的消息
private List<Message> messages= new ArrayList<>();
// 通过这个代码来完成获取服务器所有消息的操作
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("application/json;charset=utf-8");
// 获取到消息列表
// 此处要做的就是把当前的 messages 数组转成 json 格式返回给浏览器
ObjectMapper objectMapper = new ObjectMapper();
// 通过 ObjectMapper 的 writeValuesAsString() 方法就可以将一个对象转换成 json 字符串
// 由于这里的 message 是一个 List,那么得到的结果是一个 json 数组
String jsonString = objectMapper.writeValueAsString(messages);
resp.getWriter().write(jsonString);
}
// 通过这个代码来完成新增消息的操作
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
ObjectMapper objectMapper = new ObjectMapper();
Message message = objectMapper.readValue(req.getInputStream(), Message.class);
messages.add(message);
resp.getWriter().write("提交成功!");
}
}
前端代码实现:
<!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>表白墙</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.container {
width: 600px;
margin: 0 auto;
}
h1 {
text-align: center;
padding: 20px 0;
color: pink;
}
p {
text-align: center;
font-size: 15px;
color: grey;
padding: 5px 0;
}
.row {
display: flex;
height: 40px;
justify-content: center;
align-items: center;
}
.row span {
width: 80px;
}
.row .edit {
width: 250px;
height: 35px;
}
.row .submit {
width: 330px;
height: 40px;
background-color: orange;
color: #fff;
border: none;
}
.row .submit:active {
background-color: grey;
}
</style>
</head>
<body>
<div class="container">
<h1>表白墙</h1>
<p>输入后点击提交,将会把消息显示在在墙上</p>
<div class="row">
<span>谁:</span>
<input type="text" class="edit">
</div>
<div class="row">
<span>对谁:</span>
<input type="text" class="edit">
</div>
<div class="row">
<span>说什么:</span>
<input type="text" class="edit">
</div>
<div class="row">
<input type="button" value="提交" class="submit">
</div>
</div>
<script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
<script>
let submitButton = document.querySelector('.submit');
submitButton.onclick = function() {
// 1. 获取到输入框里的内容
let edits = document.querySelectorAll('.edit');
let from = edits[0].value;
let to = edits[1].value;
let message = edits[2].value;
// 2. 根据输入框的内容,构造 HTML 元素,添加到页面中
if(from == '' || to == '' || message == '') {
return;
}
let div = document.createElement('div');
div.innerHTML = from + '对' + to + '说:' + message;
div.className = 'row';
let container = document.querySelector('.container');
container.appendChild(div);
// 3. 把上次输入的内容清空
for(let i = 0; i < edits.length; i++){
edits[i].value = '';
}
// 4. 把当前新增的消息发送给服务器
let body = {
from: from,
to: to,
message: message
};
$.ajax ({
url: "message",
method: "post",
contentType: "application/json;charset=utf8",
// 通过 JSON.stringify 将对象转成字符串
data: JSON.stringify(body),
success: function(data, status){
console.log(data);
}
})
}
// 服务器版本
// 1. 在页面加载的时候,从服务器获取到消息列表,并显示在网页上
function load() {
$.ajax({
method: "get",
url: "message",
success: function(data, status) {
// 此处得到的响应 data 其实已经被 jquery 转成了一个对象数组
// 但是这里的自动转换有个前提,服务器响应的 header 中 ContentType 是 json
let container = document.querySelector('.container');
for(let message of data){
// 遍历每个元素,针对每个元素拆功能键一个 div 标签
let div = document.createElement('div');
div.className = 'row';
div.innerHTML = message.from + "对" + message.to + " 说:" + message.message;
container.append(div);
}
}
})
}
load();
</script>
</body>
</html>
7.4 持久化存储
通过上述修改,原本的纯前端代码就加上了服务器,只要服务器开启后,即使刷新网页,已经添加的数据也不会消失。但是如果重启服务器的话,原本的数据就会丢失,为了解决这个问题,就需要让数据能够持久化存储。
持久化存储: 是把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘),是一种将程序数据在持久状态和瞬时状态间转换的机制。
-
先建库建表(可以先创建一个文件,将要建的数据库和表都写好)
drop database if exits messagewall; create database messagewall; use messagewall; drop table if exits message; create table message ( `from` varchar(50), `to` varchar(50), `message` varchar(1024) );
-
连接数据库,创建一个 DBUtil 类,用于封装数据库的建立连接和资源释放操作
import com.mysql.jdbc.jdbc2.optional.MysqlDataSource; import javax.sql.DataSource; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; // 通过这个类来封装数据库的建立连接操作 public class DBUtil { private static final String URL = "jdbc:mysql://127.0.0.1:3306/messagewall?characterEncoding=utf8&setSSL=false"; private static final String USERNAME = "root"; private static final String PASSWORD = "1234"; private static DataSource dataSource = new MysqlDataSource(); static { ((MysqlDataSource)dataSource).setURL(URL); ((MysqlDataSource)dataSource).setUser(USERNAME); ((MysqlDataSource)dataSource).setPassword(PASSWORD); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet){ if(resultSet != null){ try { resultSet.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } } if(statement != null){ try { statement.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } } if(connection != null){ try { connection.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } } } }
-
修改 MessageWall 类的代码,主要修改的地方有两处,将原本的 messages 数组删除
-
在获取消息时,可以增加一个
getMessages()
方法,用于拿到数据库中的所有消息// 从数据库获取到所有消息 private List<Message> getMessages() { Connection connection = null; PreparedStatement statement = null; ResultSet resultSet = null; List<Message> messages = new ArrayList<>(); try { // 1. 和数据库建立连接 connection = DBUtil.getConnection(); // 2. 构造 sql String sql = "select * from message"; statement = connection.prepareStatement(sql); // 3. 执行 sql resultSet = statement.executeQuery(); // 4. 遍历结果集合 while(resultSet.next()){ Message message = new Message(); message.from = resultSet.getString("from"); message.to = resultSet.getString("to"); message.message = resultSet.getString("message"); messages.add(message); } } catch (SQLException throwables) { throwables.printStackTrace(); } finally { DBUtil.close(connection, statement, resultSet); } return messages; }
-
在新增消息是,可以新增一个
addMessage()
方法,用于往数据库存储一条新消息// 往数据库新增一条消息 private void addMessage(Message message) { Connection connection = null; PreparedStatement statement = null; try { // 1. 和数据库建立连接 connection = DBUtil.getConnection(); // 2. 构造 sql String sql = "insert into message values(?, ?, ?)"; statement = connection.prepareStatement(sql); statement.setString(1, message.from); statement.setString(2, message.to); statement.setString(3, message.message); // 3. 执行 sql statement.executeUpdate(); } catch (SQLException throwables) { throwables.printStackTrace(); } finally { DBUtil.close(connection, statement, null); } }
到这里为止,一个完整的服务器表白程序就写好啦!在我自己撸上面的代码时,由于连接 MySQL 的 URL 中的端口号写错了,导致自己找了很久的 bug,所以大家如果尝试上述代码时遇到问题,一定要看清是不是自己哪个地方打错了!
-
8. Cookie 和 Session
8.1 Cookie 介绍
在之前的文章《HTTP 协议详解》中,就介绍过了 Cookie,可以结合本文的内容来搭配理解。
-
Cookie 是浏览器提供的在客户端存储数据的一种机制(由于浏览器禁止了网页中的代码直接访问本地磁盘的文件,因此想要在网页中实现持久化存储,就可以通过 Cookie 这样的机制)
-
Cookie 里面存什么?
Cookie 存储的数据都是程序员自定义的,存储的数据是一个字符串,是键值对结构的,键值对之间使用
;
分割,键和值之间使用=
分割 -
Cookie 从哪里来?
-
Cookie 到哪里去?
-
-
Cookie 的缺陷:
在了解 Cookie 以后,我们发现 Cookie 是不能够用于存储和用户相关的直接信息的,一是 Cookie 的存储容量有限,二是发送请求时占用带宽很多,三是不太安全。即这些数据不适合保存在客户端,保存在服务器是更合适的,通过会话(Session)的方式就能够保存这些数据。
8.2 Session 会话机制介绍
基本介绍:
在计算机中,尤其是在网络应用中,Session 称为“会话控制”。Session 对象存储特定用户会话所需的属性及配置信息。当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web 页时,如果该用户还没有会话,则 Web 服务器将自动创建一个 Session 对象。当会话过期或被放弃后,服务器将终止该会话。Session 对象最常见的一个用法就是存储用户的首选项。例如,如果用户指明不喜欢查看图形,就可以将该信息存储在 Session 对象中。注意会话状态仅在支持 Cookie 的浏览器中保留。
接着 Cooike 不适合用于存储用户相关的直接信息来讲,由于客户端不适合存储这些数据,服务器这边可以通过 Session 会话的方式来进行保存。下面将会以用户登录的流程来介绍 Session 会话机制
- 当用户成功登录之后,服务器在 Session 中会生成一个新的记录,并把 sessionId 返回给客户端(例如 HTTP 响应中可以通过 Set-Cookie 字段返回,其中 Cookie 的 key 为 “JSESSION”,value 为服务器生成的 sessionId 的具体的值)
- 然后客户端只需要保存这个 sessionId ,当后续再给服务器发送请求时,请求中就会带上 sessionId(例如 HTTP 请求中会带上 Cookie 字段用于传递 Session)
- 服务器收到请求后,就会根据请求中的 sessionId 在 Session 中查询对应用户的身份信息,在进行后续操作
Session 会话机制的好处:
注意: Servlet 的 Session 默认是保存在内存中的,如果重启服务器 Session 数据将会丢失
8.3 Cookie 和 Session 的区别
-
Cookie 和 Session 经常配合使用,但是不是必须的。
8.4 Servlet 中 Cookie 和 Session 的核心方法
方法 | 描述 |
---|---|
HttpSession getSession(参数) |
在服务器中获取会话,参数如果为 true,当不存在会话时会新建会话(包括生成一个新的 sessionId 和 HttpSession 对象),并通过 Set-Cookies 将 sessionId 返回给客户端;参数如果为 false,当不存在会话时会返回 null。如果存在 sessionId 且合法,就会根据这个 sessionId 找到对应的 HttpSession 对象并返回 |
Cookie[] getCookies() |
返回一个数组,包含客户端发送请求时的所有 Cookie 对象,会自动把 Cookie 中的格式解析成键值对 |
HttpServletResponse 类中的相关方法
方法 | 描述 |
---|---|
void addCookie(Cookie cookie) |
把指定的 cookie 添加到响应中 |
HttpSession 类中的相关方法
方法 | 描述 |
---|---|
Object getAttribute(String name) |
该方法返回在 Session 会话中具有指定名称的对象,如果没有指定名称的对象,则返回 null |
void setAttribute(String name, Object value) |
该方法使用指定的名称绑定一个对象到该 Session 会话中 |
boolean isNew() |
判定当前的会话是否是新创建的 |
Cookie 类中的相关方法
- 这个类描述了一个 Cookie,通过 Cookie 类创建的对象,每个对象就是一个键值对
- HTTP 的 Cookie 字段中实际上存储的是多个键值对,每个键值对在 Servlet 中都对应一个 Cookie 对象
方法 | 描述 |
---|---|
String getName() |
该方法返回 cookie 的名称(这个值是 Set-Cookie 字段设置给浏览器的,创建之后不能改变) |
String getValue() |
该方法获取与 Cookie 关联的值 |
void setValue(String newValue) |
该方法设置与 Cookie 关联的值 |
8.5 实现用户登录功能
接下来将使用上述的 Session 和 Cookie 的相关方法来实现一个用户登录功能,并且可以记录访问页面的次数
登录功能实现思路:
登录功能实现流程:
-
先实现一个登录页面
<!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>登录页面</title> </head> <body> <form action="login" method="post"> <input type="text" name="username"> <input type="password" name="password"> <input type="submit" value="登录"> </form> </body> </html>
-
实现一个 Servlet 来处理上面的登录请求
由于这里是通过 form 表单来构造的 post 请求,那么通过 HttpServletRequest 类中的
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 javax.servlet.http.HttpSession; import java.io.IOException; @WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf8"); // 1. 从请求中获取到用户名和密码 String username = req.getParameter("username"); String password = req.getParameter("password"); // 2. 对用户密码进行校验 if(username == null || "".equals(username) || password == null || "".equals(password)){ resp.getWriter().write("<h3>账号或密码不能为空!</h3>"); return; } // 3. 判断是否登录成功(假设用户名为 admin,密码为 1234。不过账号密码应该用数据库存储,这里只是用来测试) if(!username.equals("admin") || !password.equals("1234")){ resp.getWriter().write("<h3>账号或密码错误!</h3>"); return; } // 4. 登录成功,创建一个会话,用来记录当前用户的信息 HttpSession session = req.getSession(true); // 通过这个操作,就给会话中新增了一个程序员自定义的信息,访问次数 session.setAttribute("visitCount", 0); // 5. 把登录成功的结果反馈给客户端(这里的反馈不是简单的提示“登录成功”,而是直接跳转到指定页面) resp.sendRedirect("index"); } }
-
通过实现一个 Servlet 来表示登录成功后重定向的页面
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 javax.servlet.http.HttpSession; import java.io.IOException; @WebServlet("/index") public class IndexServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf8"); // 只有登录成功参数才能是 true,这里是拿参数,所以要填 false HttpSession session = req.getSession(false); // 判断当前用户是否登录 if(session == null){ // 可以提示未登录,也可以重定向到登录页面 // resp.getWriter().write("<h3>登录为空!</h3>"); resp.sendRedirect("login2.html"); return; } // 表示用户登录过,获取会话中的访问次数 Integer visitCount = (Integer) session.getAttribute("visitCount"); visitCount += 1; session.setAttribute("visitCount", visitCount); resp.getWriter().write("<h3>visitCount = " + visitCount + "</h3>"); } }
9. 上传文件操作
上传文件是日常开发中的一类常见需求,在 Servlet 中也进行了支持
9.1 Servlet 中上传文件的核心方法
HttpServletRequest 类中的相关方法
方法 | 描述 |
---|---|
Part getPart(String name) |
获取请求中给定 name 的文件 |
Collection<Part> getParts() |
获取所有的文件 |
Part 类中的相关方法
方法 | 描述 |
---|---|
String getSubmittedFileName() |
获取提交的文件名 |
String getContentType() |
获取提交的文件类型 |
long getSize() |
获取文件的大小,单位为字节 |
void write(String path) |
把提交的文件数据写入磁盘文件 |
9.2 上传文件操作实现
-
<!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>上传文件</title> </head> <body> <form action="upload" method="post" enctype="multipart/form-data"> <input type="file" name="MyFile"> <input type="submit" value="上传"> </form> </body> </html>
-
写一个 Servlet 用于处理上传的文件
import javax.servlet.ServletException; import javax.servlet.annotation.MultipartConfig; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.Part; import java.io.IOException; @MultipartConfig @WebServlet("/upload") public class UploadServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf8"); // 通过 getPart 方法获取到前端传来的文件 Part part = req.getPart("MyFile"); // 获取文件名 String fileName = part.getSubmittedFileName(); System.out.println("文件名为: " + fileName); // 获取提交的文件类型 String fileType = part.getContentType(); System.out.println("文件类型为: " + fileType); // 获取文件的大小 long fileSize = part.getSize(); System.out.println("文件大小为: " + fileSize); // 把文件数据写入磁盘文件 part.write("C:\Users\bbbbbge\Desktop\upload.jpg"); resp.getWriter().write("上传成功!"); } }
到这里为止,一个简单的文件上传操作就实现好了,我们可以通过抓包来观察下文件上传操作的请求是怎样的?
- 正文的大小和我们上传文件的大小不同,正文的比上传的文件的字节数略大
- 数据类型是
multipart/form-data
没有问题,但是后面多了一串boundary=----WebKitFormBoundaryAl26z0nbP6JzAUGL
,这个 boundary 在 body 中表示一个分隔线,第一条分割线下面是上传的文件的属性和文件的内容,当文件的内容结束时还有第二条分割线 - 由于有这个分割线和文件的一些属性,因此使得请求中正文的大小比上传的文件的内容略大
原文地址:https://blog.csdn.net/weixin_51367845/article/details/124325311
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.7code.cn/show_21616.html
如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除!