首页 / 资讯中心 / 文章详情

SSM图书管理系统实战:从零部署到二次开发

SSM图书管理系统实战:从零部署到二次开发 ★ FEATURED ARTICLE
简介本资源是一套基于SSMSpringSpringMVCMyBatis框架开发的完整图书管理系统实战项目面向Java Web初学者与中级开发者用于深入理解企业级Web应用的分层架构、前后端协作及数据库交互实践。项目涵盖用户权限登录、图书增删改查、借阅/续借/归还全流程、邮件通知及互动答题等核心功能模块具备典型业务闭环与基础安全防护设计。压缩包共3个文件1个RAR主程序包含源码与配置、1个TXT使用手册说明部署步骤与运行要点、1个SQL脚本含初始化库表与测试数据整体大小91.12MB结构精炼、开箱即用。已有3255人学习下载读者可直接导入IDEA与MySQL环境快速运行获取可调试的完整工程结构、清晰的MVC分层代码、MyBatis动态SQL实践范例及Spring事务与AOP集成案例是掌握SSM整合开发与项目落地的高价值学习样本。1. 图书管理系统SSM框架一个能真正在局域网里跑通、增删改查不报错、连后台分页和文件上传都配齐的完整教学级项目上周帮某高校实验室带毕设学生交上来一个“图书管理系统”运行报ClassNotFoundException: org.springframework.web.servlet.DispatcherServlet查了半小时才发现他把spring-webmvc.jar放在了WEB-INF/lib外面——这种血泪经验恰恰说明市面上太多所谓“SSM图书系统”只是骨架没血肉缺配置、少依赖、无验证逻辑更别提登录拦截、借阅状态校验这些真实业务细节。而这个项目不是Demo它是一个可部署到Tomcat 8、MySQL 5.7、JDK 8环境的完整闭环系统管理员能批量导入Excel图书数据读者能按ISBN模糊检索借阅记录分页查看所有操作带事务回滚比如借书失败时库存不扣减连图书封面上传都做了后缀白名单校验和路径防遍历处理。它适合两类人一是刚学完Spring MVCSpringMyBatis三件套、想用一个真实项目串起整个Web开发链路的新手二是需要快速交付一个轻量级内部图书台账系统的运维或行政人员——你不需要懂AOP原理只要改几行XML和SQL就能上线。2. 技术栈与工程结构SSM不是三个字母堆砌而是三层职责分明的协作关系这个项目没上Spring Boot是纯手工搭的SSM老派架构但正因如此每个配置文件的作用都暴露得清清楚楚。它用的是Spring 4.3.20.RELEASE Spring MVC 4.3.20 MyBatis 3.4.6 MySQL 5.7 Tomcat 8.5组合JDK版本锁死在1.8高版本会触发MyBatis的SelectProvider反射异常。整个工程是标准的Maven WebApp结构pom.xml里没有花哨插件只有最精简的依赖spring-webmvc、mybatis-spring、mysql-connector-java、commons-fileupload处理封面上传、jstlJSP标签库这五项是刚需其余如log4j、junit都是可选。2.1 目录结构即设计思想src/main/java下藏着三层契约src/main/java/ ├── com.example.bookms/ │ ├── controller/ // 接收HTTP请求只做参数校验和跳转不碰业务逻辑 │ │ ├── BookController.java // /book/list, /book/add, /book/upload │ │ └── BorrowController.java // /borrow/record, /borrow/handle │ ├── service/ // 定义接口实现类事务控制在此层Transactional │ │ ├── BookService.java │ │ ├── BookServiceImpl.java // Service注解调用Mapper封装借阅规则 │ │ └── BorrowService.java │ ├── dao/ // MyBatis Mapper接口纯方法声明无SQL │ │ └── BookMapper.java // public ListBook selectByKeyword(String keyword); │ ├── entity/ // POJO字段名与数据库列名严格一致用Param注解规避驼峰 │ │ ├── Book.java │ │ └── BorrowRecord.java │ └── util/ // 工具类Excel读取Apache POI、文件路径生成UUID时间戳 │ └── ExcelImportUtil.java注意dao包下没有XML文件——所有SQL都写在resources/mapper/BookMapper.xml里这是MyBatis官方推荐的分离方式。entity里的Book.java有private String coverPath;字段对应数据库book.cover_path VARCHAR(255)上传后的封面存放在webapp/static/uploads/cover/目录路径存进数据库前端用img src${pageContext.request.contextPath}/static/uploads/cover/${book.coverPath}直接渲染。2.2 核心配置文件四份XML撑起整个SSM骨架项目根目录下web.xml是总入口必须按顺序加载ContextLoaderListener加载applicationContext.xmlSpring容器管理Service/DAODispatcherServlet加载spring-mvc.xmlSpring MVC容器管理ControllerCharacterEncodingFilter解决POST中文乱码filter-mapping要放在servlet-mapping之前applicationContext.xml关键片段!-- 扫描service和dao包 -- context:component-scan base-packagecom.example.bookms.service,com.example.bookms.dao / !-- 数据源配置这里用Druid连接池 -- bean iddataSource classcom.alibaba.druid.pool.DruidDataSource init-methodinit destroy-methodclose property nameurl valuejdbc:mysql://localhost:3306/bookms?useSSLfalseamp;serverTimezoneUTC / property nameusername valueroot / property namepassword value123456 / /bean !-- MyBatis工厂指定Mapper XML位置 -- bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource / property namemapperLocations valueclasspath:mapper/*.xml / /bean !-- Mapper扫描器自动为dao接口生成代理对象 -- bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.example.bookms.dao / /beanspring-mvc.xml只做三件事启用注解、配置视图解析器、放行静态资源。mvc:annotation-driven / !-- 启用Controller等注解 -- bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views/ / property namesuffix value.jsp / /bean mvc:resources mapping/static/** location/static/ / !-- 放行CSS/JS/图片 --提示applicationContext.xml和spring-mvc.xml不能合并Spring容器父容器不能访问Spring MVC容器子容器的Bean所以Controller不能直接注入Dao必须通过Service中转——这是SSM分层的铁律。2.3 数据库设计5张表支撑核心业务字段命名拒绝拼音缩写MySQL建表脚本在src/main/resources/sql/bookms.sql共5张表bookid(PK), isbn(VARCHAR 13), title, author, publisher, publish_date(DATE), stock(INT), cover_path(VARCHAR 255)readerid(PK), reader_id(VARCHAR 10, 唯一), name, phone, emailborrow_recordid(PK), book_id(FK), reader_id(FK), borrow_date(DATE), return_date(DATE NULL), status(ENUM borrowed,returned,overdue)admin_userid(PK), username, password(MD5加密), role(ENUM admin,librarian)logid(PK), operator_type(ENUM book,borrow,admin), operator_id, action(ENUM add,delete,update), create_time关键设计点borrow_record.status用ENUM而非INT避免状态值被误写book.isbn设为唯一索引防止重复录入borrow_record.return_date允许NULL表示未归还所有时间字段用DATE或DATETIME不用字符串存时间——这点在MyBatis的resultMap里体现为result propertyborrowDate columnborrow_date jdbcTypeDATE/。3. 核心模块拆解从图书上传到借阅状态变更的完整链路3.1 图书批量导入Excel解析事务回滚的实战写法BookController.upload()接收MultipartFile file调用ExcelImportUtil.readBooksFromExcel()解析。该工具类用Apache POI 3.17不支持.xlsx新格式老项目兼容性考虑所以前端JSP里用input typefile accept.xls强制限制。// ExcelImportUtil.java public static ListBook readBooksFromExcel(InputStream is) throws IOException { HSSFWorkbook workbook new HSSFWorkbook(is); // 只读.xls HSSFSheet sheet workbook.getSheetAt(0); ListBook books new ArrayList(); for (int i 1; i sheet.getLastRowNum(); i) { // 跳过标题行 HSSFRow row sheet.getRow(i); if (row null) continue; Book book new Book(); book.setIsbn(getCellValue(row.getCell(0))); // A列ISBN book.setTitle(getCellValue(row.getCell(1))); // B列书名 book.setAuthor(getCellValue(row.getCell(2))); book.setPublisher(getCellValue(row.getCell(3))); book.setPublishDate(parseDate(getCellValue(row.getCell(4)))); // E列出版日期 book.setStock(Integer.parseInt(getCellValue(row.getCell(5)))); // F列库存 books.add(book); } return books; }BookServiceImpl.batchInsertBooks()用MyBatis的foreach标签实现批量插入但关键在事务控制Transactional(rollbackFor Exception.class) public int batchInsertBooks(ListBook books) { int successCount 0; for (Book book : books) { try { bookMapper.insert(book); // 单条插入失败则整批回滚 successCount; } catch (DuplicateKeyException e) { log.warn(ISBN重复跳过{}, book.getIsbn()); continue; // 允许部分失败但不中断事务 } } return successCount; }注意Transactional加在Service方法上不是Controller。如果Controller里try-catch吞掉异常事务不会回滚——这是新手翻车第一高频点。3.2 封面上传安全机制路径白名单文件类型校验BookController.uploadCover()处理单个封面上传PostMapping(/book/uploadCover) ResponseBody public Result uploadCover(RequestParam(file) MultipartFile file, RequestParam(bookId) Long bookId) { // 1. 文件类型校验非仅靠后缀 String contentType file.getContentType(); if (!image/jpeg.equals(contentType) !image/png.equals(contentType)) { return Result.fail(仅支持JPG/PNG格式); } // 2. 生成安全文件名UUID 时间戳 原后缀 String originalName file.getOriginalFilename(); String ext originalName.substring(originalName.lastIndexOf(.)); String safeName UUID.randomUUID().toString() System.currentTimeMillis() ext; // 3. 写入磁盘绝对路径需配置 String uploadDir servletContext.getRealPath(/static/uploads/cover/); File dir new File(uploadDir); if (!dir.exists()) dir.mkdirs(); try { file.transferTo(new File(dir, safeName)); // 4. 更新数据库cover_path字段 bookMapper.updateCoverPath(bookId, safeName); return Result.success(safeName); } catch (Exception e) { log.error(封面上传失败, e); return Result.fail(上传失败请重试); } }提示servletContext.getRealPath()在Tomcat 8的war包部署下可能返回null生产环境建议改用System.getProperty(user.dir) /uploads/cover/并配置Nginx反向代理静态资源。3.3 借阅状态机从“可借”到“已逾期”的状态流转逻辑BorrowService.handleBorrow()是状态变更核心Transactional(rollbackFor Exception.class) public Result handleBorrow(Long bookId, String readerId, String action) { // 1. 查询图书和读者 Book book bookMapper.selectById(bookId); Reader reader readerMapper.selectByReaderId(readerId); if (book null || reader null) { return Result.fail(图书或读者不存在); } // 2. 状态校验只有stock0才能借 if (borrow.equals(action) book.getStock() 0) { return Result.fail(库存不足); } // 3. 执行动作 if (borrow.equals(action)) { // 插入借阅记录 BorrowRecord record new BorrowRecord(); record.setBookId(bookId); record.setReaderId(readerId); record.setBorrowDate(new Date()); record.setStatus(borrowed); borrowRecordMapper.insert(record); // 扣减库存 bookMapper.decreaseStock(bookId); } else if (return.equals(action)) { // 更新归还记录 borrowRecordMapper.updateReturnDate(bookId, readerId, new Date()); // 恢复库存 bookMapper.increaseStock(bookId); } return Result.success(操作成功); }bookMapper.decreaseStock()对应的XMLupdate iddecreaseStock UPDATE book SET stock stock - 1 WHERE id #{id} AND stock 0 /update这里用AND stock 0做数据库层面的并发安全校验避免超卖——比Java层synchronized更可靠。4. 运行与部署从零开始启动项目的六步实操清单4.1 环境准备四个必须确认的版本锚点JDK 1.8执行java -version输出必须含1.8.0_XXX。若用JDK 11mybatis-spring 1.3.2会报java.lang.NoClassDefFoundError: javax/xml/bind/JAXBExceptionJAXB被移除降级或加jaxb-api依赖都麻烦不如直接切JDK 8。MySQL 5.7执行SELECT VERSION();确认是5.7.x。MySQL 8默认开启caching_sha2_password认证插件而mysql-connector-java 5.1.47不支持需在MySQL里执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 123456;Tomcat 8.5下载apache-tomcat-8.5.94.zip解压后修改conf/server.xml将Connector port8080改为Connector port8081避免端口冲突。IDE配置Eclipse/IntelliJ中Project Facets必须勾选Dynamic Web Module 3.1Java版本选1.8Target Runtime选刚配好的Tomcat 8.5。4.2 数据库初始化三步导入SQL脚本在MySQL中创建数据库CREATE DATABASE bookms CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;执行src/main/resources/sql/bookms.sql注意该SQL文件末尾有INSERT INTO admin_user初始账号用户名admin密码123456MD5后存库验证表结构DESCRIBE book;应看到13个字段其中cover_path为varchar(255)stock为int(11)。提示bookms.sql里book表的publish_date字段类型是date不是datetime。如果Excel里日期是2023-01-01 00:00:00格式POI解析时要用cell.getDateCellValue()再转java.sql.Date.valueOf()否则入库成0000-00-00。4.3 Maven构建与部署跳过测试直出WAR包在项目根目录执行mvn clean package -Dmaven.test.skiptrue成功后生成target/bookms.war。将此WAR包复制到tomcat/webapps/目录下启动Tomcat# Linux/Mac ./tomcat/bin/startup.sh # Windows tomcat\bin\startup.bat等待控制台输出INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [XXXX] milliseconds即启动成功。4.4 首次访问与登录URL路径和默认账号系统首页http://localhost:8081/bookms/注意不是/bookms/index.jspweb.xml里配置了欢迎文件列表管理员登录页http://localhost:8081/bookms/login.jsp默认账号用户名admin密码123456明文输入后端用MD5比对登录后跳转/book/list显示图书列表页右上角有“添加图书”、“批量导入”按钮注意首次访问若报404检查web.xml中welcome-file-list是否为welcome-file-list welcome-filelogin.jsp/welcome-file /welcome-file-list若此处写成index.jsp而index.jsp不存在就会404。5. 常见问题排查五个让开发者抓狂的真实报错及解法5.1 现象启动Tomcat报错java.lang.ClassNotFoundException: org.springframework.web.servlet.DispatcherServlet原因pom.xml中spring-webmvc依赖范围scope被误设为test或WEB-INF/lib下缺少spring-webmvc-4.3.20.RELEASE.jar解决检查pom.xml确保spring-webmvc没有scopetest/scope进入target/bookms/WEB-INF/lib/目录执行ls | grep spring-webmvc确认jar包存在若缺失在IDE中右键项目 →Maven→Update project勾选Force Update of Snapshots/Releases5.2 现象登录页面CSS失效按钮错位F12看Network发现/bookms/static/css/style.css404原因spring-mvc.xml中mvc:resources配置路径错误或webapp/static/目录结构不对解决确认webapp/static/css/style.css物理路径存在检查spring-mvc.xmlmvc:resources mapping/static/** location/static/ /——location值必须以/开头且与webapp下目录名完全一致若static目录在webapp/resources/下则location应为/resources/5.3 现象批量导入Excel时报错java.lang.IllegalArgumentException: Invalid date format原因Excel中“出版日期”列格式为文本如2023年1月1日而非Excel日期格式解决在Excel中选中日期列 → 右键“设置单元格格式” → “日期” → 选2023/1/1格式或修改ExcelImportUtil.parseDate()方法增加文本日期解析if (cellValue.matches(\\d{4}年\\d{1,2}月\\d{1,2}日)) { return Date.valueOf(cellValue.replaceAll((\\d{4})年(\\d{1,2})月(\\d{1,2})日, $1-$2-$3)); }5.4 现象借书成功但库存没减少数据库book.stock字段不变原因BookMapper.xml中decreaseStock语句的WHERE条件写错如写成WHERE id #{id}漏掉AND stock 0导致UPDATE影响行数为0解决在MySQL命令行执行UPDATE book SET stock stock - 1 WHERE id 1 AND stock 0;观察Rows matched: 1 Changed: 1是否出现若Changed: 0说明库存已是0检查前端传的bookId是否正确若Rows matched: 0说明WHERE条件无匹配检查SQL是否拼错字段名5.5 现象上传封面后图片无法显示浏览器地址栏显示http://localhost:8081/bookms/static/uploads/cover/xxx.jpg但返回404原因Tomcat默认不提供webapps/bookms/static/下的静态资源服务需在web.xml中配置默认servlet解决在web.xml的servlet-mapping之后添加servlet-mapping servlet-namedefault/servlet-name url-pattern/static/*/url-pattern /servlet-mapping重启Tomcat即可。这是Tomcat 8的默认行为变更——静态资源不再自动映射。6. 二次开发指南三个高频改造场景的代码级落地方案6.1 场景一增加图书分类管理三级分类文学→小说→科幻需求原系统无分类所有图书平铺。需新增category表book表加category_id外键并在前端图书列表页增加分类筛选下拉框。改造步骤建表执行SQLCREATE TABLE category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, parent_id BIGINT DEFAULT 0, -- 0表示一级分类 level TINYINT NOT NULL DEFAULT 1 -- 1:一级,2:二级,3:三级 ); INSERT INTO category(name, parent_id, level) VALUES (文学, 0, 1), (小说, 1, 2), (科幻, 2, 3);实体类新增Category.javaBook.java加private Long categoryId;及getter/setterMapper接口CategoryMapper.java定义ListCategory selectAll();和ListCategory selectByParentId(Long parentId);Controller层BookController.list()中加入model.addAttribute(categories, categoryMapper.selectAll());JSP改造在book/list.jsp的搜索表单里加select namecategoryId classform-control option value全部分类/option c:forEach items${categories} varc option value${c.id}${c.name}/option /c:forEach /selectService层BookService.list()方法接收categoryId参数Mapper XML中where动态拼接where if testcategoryId ! null and categoryId ! 0 AND category_id #{categoryId} /if /where关键点分类查询是树形结构但本项目只需三级无需递归查询。parent_id字段让前端可轻松实现级联下拉选“文学”后AJAX查parent_id1的二级分类。6.2 场景二借阅超期自动标记每天凌晨2点扫描borrow_record表需求当前系统不主动识别超期需增加定时任务将borrow_date超过30天且return_date IS NULL的记录status更新为overdue。改造方案用Spring原生Scheduled不引入Quartz启用定时在applicationContext.xml中添加context:component-scan base-packagecom.example.bookms.task / task:annotation-driven /新建Task类src/main/java/com/example/bookms/task/BorrowOverdueTask.javaComponent public class BorrowOverdueTask { Autowired private BorrowRecordMapper borrowRecordMapper; // 每天凌晨2点执行 Scheduled(cron 0 0 0 2 * ?) public void markOverdue() { Calendar cal Calendar.getInstance(); cal.add(Calendar.DAY_OF_MONTH, -30); Date threshold cal.getTime(); borrowRecordMapper.updateOverdueStatus(threshold); System.out.println(超期任务执行完毕时间 new Date()); } }Mapper XMLBorrowRecordMapper.xml中添加update idupdateOverdueStatus UPDATE borrow_record SET status overdue WHERE status borrowed AND borrow_date lt; #{threshold} AND return_date IS NULL /update注意lt;是XML转义符代表。若用会报XML解析错误。cron 0 0 0 2 * ?表示秒0分0时2点?表示不指定具体某天避免与day-of-month冲突。6.3 场景三导出借阅记录为PDF用iText 5.5.13需求管理员需要导出某读者的所有借阅记录为PDF报表含表头、表格、页眉页脚。改造步骤加依赖pom.xml中添加dependency groupIdcom.itextpdf/groupId artifactIditextpdf/artifactId version5.5.13/version /dependencyController新增接口GetMapping(/borrow/exportPdf) public void exportPdf(RequestParam String readerId, HttpServletResponse response) throws Exception { ListBorrowRecord records borrowService.getRecordsByReaderId(readerId); Document document new Document(PageSize.A4, 36, 36, 54, 24); PdfWriter.getInstance(document, response.getOutputStream()); response.setContentType(application/pdf); response.setHeader(Content-Disposition, attachment; filenameborrow_ readerId .pdf); document.open(); // 添加标题 Font titleFont FontFactory.getFont(FontFactory.HELVETICA_BOLD, 16); document.add(new Paragraph(读者借阅记录报表, titleFont)); // 构建表格 PdfPTable table new PdfPTable(5); table.setWidthPercentage(100); table.addCell(图书ISBN); table.addCell(书名); table.addCell(借阅日期); table.addCell(归还日期); table.addCell(状态); for (BorrowRecord r : records) { table.addCell(r.getIsbn()); table.addCell(r.getTitle()); table.addCell(new SimpleDateFormat(yyyy-MM-dd).format(r.getBorrowDate())); table.addCell(r.getReturnDate() null ? - : new SimpleDateFormat(yyyy-MM-dd).format(r.getReturnDate())); table.addCell(r.getStatus()); } document.add(table); document.close(); }前端按钮在borrow/record.jsp中加a href${pageContext.request.contextPath}/borrow/exportPdf?readerId${reader.id} classbtn btn-primary导出PDF/a从那以后我每次接到“加个XX功能”的需求都先打开这个图书管理系统的pom.xml和web.xml对照着加依赖、配扫描包、写Mapper——它像一本活的SSM教科书所有配置错误都会立刻报错所有逻辑漏洞都在借阅流程里暴露。比起看十篇Spring事务原理不如亲手改一次decreaseStock的SQL看着库存数字真正变少。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站