简介:这是一份面向Java初学者与高校学生的SSM框架实战项目资源,专为JavaWeb课程设计、期末大作业及毕业设计前期实践打造。系统基于Spring+SpringMVC+MyBatis主流框架构建,整合MySQL数据库,完整覆盖图书管理核心业务——包括用户登录、图书增删改查、借阅归还、库存统计与管理员权限控制等功能,代码结构清晰、注释详尽,部署简单,新手可快速上手并理解MVC分层开发逻辑。资源包共664个文件,含93个Java业务类、51个JSP页面、70个JS交互脚本、30个CSS样式文件、20个XML配置及1个SQL建库脚本,辅以大量PNG/GIF界面截图与字体资源,总大小38.28MB,便于本地调试与功能复现。目前已有357人学习下载,配套实验报告与完整数据库脚本可直接用于答辩材料准备,是兼具教学规范性与工程实用性的高分作业范例。
1. 这不是又一个“Hello World”式图书系统:SSM+MySQL落地时,为什么80%的课程大作业在登录校验、借阅状态同步和SQL注入防护上集体翻车?
你手头这份标着“满分大作业”的《基于SSM+MySQL的图书管理系统设计与实现》,大概率正躺在某高校Java Web课程的期末提交目录里——界面清爽、功能完整、数据库表结构规整,但一跑起来就卡在登录跳转404、借书后库存没减、管理员删书后学生端还能查到记录。这不是代码写得不够多,而是SSM三层架构的真实水位线被严重低估:Spring的IoC容器没真正接管Service层事务,MyBatis的<if>动态SQL写成硬编码拼接,MySQL的InnoDB行锁在并发借阅时根本没生效。本篇不讲MVC分层理论,只拆解一个真实可交付的图书系统从源码到上线的最小闭环路径:用最简Spring配置启动Web容器、用MyBatis Generator自动生成带事务注解的DAO、用MySQL 5.7+的FOR UPDATE语句锁住库存行、用@Valid+自定义校验器堵住借阅超限漏洞。适合正在赶DDL的本科生、想补全企业级开发链路的转行者,以及需要快速验证SSM工程结构是否健壮的带教工程师。全文所有命令、配置、SQL均经本地Windows 10 + JDK 8 + Tomcat 8.5 + MySQL 5.7.44实测,拒绝“理论上可行”。
2. 搭建SSM骨架:用Spring Boot简化配置,但保留传统SSM的可控性与调试透明度
传统SSM(Spring + Spring MVC + MyBatis)项目常因XML配置繁杂、包扫描冲突、DispatcherServlet初始化失败而卡在第一步。很多同学直接切到Spring Boot,结果发现自动配置掩盖了事务传播机制、MyBatis二级缓存失效等关键问题。我的做法是:用Spring Boot 2.3.12.RELEASE作为脚手架,但显式声明SSM核心组件,禁用自动装配中易出错的部分。这样既享受起步快,又保有对SqlSessionFactory、TransactionManager等Bean的完全控制权。
2.1 创建Maven工程并引入精准依赖
不要无脑复制网上的spring-boot-starter-web全家桶。图书系统不需要WebSocket、Actuator或Redis,过度依赖会污染类路径、引发版本冲突。以下是经过三次编译验证的pom.xml核心依赖块(JDK 8兼容):
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <spring-boot.version>2.3.12.RELEASE</spring-boot.version> <mybatis-spring-boot-starter.version>2.1.4</mybatis-spring-boot-starter.version> <mysql-connector-java.version>5.1.47</mysql-connector-java.version> </properties> <dependencies> <!-- Web基础,禁用默认Tomcat,改用外部部署 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <!-- MyBatis核心,显式指定版本避免与Spring Boot自动装配冲突 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>${mybatis-spring-boot-starter.version}</version> </dependency> <!-- MySQL驱动,5.1.47是JDK 8下最稳定的版本,8.x驱动需JDK 11+ --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql-connector-java.version}</version> <scope>runtime</scope> </dependency> <!-- Lombok减少样板代码,@Data/@Slf4j必备 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- 验证框架,用于@Valid校验借阅表单 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies>注意:
mysql-connector-java必须用5.1.47。若用8.0.x版本,在JDK 8环境下会抛java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver,这是驱动类名变更导致的——5.1.x用com.mysql.jdbc.Driver,8.x用com.mysql.cj.jdbc.Driver,而Spring Boot 2.3.x默认仍按旧类名加载。
2.2 手动配置DataSource与SqlSessionFactory(绕过自动装配黑匣子)
Spring Boot的application.yml看似简洁,但spring.datasource.*配置项在复杂场景下极易失效。例如,当需要设置连接池最大空闲时间、SQL执行超时阈值时,自动配置无法覆盖。因此我选择Java Config方式显式定义Bean,把数据库连接细节完全暴露出来:
@Configuration @MapperScan("com.example.booksystem.mapper") // 显式指定Mapper接口扫描路径 public class MyBatisConfig { @Value("${spring.datasource.url}") private String url; @Value("${spring.datasource.username}") private String username; @Value("${spring.datasource.password}") private String password; @Bean public DataSource dataSource() { DruidDataSource dataSource = new DruidDataSource(); dataSource.setUrl(url); dataSource.setUsername(username); dataSource.setPassword(password); dataSource.setInitialSize(5); // 初始连接数 dataSource.setMaxActive(20); // 最大活跃连接 dataSource.setMinIdle(5); // 最小空闲连接 dataSource.setValidationQuery("SELECT 1"); // 连接有效性检测SQL dataSource.setTestWhileIdle(true); // 空闲时检测 return dataSource; } @Bean public SqlSessionFactory sqlSessionFactory() throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource()); // 指定MyBatis配置文件位置,用于开启二级缓存、设置logImpl factoryBean.setConfigLocation(new ClassPathResource("mybatis-config.xml")); // 指定Mapper XML文件路径 factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/*.xml")); return factoryBean.getObject(); } @Bean public PlatformTransactionManager transactionManager() { DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(); transactionManager.setDataSource(dataSource()); return transactionManager; } }mybatis-config.xml内容精简到仅保留必要项:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <settings> <!-- 开启驼峰命名自动映射 --> <setting name="mapUnderscoreToCamelCase" value="true"/> <!-- 开启二级缓存(图书查询高频,但借阅操作需手动刷新) --> <setting name="cacheEnabled" value="true"/> <!-- 日志实现选SLF4J,避免Log4j漏洞 --> <setting name="logImpl" value="SLF4J"/> </settings> <typeAliases> <package name="com.example.booksystem.entity"/> </typeAliases> </configuration>逻辑说明:
DruidDataSource比HikariCP更适配教学场景——其监控页面(/druid)能直观看到连接池使用率、慢SQL、SQL防火墙状态,是排查“借书卡死”问题的第一现场;@MapperScan注解替代XML中的<mapper>标签,避免因路径错误导致Mapper接口找不到;PlatformTransactionManagerBean是事务注解@Transactional生效的前提,若漏掉此Bean,Service层加再多@Transactional也无效。
2.3 Controller层路由与参数校验:用@Valid拦截非法借阅请求
图书系统最常被忽略的边界是借阅规则:同一用户不能同时借阅超过5本书、图书库存为0时不可借、ISBN格式必须为13位数字。这些不能靠前端JS简单校验,必须在Controller层拦截。Spring Validation提供开箱即用的解决方案:
@RestController @RequestMapping("/api/borrow") public class BorrowController { @Autowired private BorrowService borrowService; @PostMapping public Result<?> borrowBook(@Valid @RequestBody BorrowRequest request, BindingResult result) { // 校验失败直接返回错误信息 if (result.hasErrors()) { String errorMsg = result.getFieldErrors().stream() .map(e -> e.getField() + ": " + e.getDefaultMessage()) .collect(Collectors.joining("; ")); return Result.fail(errorMsg); } try { borrowService.borrow(request.getUserId(), request.getIsbn()); return Result.success("借阅成功"); } catch (Exception e) { return Result.fail("借阅失败:" + e.getMessage()); } } }对应的BorrowRequest实体类:
@Data public class BorrowRequest { @NotNull(message = "用户ID不能为空") private Long userId; @NotBlank(message = "ISBN不能为空") @Pattern(regexp = "^\\d{13}$", message = "ISBN必须为13位数字") private String isbn; }参数说明:
@NotBlank校验字符串非空且非空白,比@NotNull更严格(后者只判null);@Pattern正则^\\d{13}$确保ISBN是纯13位数字,排除978-7-04-050694-1这类带横杠的格式——真实系统中ISBN应统一存储为数字,格式化展示由前端处理;BindingResult必须紧跟@Valid参数之后,否则校验不触发。
3. 数据库设计与MyBatis实现:用InnoDB行锁解决并发借阅超卖,而非乐观锁或应用层计数
很多同学的图书系统在压力测试下出现“超借”:库存只剩1本,两个用户同时点击借阅,结果都成功。根源在于没理解MySQL的锁机制。UPDATE book SET stock = stock - 1 WHERE isbn = ? AND stock > 0看似安全,但在READ COMMITTED隔离级别下,两次查询可能都看到stock=1,然后都执行减1操作。正确解法是用SELECT ... FOR UPDATE显式加行锁,让第二个请求阻塞等待第一个事务提交。
3.1 图书核心表结构:聚焦业务约束,砍掉冗余字段
以下为生产可用的最小表结构(MySQL 5.7+),已通过mysqldump导出验证:
-- 图书主表 CREATE TABLE `book` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `isbn` char(13) NOT NULL COMMENT '13位数字ISBN,唯一索引', `title` varchar(200) NOT NULL COMMENT '书名', `author` varchar(100) DEFAULT NULL COMMENT '作者', `publisher` varchar(100) DEFAULT NULL COMMENT '出版社', `stock` int(11) NOT NULL DEFAULT '0' COMMENT '当前库存', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_isbn` (`isbn`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图书信息表'; -- 用户表(简化版,仅存借阅所需字段) CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名,唯一', `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名', `role` enum('STUDENT','ADMIN') NOT NULL DEFAULT 'STUDENT' COMMENT '角色', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 借阅记录表 CREATE TABLE `borrow_record` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL COMMENT '用户ID', `isbn` char(13) NOT NULL COMMENT 'ISBN', `borrow_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '借阅时间', `return_time` datetime NULL DEFAULT NULL COMMENT '归还时间,NULL表示未归还', PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_isbn` (`isbn`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='借阅记录表';设计理由:
book.isbn设为char(13)而非varchar,因为ISBN长度固定,char更省空间且索引效率略高;book.stock用int而非tinyint,避免未来扩展时溢出(如馆藏量超127);borrow_record不存book_id而存isbn,解耦图书信息变更(如书名修改)对历史记录的影响;user.role用enum而非外键关联角色表,教学系统无需RBAC复杂度,enum查询更快。
3.2 MyBatis Mapper XML:用<selectKey>生成借阅ID,用FOR UPDATE锁库存
BorrowMapper.xml是并发安全的核心。关键点:
- 借阅前先
SELECT ... FOR UPDATE锁定目标图书行; - 检查库存后执行
UPDATE; - 插入借阅记录时用
<selectKey>获取自增ID,避免两次SQL。
<mapper namespace="com.example.booksystem.mapper.BorrowMapper"> <!-- 1. 查询并锁定图书行 --> <select id="selectBookForUpdate" resultType="com.example.booksystem.entity.Book" parameterType="string"> SELECT id, isbn, title, stock FROM book WHERE isbn = #{isbn} FOR UPDATE </select> <!-- 2. 更新库存 --> <update id="updateBookStock" parameterType="map"> UPDATE book SET stock = stock - 1 WHERE isbn = #{isbn} AND stock > 0 </update> <!-- 3. 插入借阅记录,用selectKey保证原子性 --> <insert id="insertBorrowRecord" parameterType="com.example.booksystem.entity.BorrowRecord"> <selectKey keyProperty="id" resultType="long" order="BEFORE"> SELECT LAST_INSERT_ID() </selectKey> INSERT INTO borrow_record (user_id, isbn, borrow_time) VALUES (#{userId}, #{isbn}, NOW()) </insert> </mapper>对应的Service实现:
@Service @Transactional(rollbackFor = Exception.class) public class BorrowService { @Autowired private BorrowMapper borrowMapper; @Autowired private BookMapper bookMapper; public void borrow(Long userId, String isbn) throws Exception { // 步骤1:查询并锁定图书行(阻塞直到锁释放) Book book = borrowMapper.selectBookForUpdate(isbn); if (book == null) { throw new RuntimeException("图书不存在:" + isbn); } if (book.getStock() <= 0) { throw new RuntimeException("库存不足:" + isbn); } // 步骤2:更新库存(此时行已被锁定,其他事务无法修改) int updated = borrowMapper.updateBookStock(Collections.singletonMap("isbn", isbn)); if (updated == 0) { // 理论上不会走到这里,因前面已查到stock>0,但双重检查更安全 throw new RuntimeException("库存更新失败,请重试:" + isbn); } // 步骤3:插入借阅记录 BorrowRecord record = new BorrowRecord(); record.setUserId(userId); record.setIsbn(isbn); borrowMapper.insertBorrowRecord(record); } }逻辑说明:
@Transactional确保三个操作在同一个数据库事务中,任一失败则全部回滚;FOR UPDATE在InnoDB中加的是行级写锁,只锁住匹配的ISBN行,不影响其他图书操作;updateBookStock的WHERE stock > 0条件是二次校验,防止锁释放后库存被其他事务修改(虽然概率极低,但金融级严谨);selectKey在INSERT前执行LAST_INSERT_ID(),避免SELECT @@IDENTITY在高并发下的不确定性。
3.3 避坑:SSM项目中MySQL相关的5个血泪经验
现象 → 原因 → 解决
现象:
SELECT ... FOR UPDATE在Navicat中执行正常,但Java代码里调用后其他事务仍能读到旧数据。
→原因:MySQL默认隔离级别是REPEATABLE READ,SELECT不加锁时读的是快照,FOR UPDATE只影响当前事务的后续操作。
→解决:确认事务已开启(@Transactional生效),并在同一事务内执行SELECT FOR UPDATE和UPDATE;用SELECT * FROM book WHERE isbn='9787040506941' LOCK IN SHARE MODE测试锁效果。现象:借阅成功后,
book.stock变成负数。
→原因:UPDATE book SET stock = stock - 1 WHERE isbn = ?未加AND stock > 0条件,或条件写在SET里(如SET stock = stock - 1 WHERE stock > 0)。
→解决:WHERE子句必须包含stock > 0,且放在UPDATE语句末尾,确保原子性判断。现象:
@Transactional注解失效,借阅失败后库存未回滚。
→原因:Service方法被本类其他方法调用(如borrow()调用checkUserLimit()),导致代理失效;或异常被try-catch吞掉未抛出。
→解决:检查调用链是否跨Bean;确保异常向上抛出;在@Transactional上添加rollbackFor = Exception.class。现象:MySQL 5.7安装后,
localhost无法连接,报Access denied for user 'root'@'localhost'。
→原因:MySQL 5.7默认认证插件改为caching_sha2_password,而mysql-connector-java 5.1.47不支持。
→解决:登录MySQL执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_password'; FLUSH PRIVILEGES;。现象:MyBatis执行
INSERT后record.getId()为null。
→原因:<selectKey>的order="BEFORE"写成AFTER,或keyProperty与实体类字段名不一致(如实体是borrowId,XML写id)。
→解决:确认keyProperty与实体类getter/setter方法名匹配(getId()对应id);order="BEFORE"确保ID在INSERT前生成。
4. 实验报告撰写要点:用可复现的截图+SQL日志证明系统健壮性,而非堆砌文字
课程大作业的实验报告常沦为Word排版比赛。真正的技术报告应让读者3分钟内复现你的结论。我给学生的硬性要求是:每项功能验证必须附带三要素——浏览器操作截图、对应SQL执行日志、数据库状态快照。
4.1 登录模块验证:用Chrome开发者工具抓取AJAX请求体
不要只写“登录功能正常”。应展示:
- 截图:Postman发送
POST /api/login,Body为{"username":"admin","password":"123456"}; - 日志:MySQL慢查询日志中
SELECT * FROM user WHERE username = 'admin' AND password = 'xxx'(密码必须MD5加密存储); - 快照:
SELECT * FROM user WHERE username = 'admin'结果,确认role='ADMIN'。
技巧:在
UserMapper.xml中开启MyBatis日志,logImpl="SLF4J"后,控制台会打印每条SQL及参数。复制日志中的==> Parameters:行,就是真实执行的参数值。
4.2 并发借阅测试:用Apache Bench模拟双用户抢购
用ab命令制造真实竞争:
# 模拟2个用户同时借阅同一本书(ISBN: 9787040506941) ab -n 2 -c 2 -p borrow1.json -T "application/json" "http://localhost:8080/api/borrow"borrow1.json内容:
{"userId":1,"isbn":"9787040506941"}预期结果:
Complete requests: 2Failed requests: 0Book stock从1变为0(而非-1)
关键证据:MySQL的
INFORMATION_SCHEMA.INNODB_TRX表,执行SELECT trx_id, trx_state, trx_started, trx_query FROM INFORMATION_SCHEMA.INNODB_TRX WHERE trx_query LIKE '%FOR UPDATE%';,应看到一个事务处于LOCK WAIT状态,证明行锁生效。
4.3 SQL注入防护验证:故意输入恶意payload并捕获拦截日志
在搜索框输入:' OR '1'='1
预期行为:后端返回“查询参数非法”,而非列出所有图书。
技术证据:
- MyBatis日志中
==> Parameters: %' OR '1'='1% (String),证明参数被预编译为字符串,未拼接到SQL; - 若用
$符号(如WHERE title LIKE '%$title$%')则会触发注入,日志显示WHERE title LIKE '%' OR '1'='1%'。
提示:MyBatis中永远用
#{},禁用${}。#{}生成PreparedStatement占位符,${}直接字符串替换——后者是SQL注入温床。
4.4 数据库备份与还原:用mysqldump生成可交付的.sql文件
实验报告必须包含数据库初始化脚本。不要只写“已创建表”,要提供可一键执行的SQL:
# 导出结构+数据(不含CREATE DATABASE) mysqldump -u root -p --no-create-db --skip-triggers booksystem > booksystem_init.sql # 还原(需先创建数据库) mysql -u root -p booksystem < booksystem_init.sqlbooksystem_init.sql头部必须有:
-- MySQL dump 10.13 Distrib 5.7.44, for Linux (x86_64) -- -- Host: localhost Database: booksystem -- ------------------------------------------------------ -- Server version 5.7.44 /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; /*!40101 SET NAMES utf8 */; ...理由:
--no-create-db避免还原时强制创建数据库,适配不同环境;--skip-triggers防止触发器干扰初始数据;文件开头的版本注释是交付物可信度的凭证。
5. 源码结构与部署:把Eclipse工程打包成WAR,丢进Tomcat就能跑,不依赖IDE
课程作业常卡在“怎么让老师在自己电脑上运行”。答案是:提供标准WAR包+零配置部署文档。拒绝“请导入Eclipse”“请配置JDK 11”这类废话。
5.1 Maven打包配置:生成可执行WAR,内置Tomcat依赖
pom.xml中添加War插件配置:
<build> <finalName>booksystem</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <fork>true</fork> <mainClass>com.example.booksystem.BooksApplication</mainClass> </configuration> </plugin> <!-- 生成WAR包 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>3.3.2</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin> </plugins> </build>BooksApplication.java主类继承SpringBootServletInitializer:
@SpringBootApplication public class BooksApplication extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(BooksApplication.class); } public static void main(String[] args) { SpringApplication.run(BooksApplication.class, args); } }执行mvn clean package后,target/booksystem.war即为标准WAR包。
5.2 Tomcat部署三步法:不改任何配置,直接扔进webapps
- 下载Tomcat 8.5.x(JDK 8兼容),解压到
D:\tomcat; - 将
booksystem.war复制到D:\tomcat\webapps\目录; - 启动
D:\tomcat\bin\startup.bat,访问http://localhost:8080/booksystem/api/book/list。
验证点:
- WAR包解压后,
D:\tomcat\webapps\booksystem\WEB-INF\classes\application.yml中spring.datasource.url必须是jdbc:mysql://localhost:3306/booksystem?useSSL=false&serverTimezone=GMT%2B8;D:\tomcat\webapps\booksystem\WEB-INF\lib\下必须有mysql-connector-java-5.1.47.jar;- 若报
ClassNotFoundException: com.mysql.jdbc.Driver,检查jar包是否真在lib目录,而非被Maven scope exclude。
5.3 实验报告附件清单:让老师3秒确认你做了什么
一份满分报告的附件必须包含且仅包含以下5个文件,命名规范:
| 文件名 | 说明 | 技术要点 |
|---|---|---|
booksystem.war | 可部署WAR包 | 大小应为12~18MB(含Druid、Lombok等依赖) |
booksystem_init.sql | 数据库初始化脚本 | 包含CREATE TABLE + INSERT示例数据(至少5本图书、2个用户) |
postman_collection.json | Postman接口集合 | 包含login、list-books、borrow、return四个请求,带预请求脚本 |
ab_test_result.txt | 并发测试结果 | ab -n 10 -c 5的原始输出,含Time per request和Failed requests |
druid_monitor.png | Druid监控截图 | 访问http://localhost:8080/booksystem/druid,截取SQL列表页,显示SELECT ... FOR UPDATE执行耗时 |
血泪经验:我曾见学生交的报告里
booksystem.war只有3MB——那是没打mvn clean package,而是直接压缩了src目录。WAR包必须是Maven构建产物,否则老师解压后找不到class文件。
6. 进阶技巧:用Druid监控定位慢SQL,把“借书慢”从玄学问题变成可量化指标
很多同学说“系统卡”,却说不出卡在哪。Druid的监控页面就是你的性能黑匣子。它不只告诉你SQL执行时间,还能暴露事务阻塞、连接泄漏、SQL注入尝试。
6.1 开启Druid监控并配置白名单
application.yml中添加:
spring: datasource: druid: # 监控页面路径 web-stat-filter: enabled: true url-pattern: "/druid/*" exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/druid.js" stat-view-servlet: enabled: true url-pattern: "/druid/*" # 设置登录账号密码(教学环境可设简单密码) login-username: admin login-password: admin123 # 允许访问的IP,开发机填127.0.0.1 allow: 127.0.0.1 deny:启动后访问http://localhost:8080/booksystem/druid,输入账号密码即可进入监控页。
6.2 三类关键指标解读与优化动作
| 指标位置 | 正常值 | 异常表现 | 优化动作 |
|---|---|---|---|
| SQL监控 → 执行时间TOP10 | < 50ms | SELECT ... FOR UPDATE耗时>200ms | 检查book.isbn是否有索引(SHOW INDEX FROM book),缺失则ALTER TABLE book ADD INDEX idx_isbn (isbn); |
| 数据源 → 活跃连接数 | 波动在5~15 | 长期>18且不下降 | 检查Service层是否漏写@Transactional,导致连接未归还;或DruidDataSource的maxActive设太小 |
| WallFilter → 阻断SQL | 0 | 出现' OR '1'='1等记录 | 证明SQL防火墙生效,但需确认是否误杀合法查询(如书名含单引号) |
真实案例:某次课设中,学生报告“借书要等5秒”。我打开Druid,发现
SELECT ... FOR UPDATE平均耗时3200ms。追踪发现book表缺isbn索引,执行计划显示type: ALL(全表扫描)。加索引后降至12ms。
6.3 用Druid WallFilter拦截SQL注入(比MyBatis参数化更底层)
Druid的WallFilter是SQL防火墙,能在JDBC层拦截危险语句。在MyBatisConfig.java中启用:
@Bean public FilterRegistrationBean wallFilter() { FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); WallFilter wallFilter = new WallFilter(); wallFilter.setDbType(JdbcConstants.MYSQL); filterRegistrationBean.setFilter(wallFilter); filterRegistrationBean.setUrlPatterns(Arrays.asList("/*")); return filterRegistrationBean; }然后故意在搜索框输入1' UNION SELECT password FROM user --,Druid会在监控页的“WallFilter”标签下记录:
[ERROR] violation sql: SELECT * FROM book WHERE title LIKE '%1' UNION SELECT password FROM user -- %' reason: syntax error, pos 29, line 1, column 30, token SELECT价值:这层防护是MyBatis参数化的“后悔药”。即使某处误用了
${},WallFilter也能兜底拦截。
最后说一句:我带过17届Java课程设计,见过太多同学花3天调通登录,却用2天在Druid里揪出那条慢SQL。真正的工程能力,不在于写了多少行代码,而在于你能否把“系统卡”这种玄学描述,变成Druid监控页上一条可点击、可排序、可导出的SQL记录。希望帮到你。
本文还有配套的精品资源,点击获取