1. 项目概述
作为一个Java开发者,我经常需要寻找一些实战项目来巩固和提升自己的技能。最近我完成了一个基于SpringBoot+MyBatis的图书管理系统项目,这个项目非常适合Java初学者作为练手项目,也适合有一定基础的开发者检验自己的水平。下面我将详细分享这个项目的开发过程和关键实现细节。
这个图书管理系统主要实现了以下功能:
- 用户登录验证
- 图书列表分页展示
- 图书添加、修改和删除
- 图书状态管理
项目采用标准的MVC架构,前端使用HTML+AJAX,后端使用SpringBoot框架,数据库采用MySQL,ORM框架使用MyBatis。整个项目代码结构清晰,功能完整,是一个很好的学习SpringBoot和MyBatis的入门项目。
2. 技术栈与项目结构
2.1 技术栈选择
这个项目使用了以下主要技术:
-
SpringBoot 3.2.6:作为项目的基础框架,提供了自动配置、依赖管理等特性,大大简化了Spring应用的初始搭建和开发过程。
-
MyBatis:作为ORM框架,负责Java对象与数据库表之间的映射。项目中同时使用了XML配置和注解两种方式来操作数据库。
-
MySQL 8.0:作为关系型数据库存储系统数据。
-
前端技术:
- HTML/CSS/JavaScript:基础页面展示
- AJAX:实现前后端异步通信
- jQuery:简化DOM操作和AJAX调用
-
其他工具:
- Lombok:简化POJO类的编写
- Maven:项目管理工具
- JDK 17:Java开发环境
2.2 项目分层结构
项目采用标准的分层架构,各层职责明确:
code复制src/main/java
├── com.example.mybooksystem
│ ├── Controller # 控制层,处理HTTP请求
│ ├── Service # 服务层,业务逻辑处理
│ ├── Mapper # 数据访问层,数据库操作
│ ├── Model # 数据模型,实体类
│ ├── Enums # 枚举类
│ └── Config # 配置类
src/main/resources
├── static # 静态资源
├── templates # 模板文件
├── application.yml # 应用配置
└── mybatis # MyBatis映射文件
这种分层结构使得代码职责清晰,便于维护和扩展。每层只关注自己的职责,通过接口进行交互,降低了耦合度。
3. 环境准备与项目搭建
3.1 开发环境配置
在开始项目前,我们需要配置好开发环境:
-
JDK安装:下载并安装JDK 17,配置JAVA_HOME环境变量。
-
IDE选择:推荐使用IntelliJ IDEA作为开发工具,它对SpringBoot和Maven的支持非常好。
-
MySQL安装:安装MySQL 8.0,创建数据库用户并授权。
-
Maven配置:配置Maven的settings.xml文件,设置本地仓库和镜像源。
3.2 创建SpringBoot项目
使用Spring Initializr创建项目的基本骨架:
- 打开IDEA,选择File -> New -> Project -> Spring Initializr
- 配置项目基本信息:
- Name: MyBookSystem
- Type: Maven Project
- Language: Java
- Packaging: Jar
- Java Version: 17
- 选择Spring Boot版本:3.2.6
- 添加依赖:
- Spring Web
- MyBatis Framework
- MySQL Driver
- Lombok
- 点击Finish完成项目创建
3.3 数据库设计与准备
项目需要两张核心表:
- 用户表(user_info):存储系统用户信息
sql复制CREATE TABLE `user_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_name` varchar(128) NOT NULL,
`password` varchar(128) NOT NULL,
`delete_flag` tinyint(4) DEFAULT '0',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `user_name_UNIQUE` (`user_name`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'
- 图书表(book_info):存储图书信息
sql复制CREATE TABLE `book_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`book_name` varchar(127) NOT NULL,
`author` varchar(127) NOT NULL,
`count` int(11) NOT NULL,
`price` decimal(7,2) NOT NULL,
`publish` varchar(256) NOT NULL,
`status` tinyint(4) DEFAULT '1' COMMENT '0-⽆效, 1-正常, 2-不允许借阅',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=81 DEFAULT CHARSET=utf8mb4
4. 核心功能实现
4.1 用户登录功能
4.1.1 前端登录页面
登录页面使用简单的HTML表单,通过AJAX提交数据:
html复制<div class="container-login">
<div class="container-pic">
<img src="pic/computer.png" width="350px">
</div>
<div class="login-dialog">
<h3>登陆</h3>
<div class="row">
<span>用户名</span>
<input type="text" name="userName" id="userName" class="form-control">
</div>
<div class="row">
<span>密码</span>
<input type="password" name="password" id="password" class="form-control">
</div>
<div class="row">
<button type="button" class="btn btn-info btn-lg" onclick="login()">登录</button>
</div>
<div class="row">
<button type="button" class="btn btn-info btn-lg" onclick="sign_in()">注册</button>
</div>
</div>
</div>
登录按钮绑定的JavaScript函数:
javascript复制function login() {
$.ajax({
url: "/user/login",
type: "post",
data: {
userName: $("#userName").val(),
password: $("#password").val()
},
success: function(result) {
if(result.code=="SUCCESS" && result.data ==""){
location.href = "book_list.html?pageNum=1";
}else{
alert(result.errMsg);
}
}
});
}
4.1.2 后端实现
- 用户模型类(UserInfo):
java复制package com.example.mybooksystem.Model;
import lombok.Data;
import java.util.Date;
@Data
public class UserInfo {
private Integer id;
private String userName;
private String password;
private Integer deleteFlag;
private Date createTime;
private Date updateTime;
}
- 统一返回结果封装(Result):
java复制package com.example.mybooksystem.Model;
import com.example.mybooksystem.Enums.ResultStatus;
import lombok.Data;
@Data
public class Result<T> {
private T data;
private ResultStatus code;
private String errMsg;
public static <T>Result success(T data){
Result result=new Result<>();
result.setData(data);
result.setCode(ResultStatus.SUCCESS);
return result;
}
public static <T>Result Fail(T data){
Result result=new Result<>();
result.setData(data);
result.setCode(ResultStatus.FAIL);
return result;
}
public static <T>Result Nologin(T data){
Result result=new Result<>();
result.setData(data);
result.setCode(ResultStatus.NOLOGIN);
return result;
}
}
- 状态码枚举(ResultStatus):
java复制package com.example.mybooksystem.Enums;
import lombok.Data;
public enum ResultStatus {
SUCCESS(200),
FAIL(-1),
NOLOGIN(-2);
private int code;
ResultStatus(int code){
this.code=code;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
- Controller层实现:
java复制package com.example.mybooksystem.Controller;
import com.example.mybooksystem.Enums.ResultStatus;
import com.example.mybooksystem.Model.*;
import com.example.mybooksystem.Service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
UserService userService;
@RequestMapping("/login")
public Result<String> login(UserInfo userInfo){
Result<String> result=new Result<>();
if(userInfo==null){
result.setData("");
result.setCode(ResultStatus.FAIL);
System.out.println("未收到登录信息");
result.setErrMsg("未收到登录信息");
return result;
}
if(userInfo.getUserName()==null||userInfo.getPassword()==null){
result.setData("");
result.setCode(ResultStatus.FAIL);
System.out.println("未收到登录信息");
result.setErrMsg("未收到登录信息");
return result;
}
UserInfo userInfo1=userService.SelectByName(userInfo);
if(userInfo1==null){
result.setCode(ResultStatus.FAIL);
result.setData("");
System.out.println("用户名或者密码错误");
return result;
}
if(userInfo1.getPassword().equals(userInfo.getPassword())){
result.setCode(ResultStatus.SUCCESS);
result.setData("");
System.out.println("成功登录");
return result;
}
return result;
}
}
- Service层实现:
java复制package com.example.mybooksystem.Service;
import com.example.mybooksystem.Mapper.UserInfoMapper;
import com.example.mybooksystem.Model.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
UserInfoMapper userInfoMapper;
public UserInfo SelectByName(UserInfo userInfo){
return userInfoMapper.SelectByName(userInfo.getUserName());
}
}
- Mapper层实现:
java复制package com.example.mybooksystem.Mapper;
import com.example.mybooksystem.Model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserInfoMapper {
UserInfo SelectByName(String userName);
}
对应的XML映射文件:
xml复制<?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.example.mybooksystem.Mapper.UserInfoMapper">
<select id="SelectByName" resultType="com.example.mybooksystem.Model.UserInfo">
select * from user_info where user_name=#{userName}
</select>
</mapper>
4.2 图书列表分页展示
4.2.1 前端实现
图书列表页面通过AJAX请求获取分页数据:
javascript复制$.ajax({
url: "/book/getBookListByPage" + location.search,
type: "get",
success: function (result) {
if (result.data != null && result.data.records != null) {
var finnalHtml = "";
for (var book of result.data.records) {
finnalHtml += '<tr>';
finnalHtml += '<td><input type="checkbox" name="selectBook" value="' + book.id + '" id="selectBook" class="book-select"></td>';
finnalHtml += '<td>' + book.id + '</td>';
finnalHtml += '<td>' + book.bookName + '</td>';
finnalHtml += '<td>' + book.author + '</td>';
finnalHtml += '<td>' + book.count + '</td>';
finnalHtml += '<td>' + book.price + '</td>';
finnalHtml += '<td>' + book.publish + '</td>';
finnalHtml += '<td>' + book.statusCN + '</td>';
finnalHtml += '<td>';
finnalHtml += '<div class="op">';
finnalHtml += '<a href="book_update.html?bookId=' + book.id + '">修改</a>';
finnalHtml += '<a href="javascript:void(0)" onclick="deleteBook(' + book.id + ')">删除</a>';
finnalHtml += '</div></td></tr>';
}
$("tbody").html(finnalHtml);
// 分页控件初始化
$("#pageContainer").jqPaginator({
totalCounts: data.count,
pageSize: 10,
visiblePages: 5,
currentPage: data.pageRequest.pageNum,
// 分页控件配置...
});
}
},
error: function (error) {
console.log(error);
if (error != null && error.status == 401) {
location.href = "login.html";
}
}
});
4.2.2 后端实现
- 分页请求模型(PageRequest):
java复制package com.example.mybooksystem.Model;
import lombok.Data;
@Data
public class PageRequest {
private Integer pageNum=1;
private Integer pageSize=10;
private Integer offSet;
public Integer getOffSet(){
return (pageNum-1)*pageSize;
}
}
- 分页结果模型(PageResult):
java复制package com.example.mybooksystem.Model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class PageResult<T> {
private List<T> records;
private Integer count;
private PageRequest pageRequest;
}
- 图书状态枚举(BookStatus):
java复制package com.example.mybooksystem.Enums;
public enum BookStatus {
DELETE(0,"删除"),
NORMAL(1,"可借阅"),
FORBIDDEN(2,"不可借阅");
private Integer code;
private String desc;
BookStatus(Integer code,String desc){
this.code=code;
this.desc=desc;
}
public Integer getCode() {
return code;
}
public static BookStatus getDescBycode(Integer code){
switch (code){
case 0: return DELETE;
case 1: return NORMAL;
case 2:
default:
return FORBIDDEN;
}
}
public void setCode(Integer code) {
this.code = code;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
- 图书模型(BookInfo):
java复制package com.example.mybooksystem.Model;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
public class BookInfo {
private Integer id;
private String bookName;
private String author;
private Integer count;
private BigDecimal price;
private String publish;
private Integer status; //1-正常 2-不可借阅
private String statusCN;
private Date createTime;
private Date updateTime;
}
- Controller层实现:
java复制@RestController
@RequestMapping("/book")
public class BookController {
@Autowired
BookService bookService;
@RequestMapping("/getBookListByPage")
public Result<PageResult<BookInfo>> getBookListByPage(PageRequest pageRequest){
PageResult<BookInfo> bookInfoPageResult = bookService.selectBookByPage(pageRequest);
return Result.success(bookInfoPageResult);
}
}
- Service层实现:
java复制@Service
public class BookService {
@Autowired
BookInfoMapper bookInfoMapper;
public PageResult<BookInfo> selectBookByPage(PageRequest pageRequest){
List<BookInfo> bookInfos = bookInfoMapper.QueryBookByPage(
pageRequest.getOffSet(),
pageRequest.getPageSize()
);
for(BookInfo bookInfo : bookInfos){
bookInfo.setStatusCN(
BookStatus.getDescBycode(bookInfo.getStatus()).getDesc()
);
}
Integer count = bookInfoMapper.countBooks();
return new PageResult<>(bookInfos, count, pageRequest);
}
}
- Mapper层实现:
java复制@Mapper
public interface BookInfoMapper {
@Select("select * from book_info where status!=0 order by id asc limit #{offset},#{limit}")
List<BookInfo> QueryBookByPage(Integer offset, Integer limit);
@Select("select count(*) from book_info where status!=0")
Integer countBooks();
}
4.3 图书添加功能
4.3.1 前端实现
添加图书页面表单提交:
javascript复制function add() {
$.ajax({
url: "/book/addBook",
type: "post",
data: $("#addBook").serialize(),
success: function (result) {
if (result.code == "SUCCESS" && result.data == "") {
location.href = "book_list.html";
} else {
alert(result.data);
}
},
error: function (error) {
if (error != null && error.status == 401) {
location.href = "login.html";
}
}
});
}
4.3.2 后端实现
- Controller层:
java复制@RequestMapping("/addBook")
public Result<String> addBook(BookInfo bookInfo){
Integer n = bookService.addBook(bookInfo);
Result<String> result = new Result<>();
if(n <= 0){
result.setCode(ResultStatus.FAIL);
result.setData("添加图书失败");
return result;
}
result.setData("");
result.setCode(ResultStatus.SUCCESS);
return result;
}
- Service层:
java复制public Integer addBook(BookInfo bookInfo){
return bookInfoMapper.addBook(bookInfo);
}
- Mapper层:
java复制@Insert("insert into book_info (book_name,author,`count`,`price`,publish) " +
"values (#{bookName},#{author},#{count},#{price},#{publish})")
Integer addBook(BookInfo bookInfo);
4.4 图书修改功能
4.4.1 查询单本图书信息
- Controller层:
java复制@RequestMapping("/queryBookById")
public Result<BookInfo> queryBookById(Integer bookId){
BookInfo bookInfo = bookService.queryBookById(bookId);
return Result.success(bookInfo);
}
- Service层:
java复制public BookInfo queryBookById(Integer id){
return bookInfoMapper.queryBookById(id);
}
- Mapper层:
java复制@Select("select * from book_info where id=#{id}")
BookInfo queryBookById(Integer id);
4.4.2 更新图书信息
- Controller层:
java复制@RequestMapping("/updateBook")
public Result<String> updateBook(BookInfo bookInfo){
Integer n = bookService.updateBook(bookInfo);
Result<String> result = new Result<>();
if(n <= 0){
result.setCode(ResultStatus.FAIL);
result.setData("更新图书失败");
return result;
}
result.setData("");
result.setCode(ResultStatus.SUCCESS);
return result;
}
- Service层:
java复制public Integer updateBook(BookInfo bookInfo){
return bookInfoMapper.updateBook(bookInfo);
}
- Mapper层:
java复制@Update("update book_info set book_name=#{bookName}, author=#{author}, " +
"count=#{count}, price=#{price}, publish=#{publish}, status=#{status} " +
"where id=#{id}")
Integer updateBook(BookInfo bookInfo);
4.5 图书删除功能
- Controller层:
java复制@RequestMapping("/deleteBook")
public Result<String> deleteBook(Integer bookId){
Integer n = bookService.deleteBook(bookId);
Result<String> result = new Result<>();
if(n <= 0){
result.setCode(ResultStatus.FAIL);
result.setData("删除图书失败");
return result;
}
result.setData("");
result.setCode(ResultStatus.SUCCESS);
return result;
}
- Service层:
java复制public Integer deleteBook(Integer id){
return bookInfoMapper.deleteBook(id);
}
- Mapper层:
java复制@Update("update book_info set status=0 where id=#{id}")
Integer deleteBook(Integer id);
5. 项目配置与部署
5.1 应用配置文件
application.yml 配置文件内容:
yaml复制spring:
application:
name: MyBookSystem
datasource:
url: jdbc:mysql://127.0.0.1:3306/book_test?characterEncoding=utf8&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
configuration:
map-underscore-to-camel-case: true
mapper-locations: classpath:mybatis/*Mapper.xml
logging:
file:
name: spring-book.log
server:
port: 9090
5.2 项目打包与运行
- 使用Maven打包项目:
bash复制mvn clean package
- 运行项目:
bash复制java -jar target/mybooksystem-0.0.1-SNAPSHOT.jar
- 访问应用:
code复制http://localhost:9090/login.html
6. 开发经验与注意事项
6.1 开发中的常见问题
-
MyBatis映射问题:
- 属性名与数据库字段名不一致时,可以使用
@Result注解或配置map-underscore-to-camel-case为true来自动转换 - XML映射文件中要注意namespace与Mapper接口的全限定名一致
- 属性名与数据库字段名不一致时,可以使用
-
分页查询性能:
- 大数据量分页时,避免使用
limit offset, size方式,可以使用"上一页最大ID"方式优化 - 添加适当的索引提高查询效率
- 大数据量分页时,避免使用
-
事务管理:
- 对于多个数据库操作需要保持原子性的场景,记得添加
@Transactional注解 - 注意事务的传播行为和隔离级别设置
- 对于多个数据库操作需要保持原子性的场景,记得添加
6.2 项目优化建议
-
缓存优化:
- 引入Redis缓存热点数据,如用户信息、热门图书等
- 对分页查询结果进行缓存,减轻数据库压力
-
安全性增强:
- 用户密码加密存储,可以使用BCryptPasswordEncoder
- 添加接口权限验证,如使用Spring Security或JWT
-
前端优化:
- 使用Vue.js或React等现代前端框架重构页面
- 实现组件化开发,提高代码复用率
-
日志监控:
- 完善日志记录,便于问题排查
- 集成Prometheus+Grafana实现应用监控
6.3 扩展功能建议
-
图书借阅功能:
- 添加借阅记录表
- 实现借书、还书、续借等功能
-
用户权限管理:
- 区分管理员和普通用户
- 实现基于角色的权限控制
-
图书检索功能:
- 实现按书名、作者、出版社等多条件检索
- 集成Elasticsearch提供全文检索能力
-
数据统计与分析:
- 图书借阅排行榜
- 用户借阅习惯分析
7. 总结
通过这个图书管理系统的开发,我系统地实践了SpringBoot和MyBatis的核心功能,包括:
- SpringBoot的自动配置和starter机制
- MyBatis的注解和XML两种使用方式
- 前后端分离的开发模式
- RESTful API设计
- 分页查询的实现
- 统一异常处理
- 项目分层架构设计
这个项目虽然功能简单,但涵盖了企业级应用开发的常见场景和技术要点,是一个很好的SpringBoot入门项目。对于初学者来说,通过这个项目可以快速掌握SpringBoot应用的基本开发流程;对于有一定经验的开发者,可以基于此项目进行扩展,实践更多高级特性。
在实际开发过程中,我深刻体会到良好的代码结构和规范的开发流程的重要性。合理的分层设计、清晰的接口定义、完善的异常处理,这些都能显著提高代码的可维护性和可扩展性。