这里的博客系统主要是四个界面
这里主要来写后端的代码,前端代码已经准备就绪,直接导入即可
任务:
基于上述的页面,编写服务器/前后端交互代码
通过这些代码,完成博客系统,完整的功能
在这些功能搞定之后,一个功能相对完整的博客网站,就初具规模了
设计好对应的表结构,并且把数据库相关代码,也进行封装
blog (blogId, title, content, postTime, userId)
user (userId, username, password)
在进行网站开发的工程中,一种常见的代码组织结构,MVC
M model:操作数据的代码
V view:操作/构造界面的代码
C controler:业务逻辑,处理前端请求
由于这套组织结构比较古老,在现在写的过程中,也不会完全遵守
当前这个懒汉模式是不安全的
当前 servlet 本身就是在多线程环境下执行的
tomcat 收到多个请求的时候,就会使用 多线程 的方式,执行不同的 servlet 代码
这里就可能有现成不安全的问题
package model;
import com.mysql.jdbc.Connection;
import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;
import javax.sql.DataSource;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
// 通过这个类, 封装数据库建立连接的操作.
// 由于接下来代码中, 有多个 Servlet 都需要使用数据库. 就需要有一个单独的地方来把 DataSource 这里的操作进行封装.
// 而不能只是放到某个 Servlet 的 init 中了.
// 此处可以使用 单例模式 来表示 dataSource
public class DBUtil {
private volatile static DataSource dataSource = null;
private 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/servlet_blog_system?characterEncoding=utf8&useSSL=false");
((MysqlDataSource) dataSource).setUser("root");
((MysqlDataSource) dataSource).setPassword("123456");
}
}
}
return dataSource;
}
public static Connection getConnection() throws SQLException {
return (Connection) getDataSource().getConnection();
}
public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet) {
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
}
DBUtil 完成对于数据库建立连接和关闭连接的实现
大部分表,都需要搞一个专门的类来表示
表里的一条数据,就会对应到这个类的一个对象
这样就可以把数据库中的数据和代码联系起来了
这里再创建两个类,来完成准对博客表和用户表的增删改查操作
这两个类,叫做BlogDao 和 UserDao
DAO(Data Access Object):数据访问对象
通过这两个类的对象,来完成针对数据库表的操作
写一个复杂一些的代码,往往需要先理清楚思路
相比于细节来说,理清思路是更复杂的
(为了实现这个代码,要写哪些类,有哪些方法)
package model;
import com.mysql.jdbc.Connection;
import com.mysql.jdbc.JDBC4PreparedStatement;
import javax.xml.stream.events.DTD;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
// 通过 BlogDao 来完成针对 blog 表的操作
public class BlogDao {
// 1. 新增操作 (提交博客就会用到)
public void insert(Blog blog) {
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 {
DBUtil.close(connection, statement, null);
}
}
// 2. 查询博客列表 (博客列表页)
// 把数据库里所有的博客都拿到.
public List<Blog> getBlogs() {
List<Blog> blogList = new ArrayList<>();
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
try {
connection = DBUtil.getConnection();
String sql = "select * from blog order by postTime desc";
statement = connection.prepareStatement(sql);
resultSet = statement.executeQuery();
while (resultSet.next()) {
Blog blog = new Blog();
blog.setBlogId(resultSet.getInt("blogId"));
blog.setTitle(resultSet.getString("title"));
// 此处读到的正文是整个文章内容. 太多了. 博客列表页, 只希望显示一小部分. (摘要)
// 此处需要对 content 做一个简单截断. 这个截断长度 100 这是拍脑门出来的. 具体截取多少个字好看, 大家都可以灵活调整.
String content = resultSet.getString("content");
if (content.length() > 100) {
content = content.substring(0, 100) + "...";
}
blog.setContent(content);
blog.setPostTime(resultSet.getTimestamp("postTime"));
blog.setUserId(resultSet.getInt("userId"));
blogList.add(blog);
}
} catch (SQLException e) {
e.printStackTrace();
}finally {
DBUtil.close(connection, statement, resultSet);
}
return blogList;
}
// 3. 根据博客 id 查询指定的博客
public Blog getBlog(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();
// 由于此处是拿着 blogId 进行查询. blogId 作为主键, 是唯一的.
// 查询结果非 0 即 1 , 不需要使用 while 来进行遍历
if (resultSet.next()) {
Blog blog = new Blog();
blog.setBlogId(resultSet.getInt("bligId"));
blog.setTitle(resultSet.getString("title"));
// 这个方法是期望在获取博客详情页的时候, 调用. 不需要进行截断, 应该要展示完整的数据内容
blog.setContent(resultSet.getString("content"));
blog.setPostTime(resultSet.getTimestamp("postTime"));
blog.setUserId(resultSet.getInt("userId"));
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 e) {
e.printStackTrace();
}finally {
DBUtil.close(connection, statement, null);
}
}
}
这里的 JDBC 代码,大同小异
因此,后面就会有一些数据库的框架
(MyBatis,MyBatisPlus,JPA…)
这里封装了 JDBC 的代码
这些框架 本质上 就是帮我们自动生成 JDBC 的代码
在博客列表页加载的时候,通过 ajax 给服务器发起请求
从服务器(数据库)拿到博客列表数据,并求显示到页面上
这里构造页面的过程,还是之前的 api
(1)querySelector:获取页面已有的元素
(2)createElement:创建新的元素
(3).innerHtml:设置元素里的内容
(4).className:设置元素的 class 属性
(5)appendChild:把这个元素添加到另一个元素的末尾
html 中
显示 >:
需要使用转义字符 >;
显示 <:
也需要使用转义字符 <;
a 标签在 html 中称为“超链接”
点击之后能够跳转到新的页面
对于前端页面来说
生成页面的方式其实有很多种
此处使用的比较朴素的方式(基于 dom api 的方式)
dom api 就是属于是浏览器提供的标准的 api(不属于任何的第三方框架和库)
定位就类似于 jdbc api
前端也有一些框架和库,是把 dom api 又进行了封装,用起来更简单一些
在这里,Jackson 在进行主要的工作
(1)Jackson 发现 blogs 是一个 list,于是就会循环遍历里面的每个元素
(2)针对每个元素(Blog 对象),通过反射的方式,获取到都是哪些属性,属性的名字,属性的值
在获取属性值的时候,就是通过调取 get 方法
这个时候,我们就要对代码进行改变
修改 getPostTime 方法,让其直接返回一个“格式化时间”
制定了一个格式化字符串
描述了当前时间日期具体的格式
各种语言,表示格式化时间都有这样的字符串
但是不同语言,表示的含义是不同的
此时还有一个问题,就是希望新加入的博客在上面,以前写的博客在下面
这个时候,该如何做呢?
返回 list,把 list 先逆序一下?
此处的结果顺序,是从数据库里查询出来的
一个 sql 如果不加 order by,结果的顺序是不可预期的
此处科学的做法,应该是加上 order by,时间逆序
点击查看全文,就可以跳转到 带有不同 blogId 的 query string
后续在博客详情页中,就可以给服务器发起 ajzx 请求,根据这里的 blogId ,查询数据库中,博客的具体内容再返回
前端还是把得到的数据给构造到页面上
这个请求,是希望在博客详情页的时候,通过 ajax 发给服务器
此处有个问题,发起 ajax 请求的时候要带有 blogId
blogId 当前处于 博客详情页 url 中
这里我们可以通过 location.search 方式拿到 页面 url 中的 query string
这里依然使用 servlet 处理,一个路径对应到一个 servle
当前是使用一个 servlet 处理两种请求
博客列表页,不带 query string
博客详情页,带有 query string
就可以根据 query string 是否存在的情况,来区分是哪种请求
分别返回不同的数据即可
使用两个 servlet 处理这里的两个请求,也可以
就约定成不同的路径即可
使用一个 servlet 也可以,这里没有一个明确的标准
写完代码之后,再点击某个博客,就可以看到,有的博客里面的详情页,还是之前的旧的内容
这个问题是浏览器缓存引起的
浏览器加载页面的时候,是通过网络获取的(网络速度比较慢)
浏览器有时候就会把已经加载过的界面,在本地硬盘保存一份,后续再访问同一个界面
就不需要通过网络加载,直接加载本地硬盘的这一份
默认认为 html 出现修改的概率比较低
但是也不是完全不会修改
如何克服缓存的干扰,前端有专业的解决方案
我们在这里可以直接使用 ctrl + F5,强制刷新界面
当前博客详情页,虽然能用出博客的正文,但是显示的是正文的 md 原始数据
作为博客网站,正确的做法应该是显示出 md 渲染后的效果
这个是 editormd 这个库给的一个全局变量
把依赖正确引入了,这个变量就能直接使用
这个方法的效果,就是把 blog.content 这里的 md 的原始数据,渲染成 html,放到 id 为 content 的 div 中
一个 html 标签,可以有很多的属性
class 属性,往往是用来和 css 样式配合的
id 属性,则是一个“身份标识”要求一个界面中,id 必须是唯一的
在登录界面,在输入框中填写 用户名和密码
点击登录,就会给服务器发起 http 请求(这里使用 form)
服务器处理登录请求,读取用户名密码,在数据库查询、匹配
如果正确,就登录成功,创建会话,跳转到博客列表页
由于这里,登录成功,直接进行重定向跳转,就不要浏览器额外写代码处理,直接浏览器自动跳转
form 表单,提交成功,可以直接使用 302 重定义跳转
如果使用 ajax,ajax 处理响应就需要写代码来完成跳转(不是浏览器自动完成了)
username=zhangsan&password-123
这里的 input 标签,name 属性就是这里 body 中的 key
什么时候一个元素要有 id,什么时候没有呢?
看个人的需要,灵活处理
package servlet;
import model.User;
import model.UserDao;
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;
import java.time.temporal.Temporal;
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1. 读取参数中的用户名和密码
req.setCharacterEncoding("utf8");
String username = req.getParameter("username");
String password = req.getParameter("password");
// 验证一下参数, 看下是否合理.
if (username == null || username.length() == 0 || password == null || password.length() == 0) {
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write("您输入的用户名或者密码为空!");
return;
}
// 2. 查询数据库, 看看这里的用户名密码是否正确.
UserDao userDao = new UserDao();
User user = userDao.getUserByName(username);
if (user == null) {
// 用户名不存在!
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write("您输入的用户名或密码不正确!");
return;
}
if (!password.equals(user.getPassword())) {
// 密码不正确!
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write("您输入的用户名或密码不正确!");
return;
}
// 3. 创建会话
HttpSession session = req.getSession(true);
session.setAttribute("user", user);
// 4. 跳转到主页了.
resp.sendRedirect("blog_list.html");
}
}
在博客列表页、详情页、编辑页,判断当前用户是否已经登录
如果未登录,则强制跳转到登录页(要求用户必须登录后才能使用)
在上述的页面中,在页面加载中,给服务器发起 ajax
从服务器获取一下当前的登录状态
GET /login
登录成功:HTTP/1.1 200
登录失败:HTTP/1.1 403
也可以通过其他的方式,比如都是返回 200,但是在 body 中给不同的结果
一个页面,触发的 ajax是可以有多个的
一个页面通常都会触发多个 ajax
这些 ajax 之间是“并发执行”这样的效果
js 中是没有“多线程”这样的机制
而 ajax 是一种特殊的情况,能够起到类似于“多线程”的效果
当页面中发起两个或者多个 ajax 的时候,这些 ajax 请求就相当于并发的发送出去
彼此之间不会相互干预
(不是 串行 执行,不是执行完一个 ajax,得到响应之后,再执行下一个)
同时发出去多个请求,谁的响应先回来了,就先执行谁的回调函数
当前虽然等登录服了,一旦重启服务器,仍然会被判定为未登录状态
登录状态是通过服务器这里的 session 来存储的
session 这是服务器内存中的类似于 hashmap 这样的结构
一旦服务器重启了,hashmap 里面原有的内容就没了
但是这种设定,并不科学,相比支架,我们还有更好的解决方案
这里我们需要让多个界面都有这样的机制,这里就可以把一些公共的代码,单独拿出来
放到某个 .js 文件中
通过 html 中的 script 标签,来引用这样的文件内容
此时,就可以在 html 中调用对应的公共代码了
博客列表页:显示的是当前登录的用户的信息
博客详情页:显示的是当前文章的作者信息
在页面加载的时候,给服务器发起 ajax 请求
在服务器返回对应的用户数据
根据发起请求不同的界面,服务器返回不同的信息即可
博客列表页,获取当前登录的用户信息
请求:
GET /userInfo
响应:
HTTP/1.1 200 OK
application/json
博客详情页,获取当前文章的作者信息
请求:
GET /authorInfo?blogId=1
响应:
HTTP/1.1 200 OK
application/json
博客列表页:
博客详情页:
这里是通过两步 sql 分别查询的
先查 blog 表里面的 blog 对象
再查 user 表
其实也可以一步 sql 搞定
比如:可以使用联合查询,把 blog user 进行笛卡尔积,找出匹配的结果
也可以使用子查询,把两个 sql 合并在一个完成
当然,一步完成是要付出代价的
联合查询来说,笛卡尔积,对于数据库是一个不小的开销
子查询来说,这样的 sql 可读性可能比较差
以下这个请求从服务器拿到了当前用户的信息
进一步的就把用户的名字显示到页面上了
博客列表、博客详情、博客编辑 的导航栏中,都有一个“注销”按钮
注销 这个东西是 a 标签
可以有一个 href 属性
点击就会触发一个 http 请求
并且可能会引起浏览器跳转到另一个页面
让用户点击“注销”的时候,就能够触发一个 HTTP 请求(GET 请求)
服务器收到这个 GET 请求的时候,就会把会话里的 user 这个 Attribute 给删了
由于在判断用户是否是登录状态的逻辑中,需要同时验证,会话存在,且 这里的 user Attribute 也存在
只要破坏一个,就可以是登录状态发生改变了
为什么不直接删除 session 本身?
主要因为,sevlet 没有提供,删除 session 的方法
虽然有间接的方式(session 可以摄者国企时间,设置一个非常短的过期时间),也可以起到删除的效果,但是不太优雅
session 提供了 removeAttribute 这样的方法,可以把 user 这个 Attribute 给删了
当点击提交的时候,就需要构造 http 请求,把此时的页面中的标题和正文都传输到服务器这边
服务器把这个数据存入数据库即可
此处这里的 http 请求,可以使用 ajax,也可以使用 form
(这种填写输入框,提交数据的场景,使用 form 会更方便)
请求:
POST blog
Content-Type: x-www-form-urlencoded
title=这是标题&content=这是正文
(上面的中文 都是要 urlencode,form 表单直接就能完成这个操作)
响应:
HTTP/1.1 302
Location: blog_list.html
标题本身就是自己写的一个 input,给他加上 name 属性,很容易
但是博客正文,是由 editor md 构成的一个编辑器,这里如何添加 name 属性呢?
editor md 的开发者们,也考虑到了这种情况
在官方文档中也有这样的例子
这个 div 就是 editor.md 的编辑器的容器
在这个 div 里,搞一个隐藏的 textarea 标签(多行编辑框,把 name 属性加到 textarea 属性上)
并且在初始化 editormd 对象的时候,加上一个 对应的属性即可
name=“content”:是 form 中键值对的 key
“display: none;” :让这个 textarea 隐藏起来
这个代码是初始化 editormd 的编辑器的代码
这个时候,我们可以使用抓包来实现
抓包的目的,是为了先确定,在点击刷新这个过程中,浏览器和服务器之间有几次 http 交互
每一次交互,请求是什么样的,响应是什么样的
解下来就需要观察,抓包结果中,这几个 http 交互的请求是否都符合预期
先开请求发没有发
如果你的浏览器都没发这个请求,说明前端代码有问题
就需要检查你的前端带啊,ajax 是怎么写的
再看请求中各个部分是否正确,是否符合约定的接口要求
如果请求不符合预期,说明还是前端代码有问题,检查 ajax 代码
如果请求没问题,需要再检查响应数据
如果请求正常,相应数据不符合预期
此时就下检查后端代码,是否你的后端代码
没能正确的完成数据库查询操作等
(尤其要注意服务器的控制套是否出现异常信息)
如果请求和响应都没有问题,说明服务器已经返回正确的数据了,但是页面没有把这些数据正确的显示出来
此时还是要检查前端代码,尤其是检查前端处理响应的这里的逻辑
(尤其要注意,浏览器控制台是否有报错)
当确定范围之后,进一步排查问题,还需要在代码中,加入更多的日志
System.out.println
console.log