目录
一、准备工作
二、数据库的表设计
三、封装JDBC数据库操作
1、创建数据表对应的实体类
2、封装增删改查操作
四、前后端交互逻辑的实现
1、博客列表页
1.1、展示博客列表
1.2、博客详情页
1.3、登录页面
1.4、强制要求用户登录,检查用户的登录状态
1.5、实现显示用户信息的功能
1.5.1、针对列表页进行处理
1.5.2、针对详情页进行处理
1.6、用户退出登录功能
1.7、实现发布博客功能
一、准备工作
1️⃣创建新的maven项目
2️⃣引入依赖
我们需要使用的依赖有servlet、Jackson、MySQL。在中央仓库中搜索Java Servlet API,选择3.1.0版本,将maven中的代码复制到pom.xml中。
搜索点击Jackson Databind,引入Jackson没有特定的版本,随便选一个版本的maven代码复制到pom.xml的<dependencies></dependencies>标签中。
引入数据库,搜索MySQL选择5.1版本的maven中的代码,复制到pom.xml的<dependencies></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>
二、数据库的表设计
我们之前的设计的博客前端页面中有博客详情页,博客编辑页,博客列表页,博客登录页。这些页面中,需要使用数据存储的数据主要有两部分,一个是编写的博客数据,一个是用户数据。这里的建表操作和我们之前的直接在数据库中间表的方式有一些区别。首先我们在blog_system项目的main目录下创建一个.sql文件,用来保存建表的过程。如果我们写的服务器需要部署到不同的机器上,就需要在对应的主机上也将数据库建号。这个时候我们只需要将这里的代码拷贝到数据中就可以了。
-- 一般对于建表的sql都会单独用一个 .sql文件来保存
-- 后续程序可能需要在不同的主机上部署,部署的时候就需要在对应的主机上把数据库也给建好。
-- 把建表sql保存好,方便在不同的机器上进行建库建表。
--表示当前电脑的数据库中不存在blog_sysyem这个库就创建,存在就不创建了。
create database if not exists blog_system;
use blog_system;
--表示如果这个库中有blog表,就先删除这个表
drop table if exists blog;
create table blog(
blogId int primary key auto_increment,
title varchar(128),
content varchar(4096), --正文
userId int,
posTime datetime
);
drop table if exists user;
create table user(
userId int primary key auto_increment,
username varchar(50) unique,
password varchar(50)
);
上述我们在建表的时候,先删除库中存在的我们要创建的表,是为了清空之前残留的数据。
由于我们的博客系统并没有实现注册功能,所以小编事先在数据库中存入两个用户信息。
三、封装JDBC数据库操作
JDBC中提供了简单的API,但是我们写的类的太多,如果每个类都需要初始化数据源、建立连接、关闭资源,那就太麻烦了。所以这里我们将这些操作封装到一个类中,需要使用的时候,直接调用封装好的方法。
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 {
//这个类中提供DataSource,DataSource对于一个项目来说,有一个就行了(单例)
//DataSource是用来描述数据源的,也就是用来描述数据库服务器在哪里。
//实例并初始化数据源
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("jdbc:mysql://127.0.0.1:3306/blog_system?characterEncoding=utf8&&useSSL=false");
((MysqlDataSource)dataSource).setUser("root");
((MysqlDataSource)dataSource).setPassword("991218zf");
}
}
}
return dataSource;
}
//建立连接
public static Connection getConnection() throws SQLException {
//这里调用getDataSource方法的作用就是没有实例化数据源,就会被实例先一下
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();
}
}
}
}
需要了解的是,我们实例并初始化数据源的时候,使用了单例模型中的懒汉模式。懒汉模式在不加锁的情况下,是多线程不安全的。
单例模型是指我们在一个项目中某个类只能有一个实例,也就是我们这里DataSource,它在我们的项目中只需要出现实例一次,只需要描述一次数据库服务器所在位置。
这里使用懒汉模式有两个线程问题,一个是保证创建MysqlDataSource实例时的原子性,一个是保证内存可见性。
- 保证原子性,就是让我们在实例化MysqlDataSource时,不会出现创建多个MysqlDataSource对象,针对DBUtil类对象进行加锁,一个线程在创建对象的时候,其他线程阻塞等待。当一个对象创建好之后,下次需要使用的时候,就不需要再创建这个对象了,也就不需要进程加锁了,所以这里在外层添加了一个if判断,如果MysqlDataSource对象存在,直接返回创建好的对象。因为加锁会导致程序执行速度变慢,所以必要的时候加锁,不必要的时候就不需要加锁。
- 保证内存可见性,使用volatile关键字,防止在读取数据表中的数据时,从内存中读数据的操作被编译器优化掉,而另一个线程修改数据的时候,读取数据的线程感知不到。
1、创建数据表对应的实体类
这里我们在数据库中已经创建好了两个表,一个用来表示博客信息,一个用来表示用户信息。但是我们还需要在Java代码中,创建对应的类,表示这两个实体,比如创建Blog类,Blog类的每个对象,就代表数据库的一个记录。
1️⃣用户信息类(User)
package model;
/*
* 这个类表示数据库中user表的内容
* 每个user对象,就对应user表中的一条记录
* */
public class User {
private int userId;
private String username;
private String password;
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
2️⃣博客信息类(Blog)
package model;
/*
* 这里类表示数据库中Blog表的内容
* 没给Blog对象,就对应blog表中的一条记录
* */
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
public class Blog {
private int bolgId;
private String title;
private String content;
private int userId;
private Timestamp postTime;
public int getBolgId() {
return bolgId;
}
public void setBolgId(int bolgId) {
this.bolgId = bolgId;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public String getPostTime() {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//format方法是用来转换时间戳为上述规定的格式。
return format.format(postTime);
}
public void setPostTime(Timestamp postTime) {
this.postTime = postTime;
}
}
2、封装增删改查操作
这里我们创建两个类分别为BlogDao和UserDao,这里的Dao是 Data Access Object的缩写,表示的意思是数据访问对象,通过这个类的对象来访问数据。所以这里我们通过在这两个类的方法中封装JDBC来操作数据库。
1️⃣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 {
//把一个Blog对象插入到数据库中
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,?,?,?,?)";
statement = connection.prepareStatement(sql);
statement.setString(1, blog.getTitle());
statement.setString(2, blog.getContent());
statement.setInt(3, blog.getUserId());
//如果数据库表里面是datetime类型,插入数据的时候,按照TimeStamp来插入或者按照格式化时间来插入都是可以的
statement.setString(4, blog.getPostTime());
//3.执行sql
statement.executeUpdate();
}catch(SQLException e){
e.printStackTrace();
}finally{
DBUtil.close(connection,statement,null);
}
}
//查询blog表中所有的博客数据
public List<Blog> selectAll() {
List<Blog> blogs = new ArrayList<>();
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
try {
//1.建立连接
connection = DBUtil.getConnection();
//2.构造并执行sql
String sql = "select * from blog order by postTime desc";
statement = connection.prepareStatement(sql);
resultSet = statement.executeQuery();
//遍历结果
while(resultSet.next()){//这里的resultSet是一个结果集
Blog blog = new Blog();
blog.setBolgId(resultSet.getInt("blogId"));
blog.setTitle(resultSet.getString("title"));
//先把博客的正文取出来
String content = resultSet.getString("content");
//判断如果正文超过了100,从0到100截取出来。
if(content.length() > 100){
content = content.substring(0,100)+"...";
}
//最后将这个content放入到blog对象中。最后通过响应显示在博客简介中
blog.setContent(content);
blog.setUserId(resultSet.getInt("userId"));
blog.setPostTime(resultSet.getTimestamp("postTime"));
//将每次遍历到的结果都放在list中保存。
blogs.add(blog);
}
} catch (SQLException e) {
e.printStackTrace();
}finally{
DBUtil.close(connection,statement,resultSet);
}
return blogs;
}
//指定一个博客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();
if(resultSet.next()){
Blog blog = new Blog();
blog.setBolgId(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;
}
//指定一个博客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);
}
}
}
2️⃣UserDao
package model;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class UserDao {
//根据用户id进行查询
public User selectUserById(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();
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 e) {
e.printStackTrace();
} finally{
DBUtil.close(connection,statement,resultSet);
}
return null;
}
//根据用户名进行查询
public User selectUserByName(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();
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 e) {
e.printStackTrace();
} finally{
DBUtil.close(connection,statement,resultSet);
}
return null;
}
}
✨这里需要注意的是执行sql语句时,使用了两个方法executeQuery和executeUpdate方法,这两个方法都表示执行sql语句,但是在使用的时机上存在差异。
- executeQuery方法用于执行从数据库中检索某些数据的SQL语句。例如select
- executeUpdate方法用于执行更新或修改数据库的sql语句。例如:insert into ,update
四、前后端交互逻辑的实现
1、博客列表页
1.1、展示博客列表
1️⃣约定前后端交互接口
请求:GET/blog :这里的GET表示的是HTTP请求方法,blog表示路径
响应:由于博客列表页中存在多个博客,所以使用数组,来存放这个博客对象。
[
{
blogId:1,
title:"这是标题",
content:"这是正文",
userId:1,
postTime:"2023-07-27 12:00:00"
},
{
blogId:1,
title:"这是标题",
content:"这是正文",
userId:1,
postTime:"2023-07-27 12:00:00"
},
{
blogId:1,
title:"这是标题",
content:"这是正文",
userId:1,
postTime:"2023-07-27 12:00:00"
},
]
2️⃣编写后端代码(BlogServlet类)
/*
*通过这个类,来实现一些后端提供的接口
* */
@WebServlet("/blog")
public class BlogServlet extends HttpServlet {
//实例化一个ObjectMapper对象,用来将数据转换为json格式
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();
//将从数据库中查询到的数据转换为json格式
String respString = objectMapper.writeValueAsString(blogs);
//使客户端浏览器区分不同种类的数据,并根据不同的MIME调用浏览器内不同的程序嵌入模块来处理相应的数据。
resp.setContentType("application/json;charset=utf8");
resp.getWriter().write(respString);
}
}
3️⃣修改前端代码
之前写的前端代码的博客列表页,都是写死的,现在我们的博客列表页使用数据库中获取数据,所以就需要我们使用JavaScript根据之前写死的样式来编写标签以及其中的内容。
这是之前写死的博客列表页中的组成一个博客的标签。
根据上述的样式,在回调函数中利用JavaScript编写博客列表页的样式,现在只是对博客列表页的右半部分进行了修改。
<!-- 右侧信息 -->
<div class="container-right">
</div>
</div>
<script>
//通过ajax给服务器发请求,获取到所有的博客数据,并且构造到页面上。
function getBlogs(){
$.ajax({
type:'get', //这是请求的方法
url:'blog', //这是请求的路径
success:function(body){
//根据返回的响应数据,构造出页面中对应的元素
//由于返回的响应的数据是一个application/json,所以jQuery自动的将字符串转化为了数组对象
let containerRight = document.querySelector('.container-right')
for(let blog of body){
//拿到的body数据可以当作数组来使用的
let blogDiv = document.createElement("div");
blogDiv.className = 'blog';
let titleDiv = document.createElement("div");
titleDiv.className = 'title';
titleDiv.innerHTML = blog.title;
let dateDiv = document.createElement('div');
dateDiv.className = 'date';
dateDiv.innerHTML = blog.postTime;
let descDiv = document.createElement("div");
descDiv.className = 'desc';
descDiv.innerHTML = blog.content;
let a = document.createElement("a");
a.href = 'blog_detail.html?blogId=' + blog.blogId;
a.innerHTML = '查看全文 >>';
//把上述标签构造好了之后,还需要组合起来
blogDiv.appendChild(titleDiv);
blogDiv.appendChild(dateDiv);
blogDiv.appendChild(descDiv);
blogDiv.appendChild(a);
containerRight.appendChild(blogDiv);
}
}
});
}
getBlogs();
</script>
🍂我们博客显示的发布时间,在页面上显示的是时间戳,这里需要我们在Blog类中对getPostTime方法进行修改,使用SimpleDateFormat类的format方法对时间戳转换成我们设置的格式。
public String getPostTime() {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//format方法是用来转换时间戳为上述规定的格式。
return format.format(postTime);
}
这里的我们设置的格式中小时的HH,一定要大写,使用大写表示的是24小时制,使用的hh表示的是12小时制。
🍂我们写博客的时候,看见的都是最近写的博客都在最上面,所以我们这里设置在数据库中根据博客发布的时间倒序排序,这样在页面中显示的时候就会将最近写的博客放在最上面。
select * from blog order by postTime desc
🍂我们写的博客中,摘要只出现正文的一部分,所以给我们自己博客系统也实现一个,在BlogDao类中selectAll进行一下改造,让其先获取正文部分,然后再对正文内容的长度进行判断,长度超过100,去正文的前100个字最为摘要。
//先把博客的正文取出来
String content = resultSet.getString("content");
//判断如果正文超过了100,从0到100截取出来。
if(content.length() > 100){
content = content.substring(0,100)+"...";
}
//最后将这个content放入到blog对象中。最后通过响应显示在博客简介中
blog.setContent(content);
将这三个问题修改完成之后,出现的页面就是这样的。
1.2、博客详情页
1️⃣约定前后端交互接口
请求:GET /blog?blogId=1;这里后面添加blogId= 1表示的意思就是指定获取某个博客内容
响应:只获取一个博客内容。
{
blogId:1,
title:"这是一篇博客",
content:"这是正文",
userId:1,
postTime:"2023-07-27 12:00:00"
}
2️⃣编写后端代码
这里是否创建新的类,是根据我们约定前后端接口的时候,请求指定的路径来区分的,由于这里我们约定的路径与博客列表页的请求路径是一样,所以博客详情页的代码继续在BlogServlet类中来编写。
这里就是根据请求的query string中是否有blogId来区分是获取一个博客还是获取所有的博客。
mport java.util.List;
/*
*通过这个类,来实现一些后端提供的接口
* */
@WebServlet("/blog")
public class BlogServlet extends HttpServlet {
//实例化一个ObjectMapper对象,用来将数据转换为json格式
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String blogId = req.getParameter("blogId");
//从query string中查询一下看是否有blogId,如果有就认为是查询指定博客;如果没有就是查询所有博客。
BlogDao blogDao = new BlogDao();
if(blogId == null){
List<Blog> blogs = blogDao.selectAll();
//将从数据库中查询到的数据转换为json格式
String respString = objectMapper.writeValueAsString(blogs);
//使客户端浏览器区分不同种类的数据,并根据不同的MIME调用浏览器内不同的程序嵌入模块来处理相应的数据。
resp.setContentType("application/json;charset=utf8");
resp.getWriter().write(respString);
}else{
//上述使用getParameter从query string中得到的blogId是一个字符串,这里需要将其转为数字
Blog blog = blogDao.selectOne(Integer.parseInt(blogId));
String respString = objectMapper.writeValueAsString(blog);
resp.setContentType("application/json;charset=utf8");
resp.getWriter().write(respString);
}
}
}
3️⃣修改前端代码
这里需要在博客列表页点击查看全文跳转,跳转后的博客详情页,是由markdown格式构成的数据,所以在前端页面显示的时候需要引入editor.md的依赖,然后使用markdown官方提供的editormd.markdownToHTML方法来对正文进行渲染,然后显示在页面上。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>博客详情页</title>
<link rel="stylesheet" href="css/common.css">
<link rel="stylesheet" href="css/blog-detail.css">
<!-- 引入jquery -->
<script src="jquery.min.js"></script>
<!-- 引入editor.md依赖 -->
<link rel="stylesheet" href="editor.md/css/editormd.min.css">
<script src="editor.md/lib/marked.min.js"></script>
<script src="editor.md/lib/prettify.min.js"></script>
<script src="editor.md/editormd.js"></script>
</head>
<body>
<!-- 导航栏 nav 是导航整个次的缩写 -->
<div class="nav">
<!-- logo -->
<img src="image/logo.png" alt="">
<div class="title">我的博客系统</div>
<!-- 只是一个空白,用来把后面的链接挤过去 -->
<!-- 这是一个简单粗暴的写法 -->
<div class="spancer"></div>
<a href="blog_list.html">主页</a>
<a href="blog_edit.html">写博客</a>
<!-- 这里的地址回头在说 -->
<a href="#">注销</a>
</div>
<!-- 页面的主题部分 -->
<div class="container">
<!-- 左侧信息 -->
<div class="container-left">
<!-- 这个div表示整个用户信息的区域 -->
<div class="card">
<!-- 用户的头像 -->
<img src="image/head_portrait.jpg" alt="">
<!-- 用户名 -->
<h3>小张学编程</h3>
<!-- GitHub地址 -->
<a href="https://github.com">github 地址</a>
<!-- 统计信息 -->
<div class="counter">
<span>文章</span>
<span>分类</span>
</div>
<div class="counter">
<span>2</span>
<span>1</span>
</div>
</div>
</div>
<!-- 右侧信息 -->
<div class="container-right">
<h3></h3>
<div class="date"></div>
<div id="content">
</div>
</div>
</div>
<script>
function getBlog(){
$.ajax({
type:'get',
//location.search
url:'blog'+location.search,
success:function(body){
//设置博客标题
let h3 = document.querySelector('.container-right h3');
h3.innerHTML = body.title;
//设置博客发布时间
let dateDiv = document.querySelector('.container-right .date');
dateDiv.innerHTML = body.postTime;
//设置正文,正文内容应该是markdown格式的数据
//此处要显示的应该是渲染过的markdown的内容,而不是markdown的原始字符串。
//第一个参数,是一个html元素的id,接下来渲染的结果机会放到对应的元素中
editormd.markdownToHTML('content',{markdown:body.content})
}
});
}
getBlog();
</script>
</body>
</html>
上述代码中需要注意的是location.search,这个代码可以获取当前的页面的URL中的查询字符串内容。location与document一样是一个全局变量。
1.3、登录页面
1️⃣约定前后端交互接口
这里提交用户名和密码,可以使用form也可以使用ajax,但是form更简单一点,所以我们这里使用form构造请求
请求:
POST/login
Content-Type:application/x-www-form-urlencoded 这种数据的组织类型就是form专属的类型
响应:
登录成功,直接跳转到主页,302表示重定向
HTTP/1.1 302
Location:blog_list.html
✨注意:
如果通过302来跳转页面。前端必须使用form,不能使用ajax.如果使用ajax,当收到302响应,不会触发页面跳转。
2️⃣编写后端代码
这里的前端代码由于我们只是使用费form发送请求,所以改动就非常小。
<!-- 登录页的版心 -->
<div class="login-container">
<!-- 登录对话框 -->
<div class="login-dialog">
<h3>登录</h3>
<!-- 这里使用form包裹一下 下列内容,便于后续给服务器提交数据 -->
<form action="login" method="post">
<div class="row">
<span>用户名</span>
<input type="text" id="username" name="username">
</div>
<div class="row">
<span>密码</span>
<input type="password" id="password" name="password">
</div>
<div class="row">
<input type="submit" id="submit" value="登录">
</div>
</form>
</div>
</div>
这里我们在每个input标签中添加一个name属性,这个属性的值和id属性值相同。但是他们的作用大不相同。id属性,只是针对html生效,只是用来方便获取到该元素。name属性是针对form表单构造http请求的
3️⃣编写前端代码
由于我们约定的路径发生了变化,所以我们在编写后端代码的时候,重新创建一个类,用来实现登录页面的后端程序。
package controller;
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;
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//设置请求传过来的字符服务器以utf8字符集进行解析
req.setCharacterEncoding("utf8");
//1.从请求中,获取到用户名和密码
String username = req.getParameter("username");
String password = req.getParameter("password");
if(username == null || username.equals("") || password ==null ||password.equals("")){
//用户名或者密码不全,登录必然失败
String html = "<h3>登录失败!缺少用户名或者密码</h3>";
resp.setContentType("text/html;charset=utf8");
resp.getWriter().write(html);
return;
}
//2.读取数据库,看这里的用户名和密码,是否和数据库中的匹配
UserDao userDao = new UserDao();
User user = userDao.selectUserByName(username);
if(user == null){
//用户名不存在
String html = "<h3>用户名或密码错误!</h3>";
resp.setContentType("text/html;charset=utf8");
resp.getWriter().write(html);
return;
}
if(password.equals(user.getPassword())){
//密码错误
String html = "<h3>用户名或密码错误!</h3>";
resp.setContentType("text/html;charset=utf8");
resp.getWriter().write(html);
return;
}
//3.用户名和密码都正确,登录成功,需要设置会话
//先要创建一个会话
HttpSession session = req.getSession(true);
//此处就把用户对象存储到session中了,下次用户访问其他页面,就可以直接拿到会话,进一步就能拿到之前的user对象了
session.setAttribute("user",user);
//4.返回一个重定向响应,能够跳转到博客列表页
resp.sendRedirect("blog_list.html");
}
}
1.4、强制要求用户登录,检查用户的登录状态
当用户想要访问博客列表页/详情页/编辑页的时候,必须是登录状态,如果是未登录状态,则直接跳转到登录页,要求用户登录。
在博客列表页/详情页/登录页,页面加载的时候,发起一个ajax请求,通过这个请求,访问服务器,获取到当前的登录状态,如果当前为登录,则跳转到登录页面,如果已登录,则不做任何操作。
1️⃣约定前后端交互接口
- 请求:
GET/login
- 响应:
登录成功,就返回一个200这样的响应。body可以不要,登录失败(未登录),就返回403这样的响应。
2️⃣编写后端代码
我们可以直接在之前的LoginServlet类中写一个doGet判定当前的登录状态。
//通过这个方法,判定用户的登录状态。已登录,返回200.未登录返回403
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//看当前请求是否已经存在会话,并且当前的会话是否包含user对象。
HttpSession session = req.getSession(false);
if(session == null){
//会话不存在,未登录
resp.setStatus(403);
return;
}
//因为当前的getAttribute返回的是一个Object类型的数值,这里我们使用User将其强转
User user = (User)session.getAttribute("user");
if(user == null){
//虽然会话对象存在,但是用户对象没有,也将其判定为未登录状态
resp.setStatus(403);
return;
}
//如果会话存在并且用户也存在,这个时候就是已登陆状态
//200是默认的状态,这里的这句代码也可以不用写
resp.setStatus(200);
}
}
3️⃣编写前端代码
由于判定是否登录,每个页面都需要,所以这里我们将这个判定的方法单独取出来,放到一个文件中,其他的页面代码中引用这个外部代码即可。
function getLoginStatus(){
$.ajax({
type:'get',
url:'login',
success:function(body){
//响应返回200的时候,执行success回调函数
//用户已经登录,不用进行任何操作
console.log("用户已经登录");
},
error:function(body){
//只要返回的不是2开头的状态码,都会触发error回调函数
//assign方法用来跳转到login.html主页
location.assign("login.html");
}
})
}
其他的页面使用这种样式引用即可,但是需要注意的是引入的路径是否正确
<script src="js/app.js"></script>
//引入完成之后调用这个方法
getLoginStatus();
1.5、实现显示用户信息的功能
我们编写的页面中博客列表页和博客详情页的左边页面中都有显示用户信息,但是这两个地方显示的不是一个用户信息,在博客列表页显示的是登录用户的信息,但是当用户点击到别人写的博客中,也就是博客详情页,这个时候就会显示本篇博客作者的信息。
所以实现显示用户信息的功能,需要获取两部分的用户信息
- 一个是博客列表页,此处显示的是登录的用户的信息
- 一个是博客详情页,显示的是文章作者的信息
所以针对这两部分的数据,就需要分别进行获取
1.5.1、针对列表页进行处理
1️⃣约定前后端交互接口
在博客列表页,要获取到登录用户的信息
前端部分可以直接复用getLoginStatus方法,之前登录成功之后什么也没有做,这里将其修改一下,登录成功之后,将响应返回的用户名设置到左侧的用户信息中。
后端代码,之前的LoginServlet类中,使用doGet方法判定用户的登录状态,判定登录成功之后只返回了一个200的状态码,这里我们登录成功之后的用户名,返回给前端。
请求:
GET/login
响应:
HTTP/1.1 200
Content-Type:application/json;
{
userId:1
username:zhangsan,
}
2️⃣修改后端代码
3️⃣修改后端代码
只有博客列表页显示登录用户的信息,所以这里我们将之间的getLoginStatus方法,复制出来放在blog_list的代码中,对齐进行修改,之前的这个方法中当服务器返回200的时候,这个代码只是打印了日志,什么都没有做,现在我们通过querySelector方法找到h3标签,然后修改h3标签中的内容。
function getLoginStatus(){
$.ajax({
type:'get',
url:'login',
success:function(body){
//响应返回200的时候,执行success回调函数
//用户已经登录,不用进行任何操作
console.log("用户已经登录");
//把返回的用户名,设置到页面中。
let h3 = document.querySelector('.card h3');
//这里的body是一个js对象,就是前面服务器返回的json格式的user对象
h3.innerHTML = body.username;
},
error:function(body){
//只要返回的不是2开头的状态码,都会触发error回调函数
//assign方法用来跳转到login.html主页
location.assign("login.html");
}
})
}
1.5.2、针对详情页进行处理
这里的详情页,用户信息这里显示的是当前文章的作者,首先我们需要根据blogId查询到文章对象,然后拿到文章作者的id,在根据作者id查询对应的作者名字,显示到页面上。
1️⃣约定前后端交互接口
请求
GET/user?blogId=1
响应:
HTTP/1.1 200
Content-Type:application/json
{
userId:1,
username:"zhangsan"
}
2️⃣编写后端代码
由于我们约定的时候,指定了新的路径,所以我们需要创建新的Servlet类来处理请求。
package controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import model.Blog;
import model.BlogDao;
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 java.io.IOException;
@WebServlet("/user")
public class UserServlet extends HttpServlet {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1、先读取出blogId,由于我们的blogId是通过query string 传递的,所以我们使用getParameter获取
String blogId = req.getParameter("blogId");
if(blogId == null || blogId.equals("")){
//直接返回一个userId为0的对象,因为最终返回的是一个Json数据
//此时也是返回json格式比较好,如果返回一个html,前端处理的时候还需要判断
//这里new的user对象,是一个空的对象,也就满足了返回一个userId为0的对象
String respJson = objectMapper.writeValueAsString(new User());
resp.setContentType("application/json;charset=utf8");
resp.getWriter().write(respJson);
System.out.println("参数给定的blog为空");
return;
}
//2.查询数据库,查询对应的Blog对象
BlogDao blogDao = new BlogDao();
Blog blog = blogDao.selectOne(Integer.parseInt(blogId));
if(blog == null){
String respJson = objectMapper.writeValueAsString(new User());
resp.setContentType("application/json;charset=utf8");
resp.getWriter().write(respJson);
System.out.println("参数给定的blog不存在");
return;
}
//3.根据blog中的userId,查询作者信息
UserDao userDao = new UserDao();
User user = userDao.selectUserById(blog.getUserId());
if(user == null){
String respJson = objectMapper.writeValueAsString(new User());
resp.setContentType("application/json;charset=utf8");
resp.getWriter().write(respJson);
System.out.println("该博客对应的作者不存在");
}
//4.把user对象返回给页面
String respJson = objectMapper.writeValueAsString(user);
resp.getWriter().write(respJson);
return;
}
}
3️⃣修改前端代码
在前端的博客详情页(blog_detail)中添加getAuthor方法。将之前左侧的用户信息框中的h3标签中写死的内容删除掉。
function getAuthor(){
$.ajax({
type:'get',
url:'user'+location.search,
success:function(body){
//把响应中得到的user对象的数据,构造到页面上
if(body.userId == 0){
//表示服务器没有找到匹配的用户
alert("当前未找到作者信息!");
return;
}
//找到了 没有进if表示这是一个合法的user对象
let h3 = document.querySelector('.card h3');
h3.innerHTML = body.username;
}
});
}
getAuthor();
1.6、用户退出登录功能
之前我们在导航栏中写了注销,这是一个a标签,点击这个标签的时候就会触发一个GET请求,服务器收到这个get请求,就可以把当前用户的会话中的user对象给删除掉。这里为什么不直接删除会话session,但是由于servlet中没有提供直接删除会话的操作,只是提供了设置过期时间的方法,在我们的现在的业务需求上不适用,所以这里直接删除session中的user对象,也就实现了退出登录。
1️⃣约定前后端交互接口
- 请求:
GET/logout
- 响应:
HTTP/1.1 302
Lcation:login.html
2️⃣编写后端代码
这里我们约定的请求使用新的路径,所以我们需要创建新的Servlet,来实现业务。由于之前的代码中用来判定用户是否登录的时候,判断user对象是否存在,所以这里我们直接将user对象删除,并且重定向到登录页。
package controller;
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("/logout")
public class LogoutServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
HttpSession session = req.getSession(false);
if(session == null){
//session不存在,说明用户本来就没有登录,这里直接跳转到登录页
//重定向
resp.sendRedirect("login.html");
return;
}
//拿到session之后 删除session中的user对象即可
//这里也不要判断session中的user对象存在不存在,存在删除,不存在也不影响
session.removeAttribute("user");
//重定向
resp.sendRedirect("login.html");
}
}
3️⃣编写后端代码
由于我们的a标签本身就可以发送http请求,所以我们只需要在注销所在a标签的href属性设置为我们约定的路径即可
1.7、实现发布博客功能
1️⃣约定前后端交互接口
前端使用form提交博客
- 请求:
POST/blog
Content-Type:application/x-www-form-urlencoded
body: title=标题&content=......
- 响应:
HTTP/1.1 302
Location:blog_list.html
发布成功直接跳转到列表页
2️⃣编写后端代码
从请求中拿到标题和正文,从会话中拿到用户的登录状态(作者id)、获取到系统时间。将这些进行拼接,就构成了一个blog对象,然后将这个对象插入到数据库中。这里由于我们约定的时候,路径为blog,所以我们在BlogServlet类中,添加一个doPost方法来处理博客发布的请求。
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf8");
//1.先从请求中拿到标题和正文
String title = req.getParameter("title");
String content = req.getParameter("content");
if(title == null || title.equals("") || content == null || content.equals("")){
//只要其中有一个符合条件进入判断,就认为内容是不全的。没有标题或者内容,博客发布失败
String html = "<h3>title 或者 content 为空!新增博客是失败!</h3>";
resp.setContentType("text/html;charset=uft8");
resp.getWriter().write(html);
return;
}
//2.从会话中拿到作者的id
HttpSession session = req.getSession(false);
if(session == null){
String html = "<h3>当前用户未登录!新增博客是失败!</h3>";
resp.setContentType("text/html;charset=uft8");
resp.getWriter().write(html);
return;
}
User user = (User)session.getAttribute("user");
if(user == null){
String html = "<h3>当前用户未登录!新增博客是失败!</h3>";
resp.setContentType("text/html;charset=uft8");
resp.getWriter().write(html);
return;
}
//3.构造Blog对象
Blog blog = new Blog();
blog.setUserId(user.getUserId());
blog.setTitle(title);
blog.setContent(content);
blog.setPostTime(new Timestamp(System.currentTimeMillis()));
//4.插入blog对象到数据库中
BlogDao blogDao = new BlogDao();
blogDao.insert(blog);
//5.跳转到博客列表页(重定向)
resp.sendRedirect("blog_list.html");
}
}
3️⃣编写前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>博客编辑页</title>
<link rel="stylesheet" href="css/common.css">
<link rel="stylesheet" href="css/blog-edit.css">
<!-- 引入依赖 -->
<script src="jquery.min.js"></script>
<link rel="stylesheet" href="editor.md/css/editormd.min.css">
<script src="editor.md/lib/marked.min.js"></script>
<script src="editor.md/lib/prettify.min.js"></script>
<script src="editor.md/editormd.js"></script>
</head>
<body>
<!-- 导航栏 nav 是导航整个次的缩写 -->
<div class="nav">
<!-- logo -->
<img src="image/logo.png" alt="">
<div class="title">我的博客系统</div>
<!-- 只是一个空白,用来把后面的链接挤过去 -->
<!-- 这是一个简单粗暴的写法 -->
<div class="spancer"></div>
<a href="blog_list.html">主页</a>
<a href="blog_edit.html">写博客</a>
<!-- 这里的地址回头在说 -->
<a href="#">注销</a>
</div>
<!-- 博客编辑页的版心 -->
<div class="blog-edit-container">
<form action="blog" method="post">
<!-- 标题编辑区 -->
<div class="title">
<input type="text" id="title-input" name="title">
<input type="submit" id="submit">
</div>
<!-- 博客编辑器 -->
<!-- 把 mackdown编辑器放到这个div中-->
<div id="editor">
<textarea name="content" system="display:none;"></textarea>
</div>
</form>
</div>
<script src="js/app.js"></script>
<!-- 针对editor.md初始化,创建一个编辑器对象,并关联到页面的某个元素中 -->
<script>
var editor =editormd("editor",{
// 设置编辑器的宽度和高度
width:"100%",
height:"calc(100% - 50px)",
// 这是编辑器中的初始内容
markdowm:"# 在这里写下一篇博客",
// 指定editor.md依赖的插件路径
path:"editor.md/lib/",
saveHTMLToTextarea:true
});
getLoginStatus();
</script>
</body>
</html>