☰
SSM+MySQL图书系统并发安全与SQL注入防护实战
2026/10/6 8:12:53 网站建设 项目流程

简介:这是一份面向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是并发安全的核心。关键点:

  1. 借阅前先SELECT ... FOR UPDATE锁定目标图书行;
  2. 检查库存后执行UPDATE;
  3. 插入借阅记录时用<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个血泪经验

现象 → 原因 → 解决

  1. 现象: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测试锁效果。

  2. 现象:借阅成功后,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语句末尾,确保原子性判断。

  3. 现象:@Transactional注解失效,借阅失败后库存未回滚。
    →原因:Service方法被本类其他方法调用(如borrow()调用checkUserLimit()),导致代理失效;或异常被try-catch吞掉未抛出。
    →解决:检查调用链是否跨Bean;确保异常向上抛出;在@Transactional上添加rollbackFor = Exception.class。

  4. 现象: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;。

  5. 现象: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: 2
  • Failed requests: 0
  • Book 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.sql

booksystem_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

  1. 下载Tomcat 8.5.x(JDK 8兼容),解压到D:\tomcat;
  2. 将booksystem.war复制到D:\tomcat\webapps\目录;
  3. 启动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.jsonPostman接口集合包含login、list-books、borrow、return四个请求,带预请求脚本
ab_test_result.txt并发测试结果ab -n 10 -c 5的原始输出,含Time per request和Failed requests
druid_monitor.pngDruid监控截图访问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< 50msSELECT ... 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 → 阻断SQL0出现' 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记录。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询