Spring Boot 博客项目深度分析报告
本文档对 spring-blog-demo-2
项目进行全面深入的分析,涵盖项目概述、核心功能模块、关键技术点、数据库设计以及部署与运行等方面。
阅读本文章需要1.5小时
1. 项目概述与框架
1.1 项目主要功能和目标
spring-blog-demo-2
项目是一个基于 Spring Boot 技术栈实现的博客系统。其主要目标是提供一个简洁、易用的在线内容发布和管理平台。核心功能预计包括用户注册与登录、个人信息管理、博客文章的发布、编辑、删除、列表展示以及详情查看等。项目旨在演示如何使用现代 Java Web 技术栈构建一个功能完整的 Web 应用程序。
1.2 项目核心框架和技术
根据项目配置文件 (pom.xml
) 和代码结构分析,该项目采用了以下核心框架和技术:
- 后端框架:
- Spring Boot (3.4.5): 作为项目的基础框架,简化了 Spring 应用的初始搭建以及开发过程,提供了自动配置、起步依赖等特性。
- Spring MVC: 用于构建 Web 应用程序,处理 HTTP 请求和响应,实现 RESTful API 接口。
- 数据持久层:
- MyBatis-Plus (3.5.5 for Spring Boot 3): 一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,简化了 CRUD 操作,提供了代码生成器、分页插件等功能。
- MySQL Connector/J: 用于 Java 应用程序连接 MySQL 数据库。
- 工具库与辅助技术:
- Lombok: 通过注解简化 Java 代码,如自动生成 Getter/Setter, Constructor,
toString()
等方法,减少样板代码。 - JSON Web Token (JWT - jjwt-api, jjwt-impl, jjwt-jackson 0.11.5): 用于实现用户认证和授权机制,通过 Token 进行无状态的身份验证。
- Spring Boot Starter Validation: 提供数据校验功能,确保输入数据的有效性。
- Thymeleaf (推测,通常与 Spring Boot 结合用于服务端渲染,待确认): 虽然
pom.xml
中未直接列出spring-boot-starter-thymeleaf
,但通常博客类项目会使用模板引擎渲染前端页面。后续分析 HTML 文件可以确认。
- Lombok: 通过注解简化 Java 代码,如自动生成 Getter/Setter, Constructor,
- 开发与构建:
- Java 17: 项目使用的 JDK 版本。
- Maven: 作为项目管理和构建工具,负责依赖管理、项目编译、打包等。
1.3 项目整体架构和模块划分
该项目遵循了典型的分层架构设计模式,主要可以划分为以下几个层次和模块:
表现层 (Controller):
- 位于
org.example.springblogdemo2.controller
包下。 - 负责接收前端 HTTP 请求,调用业务逻辑层处理请求,并返回响应数据给前端。
- 主要包含
UserController.java
(处理用户相关操作) 和BlogController.java
(处理博客文章相关操作)。
- 位于
业务逻辑层 (Service):
- 接口定义在
org.example.springblogdemo2.service
包下 (如UserService.java
,BlogService.java
)。 - 具体实现在
org.example.springblogdemo2.service.impl
包下 (如UserServiceImpl.java
,BlogServiceImpl.java
)。 - 负责处理核心业务逻辑,对数据进行加工处理,并调用数据访问层与数据库交互。
- 接口定义在
数据访问层 (Mapper/DAO):
- 位于
org.example.springblogdemo2.mapper
包下 (如UserInfoMapper.java
,BlogInfoMapper.java
)。 - 基于 MyBatis-Plus 实现,定义了与数据库表进行交互的接口方法,负责数据的持久化操作。
- 位于
数据对象层 (POJO/Entity/DTO):
- 位于
org.example.springblogdemo2.pojo
包下,并进一步细分为:dataobject
:存放与数据库表结构对应的实体类 (如UserInfo.java
,BlogInfo.java
)。request
:存放用于接收前端请求参数的数据传输对象 (DTO),如UserLoginRequest.java
,AddBlogRequest.java
。response
:存放用于向前端返回结果的数据传输对象 (DTO),如UserLoginResponse.java
,BlogInfoResponse.java
, 以及通用的结果封装类Result.java
。
- 位于
通用模块 (Common):
- 位于
org.example.springblogdemo2.common
包下,包含项目通用的组件和工具类:advice
:全局异常处理 (ExceptionAdvice.java
) 和统一响应体封装 (ResponseAdvice.java
)。config
:项目配置类,如WebConfig.java
用于配置拦截器等。constants
:定义项目中使用的常量 (Constants.java
)。exception
:自定义业务异常类 (BlogException.java
)。interceptor
:拦截器,如LoginInterceptor.java
用于实现登录验证。util
:工具类集合,如BeanTransUtils.java
(对象属性复制)、DateUtils.java
(日期处理)、JwtUtils.java
(JWT 生成与校验)、SecurityUtils.java
(可能包含密码加密等安全相关工具)。
- 位于
枚举类 (Enums):
- 位于
org.example.springblogdemo2.enums
包下,如ResultCodeEnum.java
定义了操作结果的状态码和消息。
- 位于
启动类:
org.example.springblogdemo2.SpringBlogDemo2Application.java
:Spring Boot 应用的入口启动类。
整体来看,项目结构清晰,模块划分合理,符合 Spring Boot 项目的典型开发规范,易于理解和维护。
2. 核心功能模块分析
2.1 模块一:用户模块
用户模块是博客系统的基础,负责处理用户注册 (本项目中似乎未直接体现注册功能,而是预置用户)、登录、信息获取等功能。
2.1.1 代码实现
Controller (UserController.java
):
package org.example.springblogdemo2.controller;
import jakarta.annotation.Resource;
import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j;
import org.example.springblogdemo2.pojo.request.UserLoginRequest;
import org.example.springblogdemo2.pojo.response.UserInfoResponse;
import org.example.springblogdemo2.pojo.response.UserLoginResponse;
import org.example.springblogdemo2.service.UserService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
@Resource(name = "userServiceImpl")
private UserService userService;
@RequestMapping("/login")
public UserLoginResponse login(@RequestBody @Validated UserLoginRequest userLoginRequest) {
log.info("用户登录,用户名:" + userLoginRequest.getUserName());
return userService.checkPassword(userLoginRequest);
}
@RequestMapping("/getUserInfo")
public UserInfoResponse getUserInfo(@NotNull(message = "userId 不能为 null") Integer userId) {
log.info("获取用户信息,userId: {}", userId);
return userService.getUserInfo(userId);
}
@RequestMapping("/getAuthorInfo")
public UserInfoResponse getAuthorInfo(@NotNull(message = "blogId 不能为 null") Integer blogId) {
log.info("获取当前文章作者的用户信息,blogId: {}", blogId);
return userService.getAuthorInfo(blogId);
}
}
Service Interface (UserService.java
):
package org.example.springblogdemo2.service;
import org.example.springblogdemo2.pojo.request.UserLoginRequest;
import org.example.springblogdemo2.pojo.response.UserInfoResponse;
import org.example.springblogdemo2.pojo.response.UserLoginResponse;
public interface UserService {
UserLoginResponse checkPassword(UserLoginRequest userLoginRequest);
UserInfoResponse getUserInfo(Integer userId);
UserInfoResponse getAuthorInfo(Integer blogId);
}
Service Implementation (UserServiceImpl.java
):
package org.example.springblogdemo2.service.impl;
// ... imports ...
import org.example.springblogdemo2.common.util.JwtUtils;
import org.example.springblogdemo2.common.util.SecurityUtils;
import org.example.springblogdemo2.mapper.UserInfoMapper;
import org.example.springblogdemo2.pojo.dataobject.BlogInfo;
import org.example.springblogdemo2.pojo.dataobject.UserInfo;
import org.example.springblogdemo2.pojo.request.UserLoginRequest;
import org.example.springblogdemo2.pojo.response.UserInfoResponse;
import org.example.springblogdemo2.pojo.response.UserLoginResponse;
import org.example.springblogdemo2.service.BlogService;
import org.example.springblogdemo2.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserInfoMapper userInfoMapper;
@Resource(name = "blogServiceImpl")
private BlogService blogService;
@Override
public UserLoginResponse checkPassword(UserLoginRequest userLoginRequest) {
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda()
.eq(UserInfo::getUserName, userLoginRequest.getUserName())
.eq(UserInfo::getDeleteFlag, 0);
UserInfo userInfo = null;
try {
userInfo = userInfoMapper.selectOne(queryWrapper);
} catch (Exception e) {
throw new BlogException("数据库查询失败:" + e.getMessage());
}
if (userInfo == null) {
throw new BlogException("用户不存在");
}
if (!SecurityUtils.verify(userLoginRequest.getPassword(), userInfo.getPassword())) {
throw new BlogException("用户密码错误");
}
Map<String ,Object> map = new HashMap<>();
map.put("userId", userInfo.getId());
map.put("name", userInfo.getUserName());
String token = JwtUtils.genToken(map);
return new UserLoginResponse(userInfo.getId(), token);
}
@Override
public UserInfoResponse getUserInfo(Integer userId) {
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda()
.eq(UserInfo::getDeleteFlag, 0)
.eq(UserInfo::getId, userId);
UserInfo userInfo = userInfoMapper.selectOne(queryWrapper);
return BeanTransUtils.trans(userInfo); // Converts UserInfo to UserInfoResponse
}
@Override
public UserInfoResponse getAuthorInfo(Integer blogId) {
BlogInfo blogInfo = blogService.getBlogInfo(blogId);
if (blogInfo == null || blogInfo.getUserId() < 1) {
throw new BlogException("博客不存在");
}
return getUserInfo(blogInfo.getUserId());
}
}
Data Object (UserInfo.java
):
package org.example.springblogdemo2.pojo.dataobject;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.time.LocalDate;
@Data
public class UserInfo {
@TableId(type = IdType.AUTO)
private Integer id;
private String userName;
private String password; // Stored as hashed value
private String githubUrl;
private Integer deleteFlag;
private LocalDate createTime;
private LocalDate updateTime;
}
Request DTO (UserLoginRequest.java
):
package org.example.springblogdemo2.pojo.request;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
@Data
public class UserLoginRequest {
@NotNull(message = "用户名不能为 null")
private String userName;
@NotNull(message = "密码不能为 null")
@Length(min = 5, max = 11)
private String password;
}
Response DTOs (UserInfoResponse.java
, UserLoginResponse.java
):
// UserInfoResponse.java
package org.example.springblogdemo2.pojo.response;
import lombok.Data;
@Data
public class UserInfoResponse {
private Integer id;
private String userName;
private String githubUrl;
}
// UserLoginResponse.java
package org.example.springblogdemo2.pojo.response;
import lombok.AllArgsConstructor;
import lombok.Data;
@AllArgsConstructor
@Data
public class UserLoginResponse {
private Integer userId;
private String token;
}
Mapper (UserInfoMapper.java
):
package org.example.springblogdemo2.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.example.springblogdemo2.pojo.dataobject.UserInfo;
@Mapper
public interface UserInfoMapper extends BaseMapper<UserInfo> {
}
2.1.2 设计思路/逻辑
用户模块的核心逻辑围绕用户身份验证和信息查询展开:
用户登录 (
/user/login
):- 接收客户端通过 JSON 格式提交的用户名 (
userName
) 和密码 (password
)。 - 使用
@Validated
对UserLoginRequest
对象进行参数校验 (用户名非空,密码非空且长度在5-11位之间)。 UserServiceImpl.checkPassword
方法处理登录逻辑:- 根据用户名查询
user_info
表中未被删除 (delete_flag = 0
) 的用户记录。 - 如果用户不存在,抛出
BlogException
。 - 如果用户存在,使用
SecurityUtils.verify
方法校验客户端提交的密码与数据库中存储的加密密码是否匹配。本项目中SecurityUtils
可能是对密码哈希和校验的封装 (例如 BCrypt)。 - 密码校验成功后,将用户 ID (
userId
) 和用户名 (name
) 存入一个Map
。 - 调用
JwtUtils.genToken
方法,使用该Map
作为 payload 生成 JWT (JSON Web Token)。 - 返回包含用户 ID 和生成的 JWT 的
UserLoginResponse
对象。
- 根据用户名查询
- 接收客户端通过 JSON 格式提交的用户名 (
获取用户信息 (
/user/getUserInfo
):- 接收客户端传递的
userId
参数。 UserServiceImpl.getUserInfo
方法处理:- 根据
userId
查询user_info
表中未被删除的用户记录。 - 使用
BeanTransUtils.trans
方法将查询到的UserInfo
(包含密码等敏感信息) 转换为UserInfoResponse
(只包含 ID、用户名、GitHub URL 等公开信息) 后返回。
- 根据
- 接收客户端传递的
获取文章作者信息 (
/user/getAuthorInfo
):- 接收客户端传递的
blogId
参数。 UserServiceImpl.getAuthorInfo
方法处理:- 首先调用
BlogService.getBlogInfo
方法,根据blogId
获取博客文章信息,从而得到文章的作者 ID (userId
)。 - 如果博客不存在或作者 ID 无效,抛出
BlogException
。 - 然后调用本模块的
getUserInfo
方法,根据获取到的作者 ID 查询并返回作者的公开信息 (UserInfoResponse
)。
- 首先调用
- 接收客户端传递的
关键决策点:
- 密码存储: 数据库中存储的是加密后的密码,登录时进行密文比对,增强了安全性。具体加密方式由
SecurityUtils
实现。 - 身份验证: 采用 JWT 进行无状态身份验证。登录成功后颁发 Token,后续请求通过 Token 验证用户身份 (通常由拦截器
LoginInterceptor
实现,后续会分析)。 - 数据校验: 使用 Spring Validation (
@Validated
,@NotNull
,@Length
) 对输入参数进行校验,保证数据的合法性。 - 数据转换: 使用
BeanTransUtils
工具类将包含敏感信息的UserInfo
DO (Data Object) 转换为不含敏感信息的UserInfoResponse
DTO (Data Transfer Object),避免敏感数据泄露到前端。 - 异常处理: 统一使用自定义的
BlogException
抛出业务异常,便于全局异常处理器 (ExceptionAdvice
) 捕获和处理。
2.1.3 功能说明
用户模块主要实现了以下功能:
- 用户登录: 验证用户凭据,成功后生成并返回 JWT,用于后续的身份认证。
- 获取指定用户信息: 根据用户 ID 查询并返回用户的基本公开信息。
- 获取文章作者信息: 根据博客文章 ID,间接查询并返回该文章作者的基本公开信息。
该模块为整个博客系统的用户身份管理和信息展示提供了基础支撑。
2.2 模块二:博客文章模块
博客文章模块是系统的核心,负责博客文章的创建、读取、更新和删除 (CRUD) 操作,以及文章列表的展示。
2.2.1 代码实现
Controller (BlogController.java
):
package org.example.springblogdemo2.controller;
import jakarta.annotation.Resource;
import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j;
import org.example.springblogdemo2.pojo.request.AddBlogRequest;
import org.example.springblogdemo2.pojo.request.UpdateRequest;
import org.example.springblogdemo2.pojo.response.BlogInfoResponse;
import org.example.springblogdemo2.service.BlogService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Slf4j
@RequestMapping("/blog")
@RestController
public class BlogController {
@Resource(name = "blogServiceImpl")
private BlogService blogService;
@RequestMapping("/getList")
public List<BlogInfoResponse> getList() {
log.info("获取博客列表~");
return blogService.getList();
}
@RequestMapping("/getBlogDetail")
public BlogInfoResponse getBlogDetail(@NotNull(message = "blogId 不能为 null") Integer blogId) {
log.info("获取博客详情,blogId: {}", blogId);
return blogService.getBlogDetail(blogId);
}
@RequestMapping("/addBlog")
public Boolean addBlog(@RequestBody @Validated AddBlogRequest addBlogRequest) {
log.info("发布博客,userId: {}, title: {}", addBlogRequest.getUserId(), addBlogRequest.getTitle());
return blogService.addBlog(addBlogRequest);
}
@RequestMapping("/update")
public Boolean update(@RequestBody @Validated UpdateRequest updateRequest) {
log.info("更新博客,request:{}", updateRequest);
return blogService.updateBlog(updateRequest);
}
@RequestMapping("/delete")
public Boolean delete(@NotNull(message = "blogId 不能为 null") Integer blogId) {
log.info("删除博客,blogId: {}", blogId);
return blogService.deleteBlog(blogId);
}
}
Service Interface (BlogService.java
):
package org.example.springblogdemo2.service;
import org.example.springblogdemo2.pojo.dataobject.BlogInfo;
import org.example.springblogdemo2.pojo.request.AddBlogRequest;
import org.example.springblogdemo2.pojo.request.UpdateRequest;
import org.example.springblogdemo2.pojo.response.BlogInfoResponse;
import java.util.List;
public interface BlogService {
List<BlogInfoResponse> getList();
BlogInfoResponse getBlogDetail(Integer blogId);
BlogInfo getBlogInfo(Integer blogId); // Internal method to get raw BlogInfo
boolean addBlog(AddBlogRequest addBlogRequest);
Boolean updateBlog(UpdateRequest updateRequest);
Boolean deleteBlog(Integer blogId);
}
Service Implementation (BlogServiceImpl.java
):
package org.example.springblogdemo2.service.impl;
// ... imports ...
import org.example.springblogdemo2.common.constants.Constants;
import org.example.springblogdemo2.mapper.BlogInfoMapper;
import org.example.springblogdemo2.pojo.dataobject.BlogInfo;
import org.example.springblogdemo2.pojo.request.AddBlogRequest;
import org.example.springblogdemo2.pojo.request.UpdateRequest;
import org.example.springblogdemo2.pojo.response.BlogInfoResponse;
import org.example.springblogdemo2.service.BlogService;
import org.example.springblogdemo2.common.util.BeanTransUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
public class BlogServiceImpl implements BlogService {
@Autowired
private BlogInfoMapper blogInfoMapper;
@Override
public List<BlogInfoResponse> getList() {
QueryWrapper<BlogInfo> queryWrapper =new QueryWrapper<>();
queryWrapper.lambda().eq(BlogInfo::getDeleteFlag, Constants.BLOG_NORMAL);
List<BlogInfo> blogInfos = blogInfoMapper.selectList(queryWrapper);
return blogInfos.stream()
.map(blogInfo -> BeanTransUtils.trans(blogInfo))
.collect(Collectors.toList());
}
@Override
public BlogInfoResponse getBlogDetail(Integer blogId) {
return BeanTransUtils.trans(getBlogInfo(blogId));
}
@Override
public BlogInfo getBlogInfo(Integer blogId){
QueryWrapper<BlogInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda()
.eq(BlogInfo::getDeleteFlag, Constants.BLOG_NORMAL)
.eq(BlogInfo::getId, blogId);
return blogInfoMapper.selectOne(queryWrapper);
}
@Override
public boolean addBlog(AddBlogRequest addBlogRequest) {
BlogInfo blogInfo = new BlogInfo();
BeanUtils.copyProperties(addBlogRequest, blogInfo);
try {
Integer result = blogInfoMapper.insert(blogInfo);
return result == 1;
} catch (Exception e) {
log.error("博客插入失败,e: ", e);
throw new BlogException("内部错误,请联系管理员");
}
}
@Override
public Boolean updateBlog(UpdateRequest updateRequest) {
BlogInfo blogInfo = BeanTransUtils.trans(updateRequest); // Converts UpdateRequest to BlogInfo
try {
Integer result = blogInfoMapper.updateById(blogInfo);
return result == 1;
} catch (Exception exception) {
log.error("编辑博客失败,e: ", exception);
throw new BlogException("内部错误,请联系管理员");
}
}
@Override
public Boolean deleteBlog(Integer blogId) {
BlogInfo blogInfo = new BlogInfo();
blogInfo.setId(blogId);
blogInfo.setDeleteFlag(Constants.BLOG_DELETE); // Logical delete
try {
Integer result = blogInfoMapper.updateById(blogInfo);
return result == 1;
} catch (Exception exception) {
log.error("删除博客失败,e: ", exception);
throw new BlogException("内部错误,请联系管理员");
}
}
}
Data Object (BlogInfo.java
):
package org.example.springblogdemo2.pojo.dataobject;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.Date;
@Data
public class BlogInfo {
@TableId(type = IdType.AUTO)
private Integer id;
private String title;
private String content;
private Integer userId; // Author's ID
private Integer deleteFlag; // 0 for normal, 1 for deleted
private Date createTime;
private LocalDateTime updateTime;
}
Request DTOs (AddBlogRequest.java
, UpdateRequest.java
):
// AddBlogRequest.java
package org.example.springblogdemo2.pojo.request;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class AddBlogRequest {
@NotNull(message = "userId 不能为 null")
private Integer userId;
@NotBlank(message = "标题不能为空")
private String title;
@NotBlank(message = "内容不能为空")
private String content;
}
// UpdateRequest.java
package org.example.springblogdemo2.pojo.request;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class UpdateRequest {
@NotNull(message = "id 不能为 null")
private Integer id;
private String title;
private String content;
}
Response DTO (BlogInfoResponse.java
):
package org.example.springblogdemo2.pojo.response;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.example.springblogdemo2.common.util.DateUtils;
import java.util.Date;
@Data
public class BlogInfoResponse {
private Integer id;
private String title;
private String content;
private Integer userId;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date createTime;
// Custom getter to format date string
public String getCreateTime() {
return DateUtils.dateFormat(createTime);
}
// This method seems to be for display purposes, might not be directly from DB field
public String getCurrentTime() {
return DateUtils.dateFormat(new Date());
}
}
Mapper (BlogInfoMapper.java
):
package org.example.springblogdemo2.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.example.springblogdemo2.pojo.dataobject.BlogInfo;
@Mapper
public interface BlogInfoMapper extends BaseMapper<BlogInfo> {
}
2.2.2 设计思路/逻辑
博客文章模块提供了文章管理的核心功能:
获取博客列表 (
/blog/getList
):BlogServiceImpl.getList
方法处理:- 查询
blog_info
表中所有未被逻辑删除 (delete_flag = Constants.BLOG_NORMAL
) 的博客文章。 - 将查询到的
BlogInfo
列表通过stream().map()
转换为BlogInfoResponse
列表。BeanTransUtils.trans
用于此转换,可能还包含日期格式化等处理。 - 返回
BlogInfoResponse
列表。
- 查询
获取博客详情 (
/blog/getBlogDetail
):- 接收客户端传递的
blogId
参数。 BlogServiceImpl.getBlogDetail
方法处理:- 内部调用
getBlogInfo(blogId)
方法获取原始的BlogInfo
对象。 getBlogInfo
方法根据blogId
查询blog_info
表中未被逻辑删除的特定博客文章。- 将获取到的
BlogInfo
对象通过BeanTransUtils.trans
转换为BlogInfoResponse
后返回。
- 内部调用
- 接收客户端传递的
添加博客 (
/blog/addBlog
):- 接收客户端通过 JSON 格式提交的
AddBlogRequest
(包含userId
,title
,content
)。 - 使用
@Validated
对请求参数进行校验。 BlogServiceImpl.addBlog
方法处理:- 创建一个新的
BlogInfo
对象。 - 使用
BeanUtils.copyProperties
将AddBlogRequest
中的属性复制到BlogInfo
对象。 - 调用
blogInfoMapper.insert
方法将新的博客文章插入数据库。 - 返回操作成功与否的布尔值。
- 创建一个新的
- 接收客户端通过 JSON 格式提交的
更新博客 (
/blog/update
):- 接收客户端通过 JSON 格式提交的
UpdateRequest
(包含id
,title
,content
)。 - 使用
@Validated
对请求参数进行校验 (ID 不能为空)。 BlogServiceImpl.updateBlog
方法处理:- 使用
BeanTransUtils.trans
(或BeanUtils.copyProperties
) 将UpdateRequest
转换为BlogInfo
对象。 - 调用
blogInfoMapper.updateById
方法根据博客 ID 更新数据库中的文章信息。 - 返回操作成功与否的布尔值。
- 使用
- 接收客户端通过 JSON 格式提交的
删除博客 (
/blog/delete
):- 接收客户端传递的
blogId
参数。 BlogServiceImpl.deleteBlog
方法处理:- 创建一个
BlogInfo
对象,并设置其id
为要删除的blogId
。 - 设置
deleteFlag
为Constants.BLOG_DELETE
(表示逻辑删除)。 - 调用
blogInfoMapper.updateById
方法更新该博客的删除标记。 - 返回操作成功与否的布尔值。
- 创建一个
- 接收客户端传递的
关键决策点:
- 逻辑删除: 博客文章的删除采用逻辑删除 (
deleteFlag
标记),而不是物理删除,便于数据恢复和审计。 - 数据转换: 同样使用
BeanTransUtils
或BeanUtils.copyProperties
在 DTO 和 DO 之间进行转换。BlogInfoResponse
中对createTime
进行了格式化处理,并额外提供了一个getCurrentTime
方法。 - 参数校验: 对API接口的输入参数使用 Spring Validation 进行校验。
- 常量使用: 使用
Constants.BLOG_NORMAL
和Constants.BLOG_DELETE
来表示博客的正常和删除状态,提高了代码的可读性和可维护性。
2.2.3 功能说明
博客文章模块实现了以下核心功能:
- 文章列表展示: 获取所有未被删除的博客文章列表。
- 文章详情查看: 根据文章 ID 获取特定文章的详细内容。
- 发布新文章: 允许已登录用户创建并发布新的博客文章。
- 编辑文章: 允许文章作者修改已发布的博客文章内容。
- 删除文章: 允许文章作者逻辑删除自己的博客文章。
该模块是博客系统的主要内容管理部分,支撑了博客的核心业务。
2.3 模块三:评论模块 (如果存在)
经过对项目文件结构 (ls -R
输出) 和已分析代码的检查,当前项目中未发现明确的评论模块相关代码,例如 CommentController.java
、CommentService.java
、CommentInfo.java
或 CommentMapper.java
等。因此,可以认为本项目当前版本不包含评论功能模块。
3. 关键技术点深度剖析
本项目 spring-blog-demo-2
运用了多项现代 Java Web 开发中的关键技术,以下将对其中一些重要技术点进行深度剖析。
3.1 技术点一:Spring Boot 自动配置与起步依赖
Spring Boot 极大地简化了 Spring 应用的搭建和开发过程,其核心特性之一便是自动配置 (Auto-configuration) 和起步依赖 (Starter Dependencies)。
3.1.1 相关代码示例
pom.xml
(部分依赖):
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Other dependencies -->
</dependencies>
Main Application Class (SpringBlogDemo2Application.java
):
package org.example.springblogdemo2;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("org.example.springblogdemo2.mapper") // MyBatis-Plus mapper scan
public class SpringBlogDemo2Application {
public static void main(String[] args) {
SpringApplication.run(SpringBlogDemo2Application.class, args);
}
}
3.1.2 应用方式和原因
spring-boot-starter-parent
: 项目继承自spring-boot-starter-parent
,它提供了一系列预定义的依赖管理和插件配置。这使得开发者无需手动指定常用库的版本号,避免了版本冲突问题,并统一了构建配置。spring-boot-starter-web
: 引入此起步依赖会自动配置构建 Web 应用所需的核心组件,包括 Spring MVC、嵌入式的 Tomcat (默认)、Jackson (JSON处理) 等。开发者无需再手动配置 DispatcherServlet、视图解析器等繁琐的 XML 或 Java 配置。mybatis-plus-spring-boot3-starter
: 这是 MyBatis-Plus 针对 Spring Boot 3 提供的起步依赖。它会自动配置 MyBatis-Plus 所需的 SqlSessionFactory、SqlSessionTemplate,并与 Spring Boot 的数据源配置集成。开发者只需在application.properties
或application.yml
中配置数据源信息即可。spring-boot-starter-validation
: 提供了对 JSR-303/JSR-380 (Bean Validation) 的支持,允许使用注解 (如@NotNull
,@NotBlank
,@Length
) 对请求参数或 JavaBean 进行数据校验。Spring Boot 会自动配置相关的校验器。@SpringBootApplication
注解: 这是一个复合注解,它包含了@SpringBootConfiguration
(标记该类为配置类)、@EnableAutoConfiguration
(启用 Spring Boot 的自动配置机制) 和@ComponentScan
(自动扫描当前包及其子包下的 Spring 组件)。@MapperScan
注解: 虽然不是 Spring Boot 直接提供的,但与 MyBatis-Plus starter 结合使用,用于指定 MyBatis Mapper 接口所在的包路径,以便 Spring 容器能够扫描并创建代理实现。
原因:
采用 Spring Boot 的自动配置和起步依赖,主要目的是:
- 快速开发: 大幅减少了项目的初始配置工作,让开发者能更快地专注于业务逻辑实现。
- 简化管理: 统一管理依赖版本,减少版本冲突的风险。
- 约定优于配置: Spring Boot 遵循“约定优于配置”的原则,提供了许多默认配置,同时也允许开发者在需要时进行自定义覆盖。
3.1.3 主要思路和注意事项
主要思路:
Spring Boot 的自动配置机制基于 classpath 下的 jar 包、特定类的存在以及已定义的 Bean。当应用启动时,@EnableAutoConfiguration
会触发一系列 AutoConfiguration
类的加载。这些类通常带有 @ConditionalOnClass
、@ConditionalOnMissingBean
等条件注解,根据当前环境判断是否需要应用某个配置。例如,当 spring-boot-starter-web
在 classpath 中时,ServletWebServerFactoryAutoConfiguration
会自动配置一个嵌入式的 Web 服务器。
注意事项:
- 理解自动配置原理: 虽然自动配置很方便,但了解其大致原理有助于在出现问题时进行排查。可以通过启动应用时添加
--debug
(或在application.properties
中设置debug=true
) 查看自动配置报告,了解哪些配置被应用了,哪些没有。 - 排除特定自动配置: 如果某个自动配置不符合需求,可以通过
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
或在配置文件中使用spring.autoconfigure.exclude
属性来排除它。 - 自定义配置: 当默认配置不满足需求时,可以通过创建自定义的
@Configuration
类并声明相应的 Bean 来覆盖自动配置。Spring Boot 会优先使用用户自定义的 Bean。 - 依赖版本兼容性: 虽然
spring-boot-starter-parent
管理了大部分依赖版本,但在引入非 Spring Boot 管理的依赖时,仍需注意版本兼容性问题。
3.2 技术点二:Spring MVC 请求处理与响应
Spring MVC 是 Spring框架中用于构建 Web 应用程序的核心模块,它提供了基于 MVC (Model-View-Controller) 设计模式的强大功能来处理 HTTP 请求并生成响应。在本后端为主的项目中,响应通常是 JSON 数据。
3.2.1 相关代码示例
Controller (UserController.java
- 部分):
package org.example.springblogdemo2.controller;
// ... imports ...
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RequestMapping("/user") // 1. 类级别请求映射
@RestController // 2. 声明为 RESTful 控制器
public class UserController {
@Resource(name = "userServiceImpl")
private UserService userService;
@RequestMapping("/login") // 3. 方法级别请求映射
public UserLoginResponse login(@RequestBody @Validated UserLoginRequest userLoginRequest) { // 4. @RequestBody 和 @Validated
log.info("用户登录,用户名:" + userLoginRequest.getUserName());
return userService.checkPassword(userLoginRequest); // 5. 返回对象,自动序列化为 JSON
}
@RequestMapping("/getUserInfo")
public UserInfoResponse getUserInfo(@NotNull(message = "userId 不能为 null") Integer userId) { // 6. 请求参数绑定
log.info("获取用户信息,userId: {}", userId);
return userService.getUserInfo(userId);
}
// ... other methods ...
}
Controller (BlogController.java
- 部分):
package org.example.springblogdemo2.controller;
// ... imports ...
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Slf4j
@RequestMapping("/blog")
@RestController
public class BlogController {
@Resource(name = "blogServiceImpl")
private BlogService blogService;
@RequestMapping("/getList")
public List<BlogInfoResponse> getList() {
log.info("获取博客列表~");
List<BlogInfoResponse> blogInfos = blogService.getList();
return blogInfos;
}
// ... other methods ...
}
统一响应处理 (ResponseAdvice.java
- 如果存在并启用):
// Located in: org.example.springblogdemo2.common.advice.ResponseAdvice.java
package org.example.springblogdemo2.common.advice;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.example.springblogdemo2.common.exception.BlogException;
import org.example.springblogdemo2.pojo.response.Result;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@RestControllerAdvice(basePackages = "org.example.springblogdemo2.controller") // 指定对哪些包下的 Controller 生效
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof Result) {
return body;
}
if (body instanceof String) {
ObjectMapper objectMapper = new ObjectMapper();
try {
return objectMapper.writeValueAsString(Result.success(body));
} catch (JsonProcessingException e) {
throw new BlogException("返回 String 类型结果错误");
}
}
return Result.success(body);
}
}
通用结果封装类 (Result.java
):
// Located in: org.example.springblogdemo2.pojo.response.Result.java
package org.example.springblogdemo2.pojo.response;
import lombok.Data;
import org.example.springblogdemo2.enums.ResultCodeEnum;
@Data
public class Result<T> {
private Integer code; // 状态码
private String message; // 状态信息
private T data; // 返回数据
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(ResultCodeEnum.SUCCESS.getCode());
result.setMessage(ResultCodeEnum.SUCCESS.getMessage());
result.setData(data);
return result;
}
public static <T> Result<T> fail(Integer code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
return result;
}
}
3.2.2 应用方式和原因
@RequestMapping
(类级别和方法级别): 定义API路径。@RestController
: 声明为RESTful控制器,返回值直接写入响应体。@RequestBody
: 将请求体JSON反序列化为Java对象。@Validated
: 触发对请求对象的校验。- 返回对象自动序列化: Java对象自动转为JSON响应。
- 请求参数绑定: 自动绑定URL参数到方法参数。
@RestControllerAdvice
和ResponseBodyAdvice
(ResponseAdvice.java
) 实现统一响应格式封装。
3.2.3 主要思路和注意事项
- 请求分发:
DispatcherServlet
核心作用。 - 参数解析:
HandlerMethodArgumentResolver
处理。 - 数据转换:
HttpMessageConverter
负责序列化/反序列化。 - 响应封装:
ResponseBodyAdvice
统一处理。 - 注意事项:
Content-Type
,异常处理,路径变量与查询参数选择,ResponseBodyAdvice
顺序,String类型特殊处理。
3.3 技术点三:MyBatis-Plus 数据持久化操作
MyBatis-Plus作为MyBatis的增强工具,简化了数据库操作。
3.3.1 相关代码示例
pom.xml
(MyBatis-Plus Starter):
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
</dependency>
Application Configuration (示例):
spring.datasource.url=jdbc:mysql://localhost:3306/your_database_name
# ... other datasource properties ...
mybatis-plus.global-config.db-config.logic-delete-field=deleteFlag
Mapper Interface (UserInfoMapper.java
):
@Mapper
public interface UserInfoMapper extends BaseMapper<UserInfo> {}
Data Object (UserInfo.java
- 部分):
@Data
public class UserInfo {
@TableId(type = IdType.AUTO)
private Integer id;
@TableLogic
private Integer deleteFlag;
}
Service Implementation (UserServiceImpl.java
- 使用 QueryWrapper):
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(UserInfo::getUserName, userLoginRequest.getUserName());
UserInfo userInfo = userInfoMapper.selectOne(queryWrapper);
3.3.2 应用方式和原因
mybatis-plus-spring-boot3-starter
: 简化集成。@Mapper
注解: 标记Mapper接口。BaseMapper<T>
接口: 提供通用CRUD方法。- 实体类注解 (
@TableId
,@TableLogic
): 配置主键和逻辑删除。 QueryWrapper
/LambdaQueryWrapper
: 构建复杂查询条件。
3.3.3 主要思路和注意事项
- 主要思路: 约定优于配置,通用CRUD,条件构造器,逻辑删除,分页插件。
- 注意事项: Mapper扫描,实体与表映射,逻辑删除配置,
QueryWrapper
使用,事务管理,自定义SQL,性能,版本兼容性。
3.4 技术点四:用户认证与授权 (JWT & LoginInterceptor)
采用JWT进行无状态认证,通过Spring MVC拦截器实现访问控制。
3.4.1 相关代码示例
JWT 工具类 (JwtUtils.java
):
public class JwtUtils {
private static final Long EXPIRATION = 24 * 60 * 60 * 1000L;
private static final String SECRET = "mysecretkey"; // 应从配置读取
public static String genToken(Map<String, Object> claims) { /* ... */ }
public static Claims parseToken(String token) { /* ... */ }
}
登录拦截器 (LoginInterceptor.java
):
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
if (!StringUtils.hasLength(token)) { /* ... handle missing token ... */ return false; }
Claims claims = JwtUtils.parseToken(token);
if (claims == null) { /* ... handle invalid token ... */ return false; }
return true;
}
}
Web 配置类 (WebConfig.java
):
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Resource private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/user/login", "/user/getAuthorInfo", "/blog/getList", "/blog/getBlogDetail", "/**/*.html", "/**/*.css", "/**/*.js");
}
}
密码处理工具类 (SecurityUtils.java
- 示例):
public class SecurityUtils {
// MD5 + Salt (示例,不推荐)
public static String encrypt(String password) { /* ... */ }
public static boolean verify(String rawPassword, String encryptedPassword) { /* ... */ }
}
3.4.2 应用方式和原因
- JWT 生成 (
JwtUtils.genToken
): 登录成功后生成Token。 - JWT 解析与验证 (
JwtUtils.parseToken
和LoginInterceptor
): 拦截器验证Token。 LoginInterceptor
拦截请求:preHandle
进行权限检查。- 拦截路径配置 (
WebConfig.addInterceptors
): 精确控制受保护资源。 - 密码存储与校验 (
SecurityUtils
): 哈希存储密码。
3.4.3 主要思路和注意事项
- 主要思路: 登录->发Token;后续请求带Token->拦截器验证。
- 注意事项: JWT安全性 (密钥、HTTPS、Payload、有效期),拦截器配置,密码哈希 (使用强算法如BCrypt),Token存储,Token泄露与吊销,错误处理。
3.5 技术点五:全局异常处理与统一响应封装 (补充)
通过@RestControllerAdvice
和自定义异常实现统一错误响应。
3.5.1 相关代码示例
自定义业务异常类 (BlogException.java
):
public class BlogException extends RuntimeException { /* ... */ }
全局异常处理器 (ExceptionAdvice.java
):
@Slf4j
@RestControllerAdvice(basePackages = "org.example.springblogdemo2.controller")
public class ExceptionAdvice {
@ExceptionHandler(BlogException.class)
public Result<Object> handleBlogException(BlogException e) { /* ... */ }
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Object> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { /* ... */ }
@ExceptionHandler(RuntimeException.class)
public Result<Object> handleRuntimeException(RuntimeException e) { /* ... */ }
}
参数校验注解 (在 DTO 中使用):
public class UserLoginRequest {
@NotNull(message = "用户名不能为 null")
private String userName;
}
3.5.2 应用方式和原因
@RestControllerAdvice
: 标记全局异常处理器。@ExceptionHandler
: 处理特定类型异常。- 自定义异常 (
BlogException
): 区分业务错误。 - 参数校验 (
@Validated
和 JSR-303 注解): 前置数据校验。 - 统一响应 (
Result.java
): 保证响应结构一致。 @ResponseStatus
: 指定HTTP响应状态码。
3.5.3 主要思路和注意事项
- 主要思路: 集中处理,分类处理,结构化响应,日志记录。
- 注意事项: 异常类型顺序,日志级别,错误信息用户友好性,HTTP状态码正确使用,
ResponseAdvice
与ExceptionAdvice
协同,避免吞掉异常。
3.6 技术点六:前端技术与前后端交互
项目包含静态资源,通过Ajax与后端API交互。
3.6.1 相关代码示例
static
目录结构 (推测): /static/css/
, /static/js/jquery.min.js
, /static/editor.md/
前端 JavaScript (示例片段 - Ajax调用):
// 用户登录
$.ajax({
url: "/user/login", type: "POST", contentType: "application/json",
data: JSON.stringify({ userName: username, password: password }),
success: function(response) { localStorage.setItem("authToken", response.data.token); /* ... */ }
});
// 使用 Editor.md
var editor = editormd("editormd-div", { path: "../editor.md/lib/" });
3.6.2 应用方式和原因
- 静态资源服务: Spring Boot默认从
static
目录提供。 - jQuery: 简化DOM操作和Ajax。
- Ajax: 异步调用后端API。
- Token 认证: 前端存储Token并在请求头发送。
- Editor.md (推测): Markdown编辑器。
- 前后端分离 (部分): 后端API,前端渲染。
3.6.3 主要思路和注意事项
- 主要思路: 前端页面构建,Ajax数据请求,数据渲染,用户交互,简单状态管理。
- 注意事项: API设计一致性,前端错误处理,安全性 (XSS, CSRF),前端路由 (本项目可能简单页面跳转),依赖管理,浏览器兼容性,性能优化。
4. 数据库设计
本项目的数据库设计主要围绕用户 (user_info
) 和博客文章 (blog_info
) 两个核心实体展开。以下是对数据库表结构、表间关系及关键字段的描述。这些信息主要从项目的 POJO (Plain Old Java Object) 数据对象类 (UserInfo.java
, BlogInfo.java
) 中推断得出,这些类通常直接映射到数据库表。
4.1 数据库表结构的设计
4.1.1 用户表 (user_info
)
此表存储用户的基本信息,用于用户登录认证和信息展示。
对应实体类: org.example.springblogdemo2.pojo.dataobject.UserInfo.java
字段名 | 数据类型 (推测) | Java 类型 | 注解/说明 |
---|---|---|---|
id |
INT PRIMARY KEY AUTO_INCREMENT | Integer |
@TableId(type = IdType.AUTO) - 用户唯一标识,主键,自增 |
user_name |
VARCHAR(255) | String |
用户名,用于登录 |
password |
VARCHAR(255) | String |
加密后的用户密码 |
github_url |
VARCHAR(255) | String |
(可选) 用户的 GitHub 主页链接 |
delete_flag |
INT / TINYINT | Integer |
@TableLogic - 逻辑删除标记 (0:正常, 1:删除) |
create_time |
DATETIME | LocalDate |
记录创建时间 |
update_time |
DATETIME | LocalDate |
记录更新时间 |
SQL DDL (推测):
CREATE TABLE `user_info` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`user_name` VARCHAR(255) NOT NULL COMMENT '用户名',
`password` VARCHAR(255) NOT NULL COMMENT '加密密码',
`github_url` VARCHAR(255) NULL COMMENT 'GitHub链接',
`delete_flag` INT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记 (0:正常, 1:删除)',
`create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE INDEX `idx_username` (`user_name` ASC) VISIBLE
) COMMENT = '用户信息表';
4.1.2 博客文章表 (blog_info
)
此表存储博客文章的详细内容、作者信息及状态。
对应实体类: org.example.springblogdemo2.pojo.dataobject.BlogInfo.java
字段名 | 数据类型 (推测) | Java 类型 | 注解/说明 |
---|---|---|---|
id |
INT PRIMARY KEY AUTO_INCREMENT | Integer |
@TableId(type = IdType.AUTO) - 文章唯一标识,主键,自增 |
title |
VARCHAR(255) | String |
文章标题 |
content |
TEXT | String |
文章内容 (Markdown 或 HTML) |
user_id |
INT | Integer |
作者的用户 ID,外键关联 user_info 表的 id |
delete_flag |
INT / TINYINT | Integer |
@TableLogic - 逻辑删除标记 (0:正常, 1:删除) |
create_time |
DATETIME | Date |
文章创建时间 |
update_time |
DATETIME | LocalDateTime |
文章最后更新时间 |
SQL DDL (推测):
CREATE TABLE `blog_info` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '博客ID',
`title` VARCHAR(255) NOT NULL COMMENT '博客标题',
`content` TEXT NOT NULL COMMENT '博客内容',
`user_id` INT NOT NULL COMMENT '作者ID (外键关联 user_info.id)',
`delete_flag` INT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记 (0:正常, 1:删除)',
`create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
INDEX `idx_user_id` (`user_id` ASC) VISIBLE,
CONSTRAINT `fk_blog_user`
FOREIGN KEY (`user_id`)
REFERENCES `user_info` (`id`)
ON DELETE RESTRICT -- 或 ON DELETE CASCADE,取决于业务需求
ON UPDATE CASCADE
) COMMENT = '博客文章信息表';
4.2 表与表之间的关系
本项目中主要存在以下表间关系:
user_info
与blog_info
是一对多关系:- 一个用户 (
user_info
) 可以发表多篇博客文章 (blog_info
)。 - 一篇博客文章 (
blog_info
) 只属于一个用户 (user_info
)。 - 这种关系通过
blog_info
表中的user_id
字段实现,该字段作为外键指向user_info
表的id
主键。
- 一个用户 (
4.3 关键字段的含义
id
(在两个表中):- 含义:记录的唯一标识符,主键。
- 说明:通常设置为自增,由数据库自动生成,确保每条记录的唯一性。
user_name
(user_info
表):- 含义:用户的登录名。
- 说明:具有唯一性约束,用于用户身份识别。
password
(user_info
表):- 含义:用户密码。
- 说明:存储的是经过哈希加密后的密码字符串,不存储明文密码,以保证安全性。
delete_flag
(在两个表中):- 含义:逻辑删除标记。
- 说明:用于实现软删除。值为
0
(或配置的未删除值) 表示记录有效,值为1
(或配置的已删除值) 表示记录已被逻辑删除,在常规查询中通常会被过滤掉。MyBatis-Plus 的@TableLogic
注解可以自动处理逻辑删除的查询和更新操作。
user_id
(blog_info
表):- 含义:文章作者的用户 ID。
- 说明:外键,关联到
user_info
表的id
字段,表明该文章的作者是谁。
title
(blog_info
表):- 含义:博客文章的标题。
content
(blog_info
表):- 含义:博客文章的主要内容。
- 说明:通常存储 Markdown 格式的文本或经过 Markdown 解析后的 HTML 文本。
create_time
和update_time
(在两个表中):- 含义:记录的创建时间和最后更新时间。
- 说明:用于审计和追踪记录的生命周期。
create_time
通常在记录插入时设置,update_time
在记录每次更新时自动更新 (可以通过数据库触发器或 ORM 框架的特性实现,如 MyBatis-Plus 的自动填充功能)。
该数据库设计简洁明了,满足了基本的博客系统功能需求。如果未来需要扩展功能,例如评论、标签、分类等,则需要相应地增加新的表并建立关联。
5. 部署与运行
本节将简述 spring-blog-demo-2
项目的部署流程、运行环境要求和基本配置。这些信息主要基于 Spring Boot 项目的通用实践以及项目本身的结构和配置文件 (pom.xml
, application.properties
等) 推断得出。
5.1 项目的部署流程
作为一个典型的 Spring Boot 项目,spring-blog-demo-2
可以通过多种方式部署,最常见的是将其打包成一个可执行的 JAR 文件。
环境准备:
- 确保目标服务器已安装 Java 运行环境 (JRE 或 JDK),版本需与项目
pom.xml
中指定的java.version
(本项目为 Java 17) 兼容或更高。 - 确保目标服务器已安装并运行 MySQL 数据库服务,并且网络可访问。
- 根据
application.properties
(或application.yml
) 中的数据库连接信息,在 MySQL 中创建对应的数据库和表结构 (如上一节所述的user_info
和blog_info
表)。
- 确保目标服务器已安装 Java 运行环境 (JRE 或 JDK),版本需与项目
项目打包:
- 在项目根目录下 (包含
pom.xml
文件的目录),执行 Maven 打包命令:mvn clean package
- 此命令会执行项目的清理、编译、测试 (可跳过测试:
mvn clean package -DskipTests
) 并最终在target/
目录下生成一个可执行的 JAR 文件,文件名通常为spring-blog-demo-2-0.0.1-SNAPSHOT.jar
(根据pom.xml
中的artifactId
和version
)。 - Spring Boot Maven 插件 (
spring-boot-maven-plugin
) 会将所有依赖项和嵌入式 Web 服务器 (如 Tomcat) 打包到这个 JAR 文件中,使其成为一个“胖 JAR”(fat JAR)。
- 在项目根目录下 (包含
配置文件准备:
- Spring Boot 项目的配置文件是
src/main/resources/application.properties
(或application.yml
)。 - 在部署时,可以通过多种方式覆盖或指定外部配置文件:
- 与 JAR 同目录的
application.properties
: 将修改后的application.properties
文件放置在 JAR 文件所在的同一目录下。外部配置文件会覆盖 JAR 包内部的同名配置。 - 与 JAR 同目录的
config/
子目录下的application.properties
: 将配置文件放在 JAR 文件同级目录的config/
子目录下。 - 通过命令行参数指定: 在启动命令中使用
--spring.config.location
参数指定外部配置文件的路径,例如:java -jar spring-blog-demo-2-0.0.1-SNAPSHOT.jar --spring.config.location=file:/path/to/your/custom-application.properties
- 与 JAR 同目录的
- 关键配置项包括数据库连接信息 (
spring.datasource.url
,spring.datasource.username
,spring.datasource.password
),服务器端口 (server.port
,默认为 8080) 等。
- Spring Boot 项目的配置文件是
项目运行:
- 将打包好的 JAR 文件和准备好的外部配置文件 (如果使用) 上传到目标服务器。
- 在服务器上,通过命令行启动项目:
java -jar spring-blog-demo-2-0.0.1-SNAPSHOT.jar
- 如果需要指定特定的 Spring Profile (如
dev
或prod
,在pom.xml
中有定义,但application.properties
中未见明显使用profile特定配置),可以使用:java -jar spring-blog-demo-2-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod
- 项目启动后,可以通过
http://<服务器IP>:<端口号>
(例如http://localhost:8080
如果在本地运行且端口为8080) 访问。
后台运行与进程管理 (可选但推荐):
- 为了使应用在后台持续运行,可以使用
nohup
命令和&
:
日志会输出到nohup java -jar spring-blog-demo-2-0.0.1-SNAPSHOT.jar > app.log 2>&1 &
app.log
文件。 - 更健壮的方式是使用进程管理工具,如
Systemd
(Linux)、Supervisor
,或者将应用容器化 (使用 Docker)。
- 为了使应用在后台持续运行,可以使用
5.2 项目运行的环境要求和配置
5.2.1 环境要求
- Java: JDK 17 或更高版本 (根据
pom.xml
中的<java.version>17</java.version>
)。 - Maven: 用于项目构建和打包 (例如 Maven 3.6.x 或更高版本)。
- 数据库: MySQL 数据库 (版本如 5.7.x, 8.0.x)。需要确保数据库服务正在运行,并且项目配置的数据库用户具有对目标数据库的读写权限。
- 操作系统: 任何支持 Java 运行的操作系统 (如 Linux, Windows, macOS)。
5.2.2 核心配置 (application.properties
或 application.yml
)
以下是项目中需要关注的核心配置项 (通常在 src/main/resources/application.properties
文件中定义):
服务器配置:
server.port
: 应用监听的端口号 (例如server.port=8080
)。如果未指定,默认为 8080。
数据库连接配置:
spring.datasource.url
: 数据库连接 URL (例如jdbc:mysql://localhost:3306/spring_blog_demo2?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
)。spring.datasource.username
: 数据库用户名。spring.datasource.password
: 数据库密码。spring.datasource.driver-class-name
: 数据库驱动类名 (例如com.mysql.cj.jdbc.Driver
for MySQL 8+)。
MyBatis-Plus 配置 (可选,部分可以在代码中配置):
mybatis-plus.mapper-locations
: Mapper XML 文件路径 (如果使用 XML 映射,本项目似乎主要使用注解和BaseMapper
)。mybatis-plus.type-aliases-package
: 实体类别名包路径。mybatis-plus.global-config.db-config.logic-delete-field
: 全局逻辑删除字段名 (如delete_flag
)。mybatis-plus.global-config.db-config.logic-delete-value
: 逻辑删除值 (如1
)。mybatis-plus.global-config.db-config.logic-not-delete-value
: 逻辑未删除值 (如0
)。
JWT 配置 (如果密钥或有效期从配置读取):
jwt.secret
: JWT 签名密钥 (如JwtUtils.java
中的SECRET
,强烈建议从配置读取)。jwt.expiration
: JWT 有效期 (如JwtUtils.java
中的EXPIRATION
)。
日志配置 (可选,Spring Boot 有默认日志配置):
logging.level.<package>
: 设置特定包的日志级别 (例如logging.level.org.example.springblogdemo2=DEBUG
)。logging.file.name
/logging.file.path
: 配置日志输出到文件。
注意: 实际的 application.properties
文件内容未在项目压缩包的顶层直接提供,通常位于 src/main/resources/
目录下。上述配置项是基于 Spring Boot 和项目所用技术的典型配置。部署时,应确保这些配置项根据目标环境进行了正确设置。
该项目由于使用了 spring-boot-starter-web
,内置了 Tomcat 作为 Web 服务器,因此无需单独部署 WAR 包到外部 Tomcat 服务器,直接运行 JAR 文件即可启动整个应用。