资讯动态

Spring Boot图书管理系统拆解:权限分离与MyBatis动态SQL实战

发布时间:2026/9/16 3:29:38 来源:尧图企业网站定制
简介基于SpringBoot实现的图书管理系统源码与数据库资料主要面向计算机相关专业毕设学子及需项目实战练习的Java学习者覆盖SpringBoot、MyBatis、Thymeleaf、SpringSecurity等技术栈。系统包含管理员与读者两种角色支持登录注销、密码修改、图书信息增删改查、借阅记录查看及个人信息维护可直接作为毕业设计或课程项目基础。资源共143个文件包含23个Java源码、30个HTML页面、22个class编译文件、20个MyBatis映射文件、CSS/JS前端资源及SQL数据库脚本等压缩包整体30.46MB目录结构清晰便于快速导入运行。目前已有5322人学习/下载适合需要完整项目方案、数据库脚本及配置参考的入门至进阶学习者按文档与源码对照即可理解权限控制与业务逻辑实现。1. 从毕业设计到生产基线这套Spring Boot图书管理系统拆给你看拿到这套基于Spring Boot的图书管理系统源码时我第一反应是“这不又是一套CRUD”。但把SecurityConfig、SessionFilter、AdminBookController、UserBookController、Reader、Record这些类翻过一遍之后发现它其实是一套非常标准的权限分离样板管理员管书、读者看书、借阅记录单独落表配合MySQL 5.7和MyBatis刚好覆盖后端开发从数据库设计到权限控制的完整链路。对正在准备基于java的图书管理系统毕业设计的学生来说可以直接当骨架对想快速上手Spring Boot Spring Security MyBatis的Java开发者也是一份能跑通、能改、能讲清楚的实战素材。很多人拿到源码第一步就去看Controller但真正决定这套系统能不能稳定运行的是表结构、安全配置和会话管理这三块地基。2. 表结构与MyBatis映射把“图书-读者-借阅记录”拆成可查询的关系2.1 从实体类反推数据库设计Book、Reader、Record 的字段取舍项目里有三个核心实体类Book.class、Reader.class、Record.class。从命名就能看出它们分别对应图书表、读者表和借阅记录表。源码包里的数据库脚本是MySQL 5.7版本的所以建表语句用InnoDB引擎字符集建议utf8mb4避免中文乱码。图书表的核心字段一般包含book_id、book_name、author、publisher、isbn、category、stock、borrowed_count读者表包含reader_id、username、password、real_name、phone、email、status借阅记录表则要关联双方主键并记录借书时间、应还时间、实际归还时间和状态。2.1.1 图书表的建表脚本与字段选择CREATE TABLE book ( book_id int(11) NOT NULL AUTO_INCREMENT, book_name varchar(128) NOT NULL COMMENT 书名, author varchar(64) DEFAULT NULL COMMENT 作者, publisher varchar(64) DEFAULT NULL COMMENT 出版社, isbn varchar(32) DEFAULT NULL COMMENT ISBN号, category varchar(32) DEFAULT NULL COMMENT 分类, stock int(11) NOT NULL DEFAULT 0 COMMENT 库存, borrowed_count int(11) NOT NULL DEFAULT 0 COMMENT 借出次数, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (book_id), UNIQUE KEY idx_isbn (isbn) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书表;这里的borrowed_count和stock是容易忽略的两个字段。stock控制可借数量borrowed_count用来做热门图书排序两个字段分开存比每次join借阅记录后count要快得多。isbn加唯一索引是为了避免同一本书重复录入。create_time和update_time都用了MySQL的默认值MyBatis插入时就不用显式赋值减少代码量。各表的关系用下面这张表可以看得很清楚实体类对应表关键字段关系说明Bookbookbook_id, book_name, stock被Record引用Readerreaderreader_id, username, password被Record引用密码由Spring Security加密Recordrecordrecord_id, book_id, reader_id, borrow_time, due_time, return_time, status连接图书与读者的中间表2.1.2 读者表与借阅记录表的外键设计CREATE TABLE reader ( reader_id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(100) NOT NULL COMMENT BCrypt密文, real_name varchar(30) DEFAULT NULL, phone varchar(20) DEFAULT NULL, email varchar(64) DEFAULT NULL, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 1正常 0禁用, PRIMARY KEY (reader_id), UNIQUE KEY idx_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT读者表; CREATE TABLE record ( record_id int(11) NOT NULL AUTO_INCREMENT, book_id int(11) NOT NULL, reader_id int(11) NOT NULL, borrow_time datetime DEFAULT NULL, due_time datetime DEFAULT NULL, return_time datetime DEFAULT NULL, status tinyint(1) NOT NULL DEFAULT 0 COMMENT 0借出 1已还, PRIMARY KEY (record_id), KEY idx_reader (reader_id), KEY idx_book (book_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT借阅记录表;这里的status用0和1两个值表示借出和已还比直接存字符串更节省空间查询时配合索引也更快。borrow_time、due_time、return_time三个时间字段分开存方便统计逾期天数。如果后续要做过期提醒只需要一条SQLWHERE status 0 AND due_time NOW()。2.2 MyBatis映射文件中的resultMap与动态SQL持久层用的是MyBatis而不是Spring Data JPA原因很简单图书管理系统的查询条件经常变化例如管理员要根据书名、作者、分类组合筛选图书这种动态场景写XML里的where标签比用JPQL拼Specification更直观。项目中BookMapper.xml里通常会有这样一个resultMapresultMap idBookMap typecom.example.library.entity.Book id propertybookId columnbook_id / result propertybookName columnbook_name / result propertyauthor columnauthor / result propertypublisher columnpublisher / result propertyisbn columnisbn / result propertycategory columncategory / result propertystock columnstock / result propertyborrowedCount columnborrowed_count / /resultMapresultMap的核心价值是让数据库下划线字段与Java驼峰属性一一对应避免在SQL里写一堆as别名。对应的动态查询SQL一般是select idsearchBooks resultMapBookMap SELECT * FROM book where if testbookName ! null and bookName ! AND book_name LIKE CONCAT(%, #{bookName}, %) /if if testauthor ! null and author ! AND author LIKE CONCAT(%, #{author}, %) /if if testcategory ! null and category ! AND category #{category} /if /where ORDER BY book_id DESC /select这里用where标签会自动处理第一个AND不需要手动加where 11。LIKE CONCAT(%, #{bookName}, %)是MyBatis里防SQL注入的推荐写法不要用${bookName}直接拼接。参数bookName和author都支持模糊匹配category用精确匹配。排序用book_id DESC让新录入的图书排在最前面。MyBatis标签作用常用场景where自动剔除首个子条件前面的AND多条件组合查询if动态拼接SQL片段字段值非空时加入条件set动态生成UPDATE语句的SET部分只更新非空字段3. Spring Security认证链路与SessionFilter的作用3.1 SecurityConfig放行路径、角色规则与加密方式Spring Boot项目里的Spring Security配置通常都集中在一个SecurityConfig类中。这套图书管理系统的安全模型是/login页面允许匿名访问/admin/**路径需要管理员角色/user/**路径需要读者角色其他静态资源如CSS、JS直接放行。密码加密用的是BCryptPasswordEncoder这是Spring Security自带的单向加密算法每次加密结果不同但校验时能匹配同一个明文。核心配置代码如下Configuration EnableWebSecurity public class SecurityConfig { Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers(/login, /css/**, /js/**, /images/**).permitAll() .antMatchers(/admin/**).hasRole(ADMIN) .antMatchers(/user/**).hasRole(READER) .anyRequest().authenticated() .and() .formLogin() .loginPage(/login) .loginProcessingUrl(/doLogin) .defaultSuccessUrl(/index, true) .failureUrl(/login?error) .and() .logout() .logoutUrl(/logout) .logoutSuccessUrl(/login) .and() .sessionManagement() .maximumSessions(1) .expiredUrl(/login?expired); return http.build(); } }这段配置里antMatchers(/admin/**).hasRole(ADMIN)表示所有以/admin/开头的请求都必须有ADMIN角色。注意hasRole会自动给角色名加ROLE_前缀所以数据库里存的角色权限必须是ROLE_ADMIN或ROLE_READER。formLogin指定了登录页为/login表单提交路径为/doLogin这里会由Spring Security的UsernamePasswordAuthenticationFilter自动处理。maximumSessions(1)限制同一账号只能一个会话在线这是防止管理员密码被多人同时使用的一个简单办法。配置项作用参数说明authorizeRequests定义URL访问规则按顺序从上往下匹配放行规则写前面formLogin配置表单登录loginPage是GET页面loginProcessingUrl是POST地址sessionManagement会话管理maximumSessions(1)表示同账号最多一个有效会话3.2 SessionFilter从会话中加载用户并注入请求上下文项目中的SessionFilter.class是一个很关键的类。Spring Security默认在认证通过后把用户信息放进SecurityContextHolder但这个项目里还有一套自定义的SessionFilter常见作用是从session中读取当前登录对象然后在请求进入Controller之前把它绑定到request的attribute里方便Thymeleaf模板直接用${session.user}或${currentUser}展示登录状态。Component public class SessionFilter implements Filter { Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) servletRequest; HttpSession session request.getSession(false); if (session ! null) { Object user session.getAttribute(loginUser); if (user ! null) { request.setAttribute(currentUser, user); } } chain.doFilter(servletRequest, servletResponse); } }这段代码的关键点在于getSession(false)不会主动创建新session避免给静态资源请求也生成无用的会话。从session里取出loginUser再放进request的attribute里这样Controller里通过RequestAttribute就能拿到模板引擎渲染时也不用重复查数据库。这个Filter的注册顺序很重要如果直接放在Spring Security过滤器链之外它会在Security之前执行如果要读取登录用户最好让它在AuthorizationFilter之后执行否则可能出现session里还没写入用户信息的情况。常见做法是用FilterRegistrationBean设置顺序或者干脆把这段逻辑放到Spring Security的AuthenticationSuccessHandler里做。这套源码把Filter独立出来好处是安全配置和解耦逻辑分开坏处是要记得手动管理Filter执行时机。4. 核心业务图书CRUD、借阅记录查询与密码修改的实现路径4.1 管理员侧AdminBookController的请求映射与参数校验管理员操作集中在AdminBookController.class里主要接口是新增图书、删除图书、修改图书信息和分页查询图书列表。以新增图书为例Controller方法通常长这样Controller RequestMapping(/admin/book) public class AdminBookController { Autowired private BookService bookService; PostMapping(/add) public String addBook(Validated Book book, BindingResult result, RedirectAttributes attributes) { if (result.hasErrors()) { attributes.addFlashAttribute(msg, result.getAllErrors().get(0).getDefaultMessage()); return redirect:/admin/book/addPage; } int rows bookService.insertBook(book); if (rows 0) { attributes.addFlashAttribute(msg, 图书新增成功); } else { attributes.addFlashAttribute(msg, 图书新增失败库存或ISBN可能重复); } return redirect:/admin/book/list; } }这里的Validated配合实体类上的NotBlank、Min注解做参数校验比手动if判断要干净。BindingResult必须紧跟Validated Book book之后否则Spring无法捕获校验错误会直接抛MethodArgumentNotValidException。RedirectAttributes的addFlashAttribute用来做重定向时的一次性提示数据刷新页面后数据消失不会污染下一次请求。管理员功能URL请求方式说明新增图书/admin/book/addPOST提交表单后校验并插入数据库删除图书/admin/book/delete/{id}GET或POST删除前检查是否还有未还记录修改图书/admin/book/updatePOST只更新非空字段用set标签图书列表/admin/book/listGET支持书名的模糊搜索删除图书这里有一个常见坑如果某本书还有读者未归还直接删掉book表记录会导致record表出现悬空外键。所以删除前要先查record表里book_id对应的借出记录数量如果大于0则提示管理员先处理借阅关系。这是毕业设计答辩中老师经常追问的一个点。4.2 读者侧查询图书、查看借阅记录与修改个人信息读者侧主要通过UserBookController和UserReaderController实现。读者登录后可以在图书列表页搜索图书并查看详情但不能执行新增和删除。借阅记录查询只显示当前登录读者自己的记录这个隔离逻辑依赖SessionFilter从session中取出的loginUser对象。代码如下Controller RequestMapping(/user/book) public class UserBookController { Autowired private RecordService recordService; GetMapping(/records) public String myRecords(Model model, CurrentUser Reader currentUser) { ListRecord records recordService.listRecordsByReaderId(currentUser.getReaderId()); model.addAttribute(records, records); return user/records; } }这里的CurrentUser是一个自定义参数注解作用是直接从当前请求上下文中解析loginUser避免了每个方法里都要session.getAttribute的重复代码。如果项目里没有这个注解也可以直接用HttpServletRequest request获取session再拿对象但代码会显得啰嗦。修改密码的功能在UserReaderController中逻辑分三步验证旧密码、更新密码、销毁当前会话。旧密码验证需要调用passwordEncoder.matches(rawPassword, encodedPassword)不能用equals直接比较因为BCrypt每次生成的密文不一样。更新成功之后一般要调用session.invalidate()让用户重新登录这是安全处理密码变更的推荐做法。忘记这个步骤的话旧会话仍能继续访问系统存在安全隐患。读者功能对应的请求路径如下读者功能URL说明查看图书列表/user/book/list支持书名和作者搜索查看我的借阅记录/user/book/records只返回当前读者的记录修改个人资料/user/reader/profile更新手机、邮箱等字段修改密码/user/reader/password校验旧密码后更新并处理会话5. 进阶session并发、分页查询和密码失效的细节处理5.1 限制同一账号多端登录的sessionManagement配置图书管理系统部署到服务器后管理员账号如果被多人同时使用操作日志会混在一起不好追责。Spring Security提供的maximumSessions(1)可以直接踢掉旧会话但要注意配置sessionFixation()保护会话固定攻击。完整写法如下http.sessionManagement() .sessionFixation().changeSessionId() .maximumSessions(1) .maxSessionsPreventsLogin(false) .expiredUrl(/login?expired);maxSessionsPreventsLogin(false)表示当账号已在其他地方登录时允许当前登录成功并把旧的session置为过期。这里的关键是sessionFixation().changeSessionId()它在登录成功后重新生成sessionId防止攻击者在登录前固定一个sessionId诱导用户使用。如果不设置Spring Security默认也会做处理但显式写出来更清楚。实际部署时还要注意集群环境下多个Tomcat实例的session不能共享那就需要把session存储切到Redis里去。5.2 用PageHelper给图书列表加分页源码里的图书列表如果没有分页数据量一多页面就会卡。常见的做法是用MyBatis的PageHelper插件。引入依赖后在Service层的查询前调用一句话PageHelper.startPage(pageNum, pageSize, book_id desc); ListBook books bookMapper.searchBooks(condition); PageInfoBook pageInfo new PageInfo(books);startPage的第一个参数是页码从1开始第二个是每页条数第三个是排序字段字符串这里用book_id desc不需要再在SQL里写order byPageHelper会自动拼接。返回的PageInfo里有getTotal()、getPages()、getList()这些方法直接塞给Thymeleaf就能渲染分页条。注意startPage一定要放在查询方法的第一条可执行语句如果前面有其他查询会把那条查询也当成分页查询导致返回结果错误。这一点在多人协作时经常踩坑。5.3 修改密码后的会话失效处理最后说一个容易被忽略的小细节。正常的修改密码流程完成后应该移除session中的loginUser属性并清理Spring Security的认证信息。可以在Controller里这样写RequestMapping(/user/reader/password) public String changePassword(RequestParam String oldPwd, RequestParam String newPwd, HttpServletRequest request) { Reader reader (Reader) request.getSession().getAttribute(loginUser); if (!passwordEncoder.matches(oldPwd, reader.getPassword())) { return redirect:/user/reader/password?erroroldPwd; } reader.setPassword(passwordEncoder.encode(newPwd)); readerService.updatePassword(reader.getReaderId(), reader.getPassword()); request.getSession().invalidate(); return redirect:/login?changedpwd; }这里首先用passwordEncoder.matches校验旧密码注意顺序第一个参数是用户输入的明文第二个参数是数据库里存的密文。确认无误后更新密码然后直接invalidate()整个会话。这样做有一个好处就是强制其他设备上记住的登录状态全部失效避免密码已经改了旧会话还能继续操作。最后重定向到登录页并带一个changedpwd参数登录页可以用这个参数提示用户“密码已修改请重新登录”。该参数不会在地址栏产生敏感信息因为密码永远只通过POST传给后端。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价