目录
1. 项目介绍
使用SSM框架(Spring、Spring MVC、MyBatis框架)实现⼀个简单的博客系统共5个页面:
- 用户登录
- 博客发表页
- 博客编辑页
- 博客列表页
- 博客详情页
功能描述:
用户登录成功后,可以查看所有⼈的博客。点击 <<查看全文>> 可以查看该博客的正⽂内容。如果该博客作者为当前登录用户,可以完成博客的修改和删除操作,以及发表新博客。
页面预览:
用户登录页面:
博客列表页:
博客详情页:
博客编辑页:
2. 项目准备
2.1 数据库准备
-- 建表SQL
create database if not exists java_blog_spring charset utf8mb4;
-- 用户表
DROP TABLE IF EXISTS java_blog_spring.user;
CREATE TABLE java_blog_spring.user(
`id` INT NOT NULL AUTO_INCREMENT,
`user_name` VARCHAR ( 128 ) NOT NULL,
`password` VARCHAR ( 128 ) NOT NULL,
`github_url` VARCHAR ( 128 ) NULL,
`delete_flag` TINYINT ( 4 ) NULL DEFAULT 0,
`create_time` DATETIME DEFAULT now(),
`update_time` DATETIME DEFAULT now(),
PRIMARY KEY ( id ),
UNIQUE INDEX user_name_UNIQUE ( user_name ASC )) ENGINE = INNODB DEFAULT
CHARACTER
SET = utf8mb4 COMMENT = '用户表';
-- 博客表
drop table if exists java_blog_spring.blog;
CREATE TABLE java_blog_spring.blog (
`id` INT NOT NULL AUTO_INCREMENT,
`title` VARCHAR(200) NULL,
`content` TEXT NULL,
`user_id` INT(11) NULL,
`delete_flag` TINYINT(4) NULL DEFAULT 0,
`create_time` DATETIME DEFAULT now(),
`update_time` DATETIME DEFAULT now(),
PRIMARY KEY (id))
ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '博客表';
-- 新增用户信息
insert into java_blog_spring.user (user_name,password,github_url)
values("zhangsan","123456","https://gitee.com/little-fishs-code-house/java-ee2");
insert into java_blog_spring.user (user_name,password,github_url)
values("lisi","123456","https://gitee.com/little-fishs-code-house/java-ee2");
insert into java_blog_spring.blog (title,content,user_id)
values("第⼀篇博客","1我是博客正我是博客正文,这是我的第一篇博客",1);
insert into java_blog_spring.blog (title,content,user_id)
values("第⼆篇博客","2我是博客正文我是博客正文,这是我的第二篇博客",2);
2.2 创建项目
创建SpringBoot项目, 添加Spring MVC 和MyBatis对应依赖。
2.3 配置文件
在application.yml文件中配置数据库相关的信息:
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/java_blog_spring?characterEncoding=utf8&useSSL=false
username: root
password: '0124'
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
configuration: # 配置打印 MyBatis日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #配置打印sql语句
map-underscore-to-camel-case: true #配置驼峰自动转换
mapper-locations: classpath:mapper/**Mapper.xml
#设置日志文件的文件名
logging:
file:
name: spring-blog.log
2.4 准备前端页面
把博客系统静态页⾯拷贝到static目录下:
2.5 测试
访问前端页面:http://127.0.0.1:8080/blog_login.html
前端页面可以正常显示,说明项目初始化成功。
3. 项目公共模块
项⽬分为控制层(Controller), 服务层(Service), 持久层(Mapper)。各层之间的调⽤关系如下:
我们先根据需求完成实体类和公共层代码的编写。
3.1 实体类
@Data
public class UserInfo {
private Integer id;
private String userName;
private String password;
private String githubUrl;
private Integer deleteFlag;
private Date createTime;
private Date updateTime;
}
@Data
public class BlogInfo {
private Integer id;
private String title;
private String content;
private Integer userId;
private Integer deleteFlag;
private Date createTime;
private Date updateTime;
}
3.2 公共层
统一功能处理:
- 拦截器
- 统一结果返回
- 统一异常处理
拦截器内容在后面用户登录时介绍,这部分我们先写统一结果返回和统一异常处理。
- 统一结果返回为实体类:
- code:业务状态码
- 200:业务处理成功
- -1:业务处理失败
- -2:用户未登录
- errMsg:业务处理失败时,返回的错误信息
- data:业务返回的数据
- code:业务状态码
定义业务状态码:
public class Constant {
public final static Integer SUCCESS_CODE = 200;// 成功
public final static Integer FAIL_CODE = -1;// 失败
public final static Integer UNLOGIN_CODE = -2;// 未登录
}
/**
* 定义接口的统一返回结果
*/
@Data
public class Result {
private int code;// 业务码 定义:200-成功 -1-失败 -2-未登录
private String errMsg;// 接口发生错误的信息
private Object data;// 接口的返回真实结果
/**
* 接口返回成功时
*/
public static Result success(Object data) {
Result result = new Result();
result.setCode(Constant.SUCCESS_CODE);
result.setErrMsg("");
result.setData(data);
return result;
}
/**
* 接口返回失败时
*/
public static Result fail(String errMsg) {
Result result = new Result();
result.setCode(Constant.FAIL_CODE);
result.setErrMsg(errMsg);
result.setData(null);
return result;
}
public static Result fail(String errMsg, Object data) {
Result result = new Result();
result.setCode(Constant.FAIL_CODE);
result.setErrMsg(errMsg);
result.setData(data);
return result;
}
/**
* 用户未登录时
*/
public static Result unLogin(String errMsg) {
Result result = new Result();
result.setCode(Constant.UNLOGIN_CODE);
result.setErrMsg("用户未登录");
result.setData(null);
return result;
}
}
- 统一返回结果:
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
// 对于哪些方法执行统一结果返回,可以自己定义,不写默认所有方法
return true;
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 统一结果返回的具体逻辑
if (body instanceof Result) {
return body;
}
// 如果接口返回为String类型,进行单独处理
if (body instanceof String) {
// 转为JSON类型
// ObjectMapper objectMapper = new ObjectMapper();// 也可以直接注入进来
return objectMapper.writeValueAsString(Result.success(body));
}
return Result.success(body);
}
}
- 统一异常处理:
@ResponseBody
@ControllerAdvice
public class ErrorHandler {
@ExceptionHandler
public Result handler(Exception e) {
return Result.fail(e.getMessage());
}
// 两种写法
// @ExceptionHandler(NullPointerException.class)
// public Result handler(Exception e) {
// return Result.fail(e.getMessage());
// }
//
// @ExceptionHandler
// public Result handler(NullPointerException e) {
// return Result.fail(e.getMessage());
// }
}
4. 业务代码
4.1 持久层代码
根据需求, 先⼤致计算有哪些DB相关操作, 完成持久层初步代码, 后续再根据业务需求进⾏完善。
每个页面需要实现的接口:
- 用户登录页
- 用户登录:根据用户名和密码,判断是否正确。
具体实现:根据用户名,查找用户信息,对比密码是否正确。
DB操作:根据用户名,查询用户信息。
- 博客列表页
- 查询用户信息:根据用户ID,查询用户信息。
DB操作:根据用户ID,查询用户信息。
- 获取所有博客列表:查询所有博客。
DB操作:查询所有博客。
- 博客详情页
- 查询作者信息:
- 根据博客,拿到作者ID
- 根据作者ID,获取作者信息
- 查询作者信息:
DB操作:根据用户ID,查询用户信息。
- 查询博客详情:根据博客ID,查询博客信息
DB操作:根据博客ID,查询博客信息。
- 删除博客:根据博客ID,删除博客(修改delete_flag=1)
DB操作:根据博客ID,修改博客信息。
- 博客编辑页
- 修改博客:根据博客ID,修改博客信息。
DB操作:根据博客ID,修改博客信息。
- 发表博客:添加新的博客信息。
DB操作:插入新的博客数据。
总结:
用户表:
- 根据用户名,查询用户信息
- 根据用户ID,查询用户信息
博客表:
- 查询博客列表
- 根据博客ID,查询博客信息
- 根据博客ID,修改博客信息
- 插入博客
根据以上分析, 来实现持久层的代码:
@Mapper
public interface UserMapper {
// 根据用户名,查找用户信息
// *可以替换成具体字段
@Select("select * from user where user_name = #{userName} and delete_flag = 0")
UserInfo selectByName(String userName);
// 根据用户ID,查询用户信息
@Select("select * from user where id = #{userId} and delete_flag = 0")
UserInfo selectById(Integer userId);
}
@Mapper
public interface BlogMapper {
// 查询博客列表
@Select("select * from blog where delete_flag = 0 order by create_time desc")
List<BlogInfo> selectAllBlog();
// 根据博客Id,查询博客信息
@Select("select * from blog where id = #{blogId} and delete_flag = 0")
BlogInfo selectById(Integer blogId);
// 根据博客ID,修改博客信息(包含修改和删除,根据参数决定修改什么)
Integer updateBlog(BlogInfo blogInfo);
// 插入博客
@Insert("insert into blog (title, content, user_id) values(#{title}, #{content}, #{userId})")
Integer insertBlog(BlogInfo blogInfo);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiaoyu.blog.mapper.BlogMapper">
<update id="updateBlog">
update blog
<set>
<if test="title != null">
title = #{title},
</if>
<if test="content != null">
content = #{content},
</if>
<if test="deleteFlag != null">
delete_flag = #{deleteFlag}
</if>
</set>
where id = #{id}
</update>
</mapper>
书写测试用例, 简单进行单元测试:
@SpringBootTest
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void selectByName() {
System.out.println(userMapper.selectByName("zhangsan"));
}
@Test
void selectById() {
System.out.println(userMapper.selectById(2));
}
}
运行程序,观察日志:
- selectByName接口返回结果:
- selectById接口返回结果:
接口返回均正确。
@SpringBootTest
class BlogMapperTest {
@Autowired
private BlogMapper blogMapper;
@Test
void selectAllBlog() {
System.out.println(blogMapper.selectAllBlog());
}
@Test
void selectById() {
System.out.println(blogMapper.selectById(1));
}
@Test
void updateBlog() {
BlogInfo blogInfo = new BlogInfo();
blogInfo.setTitle("update更新标题");
blogInfo.setContent("update更新内容");
blogInfo.setId(1);
System.out.println(blogMapper.updateBlog(blogInfo));
}
@Test
void deleteBlog() {
BlogInfo blogInfo = new BlogInfo();
blogInfo.setId(2);//第二条数据
blogInfo.setDeleteFlag(1);
System.out.println(blogMapper.updateBlog(blogInfo));
}
@Test
void insertBlog() {
BlogInfo blogInfo = new BlogInfo();
blogInfo.setTitle("insert第三篇博客");
blogInfo.setContent("insert博客内容");
blogInfo.setUserId(2);
System.out.println(blogMapper.insertBlog(blogInfo));
}
}
- 测试接口selectAllBlog,观察日志:
- 测试接口selectById,观察日志:
- 测试接口updateBlog,观察日志:
更新前数据库信息为:
更新后数据库信息为:
- 测试接口deleteBlog,观察日志:
更新后数据库信息为:
- 测试接口insertBlog,观察日志:
更新后数据库信息为:
接口均测试成功。
4.2 实现博客列表
约定前后端交互接口:
[请求]
/blog/getList
[响应]
{
"code": 200,
"errMsg": "",
"data": [
{
"id": 4,
"title": "insert第三篇博客",
"content": "insert博客内容",
"userId": 2,
"deleteFlag": 0,
"createTime": "2024-04-26T10:49:52.000+00:00",
"updateTime": "2024-04-26T10:49:52.000+00:00"
},
...
]
}
客户端给服务器发送⼀个 /blog/getList 这样的 HTTP 请求, 服务器给客户端返回了⼀个 JSON 格式的数据。
实现服务器代码:
@RestController
@RequestMapping("/blog")
public class BlogController {
@Autowired
private BlogService blogService;
@RequestMapping("/getList")
public List<BlogInfo> queryBlogList() {
return blogService.queryBlogList();
}
}
@Service
public class BlogService {
@Autowired
private BlogMapper blogMapper;
public List<BlogInfo> queryBlogList() {
return blogMapper.selectAllBlog();
}
}
启动程序,验证服务器能否返回正确数据:访问http://127.0.0.1:8080/blog/getList,响应结果为:
实现客户端代码:
我们希望访问blog_list.html这个页面,在页面加载的时候就去调用后端接口,返回数据,并进行填充,所以修改blog_list.html,删除之前写死的博客内容(即 <divclass=“blog”> ),并新增 js 代码处理ajax 请求。
- 使用 ajax 给服务器发送 HTTP 请求。
- 服务器返回的响应是⼀个 JSON 格式的数据, 根据这个响应数据使用 DOM API 构造页⾯内容。
- 响应中的 createTime 字段为 ms 级时间戳, 需要转成格式化日期。
- 跳转到博客详情页的 url 形如 blog_detail.html?blogId=1 这样就可以让博客详情页知道当前是要访问哪篇博客。
<script>
$.ajax({
type: "get",
url: "/blog/getList",
success: function(result) {
// 如果result.code == 200 && result.data == null,页面可以提示:当前还没有任何博客,快去写博客吧!并进行页面跳转
if (result.code == 200 && result.data != null) {
var finalHtml = "";
// 页面展示
// 循环拼接result.data里面的数据
for (var blog of result.data) {
finalHtml += '<div class="blog">';
finalHtml += '<div class="title">'+ blog.title +'</div>';
finalHtml += '<div class="date">'+ blog.createTime +'</div>';
finalHtml += '<div class="desc">'+ blog.content +'</div>';
finalHtml += '<a class="detail" href="blog_detail.html?blogId='+ blog.id +'">查看全文>></a>';
finalHtml += '</div>';
}
$(".right").html(finalHtml);
}
}
});
</script>
运行程序,通过url:http://127.0.0.1:8080/blog_list.html访问服务器:
我们发现页面的日期显示为ms 级时间戳, 需要转成格式化日期,从后端对日期进行处理:
SimpleDateFormat 格式参考官⽅⽂档:
/**
* 日期工具类
*/
public class DateUtils {
public static String formatDate(Date date) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
return simpleDateFormat.format(date);
}
}
修改BlogInfo实体类:
重新启动程序,通过 URL http://127.0.0.1:8080/blog_list.html 访问服务器, 验证效果:
4.3 实现博客详情
⽬前点击博客列表⻚的 “查看全⽂” , 能进⼊博客详情⻚, 但是这个博客详情⻚是写死的内容。我们期望能够根据当前的博客 id 从服务器动态获取博客内容。
约定前后端交互接口:
[请求]
/blog/getBlogDetail?blogId=1
[响应]
{
"code": 200,
"msg": "",
"data": {
"id": 1,
"title": "第⼀篇博客",
"content": "111我是博客正⽂我是博客正⽂我是博客正⽂",
"userId": 1,
"deleteFlag": 0,
"createTime": "2023-10-21 16:56:57",
"updateTime": "2023-10-21T08:56:57.000+00:00"
}
}
实现服务器代码:
BlogController 中添加queryBlogDetail ⽅法:
@RequestMapping("/getBlogDetail")
public BlogInfo queryBlogDetail(Integer blogId) {
return blogService.queryBlogDetail(blogId);
}
在BlogService 中添加queryBlogDetail⽅法:
public BlogInfo queryBlogDetail(Integer blogId) {
return blogMapper.selectById(blogId);
}
运行程序,访问http://127.0.0.1:8080/blog/getBlogDetail?blogId=3,测试后端接口的返回结果:
实现客户端代码:
修改 blog_detail.html
- 根据当前页面 URL 中的 blogId 参数(使用 location.search 即可得到形如 ?blogId=3 的数据), 给服务器发送 GET /blog 请求。
- 根据获取到的响应数据, 显示在页面上
- 修改html⻚⾯, 去掉原来写死的博客标题, ⽇期和正⽂部分:
<div class="content">
<div class="title"></div>
<div class="date"></div>
<div class="detail"></div>
<div class="operating">
<button onclick="window.location.href='blog_update.html'">编辑</button>
<button onclick="deleteBlog()">删除</button>
</div>
</div>
- 完善 js 代码, 从服务器获取博客详情数据:
// 获取博客详情
$.ajax({
type: "get",
url: "/blog/getBlogDetail"+location.search,
success: function(result) {
if (result.code == 200 && result.data != null) {
var blog = result.data;
$(".right .content .title").text(blog.title);
$(".right .content .date").text(blog.createTime);
$(".right .content .detail").text(blog.content);
}
}
});
运行程序,访问http://127.0.0.1:8080/blog_detail.html?blogId=3,观察页面返回的数据:
未完,后见下篇文章!