文章目录
一、项目内容介绍
- 涉及到了4个页面:博客列表页、博客详情页、登录页、博客编辑页。其中 “博客编辑页” 继承了 “markdown编辑器”
- 第三方库:markdown 编辑器
markdown 是程序员常用的写文档的方式,许多的博客平台都是支持该编辑器。又因为编辑器从零手动实现比较麻烦,所以此处直接引入了第三方库:editor.md。
二、检查错误
三、编写代码
2.0 项目原理
- 大方向:前端与服务器的交互、服务器与数据库的交互
- 小方向(每一个功能的编写顺序):
- 编写数据库代码(先设计数据库)
- 涉及前后端交互接口
- 编写后端代码
- 编写前端代码
- 调试
2.1 准备工作:创建项目 + 引入依赖
- 要引入的依赖:Servlet、mysql、Jackson
- 如何确定完成了这部分工作:可以通过网络通信的方式访问网页
2.3 编写数据库代码
❤️数据库表的创建
- 数据库优化保险措施:
- 数据库创建需判定是否存在:create database if not exists blog_system;
- 如果该库已经存在该表了,直接删掉,避免后续影响:drop table if exists user;
- 数据库的字符集:默认是拉丁文,并不支持中文。如果要支持中文,就需要更改字符集,而字符集是在创建数据库时才能被指定的
- 数据库表的设计:
- 创建哪些表:根据需求来
- 数据库常见设计技巧:
- primary key:我们在表示一个表时,往往需要一个主键,且这个主键往往是一个整数 + 自动增长
- 关于 sql 文件:我们一般会把数据库操作写到一个.sql文件里保存下来。因为如果后续需要把程序部署到别的机器上,只需要直接执行sql文件即可,可提高开发效率。
- 如何执行该文件:下面两个方法都可以,但因为实际工作中,公司对数据库服务器会有严格的管理,图形化数据库可能无法使用,但命令行客户端是一定能用的,所以首选第二种方式
- 图形化的数据库客户端直接执行sql文件
- mysql 命令行客户端【复制粘贴】
- 如何执行该文件:下面两个方法都可以,但因为实际工作中,公司对数据库服务器会有严格的管理,图形化数据库可能无法使用,但命令行客户端是一定能用的,所以首选第二种方式
create database if not exists blog_system charset utf8;
use blog_system;
drop table if exists user;
drop table if exists blog;
-- 博客表
create table blog (
blogId int primary key auto_increment,
title varchar(256),
content varchar(4096),
userId int, -- 用来和userId建立联系
postTime datetime
);
-- 用户表
create table user (
userId int primary key auto_increment,
username varchar(64) unique, -- unique指的是不能重复
password varchar(64)
);
insert into user values(1, '蛋蛋', '123456'), (2, '砂金', '123456'),(3, '拉帝奥', '123456');
insert into blog values (1, '蛋蛋的当狗宣言', '我就要当wxy的狗,除了你,别人的狗我都不要当', 1, '2024-05-04 19:00:00');
insert into blog values (2, '是的我们有一个孩子(砂金篇)', '教授,又要仰仗你的智慧了', 2, '2024-05-04 19:00:00');
insert into blog values (3, '是的我们有一个孩子(教授篇)', '该死的赌徒', 3, '2024-05-04 19:00:00');
❤️封装 JDBC 代码
前言
- 原理:通过封装 JDBC 代码,来实现一些基础的数据库操作,方便我们后续直接调用
- 创建 dao 包:dao(Data Access Object 数据访问对象) ,放置了我们用来操作数据库的代码
- 里面存放了一些类,类里的方法封装了数据库操作
- 数据库就是通过该类的对象来访问的
封装建立数据库连接的代码
- 使用单例模式(懒汉模式)组织 DataSource:private static DataSource xxx
- 什么时候使用单例模式:当一个程序里的某个对象天生只需要有一个实例,多个并没有什么意义时,就可以使用【单例模式】
- 而此处的DataSource 只是用来描述你这个数据库服务器在哪之类的信息,并不需要多个,正符合单例模式
- 指令重排序:private volatile DataSource xxx
使用volatile确保当前的这个初始化操作不会涉及到【指令重排序】 - 关于两个if的解析:按照懒汉模式进行编写
- 第一次检查 if (dataSource == null):这是为了避免不必要的同步。如果 dataSource 已经被初始化,则直接返回它,不需要进入同步代码块。
- synchronized (DBUtil.class):这是一个同步块,用于确保在同一时间只有一个线程能够执行其中的代码。解决了线程不安全的问题。
- 第二次检查 if (dataSource == null):在同步块内部再次检查 dataSource 是否为 null。这是因为可能有另一个线程在第一次检查之后和进入同步块之前已经初始化了 dataSource。所以,我们需要再次检查以避免重复初始化。
- 初始化 dataSource:如果 dataSource 仍然为 null,则进行初始化
- getDataSource():我们可以通过该方法创建出唯一的 DataSource 实例
- 关于 private 的使用:因为我们不需要显示调用这个代码,而是由 getConnection() 内部调用
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_test?useSSL=false&characterEncoding=utf8");
((MysqlDataSource)dataSource).setUser("root");
((MysqlDataSource)dataSource).setPassword("2222");
}
}
}
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) {
// 如果把 3 个 close 都放到同一个 try 中, 一旦前面的 close 出现异常, 就会导致后续的 close 执行不到了.
// 相比之下, 还是分开写 try 比较好.
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();
}
}
}
❤️创建实体类
- 概念与作用:
- 与数据库的表有对应关系,一般来说,一个关键的表都需要一个实体类。
- 然后我们就可以使用类的对象来表示这个表里的一条记录了。
- 因此就要求这个对象的属性,能和表的列一一对应
- 实体的梳理:
- 找到需要创建的实体类:实体具象为【数据库需求场景中有关键性质的名词】
- 梳理关系:梳理实体和实体之间是 “一对一” 还是 “一对多” 还是 “多对多”
- 关于 timestamp:
- SQL 里有 timestamp 类型以及 datetime 类型。使用 SQL时,推荐使用datetime,因为timestamp只有4个字节,不太够用
- 但是 Java代码中的 Timestamp 是可以使用的,设计上是有8个字节。该类对应了mysql中的Datetime类,来源于 java.sql
- 关于 lombok:通过该组件引入一些注解,Java编译器看到这些注解之后,就能自动生成getter、setter等方法,使代码更加简洁
//通过这个类的对象去表示一条 blog 表中的记录
public class Blog {
private int blogId;
private String tiele;
private String content;
private int userId;
private Timestamp postTime;
}
//这个类的对象表示user表中的一条记录
public class User {
private int userId;
private String username;
private String password;
}
❤️创建 dao类,进一步封装数据库操作
- 为什么要使用 finally:确保close()方法一定会被执行到
- 当前场景:如果close()前面的代码抛出异常了,close()方法就无法执行到了
- 重要性:释放连接和资源是非常重要的操作,而且一时半会看不出问题,但一旦积累起来就会十分严重。代码中一定要确保是能关掉这个资源的
- 为什么要先把对象赋值为null:调整定义域,确保finally里的关闭操作能执行到
- 查询列表的order by 操作:使最新的博客被先展示
- Mysql从来没有承诺过,未order by的顺序是什么样的,如果不指定order by,查询结果的顺序是不可预期的
public class BlogDao {
//获取博客列表
//正常开发中,为了避免一次查太多把数据库搞挂了,我们一般是不会直接把整个表里的所有数据全部获取的
//而是会指定筛选条件/最大条数的
public List<Blog> getBlogs() throws SQLException {
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
List<Blog> blogs = new ArrayList<>();
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.setTiele(resultSet.getString("title"));
blog.setContent(resultSet.getString("content"));
blog.setTiele(resultSet.getString("title"));
blog.setUserId(resultSet.getInt("userId"));
blog.setPostTime(resultSet.getTimestamp("postTime"));
blogs.add(blog);
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DBUtil.close(connection, statement, resultSet);
}
return blogs;
}
//根据blogId获取某个博客
public Blog getBlog(int blogId) throws SQLException {
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.setBlogId(resultSet.getInt("blogId"));
blog.setTiele(resultSet.getString("title"));
blog.setContent(resultSet.getString("content"));
blog.setTiele(resultSet.getString("title"));
blog.setUserId(resultSet.getInt("userId"));
blog.setPostTime(resultSet.getTimestamp("postTime"));
return blog;
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DBUtil.close(connection, statement, resultSet);
}
return null;
}
//根据blogId删除一个博客
public void deleteBlog(int blogId) throws SQLException {
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);
}
}
public void insert(Blog blog) throws SQLException {
Connection connection = null;
PreparedStatement statement = null;
try {
connection = DBUtil.getConnection();
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());
statement.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBUtil.close(connection, statement, null);
}
}
}
2.4 实现博客列表列功能
- 思路:
- 让博客列表页在加载的时候,发起一个 ajax 的http请求,告诉后端要博客列表
- 然后后端查询数据库,把从数据库中获取的数据返回给前端
- 前端拿到数据后,由前端构造新的页面内容
- 关于约定前后端交互接口:怎么约定都行,但前后端一定要互相约定且遵守
- 编写后端代码:
- 创建名为api的包:这里面放的是Servlet代码,用来给前端提供功能支持,这些可以理解为服务器给前端提供的API(编程接口)
- 注意,写代码时,类和包的命名是有规则的,但这些规则仅供参考,无法保证是一定能适合当前的实际情况的
- 创建名为api的包:这里面放的是Servlet代码,用来给前端提供功能支持,这些可以理解为服务器给前端提供的API(编程接口)
- 关于JSON对象的构造:tring respJson = objectMapper.writeValueAsString();
Jackson 看到List/数组,就会构造出一个JSON数组[],并针对List中的每个Java对象,分别构造出JSON对象(根据 类的属性来,属性的名字就是JSON的key,属性的值就是JSON的value) - 关于时间戳:时间戳对于计算机来说比较好理解,但并不方便人进行观察,所以需要执行转换操作(将毫秒级别的时间戳转为格式化的时间)
- 如何修改:使用 Java标准库提供的【SimpleDateFormat】这个类,格式化时间戳
- 因为 Jackson 会自动调用这里的 getPostTime方法,把得到的结果作为 JSON字符串中 postTime属性的值
- 所以修改这个 getPostTime 方法,让它直接返回一个 String,就是格式化时间即可(Timestamp这个类型会被Jackson当成时间戳来显示)
- SimpleDateFormat 的使用方式:先构造一个对象并指定具体的格式,然后使用format方法,得到格式化字符串。注意,不同语言关于格式化字符串的规则不同,如m在java指分钟,在Linux Shell则指的是月份。
- 如何修改:使用 Java标准库提供的【SimpleDateFormat】这个类,格式化时间戳
public String getPostTime() {
SimpleDateFormat format = new SimpleDateFormat("yyyy/MM/dd hh:mm:ss");
return format.format(postTime);
}
- 为什么如果数据出错,不是返回null而是new一个对象:
- 解析:
- user = null,表示最终响应的数据就是一个“null”,就会导致ajax回调方法这里的解析出错。因为我们返回响应中设置的“content-Type”为“application/json”。返回一个null这样的字符串,ajax解析会出问题
- 但如果是使用new User()的做法,返回的响应就是{userId: 0, username:‘’},这个情况是合法JSOn,ajax能够正常解析。前端只要根据返回的结果判断是否为无效数据即可
- 为什么不在ajax里直接判断body == ‘null’:可以使用,但是此时后端就不能设定 application/json,同时前端要写更多的代码来判定。为了代码的简洁,还是new 一个对象比较方便。
- 解析:
//这是前端代码
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
BlogDao blogDao = new BlogDao();
String blogId = req.getParameter("blogId");
if (blogId == null){
//说明此时要获取的是博客列表
List<Blog> blogs = blogDao.getBlogs();
String respJson = objectMapper.writeValueAsString(blogs);
resp.setContentType("application/json; charset = utf8");
resp.getWriter().write(respJson);
}else {
try {
Blog blog = blogDao.getBlog(Integer.parseInt(blogId));
if (blog == null){
//返回一个id为0的blog对象,前端再根据这里进行判定
//正常来说,博客id不会是0,如果前端看到返回的blogId是0,就会知道这是一个无效数据,然后对其作出相应处理
blog = new Blog();
}
String respJson = objectMapper.writeValueAsString(blog);
resp.setContentType("application/json; charset = utf8");
resp.getWriter().write(respJson);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
- 关于前端JQuery库的导入:< script src=“http://libs.baidu.com/jquery/2.0.0/jquery.min.js”>
- 关于验证路径:前端第三方库的导入只需要提供“下载路径”即可,但网上搜到的路径可能已经过期了,不好使,所以在使用前,我们最好去验证一下该路径
- 验证方法:打开浏览器直接粘贴访问该路径,如果能正常访问(页面看到了JQuery的源代码),说明该路径可正常使用,反之就需要换一个链接了。
- JS定义的函数:因为 JS是动态弱类型的语言,所以JS中定义的函数,不必写返回值类型。如果有参数,参数也不必写类型,直接写形参名字即可。
//函数定义,使用function来定义
function getBlogs() {
}
//函数执行
getBlogs();
- JS的循环写法:
//相当于java中的for each写法
//获取到body数组的每个元素
for (let blog of body){ for (int i = 0; i < body.length; i++){
} }
- DOM(文档对象模型):
- 一个HTML页面相当于一个文档。在HTML页面上,每一个元素都会对应到JS中的一个对象。
- 双方任何一个改变了,都会影响对方。
- 这也是我们JS构造HTML代码直接插入到页面上,页面立马能发生改变的原因。
- 关于测试JS代码:F12的开发者工具的前端控制台上可以编写JS代码,并实时观看JS代码的效果
- undefined 指的是方法没有返回值
- 此处的请求问题:
- 是什么:一个是【获取博客列表页的这个静态的页面】,一个是【获取博客数据】
- 为什么获取静态页面的响应是304:因为这其实是触发了浏览器自带的缓存
- 浏览器为了提高加载页面的速度,就会把一些静态的资源(html、css、js、图片等)在本地硬盘保存一份。前面我们加载了这个页面,所以浏览器记住了这个页面,后续再访问的时候,并没有真正访问我们的服务器,而是从本地进行获取
- fidder抓到的包是灰色的,就说明当前该页面是从缓存中获取(Ps.图片的获取也是灰色的)
- 如果不想让浏览器读取缓存,可以按住ctrl并刷新,即强制刷新
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script>
function getBlogs(){
$.ajax({
type: 'get',
url: 'blog',
//由于后端返回的格式是 application/json,前端接收到后
//JQuery能自动地把此处响应的内容解析成js对象数组
success: function(body){
let container = document.querySelector('.container-right');
for (let blog of body){
let blogDiv = document.createElement('div');
blogDiv.className = 'blog';
let titleDiv = document.createElement('div');
titleDiv.className = 'title';
titleDiv.innerHTML = blog.title;
blogDiv.appendChild(titleDiv);
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.href = 'blog_detail.html?blogId=' + blog.blogId;
a.innerHTML = '查看全文 >>'; //>指的是‘>’
blogDiv.appendChild(a);
// 最后把拼好的 blogDiv 添加到 container 的后面
container.appendChild(blogDiv);
}
}
})
}
getBlogs();
</script>
2.5 实现博客详情页
- 思路:和“博客列表页”共用一个"blog"路径。
- 给一个路径复用多个功能,通过一个具体的参数来区分业务
- 关于拼接正文内容:博客的正文在数据库中是markdown格式的数据,我们需要在详情页里,把数据库中的原始数据渲染成markdown渲染后的数据
- 如何实现:引入editor.md的依赖后,使用其提供的现成的方法,这里是使用markdownToHTML方法
- editormd 指第三库中已经定义好的对象
- ‘content’ 指的是div的id
- markdown的value指的是要写的markdown的内容
- 关于editor.md的依赖引入顺序问题:由于 editor.md本身就依赖了JQuery,所以在引入时,我们要先引入JQuery再引入editor.md,否则后者的依赖无法正确生效
- 第三方库的使用:关于第三方库的学习使用,我们往往需要去阅读官方提供的文档
- 如何实现:引入editor.md的依赖后,使用其提供的现成的方法,这里是使用markdownToHTML方法
editormd.markdownToHTML('content', {markdown: body.content});
- 关于 location.search:指拿到当前页面的query string
<!-- 一定要引入 editormd 的依赖, 才能使用这里的方法 -->
<script src="http://libs.baidu.com/jquery/2.0.0/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>
<script>
function getBlog() {
$.ajax({
url: 'blog' + location.search,
type: 'get',
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;
editormd.markdownToHTML('content', { markdown: body.content });
}
});
}
getBlog();
</script>
2.6 实现登录效果
- 前后端交互:
- 约定使用Post方法
- 路径为 login
- 使用from表单的方式提交数据(username=XXX&password=XXX)
- 后端代码编写:写一个新的Servlet,处理这个请求的路径。
- 注意,每个Servlet都会关联上一个路径,前面的详情页和列表页都是用blog这个路径,用变量值进行了区分
- Session存储:Tomcat默认把Session存储在内存中,重启服务器后就无了,我们也可以通过其他方式把会话存储在硬盘、数据库、Redis等介质中
- 此处有个例外,有些版本的Smart Tomcat默认情况会把Session保存在硬盘上,此时服务器重启数据不会消失
- 如果是用war包的方式打包到webapp目录下,默认肯定是在内存存储
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1. 获取请求中的用户名和密码
// 给请求对象设置字符集, 保证说请求中的 username 或者 password 是中文, 也能正确处理.
req.setCharacterEncoding("utf8");
String username = req.getParameter("username");
String password = req.getParameter("password");
if (username == null || password == null || "".equals(username) || "".equals(password)) {
// 当前提交的用户名密码有误!
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write("当前传过来的 username 或者 password 为空");
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 中, 方便后续进行获取.
session.setAttribute("user", user);
// 4. 跳转到博客列表页.
resp.sendRedirect("blog_list.html");
}
}
- 前端代码编写:通过调整登录页面,让其能构造出form表单的请求
- action:描述了当前请求要访问的路径
- name:描述了我们最终构造出来的键值对的key的名字
<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>
2.7 实现强制登录效果
- 要求:如果用户在未登录的状态下,访问博客列表页/详情页/编辑页,都会自动跳转到登录页
- 约定前后端交互接口:
- 在博客列表页/详情页/编辑页发起一个get的ajax,路径为 “/login” 询问服务器当前是否是登陆状态
- 如果响应是200 ok,表示已经登录,跳转到业务页面
- 如果响应是 403 Forbidden,表示未登录,跳转到登录界面
- 在博客列表页/详情页/编辑页发起一个get的ajax,路径为 “/login” 询问服务器当前是否是登陆状态
- 后端代码:
// 通过这个方法, 来检测当前的登录状态.
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
HttpSession session = req.getSession(false);
//当前连会话都没有
if (session == null){
resp.setStatus(403);
return;
}
//为后续的退出登录操作服务,后续退出登录的操作就是把Session里的user给删掉
User user = (User)session.getAttribute("user");
if (user == null) {
resp.setStatus(403);
return;
}
resp.setStatus(200);
}
- 前端代码:因为这个代码在【博客详情页/列表页/编辑页】都要使用,所以我们可以将其封装成一个js文件。然后在HTML文件里引用这个js文件并执行相关函数即可
- 如何引入 js文件:
<script src="js/app.js"></script> //注意js指的是包名,app.js对应的是文件名
function checkLogin(){
$.ajax({
type: 'get',
url: 'login',
success: function(body) {
},
error: function(body){
//前端的跳转操作
location.assign('login.html');
}
});
}
2.8 显示用户个人信息
- 要求:
- 在博客详情页中,显示这个文章的作者信息
- 在博客列表页,显示当前登录的用户的个人信息
- 前端代码:
function getUser() {
$.ajax({
type: 'get',
url: 'user' + location.search,
success: function(body) {
// body 就是解析后的 user 对象了.
let h3 = document.querySelector('.card h3');
h3.innerHTML = body.username;
}
})
}
getUser();
- 后端代码:
@WebServlet("/user")
public class UserServlet extends HttpServlet {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String blogId = req.getParameter("blogId");
if (blogId == null) {
// 博客列表页
// 从 session 中拿到 user 对象.
HttpSession session = req.getSession(false);
if (session == null) {
User user = new User();
String respJson = objectMapper.writeValueAsString(user);
resp.setContentType("application/json; charset=utf8");
resp.getWriter().write(respJson);
return;
}
User user = (User) session.getAttribute("user");
if (user == null) {
user = new User();
String respJson = objectMapper.writeValueAsString(user);
resp.setContentType("application/json; charset=utf8");
resp.getWriter().write(respJson);
return;
}
String respJson = objectMapper.writeValueAsString(user);
resp.setContentType("application/json; charset=utf8");
resp.getWriter().write(respJson);
} else {
// 博客详情页
// 需要查询数据库了.
BlogDao blogDao = new BlogDao();
Blog blog = null;
try {
blog = blogDao.getBlog(Integer.parseInt(blogId));
} catch (SQLException e) {
throw new RuntimeException(e);
}
if (blog == null) {
User user = new User();
String respJson = objectMapper.writeValueAsString(user);
resp.setContentType("application/json; charset=utf8");
resp.getWriter().write(respJson);
return;
}
UserDao userDao = new UserDao();
User user = null;
try {
user = userDao.getUserById(blog.getUserId());
} catch (SQLException e) {
throw new RuntimeException(e);
}
if (user == null) {
user = new User();
String respJson = objectMapper.writeValueAsString(user);
resp.setContentType("application/json; charset=utf8");
resp.getWriter().write(respJson);
return;
}
String respJson = objectMapper.writeValueAsString(user);
resp.setContentType("application/json; charset=utf8");
resp.getWriter().write(respJson);
}
}
}
Dao层
public class UserDao {
public User getUserById(int userId) throws SQLException {
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 throwables) {
throwables.printStackTrace();
} finally {
DBUtil.close(connection, statement, resultSet);
}
return null;
}
}
2.9 退出登录
- 怎么样才能算是“登录状态”:下述两个条件都具备才能算登录,所以我们只要破坏一个,就可以达到【注销登录】的目的
- 会话存在:可以给会话设置一个很短的超时时间,会话过期了就会被自动删除,但过于复杂
- 会话中存储的user对象存在:破坏这个。因为Servlet并没有直接提供一个API来删除会话,但有API能删除会话中的Attribute
- 关于把当前会话引用指向空:HttpSession session = req.getSession(false); session = null;
- 无法实现【注销登录】的操作。因为【对象销毁】要求没有任何引用指向该数据,该数据才会被GC回收。又因为会话本体是一个被Servlet使用hashMap管理起来的对象,这里创建的session和前者一样都是指向同一个对象,所以置空了我们创建的session是不会影响到本体的
- 前端代码:
<a href="logout">注销</a> //此时点击a标签,就能自动触发Http get请求了
//href属性可以指定要访问的请求路径
- 后端代码:
@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) {
// 当前就是未登录状态, 谈不上退出登录!
resp.sendRedirect("login.html");
return;
}
// 之前在登录成功后, 就会给 session 中存储 user 这样的 Attribute .
// 把这个删掉之后, 自然就会判定为 "未登录" 了.
session.removeAttribute("user");
resp.sendRedirect("login.html");
}
}
2.10 发布博客
- 如何给引入的md编辑器里添加name属性:使用textarea(多行编辑框,editor.md文档给出的解决方案)
- 后端代码:
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1. 获取到登录的用户
// 在博客编辑页, 已经做了登录检查了. 当用户提交的时候, 必然是已经登录的状态.
HttpSession session = req.getSession(false);
if (session == null) {
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write("用户未登录! 无法发布博客!");
return;
}
User user = (User) session.getAttribute("user");
if (user == null) {
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write("用户未登录! 无法发布博客!");
return;
}
// 2. 获取到请求中传递过来的内容
req.setCharacterEncoding("utf8"); // 这个操作不要忘, 否则遇到中文可能会乱码
String title = req.getParameter("title");
String content = req.getParameter("content");
if (title == null || content == null || "".equals(title) || "".equals(content)) {
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write("标题或者正文为空");
return;
}
// 3. 构造 Blog 对象, 并且插入到数据库中.
Blog blog = new Blog();
blog.setTitle(title);
blog.setContent(content);
blog.setUserId(user.getUserId());
// 由于在 sql 插入数据的时候, 已经使用 sql 自带的 now 获取当前时间, 不需要此处代码中手动设置时间了.
// blog.setPostTime(new Timestamp(System.currentTimeMillis()));
BlogDao blogDao = new BlogDao();
blogDao.insert(blog);
// 4. 跳转到博客列表页
resp.sendRedirect("blog_list.html");
}
- 前端代码:
<form action="blog" method="post">
<!-- 标题编辑区 -->
<div class="title">
<input type="text" id="title-input" name="title">
<input type="submit" id="submit">
</div>
<!-- 博客编辑器 -->
<!-- 把 md 编辑器放到这个 div 中 -->
<div id="editor">
<textarea name="content" style="display: none;"></textarea>
</div>
</form>