【项目—前后端交互 案例】表白墙
- 代码示例: 服务器版表白墙
- 1. 准备工作
- 2. 约定前后端交互接口
- 3. 实现服务器端代码
- 创建 Message 类
- 创建 MessageServlet 类
- 4. 调整前端页面代码
- 5. 数据存入文件.
- 6. 数据存入数据库
- 1) 在 pom.xml 中引入 mysql 的依赖
- 2) 创建数据库, 创建 messages 表
- 3) 创建 DBUtil 类
- 4) 修改 load 和 save 方法, 改成操作数据库
- 代码总结
- 1. pom.xml
- 2. web.xml
- 3. 表白墙.html
- 4. DBUtil
- 5. Message
- 6. MessageServlet
先学习servlet
代码示例: 服务器版表白墙
结合上述 API, 我们可以把之前实现的表白墙程序修改成服务器版本. 这样即使页面关闭, 表白墙的内容也不会丢失.
1. 准备工作
- 创建 maven 项目.
- 创建必要的目录 webapp, WEB-INF, 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>
- 调整 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>表白墙服务器版</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!-- 加入 servlet 依赖 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<!-- servlet 版本和 tomcat 版本有对应关系,切记 -->
<version>3.1.0</version>
<!-- 这个意思是我们只在开发阶段需要这个依赖,部署到 tomcat 上时就不需要了 -->
<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.3</version>
</dependency>
</dependencies>
<packaging>war</packaging>
<build>
<finalName>MessageWall</finalName>
</build>
</project>
- 把之前实现的表白墙前端页面拷贝到 webapp 目录中.
2. 约定前后端交互接口
所谓 “前后端交互接口” 是进行 Web 开发中的关键环节.
具体来说, 就是允许页面给服务器发送哪些 HTTP 请求, 并且每种请求预期获取什么样的 HTTP 响应
-
获取全部留言
我们期望浏览器给服务器发送一个 GET /message 这样的请求, 就能返回当前一共有哪些留言记录. 结果以 json 的格式返回过来. -
发表新留言
请求: body 也为 JSON 格式.
我们期望浏览器给服务器发送一个 POST /message 这样的请求, 就能把当前的留言提交给服务
器
3. 实现服务器端代码
创建 Message 类
public class Message {
public String from;
public String to;
public String message;
}
创建 MessageServlet 类
@WebServlet("/message")
public class MessageServlet extends HttpServlet {
// 用于保存所有的留言
private List<Message> messages = new ArrayList<Message>();
// 用于转换 JSON 字符串
private ObjectMapper objectMapper = new ObjectMapper();
// 获取所有留言
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
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);
messages.add(message);
resp.getWriter().write("{ \"ok\": 1 }");
}
}
- ObjectMapper 的 readValue 方法也能直接从一个 InputStream 对象读取数据.
- ObjectMapper 的 writeValueAsString 方法也能把一个对象数组直接转成 JSON 格式的字符串
4. 调整前端页面代码
修改 “表白墙.html”
- 拷贝之前封装好的 ajax 函数
// 把之前封装的 ajax 函数拷贝过来
function ajax(args) {
// ...... 代码内容参考 HTTP 协议章节
}
- 新加 load 函数, 用于在页面加载的时候获取数据
// 从服务器加载数据, 显示在界面上
function load() {
ajax({
url: 'message',
method: 'GET',
callback: function (data, status) {
// 先把字符串格式的 body 转成 对象数组
let messages = JSON.parse(data);
// 把每个消息都构造一个 HTML 标签
for (let message of messages) {
var row = document.createElement('div');
row.className = 'row';
row.innerHTML = message.from + '对' + message.to + '说: ' +
message.message;
// 3. 把构造好的元素添加进去
var container = document.querySelector('.container');
container.appendChild(row);
}
}
});
}
// 调动 load 执行数据加载
load();
- 修改原来的点击事件回调函数. 在点击按钮的时候同时给服务器发送消息.
// 给点击按钮注册点击事件
var submit = document.querySelector('.submit');
submit.onclick = function () {
// ...... 前面的代码略, 参考 JavaScript(WebAPI) 章节.
// 给服务器发送消息
ajax({
method: "POST",
url: "message",
contentType: "application/json; charset=utf-8",
body: JSON.stringify({ from: from, to: to, message: message }),
callback: function (data, status) {
if (status == 200) {
console.log("提交消息成功!");
} else {
console.log("提交消息失败! " + status);
}
}
});
}
此时在浏览器通过 URL http://127.0.0.1:8080/MessageServlet/表白墙.html 访问服务器, 即可看到
此时我们每次提交的数据都会发送给服务器. 每次打开页面的时候页面都会从服务器加载数据. 因此及时关闭页面, 数据也不会丢失.
但是数据此时是存储在服务器的内存中 ( private List messages = new ArrayList(); ), 一旦服务器重启, 数据仍然会丢失
5. 数据存入文件.
针对上面的问题, 如果把数据保存在文件中, 那么重启服务器也不会丢失数据了…
修改 MessageServlet 代码.
- 删掉之前的 messages 成员.
- 创建新的成员 String filePath, 表示要存储的文件的路径.
- 新增 load 方法, 用来从文件中读取内容. (会在页面加载的时候调用 load)
- 新增 save 方法, 用来往文件中写入内容. (会在提交留言的时候调用 save)
- 文件格式按照 行文本 的方式存储. 每个记录占用一行, 每个记录的字段之间(from, to, message) 使用 \t 分隔.
文件格式形如:
@WebServlet("/message")
public class MessageServlet extends HttpServlet {
// 用于保存所有的留言
// private List<Message> messages = new ArrayList<Message>();
// 用于转换 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 }");
}
}
此时即使重启服务器, 留言数据也不会丢失了.
6. 数据存入数据库
使用文件的方式存储留言固然可行, 但是并不优雅.
我们还可以借助数据库完成存储工作.
1) 在 pom.xml 中引入 mysql 的依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.45</version>
</dependency>
2) 创建数据库, 创建 messages 表
set character_set_database=utf8;
set character_set_server=utf8;
create database if not exists MessageWall;
use MessageWall;
drop table if exists messages;
create table messages (`from` varchar(255), `to` varchar(255), `message`
varchar(2048));
3) 创建 DBUtil 类
DBUtil 类主要实现以下功能:
- 创建 MysqlDataSource 实例, 设置 URL, username, password 等属性.
- 提供 getConnection 方法, 和 MySQL 服务器建立连接.
- 提供 close 方法, 用来释放必要的资源.
// 负责和数据库建立连接
public class DBUtil {
private static final String URL = "jdbc:mysql://127.0.0.1:3306/MessageWall?
characterEncoding=utf8&useSSL=false";
private static final String USERNAME = "root";
private static final String PASSWORD = "";
private static DataSource dataSource = null;
private static DataSource getDataSource() {
if (dataSource == null) {
synchronized (DBUtil.class) {
if (dataSource == null) {
dataSource = new MysqlDataSource();
((MysqlDataSource)dataSource).setUrl(URL);
((MysqlDataSource)dataSource).setUser(USERNAME);
((MysqlDataSource)dataSource).setPassword(PASSWORD);
}
}
}
return dataSource;
}
public static Connection getConnection() {
try {
return getDataSource().getConnection();
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public static void close(Connection connection,
PreparedStatement statement,
ResultSet resultSet) {
try {
if (resultSet != null) {
resultSet.close();
}
if (statement != null) {
statement.close();
}
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
4) 修改 load 和 save 方法, 改成操作数据库
private List<Message> load() {
List<Message> messages = new ArrayList<>();
// 1. 和数据库建立连接
Connection connection = DBUtil.getConnection();
PreparedStatement statement = null;
ResultSet resultSet = null;
try {
// 2. 拼装 SQL
String sql = "select * from messages";
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 e) {
e.printStackTrace();
} finally {
// 5. 释放必要的资源
DBUtil.close(connection, statement, resultSet);
}
return messages;
}
private void save(Message message) {
// 1. 和数据库建立连接
Connection connection = DBUtil.getConnection();
PreparedStatement statement = null;
try {
// 2. 拼装 SQL
String sql = "insert into messages 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 {
// 4. 释放必要的资源
DBUtil.close(connection, statement, null);
}
}
重新部署程序, 此时使用数据库之后也可以保证即使服务器重启, 数据也不丢失.
代码总结
1. pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>MessageWall</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<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.13.4.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
</dependencies>
</project>
2. 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>
3. 表白墙.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>表白墙</title>
</head>
<body>
<style>
.container {
width: 400px;
/* margin 外边距. 第一个数字上下外边距, 第二个数字表示水平外边距. 如果水平设置成 auto 表示元素就水平居中~~ */
margin: 0 auto;
}
h1 {
text-align: center;
}
p {
text-align: center;
color: #666;
}
.row {
height: 40px;
display: flex;
/* 水平居中 */
justify-content: center;
/* 垂直居中 */
align-items: center;
}
.row span {
width: 100px;
}
.row input {
width: 200px;
height: 30px;
}
.row button {
width: 306px;
height: 40px;
color: white;
background: orange;
border: none;
}
.row button:active {
background-color: #666;
}
</style>
<div class="container">
<h1>表白墙</h1>
<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>提交</button>
</div>
<!-- 需要构造出一个形如 -->
<!-- <div class="row">
黑猫 对 白猫 说: 喵
</div> -->
</div>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script>
let container = document.querySelector('.container');
let button = document.querySelector('button');
button.onclick = function() {
// 1. 获取到输入框的内容
let inputs = document.querySelectorAll('input');
let from = inputs[0].value;
let to = inputs[1].value;
let message = inputs[2].value;
if (from == '' || to == '' || message == '') {
alert('当前输入框内容为空!');
return;
}
console.log(from + ", " + to + ", " + message);
// 2. 能够构造出新的 div, 用来保存用户提交的内容
let rowDiv = document.createElement('div');
rowDiv.className = 'row';
rowDiv.innerHTML = from + " 对 " + to + " 说: " + message;
container.appendChild(rowDiv);
// 3. 提交完之后, 清空输入框的内容
for (let i = 0; i < inputs.length; i++) {
inputs[i].value = '';
}
let data = {
from: from,
to: to,
message: message
};
// 4. 点击发送按钮, 给服务器发个 post 请求.
$.ajax({
type: 'post',
url: 'message',
// url: '/java105/message',
// 这里放的是 body 的内容
data: JSON.stringify(data),
contentType: "application/json; charset=utf8",
success: function(body) {
console.log("提交数据成功");
}
});
}
// 直接写在 script 标签里的代码, 都是在页面加载的时候执行的.
// 来获取服务器的数据
function getMessages() {
$.ajax({
type: 'get' ,
url: 'message',
success: function(body) {
// body 就是响应的 body 内容, json 数组.
// jquery 非常智能的帮我们把 json 数组给解析成了 js 对象数组
// 但是有个前提条件, 就是响应的 Content-Type 得是 application/json
let container = document.querySelector('.container');
for (let i = 0; i < body.length; i++) {
let message = body[i];
// 根据这个元素构造一个 div.row, 来容纳数据
// <div class="row">
// 黑猫 对 白猫 说: 喵
// </div>
let row = document.createElement('div');
row.className = 'row';
row.innerHTML = message.from + " 对 " + message.to + " 说: " + message.message;
container.appendChild(row);
}
}
});
}
getMessages();
</script>
</body>
</html>
4. DBUtil
import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;
import javax.sql.DataSource;
// 使用这个类来封装 DataSource 的单例
public class DBUtil {
private static volatile DataSource dataSource = null;
public static DataSource getDataSource() {
if (dataSource == null) {
synchronized (DBUtil.class) {
if (dataSource == null) {
dataSource = new MysqlDataSource();
((MysqlDataSource)dataSource).setUrl("jdbc:mysql://127.0.0.1:3306/java105?characterEncoding=utf8&useSSL=false");
((MysqlDataSource)dataSource).setUser("root");
// 我这里的数据库是没密码的.
((MysqlDataSource)dataSource).setPassword("");
}
}
}
return dataSource;
}
private DBUtil() {}
}
5. Message
public class Message {
private String from;
private String to;
private String message;
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
6. MessageServlet
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;
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.sql.DataSource;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
@WebServlet("/message")
public class MessageServlet extends HttpServlet {
// 这个对象在多个方法中都需要使用
private ObjectMapper objectMapper = new ObjectMapper();
// 负责让页面获取到数据
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 显式声明当前的响应数据格式 不要让客户端去猜!!!
resp.setContentType("application/json; charset=utf8");
// 把 messageList 转成 json 字符串, 并且返回给页面就行了.
List<Message> messageList = null;
try {
messageList = load();
} catch (SQLException e) {
e.printStackTrace();
}
resp.getWriter().write(objectMapper.writeValueAsString(messageList));
}
// 提交数据
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取到 body 中的数据并解析
Message message = objectMapper.readValue(req.getInputStream(), Message.class);
// 把这个 message 保存一下. 简单的办法就是保存在内存中.
// messageList.add(message);
try {
save(message);
} catch (SQLException e) {
e.printStackTrace();
}
resp.setStatus(200);
System.out.println("提交数据成功: from=" + message.getFrom()
+ ", to=" + message.getTo() + ", message=" + message.getMessage());
}
private List<Message> load() throws SQLException {
// 从数据库查询数据
// 1. 先有一个数据源
DataSource dataSource = DBUtil.getDataSource();
// 2. 建立连接
Connection connection = dataSource.getConnection();
// 3. 构造 SQL
String sql = "select * from message";
PreparedStatement statement = connection.prepareStatement(sql);
// 4. 执行 SQL
ResultSet resultSet = statement.executeQuery();
// 5. 遍历结果集合
List<Message> messageList = new ArrayList<>();
while (resultSet.next()) {
Message message = new Message();
message.setFrom(resultSet.getString("from"));
message.setTo(resultSet.getString("to"));
message.setMessage(resultSet.getString("message"));
messageList.add(message);
}
// 6. 关闭连接
statement.close();
connection.close();
return messageList;
}
private void save(Message message) throws SQLException {
// 把数据保存到数据库中
// 1. 先有一个数据源
DataSource dataSource = DBUtil.getDataSource();
// 2. 建立连接
Connection connection = dataSource.getConnection();
// 3. 构造 SQL
String sql = "insert into message values(?, ?, ?)";
PreparedStatement statement = connection.prepareStatement(sql);
statement.setString(1, message.getFrom());
statement.setString(2, message.getTo());
statement.setString(3, message.getMessage());
// 4. 执行 SQL
int ret = statement.executeUpdate();
System.out.println("ret = " + ret);
// 5. 关闭连接
statement.close();
connection.close();
}
}