实现的这个表白墙,就通过服务器,保存这里的消息数据,从而进一步的做到 “持久化” 存储~~
在实现这个程序的时候,就需要先考虑清楚,客户端和服务器之间该如何进行交互~
约定前后端交互的接口~~
(自定义应用层协议)
既然是搞一个服务器,服务器就得提供一些服务,具体是提供啥样的服务? 以及这些服务该如何触发? 都需要去考虑清楚~~
对于表白墙来说,主要要提供两个接口:
1). 告诉服务器,当前留言了一条啥样的数据~~ (当用户点击提交按钮的时候,就会给服务器发送一个 HTTP 请求,让服务器把这个消息给存下来)
约定好,为了实现这个效果,客户端要发送一个啥样的 HTTP 请求,服务器返回一个啥样的 HTTP响应~
请求:
POST /message
{
from: "黑猫",
to: "白猫",
message: "喵"
}
POST /messageWall
from=黑猫&to=白猫&message=喵
GET /message?from=黑猫&to=白猫&message=喵
按照上述的思路发散开,有无数种方式来约定请求格式!!! 方法可以变,路径可以变,参数的名字也可以变…
这么多的约定方式,到底采取哪一种??
哪种都可以!!! 只要你能够明确下来其中的一种,并且在后续编写前端 / 后端代码的时候能够严格执行,就是 ok 的!!
这也正是咱们约定前后端接口的意义!!!
自定义协议!! (正是在约束程序猿的自由,不能随便乱写代码)
在咱们未来的实际开发工作中,尤其是,前端和后端两个程序猿配合实现一个功能的时候,这种约定前后端交互接口的行为是至关重要的,一定是进行开发动作之前,第一件要完成的事情~~
我们就约定使用:
请求:
POST /message
{
from: "黑猫",
to: "白猫",
message: "喵"
}
响应:
HTTP/1.1 200 OK
{
ok: true
}
2). 从服务器获取到,当前都有哪些留言数据~~ (当页面加载,就需要从服务器获取到曾经存储的这些消息内容)
请求:
GET /message
响应:JSON 格式的数组,每个元素都是一个 JSON 对象
HTTP/1.1 200 OK
Content-Type: application/json
{
{
from: "黑猫",
to: "白猫",
message: "喵"
},
{
from: "黑猫",
to: "白猫",
message: "喵"
},
{
from: "黑猫",
to: "白猫",
message: "喵"
}
}
- 确定好接口之后,就可以编写代码了
- 既需要编写后端代码,也需要编写前端代码
- (实际工作中,往往,后端和前端是两个人分别负责,俩人就是在并行的开发) —— 联调
后端开发:
先进行准备工作
<dependencies>
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.6.1</version>
</dependency>
</dependencies>
创建目录
编写代码
// 和前面约定的 前后端交互接口 相匹配
@WebServlet("/message")
public class MessageServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 处理提交信息请求
resp.getWriter().write("hello post");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取到消息列表
resp.getWriter().write("hello get");
}
}
完整代码:
class Message {
public String from;
public String to;
public String message;
}
// 和前面约定的 前后端交互接口 相匹配
@WebServlet("/message")
public class MessageServlet extends HttpServlet {
private ObjectMapper objectMapper = new ObjectMapper();
private List<Message> messages = new ArrayList<>();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 处理提交信息请求
Message message = objectMapper.readValue(req.getInputStream(), Message.class);
// 最简单的保存放法就是保存到内存中
messages.add(message);
// 通过 ContentType 告知页面,返回的数据是 json 格式
// 有了这样的声明,此时 jquery ajax 就会自动帮我们把字符串转成 js 对象
// 如果没有,jquery ajax 只会当成字符串来处理
resp.setContentType("application/json; charset=utf8");
resp.getWriter().write("{ \"ok\": true }");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取到消息列表 将消息列表的内容整个的都返回给客户端即可
// 此处需要使用 ObjectMapper 把 Java 对象,转成 JSON 格式字符串
String jsonString = objectMapper.writeValueAsString(messages);
System.out.println("jsonString: " + jsonString);
resp.setContentType("application/json; charset=utf8");
resp.getWriter().write(jsonString);
}
}
启动服务器,使用 Postman 构造请求
POST:
构造三次 PSOT,然后回到 GET:
当前已经把后端逻辑编写完毕,接下来来写前端代码~~
就可以把之前前面写好的页面给拷贝过来
再在之前的代码基础上,加上 ajax 的操作~
let body = {
"from": from,
to: to,
message: msg
};
{
"from": "黑猫",
"to": "白猫",
"message": "喵"
},
在 JSON 中,key 要带引号,但是在 JS 中,对象这里的 key 可以带引号,也可以不带
带还是不带,都是字符串类型~~
(正常是要带的,但是 js 为了让大家写起来方便一些,允许省略引号)
如果要是 key 里面带有一些特殊符号,比如像 空格 – 这种~~ 此时就必须带引号了
对象和 JSON 字符串之间的转换:
完整代码:
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.container {
width: 100%;
}
h3 {
text-align: center;
padding: 30px 0; /* 上下内边距 20,左右为 0 */
font-size: 24px;
}
p {
text-align: center;
color: #999;
padding: 10px 0;
}
.row {
width: 400px;
height: 50px;
margin: 0 auto;
display: flex;
justify-content: center;
align-items: center;
}
.row span {
width: 60px;
font-size: 20px;
}
.row input {
width: 300px;
height: 40px;
line-height: 40px;
font-size: 20px;
text-indent: 0.5em;
/* 去掉输入框的轮廓线 */
outline: none;
}
.row #submit {
width: 200px;
height: 40px;
font-size: 20px;
line-height: 40px;
margin: 0 auto;
color: white;
background-color: orange;
/* 去掉边框 */
border: none;
border-radius: 10px;
}
/* 按下的效果 */
.row #submit:active {
background-color: grey;
}
style>
<div class="container">
<h3>表白墙h3>
<p>输入后点击提示,会将信息显示在表格中p>
<div class="row">
<span>谁:span>
<input type="text">
div>
<div class="row">
<span>对谁:span>
<input type="text">
div>
<div class="row">
<span>说:span>
<input type="text">
div>
<div class="row">
<button id="submit">提交button>
div>
div>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js">script>
<script>
// 加入 ajax 的代码,此处要加入的逻辑有两个部分
// 点击按钮提交的时候,ajax 要构造数据发送给服务器
// 页面加载的时候,从服务器获取消息列表,并在界面上直接显示
function getMessages() {
$.ajax({
type: 'get',
url: "message",
success: function(body) {
// 当前 body 已是一个 js 对象数组了,ajax 会根据响应的 content type 来自动进行解析
// 如果服务器返回的 content-type 已经是 application/json 了,ajax 就会把 body 自动转成 js 对象
// 如果客户端没有自动转,也可以通过 JSON.parse() 这个函数手动转换
let container = document.querySelector('.container');
for (let message of body) {
let div = document.createElement('div');
div.innerHTML = message.from + ' 对 ' + message.to + ' 说 ' + message.message;
div.className = 'row'; // 应用 row 的样式
container.appendChild(div);
}
}
});
}
// 加上函数调用
getMessages();
// 当用户点击 submit,就会获取 input 中的内容,把内容构造成一个 div,插入页面末尾
let submitBtn = document.querySelector('#submit');
submitBtn.onclick = function() {
// 1、获取 2 个 input
let inputs = document.querySelectorAll('input');
let from = inputs[0].value;
let to = inputs[1].value;
let msg = inputs[2].value;
if (from == '' || to == '' || msg == '') { // 用户还未填写完毕
return;
}
// 2、生成一个新的 div,内容就是 input 中的内容,新的 div 加到页面中
let div = document.createElement('div');
div.innerHTML = from + ' 对 ' + to + ' 说 ' + msg;
div.className = 'row'; // 应用 row 的样式
let container = document.querySelector('.container');
container.appendChild(div);
// 3、清空之前输入框的内容
for (let i = 0; i < inputs.length; i++) {
inputs[i].value = '';
}
// 4、把当前获取到的输入框的内容,构造成一个 HTTP POST 请求,通过 ajax 发给服务器
let body = {
"from": from, // 在 JSON 中,key 要带引号,但是在 JS 中,对象这里的 key 可以带引号,也可以不带
to: to,
message: msg
};
$.ajax({
type: "post",
url: "message",
contentType: "application/json;charset=utf8",
data: JSON.stringify(body),
success: function(body) {
alert("消息提交成功!");
},
error: function() {
alert("消息提交失败!");
}
});
}
script>
访问:127.0.0.1:8080/message_wall/messageWall.html
抓包看响应:
HTTP/1.1 200
Content-Type: application/json;charset=utf8
Content-Length: 2
Date: Sun, 22 May 2022 12:00:17 GMT
Keep-Alive: timeout=20
Connection: keep-alive
[]
当页面加载的时候,就能够看到有一个这个 ajax 请求,这个请求就是从服务器获取到消息列表的~~
此处看到这个响应是空着的!!!
刚才不是明明往服务器上提交了三条记录嘛??
刚刚这个代码是把消息保存到了这个 List 中(List 就是内存),一旦程序重启,内存中的数据就会丢失~~
因此,上述逻辑,只能保证页面刷新后数据不丢失,而不能保证服务器重启后数据不丢失~~
页面刷新的概率—定是更大的!!
当然,要想更加彻底的解决这个问题,就需要把数据保存到硬盘上 (可以是文件,也可以是数据库)
输入提交:
当前确实可以证明,即使页面关闭了,再次打开之后,之前提交的消息是不丢失的~~ (页面加载的时候,从服务器获取到了消息列表,并显示在了浏览器页面中)
针对上面的问题, 如果把数据保存在文件中,那么重启服务器也不会丢失数据了
修改 MessageServlet 代码:
文件格式形如:
@WebServlet("/message")
public class MessageServlet extends HttpServlet {
// 用于保存所有的留言
// private List messages = new ArrayList();
// 用于转换 JSON 字符串
private ObjectMapper objectMapper = new ObjectMapper();
// 数据文件的路径
private String filePath = "d:/messages.txt";
public List<Message> load() {
List<Message> messages = new ArrayList<>();
System.out.println("从文件读取数据");
try (BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath))) {
while (true) {
String line = bufferedReader.readLine();
if (line == null) {
break;
}
String[] tokens = line.split("\t");
Message message = new Message();
message.from = tokens[0];
message.to = tokens[1];
message.message = tokens[2];
messages.add(message);
}
} catch (IOException e) {
// 首次运行的时候文件不存在, 可能会在这里触发异常.
e.printStackTrace();
}
System.out.println("共读取数据 " + messages.size() + " 条!");
return messages;
}
public void save(Message message) {
System.out.println("向文件写入数据");
// 使用追加写的方式打开文件
try (FileWriter fileWriter = new FileWriter(filePath, true)) {
fileWriter.write(message.from + "\t" + message.to + "\t" +
message.message + "\n");
} catch (IOException e) {
e.printStackTrace();
}
}
// 获取所有留言
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
List<Message> messages = load();
resp.setContentType("application/json;charset=utf-8");
String respString = objectMapper.writeValueAsString(messages);
resp.getWriter().write(respString);
}
// 新增留言
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("application/json;charset=utf-8");
Message message = objectMapper.readValue(req.getInputStream(), Message.class);
save(message);
resp.getWriter().write("{ \"ok\": 1 }");
}
}
使用文件的方式存储留言固然可行,但是并不优雅
我们还可以借助数据库完成存储工作
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
class Message {
public String from;
public String to;
public String message;
}
// 和前面约定的 前后端交互接口 相匹配
@WebServlet("/message")
public class MessageServlet extends HttpServlet {
private ObjectMapper objectMapper = new ObjectMapper();
// private List messages = new ArrayList<>();
// 改成数据库,不需要整个变量
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 处理提交信息请求
Message message = objectMapper.readValue(req.getInputStream(), Message.class);
// // 最简单的保存放法就是保存到内存中
// messages.add(message);
// 通过 ContentType 告知页面,返回的数据是 json 格式
// 有了这样的声明,此时 jquery ajax 就会自动帮我们把字符串转成 js 对象
// 如果没有,jquery ajax 只会当成字符串来处理
save(message);
resp.setContentType("application/json; charset=utf8");
resp.getWriter().write("{ \"ok\": true }");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取到消息列表 将消息列表的内容整个的都返回给客户端即可
// 此处需要使用 ObjectMapper 把 Java 对象,转成 JSON 格式字符串
List<Message> messages = load();
String jsonString = objectMapper.writeValueAsString(messages);
System.out.println("jsonString: " + jsonString);
resp.setContentType("application/json; charset=utf8");
resp.getWriter().write(jsonString);
}
private void save(Message message) {
// 把一条数据保存到数据库中
}
private List<Message> load() {
// 从数据库中获取所有数据
}
}
用 JDBC 基本流程:
- 创建数据源
- 和数据库建立连接
- 构造 sql 语句
- 执行 sql 语句
- 如果是查询语句,需要遍历结果集,如果是插入 / 删除 / 修改,则不需要
- 关闭连接,释放资源
创建数据库:
create table message(`from` varchar(1024), `to` varchar(1024), message varchar(4096));
from 是 sql 中的关键字,当关键字作为表名 / 列名的时候,需要加上反引号` (键盘左上角的那个键)
当前 getConnection 是否会在多线程环境下执行??
- getConnection 会在 doGet / doPost 中调用~~
- 多个请求触发的多个 doXXX 方法,是否是多线程环境调用呢?
是的!!!
public class DBUtil {
private static final String URL = "jdbc:mysql://127.0.0.1:3306/java102?characterEncoding=utf8&useSSL=false";
private static final String USERNAME = "root";
private static final String PASSWORD = "11111";
private volatile static DataSource dataSource = null; // volatile
private static DataSource getDataSource() {
// 多线程安全
if (dataSource == null) {
synchronized (DnsUrl.class) {
if (dataSource == null) { // 懒汉模式
dataSource = new MysqlDataSource();
((MysqlDataSource) dataSource).setURL(URL);
((MysqlDataSource) dataSource).setUser(USERNAME);
((MysqlDataSource) dataSource).setPassword(PASSWORD);
}
}
}
return dataSource;
}
// 代码和数据库服务器建立连接 import java.sql.Connection;
public static Connection getConnection() throws SQLException {
return getDataSource().getConnection();
}
// 释放资源
public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet) {
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
private void save(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 e) {
e.printStackTrace();
} finally {
DBUtil.close(connection, statement, null);
}
}
private List<Message> load() {
// 从数据库中获取所有数据
List<Message> messages = new ArrayList<>();
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
try {
connection = DBUtil.getConnection();
String sql = "select * from message";
statement = connection.prepareStatement(sql);
resultSet = statement.executeQuery();
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;
}
启动,访问:127.0.0.1:8080/message_wall/messageWall.html
MySQL:
mysql> select * from message;
+--------------+-----------+------------+
| from | to | message |
+--------------+-----------+------------+
| 海棉宝宝 | 章鱼哥 | 早上好~ |
+--------------+-----------+------------+
开发一个表白墙 (一个简单网站) 基本步骤:
约定前后端交互的接口~ (请求是啥,响应是啥)
开发服务器代码
开发客户端代码
大部分的这种 “网站” 类的程序,都是类似的情况~~
MVC:
View —— Controller —— Model