一、准备工作
1). 创建 maven 项目
2). 引入依赖 servlet,jackson,mysql
<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>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
</dependencies>
3). 创建必要的目录
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>
4). 编写代码
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("hello");
}
}
5). 6). 打包部署 (直接基于 smart tomcat)
Content Path: /blog_system
7). 在浏览器中验证
启动服务器,访问:127.0.0.1:8080/blog_system/hello
浏览器页面:hello
二、拷贝前端代码
当前 V 已经实现的差不多~~ 就可以把前端页面给引入到项目中,直接拷贝到 webapp 目录下,一定要注意,目录不要放错了!
这些文件都可以理解成静态资源
后续打包部署的时候,这些静态资源也会被一并打包部署,后续也就可以在浏览器中访问到了~
C 和 M,先来实现 Model 层,实现数据库相关的代码
三、编写数据库的操作代码
1、创建数据库 / 表结构 => (数据库设计)
设计数据库,需要根据当前的需求来进行设计
博客页面:
- 博客列表页:显示博客的列表
- 博客详情页:点击博客列表页,上面列出的博客条目,跳转到的页面,显示博客的完整内容
- 登录页
- 博客编辑页:基于 editor.md 搞了一个 markdown 编辑器~~ 基于这个页面来发布博客了
存储博客,当点击发布的时候,博客被发布到服务器上,就要被存起来
获取博客,在博客列表页和博客详情页,能够拿到博客的内容,还能够进行登录校验~~
设计表,就需要抓住需求中的实体 (关键性的名词)
- 博客表,用来存储所有的博客数据
- 用户表,用户登录,就需要用到这个表
在 main 目录下,创建 db.sql
-- 编写建库建表的 sql
-- 创建数据库
create database if not exists java_blog;
use java_blog;
-- 创建博客表
drop table if exists blog;
create table blog (
blogId int primary key auto_increment,
title varchar(1024),
content mediumtext, -- 正文用更长的类型
userId int, -- 文章作者的 id
postTime datetime -- 发布时间
);
-- 创建用户表
drop table if exists user;
create table user (
userId int primary key auto_increment,
username varchar(128) unique, -- 基于用户名登录,不能重复
password varchar(128)
);
insert into user values(null, 'zhangsan', '123');
insert into user values(null, 'lisi', '123');
2、封装数据库
2.1、数据库连接 DBUtil
创建 DBUtil 类,放入 model 包下,封装数据库连接的操作
package model;
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/java_blog?characterEncoding=utf8&userSL=false";
private static final String USERNAME = "root";
private static final String PASSWORD = "11111";
private static volatile 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() 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();
}
}
}
}
2.2、实体类 Blog User
创建实体类,使用实体类来表示数据库中的一条记录
此处主要是创建了 Blog 类 和一个 User 类~~
package model;
import java.sql.Timestamp;
// 每个 model.Blog 对象, 对应 blog 表里的一条记录
public class Blog {
private int blogId;
private String title;
private String content;
private int userId;
private Timestamp postTime;
// 生成它们的 get set 方法...
}
package model;
// 每个 model.User 对象, 期望能够表示 user 表中的一条记录
public class User {
private int userId;
private String username;
private String password;
// 生成它们的 get set 方法...
}
2.3、封装针对数据的增删改查
把提供了增删改查这样的类,称为 DAO
BlogDao
package model;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
// 这个类用于去封装博客表的基本操作
public class BlogDao {
// 1. 往博客表里, 插入一个博客.
public void insert(Blog blog) {
// JDBC 基本代码
Connection connection = null;
PreparedStatement statement = null;
try {
// 1) 和数据库建立连接.
connection = DBUtil.getConnection();
// 2) 构造 SQL 语句
String sql = "insert into blog values(null, ?, ?, ?, now())";
statement = connection.prepareStatement(sql);
statement.setString(1, blog.getTitle());
statement.setString(2, blog.getContent());
statement.setInt(3, blog.getUserId());
// 3) 执行 SQL
statement.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 4) 关闭连接, 释放资源
DBUtil.close(connection, statement ,null);
}
}
// 2. 能够获取到博客表中的所有博客的信息 (用于在博客列表页, 此处每篇博客不一定会获取到完整的正文)
public List<Blog> selectAll() {
List<Blog> blogs = new ArrayList<>();
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
try {
connection = DBUtil.getConnection();
String sql = "select * from blog";
statement = connection.prepareStatement(sql);
resultSet = statement.executeQuery();
while (resultSet.next()) { // 遍历 获取每条博客 放入 blogs
Blog blog = new Blog();
blog.setBlogId(resultSet.getInt("blogId"));
blog.setTitle(resultSet.getString("title"));
// 博客列表页的摘要 太长了 要截取
// 这个数字具体写多少, 都可以灵活应对!
String content = resultSet.getString("content");
if (content.length() > 50) {
content = content.substring(0, 50) + "...";
}
blog.setContent(content);
blog.setUserId(resultSet.getInt("userId"));
blog.setPostTime(resultSet.getTimestamp("postTime"));
blogs.add(blog);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBUtil.close(connection, statement, resultSet);
}
return blogs;
}
// 3. 能够根据博客 id 获取到指定的博客内容 (用于在博客详情页)
public Blog selectOne(int blogId) {
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
try {
connection = DBUtil.getConnection();
String sql = "select * from blog where blogId = ?";
statement = connection.prepareStatement(sql);
statement.setInt(1, blogId);
resultSet = statement.executeQuery();
// 此处我们是使用 主键 来作为查询条件的. 查询结果, 要么是 1 , 要么是 0.
if (resultSet.next()) {
Blog blog = new Blog();
blog.setBlogId(resultSet.getInt("blogId"));
blog.setTitle(resultSet.getString("title"));
blog.setContent(resultSet.getString("content"));
blog.setUserId(resultSet.getInt("userId"));
blog.setPostTime(resultSet.getTimestamp("postTime"));
return blog;
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBUtil.close(connection, statement, resultSet);
}
return null; // 没有找到此条博客
}
// 4. 从博客表中, 根据博客 id 删除博客.
public void delete(int blogId) {
Connection connection = null;
PreparedStatement statement = null;
try {
connection = DBUtil.getConnection();
String sql = "delete from blog where blogId = ?";
statement = connection.prepareStatement(sql);
statement.setInt(1, blogId);
statement.executeUpdate();
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
DBUtil.close(connection, statement, null);
}
}
// 注意, 上述操作是 增删查, 没有改~~
}
UserDao
package model;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
// 提供了针对 用户表 的基本操作
public class UserDao {
// 需要实现的操作
// 针对这个类来说, 就简化的写就行了. 像注册/销号这样的功能就不考虑
// 主要实现:
// 1. 根据用户名来查找用户信息.
// 会在登录逻辑中使用~~
public User selectByName(String username) {
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
try {
connection = DBUtil.getConnection();
String sql = "select * from user where username = ?";
statement = connection.prepareStatement(sql);
statement.setString(1, username);
resultSet = statement.executeQuery();
// 此处 username 使用 unique 约束, 要么能查到一个, 要么一个都查不到.
if (resultSet.next()) {
User user = new User();
user.setUserId(resultSet.getInt("userId"));
user.setUsername(resultSet.getString("username"));
user.setPassword(resultSet.getString("password"));
return user;
}
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
DBUtil.close(connection, statement, resultSet);
}
return null;
}
// 2. 根据用户 id 来找用户信息.
// 博客详情页, 就可以根据用户 id 来查询作者的名字, 把作者名字显示出来.
public User selectById(int userId) {
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
try {
connection = DBUtil.getConnection();
String sql = "select * from user where userId = ?";
statement = connection.prepareStatement(sql);
statement.setInt(1, userId);
resultSet = statement.executeQuery();
// 此处 username 使用 unique 约束, 要么能查到一个, 要么一个都查不到.
if (resultSet.next()) {
User user = new User();
user.setUserId(resultSet.getInt("userId"));
user.setUsername(resultSet.getString("username"));
user.setPassword(resultSet.getString("password"));
return user;
}
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
DBUtil.close(connection, statement, resultSet);
}
return null;
}
}
Model 搞定了,接下里写 Controller,实现服务器后续代码
四、博客列表页
1、约定交互接口
针对这里的这四个页面
分别来 “约定前后端交互接口”,“编写服务器代码”,“编写客户端代码”
1.先从博客列表页开始
这个页面需要能够展示出数据库中的博客的列表~~
请求:
GET /blog
响应:
这里的 content 与其说是 “正文” 不如说是正文的摘要~~
博客列表页中,主要是显示都有哪些博客~~
因此,就需要在这个地方把正文进行截取 (如果正文太长,就只截取前面的一小部分)
[
{
blogld: 1,
title: '这是第一篇博客',content: '这是博客正文',userld: 1,
postTime: '2022-05-21 20:00:00'),
},
{
blogld: 2,
title: '这是第二篇博客',
content: '这是博客正文',userld: 1,
postTime: '2022-05-21 20:00:00'
},
]
2、服务器代码 BlogServlet
创建 BlogServlet 类,放入 controller 包中
package controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import model.Blog;
import model.BlogDao;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
// 通过这个类, 来处理 /blog 路径对应的请求
@WebServlet("/blog")
public class BlogServlet extends HttpServlet {
private ObjectMapper objectMapper = new ObjectMapper();
// 这个方法用来获取到数据库中的博客列表.
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
BlogDao blogDao = new BlogDao();
List<Blog> blogs = blogDao.selectAll();
// 把 blogs 对象转成 JSON 格式.
String respJson = objectMapper.writeValueAsString(blogs);
// 构造响应的时候 这里的这两行代码 顺序不能颠倒.
// 如果先 write 了 body, 再设置 ContentType,设置的 ContentType就会不生效!!
// 包括说 即使不是写 body,构造 sendRedirect 这种, 其实也是类似的情况~~
resp.setContentType("application/json; charset=utf8");
resp.getWriter().write(respJson);
}
}
——验证 修改 postTime
启动服务器,Postman 构造 Get 请求:127.0.0.1:8080/blog_system/blog
显示 body 为:[]
-- 给博客表中插入点数据, 方便测试.
insert into blog values(null, '这是第一篇博客', '从今天开始, 我要认真学 Java', 1, now());
insert into blog values(null, '这是第二篇博客', '从昨天开始, 我要认真学 Java', 1, now());
insert into blog values(null, '这是第三篇博客', '从前天开始, 我要认真学 Java', 1, now());
insert into blog values(null, '这是第一篇博客', '从今天开始, 我要认真学 C++', 2, now());
此处得到的响应,和预期的效果,有一点点差别,时间格式~~
预期得到的是一个格式化时间,实际上得到了一个毫秒时间戳,此处就需要把时间戳转成格式化时间
(可以在前端来做,也可以在后端来做)
修改方法:在 Blog 类中,修改 getPostTime
// 改为格式化时间
// 把这里的 getter 方法给改了, 不是返回一个时间戳对象, 而是返回一个 String (格式化好的时间)
public String getPostTime() {
// 使用 SimpleDateFormat 来完成时间戳到格式化日期时间的转换.
// 这个转换过程, 需要在构造方法中指定要转换的格式, 然后调用 format 来进行转换
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyy-mm-dd hh:mm:ss");
return simpleDateFormat.format(postTime);
}
重新启动,构造,此时的结果:"postTime": "2022-07-26 05:07:16"
3、客户端代码 blog_list.html
在页面加载的时候,让页面通过
ajax
访问服务器,获取到数据库中的博客数据,并且填到页面中!
在 blog_list.html 最后加入 ajax:
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script>
// 在页面加载的时候, 通过 ajax 给服务器发送数据, 获取到博客列表信息, 并且显示在界面上.
function getBlogList() {
$.ajax({
type: 'get',
url: 'blog',
success: function(body) {
// 获取到的 body 就是一个 js 对象数组, 每个元素就是一个 js 对象, 根据这个对象构造 div
// 1. 先把 .right 里原有的内容给清空
let rightDiv = document.querySelector('.right');
rightDiv.innerHTML = '';
// 2. 遍历 bo dy, 构造出一个个的 blogDiv
for (let blog of body) {
let blogDiv = document.createElement('div'); // 创建标签
blogDiv.className = 'blog'; // class 名
// -- 构造标题
let titleDiv = document.createElement('div');
titleDiv.className = 'title';
titleDiv.innerHTML = blog.title;
blogDiv.appendChild(titleDiv); // 挂入 dom 树
// -- 构造发布时间
let dateDiv = document.createElement('div');
dateDiv.className = 'date';
dateDiv.innerHTML = blog.postTime;
blogDiv.appendChild(dateDiv);
// -- 构造摘要
let descDiv = document.createElement('div');
descDiv.className = 'desc';
descDiv.innerHTML = blog.content;
blogDiv.appendChild(descDiv);
// -- 构造 查看全文
let a = document.createElement('a');
a.innerHTML = '查看全文 >>';
// 此处希望点击之后能够跳转到 博客详情页 !!
// 这个跳转过程需要告知服务器要访问的是哪个博客的详情页. 根据博客 id
a.href = 'blog_detail.html?blogId=' + blog.blogId;
blogDiv.appendChild(a);
// 把 blogDiv 挂到 dom 树上!
rightDiv.appendChild(blogDiv);
}
},
error: function() {
alert("获取博客列表页失败!");
}
});
}
// 调用
getBlogList();
</script>
——验证 修改博客列表顺序、加载页面
启动服务器,访问:127.0.0.1:8080/blog_system/blog_list.html
带到了博客列表页,貌似没有问题,但是还有两个小问题:
问题一: 当前拿到的博客列表顺序,是不太科学的~~
添加一条博客信息:
insert into blog values(null, '这是第二篇博客', '从昨天开始, 我要认真学 C++', 2, now());
观察发现,加入的博客在最下方,不利于用户访问
需要保证咱们最新的博客得在最上面才行!!
如果在 sql 中没有指定 order by
,此时查询到的结果顺序,是不确定的!! 因此,在 sql 中不加 order by 之前,是不应该依赖查询结果的顺序的!!!
可能你在当前数据库这里发现查询结果顺序都是一定的,随着你插入删除更多的数据
甚至说你更新了数据库版本,此时的查询结果都可能存在发生变化的风险~~
修改代码: BlogDao 类,修改 selectAll 方法中的查询 sql
String sql = "select * from blog order by postTime desc"; // 降序 最新数据放上面
问题二: 刷新页面的时候,内容一哆嗦~~
打断点观察:旧的内容 (测试时写死的数据) => 新的内容 (从数据库里查的数据)
是通过网络来交互的,花个几十 ms 都很正常,人眼是能捕捉到这个变化的~~
直接把旧的测试数据给删了就行了~~
<!-- 右侧内容详情 -->
<div class="right">
<!-- .blog 对应一个博客 -->
<!-- <div class="blog">
博客标题
<div class="title">
我的第一篇博客
</div>
博客发布时间
<div class="date">
2022-05-05 15:00:00
</div>
博客摘要
<div class="desc">
从今天起,我要认真写博客,Lorem ipsum dolor sit amet consectetur adipisicing elit. Impedit fugit libero deleniti a distinctio exercitationem mollitia adipisci repudiandae aliquid reiciendis, quae consequatur laboriosam illum et dicta iure, error eligendi iusto?
</div>
">>" 转义
<a href="#">查看全文 >> </a>
</div> -->
</div>