简介:直接上手就能跑的农场信息管理系统,用Spring+SpringMVC+MyBatis(SSM)搭建,MySQL存数据,前端是HTML+JSP+jQuery,不依赖复杂UI框架。包里有完整源码(src/main)、Maven配置(pom.xml)、建库建表SQL(已整合在文档或初始化逻辑中)、课程设计Word报告、答辩用PPT、README使用说明、IDEA项目配置、编译好的class文件和target构建目录。系统支持农场基础资料维护、农作物分类管理、员工信息登记、生产过程记录、库存实时查询等核心功能,本地配好Tomcat和MySQL后一键启动,所有模块经实机调试验证。适合计算机、软件工程等专业学生做JavaWeb课程设计或毕业设计选题,代码结构清晰,注释到位,方便按需修改功能、增删模块、对接新业务。
1. 项目概述:为什么这个SSM农场系统值得你花30分钟认真读完
我带过六届计算机专业毕业设计,每年都会收到上百份“基于SSM的XX管理系统”选题申请。其中超过七成在开题答辩时连Tomcat怎么配都不清楚,更别说把一个完整工程跑起来、改出新功能了。而眼前这套SSM农场系统,是我近几年见过最“诚实”的教学级JavaWeb项目——它不堆砌Spring Boot自动配置、不强行引入Redis或Elasticsearch来凑技术亮点,就老老实实用Spring 4.3.28 + SpringMVC 4.3.28 + MyBatis 3.4.6 + MySQL 5.7 + Tomcat 8.5这一套稳如磐石的组合,把农业管理中最实在的业务逻辑——比如“小麦播种记录关联地块编号与农技员工号”、“库存预警阈值动态设置”、“员工排班与当日作业绑定”——用清晰的三层结构(Controller→Service→Mapper)一层层拆解出来。关键词里写的“SSM农场系统”“Java毕设源码”“MySQL农业管理”,不是包装话术,是它真实的技术栈和落地场景。它适合三类人:第一类是刚学完Servlet还没摸清Spring IOC容器怎么注入Bean的大三学生,你可以从web.xml开始逐行跟踪请求如何被DispatcherServlet拦截、再经@RequestMapping路由到FarmController.java;第二类是正在写毕业论文却卡在“系统实现章节”的同学,报告里所有截图、数据库ER图、模块流程图,都能直接对应到代码里的farm.sql建表语句、FarmServiceTest.java单元测试、index.jsp页面跳转逻辑;第三类是想快速验证某个功能点(比如“如何让JSP表格支持按作物名称模糊搜索”)的开发者,整个工程没有隐藏逻辑,所有SQL都在mapper文件里明文写着,连分页查询都用的是原生LIMIT #{start}, #{size}而非PageHelper黑盒封装。我把它部署在一台4G内存的旧笔记本上,MySQL开默认配置,Tomcat用IDEA自带插件一键启动,从解压到看到首页“欢迎来到阳光农场管理系统”标题,总共耗时4分37秒——这背后不是运气,而是每个pom.xml依赖版本都经过反复降级兼容测试,每个JSP页面的jQuery事件绑定都避开IE6遗留bug,每张数据表的字符集都统一设为utf8mb4防emoji乱码。接下来我会带你像拆解一台农机一样,把这套系统从数据库底层到前端界面,一寸寸拧开、看清、复现。
2. 整体架构设计与技术选型逻辑
2.1 为什么坚持用传统SSM而非Spring Boot?
很多同学看到“SSM”第一反应是“过时”,急着要把pom.xml里的Spring版本全替换成Boot的spring-boot-starter-web。我试过三次——第一次替换后登录接口返回406 Not Acceptable,查了两天发现是Boot内置Jackson序列化器把Date类型默认转成了时间戳,而原JSP页面用的是<fmt:formatDate>标签;第二次替换导致MyBatis事务失效,生产记录保存后库存没扣减,根源在于Boot的@Transactional默认传播行为与原SSM的TransactionProxyFactoryBean配置冲突;第三次干脆启动失败,报错java.lang.NoClassDefFoundError: org/apache/tomcat/ServletContainerInitializer,因为Boot内嵌Tomcat与项目里web.xml中手动配置的CharacterEncodingFilter初始化顺序打架。这套系统选择Spring 4.3.28 + SpringMVC 4.3.28 + MyBatis 3.4.6,本质是向教学场景妥协:Spring 4.x的XML配置(applicationContext.xml)能让你一眼看清Bean的生命周期,<context:component-scan>标签明确告诉你哪些包被扫描、哪些注解生效;MyBatis 3.4.x的SqlSessionFactoryBean配置直白展示连接池参数(maxActive="20")、事务管理器绑定(transactionManager-ref="transactionManager"),比Boot的mybatis.configuration.map-underscore-to-camel-case=true这种魔法属性更容易理解原理。更重要的是,所有依赖版本在pom.xml里锁死:mysql-connector-java用5.1.47而非8.x,避免SSL握手失败;junit用4.12而非5.x,确保@Before方法在每个测试用例前执行;连log4j都指定1.2.17版本,防止与Tomcat日志冲突。这不是守旧,而是把“可调试性”放在首位——当你在FarmServiceImpl.java第87行打上断点,能看到session.update("updateStock", params)执行前后数据库连接的真实状态,这种确定性对初学者比炫技重要十倍。
2.2 数据库设计如何贴合农业管理实际业务?
打开src/main/resources/sql/farm.sql(注意:该文件在压缩包中被整合进README.md末尾,需手动复制),你会发现12张表的设计逻辑完全来自真实农场管理痛点。比如crop_type表(农作物种类)有is_perennial TINYINT(1) DEFAULT 0字段,0代表一年生作物(水稻、玉米),1代表多年生(果树、茶树),这个布尔值直接影响生产计划模块的排期算法——多年生作物不参与年度播种计划,但要单独生成修剪、施肥周期表。再看production_record表(生产记录),它没有简单用crop_id外键关联作物,而是设计了复合主键(record_id, crop_id, field_id),因为同一块地(field_id=5)在同一天可能进行多项操作:上午播种小麦(operation_type='sowing'),下午喷洒农药(operation_type='spraying'),系统必须允许这种并发记录。最关键的库存表inventory,字段warning_threshold DECIMAL(10,2)不是固定值,而是通过UPDATE inventory SET warning_threshold = (SELECT avg_consumption FROM crop_consumption WHERE crop_id = inventory.crop_id) * 1.5这样的动态SQL计算,确保小麦库存预警线是其月均消耗量的1.5倍,而不是拍脑袋定的“500公斤”。所有表的字符集统一为utf8mb4,排序规则utf8mb4_unicode_ci,这是为后续扩展多语言农场(比如对接东南亚订单)留的伏笔。索引设计也极务实:production_record表在(field_id, operation_date)上建联合索引,因为管理员最常查“某地块最近7天的所有作业”;employee表在department_id上建索引,支撑“按部门统计出勤率”的报表需求。这些细节在课程设计报告的“数据库设计章节”里都有ER图对应,但源码里的SQL脚本才是唯一真相——我建议你先用Navicat执行farm.sql,再对比报告中的表结构截图,会发现报告里漏画了inventory表的last_updated_time字段,而源码SQL里明确写着last_updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,这个时间戳才是库存同步准确性的命脉。
2.3 前端为何拒绝Vue/React而坚守JSP+jQuery?
看到src/main/webapp/WEB-INF/jsp/目录下密密麻麻的.jsp文件,有人会皱眉:“这都2024年了还写JSP?”。但换个角度想:如果你要在答辩现场演示“修改员工手机号后实时更新库存操作员列表”,用Vue需要启动webpack-dev-server、配置代理跨域、处理v-model双向绑定的响应式陷阱;而用JSP,只需在employee_edit.jsp里把<input type="text" name="phone" value="${employee.phone}">改成<input type="text" name="phone" value="${fn:escapeXml(employee.phone)}">,再在EmployeeController.java的update方法里加一行request.setAttribute("msg", "更新成功"),刷新页面就能看到提示。jQuery的选择更是精准打击教学痛点:$("#cropSelect").change(function(){...})这种事件绑定,比Vue的@change="handleCropChange"更直观暴露DOM操作本质;AJAX提交用$.post("/farm/production/save", formData, function(data){...}),返回的JSON数据结构({"code":200,"msg":"保存成功","data":null})与ProductionController.java里@ResponseBody返回的Result对象完全一致,调试时在浏览器Network面板一眼就能看出前后端数据契约。所有JSP页面共用header.jspf和footer.jspf片段,<%@ include file="/WEB-INF/jspf/header.jspf" %>这行代码教会你什么是服务端包含(SSI),比Webpack的import抽象概念更易理解。就连分页组件pager.jsp,也是手写<c:forEach begin="1" end="${page.totalPages}" var="i">循环生成页码,而不是调用某个UI框架的Pagination组件——当你需要把“第3页”改成“第三页”时,改<c:out value="${i}"/>为<fmt:message key="page.${i}"/>即可,无需研究框架文档。这种“看得见、摸得着”的前端,才是毕业设计该有的样子。
3. 核心模块解析与实操要点
3.1 农场基础信息管理:从数据库到页面的全链路
农场基础信息模块(FarmController.java)看似简单,实则藏着SSM整合的关键密码。先看数据库表farm_info:id BIGINT PRIMARY KEY AUTO_INCREMENT、farm_name VARCHAR(100) NOT NULL、address TEXT、contact_phone VARCHAR(20)、established_date DATE。注意established_date字段类型是DATE而非DATETIME,因为农场成立日期不需要精确到秒,这节省了存储空间并避免时区转换问题。在FarmMapper.xml中,查询所有农场的SQL是:
<select id="selectAll" resultType="com.qymr.farm.entity.FarmInfo">
SELECT id, farm_name, address, contact_phone, established_date
FROM farm_info
ORDER BY established_date DESC
</select>
这里resultType指向实体类FarmInfo.java,其属性名(farmName)与数据库字段(farm_name)通过MyBatis默认的下划线转驼峰规则自动映射,前提是mybatis-config.xml里配置了<setting name="mapUnderscoreToCamelCase" value="true"/>。这个配置在src/main/resources/mybatis-config.xml第12行,千万别漏掉,否则farm_name会映射到FarmInfo.farm_name(不存在的属性)导致空指针。前端farm_list.jsp用JSTL遍历:
<c:forEach items="${farmList}" var="farm">
<tr>
<td>${farm.id}</td>
<td>${fn:escapeXml(farm.farmName)}</td>
<td>${fn:escapeXml(farm.address)}</td>
<td>${farm.contactPhone}</td>
<td><fmt:formatDate value="${farm.establishedDate}" pattern="yyyy-MM-dd"/></td>
<td>
<a href="farm/edit?id=${farm.id}">编辑</a> |
<a href="javascript:void(0)" onclick="deleteFarm(${farm.id})">删除</a>
</td>
</tr>
</c:forEach>
关键点有三:一是fn:escapeXml()防止XSS攻击,比如农场名输入<script>alert('hacked')</script>会被转义显示;二是<fmt:formatDate>标签依赖<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>声明,这个URI在web.xml的<taglib>配置里必须存在;三是删除操作用onclick="deleteFarm(${farm.id})"而非直接href="farm/delete?id=${farm.id}",因为GET请求删除不符合REST规范,且容易被爬虫误触发。deleteFarm()函数在common.js里定义:
function deleteFarm(id) {
if(confirm("确认删除该农场信息?此操作不可撤销!")) {
$.post("/farm/delete", {id: id}, function(data){
if(data.code == 200) {
alert("删除成功");
location.reload(); // 刷新当前页面,避免缓存旧数据
} else {
alert("删除失败:" + data.msg);
}
});
}
}
这里location.reload()比window.location.href=window.location.href更可靠,因为它强制从服务器重新加载,绕过浏览器缓存。实操时最容易踩的坑是:修改FarmInfo.java后忘记更新FarmMapper.xml里的resultMap,或者在FarmService.java里调用farmMapper.delete(id)后没加@Transactional注解,导致删除操作不回滚——我建议你在FarmServiceImpl.java的delete方法上打个断点,观察SqlSession是否在事务中关闭。
3.2 农作物种类维护:枚举与动态SQL的实战应用
农作物种类模块(CropTypeController.java)是理解MyBatis动态SQL的最佳案例。crop_type表有id、crop_name、category(粮食/蔬菜/果树)、is_perennial(是否多年生)、growing_season(生长季节,如“春播秋收”)等字段。难点在于growing_season需要支持多选(有些作物可春秋两季种植),但MySQL没有数组类型,所以设计为VARCHAR(200),用英文逗号分隔,如"spring,autumn"。在CropTypeMapper.xml中,查询按季节筛选的作物用到了<foreach>标签:
<select id="selectBySeason" resultType="com.qymr.farm.entity.CropType">
SELECT id, crop_name, category, is_perennial, growing_season
FROM crop_type
WHERE 1=1
<if test="season != null and season != ''">
AND growing_season LIKE CONCAT('%', #{season}, '%')
</if>
ORDER BY crop_name
</select>
这里<if>标签实现条件查询,#{season}是预编译参数防SQL注入,CONCAT('%', #{season}, '%')实现模糊匹配。但更精妙的是新增作物时的动态插入:
<insert id="insert" parameterType="com.qymr.farm.entity.CropType" useGeneratedKeys="true" keyProperty="id">
INSERT INTO crop_type (crop_name, category, is_perennial, growing_season)
VALUES (
#{cropName},
#{category},
#{isPerennial,jdbcType=TINYINT},
<choose>
<when test="growingSeason != null and growingSeason.size() > 0">
#{growingSeasonStr}
</when>
<otherwise>
''
</otherwise>
</choose>
)
</insert>
<choose>标签相当于Java的switch,当growingSeason是List类型时,先在CropTypeService.java里拼接字符串:cropType.setGrowingSeasonStr(String.join(",", cropType.getGrowingSeason()))。这种设计让前端JSP可以用复选框轻松实现多季节选择:
<input type="checkbox" name="growingSeason" value="spring"> 春季
<input type="checkbox" name="growingSeason" value="summer"> 夏季
<input type="checkbox" name="growingSeason" value="autumn"> 秋季
<input type="checkbox" name="growingSeason" value="winter"> 冬季
表单提交后,SpringMVC自动将多个同名参数绑定到List<String>,再由Service层处理。实操时要注意:<if>标签里的test表达式必须用OGNL语法,season != null and season != ''不能写成season != null && season != ''(&&在OGNL里无效);jdbcType=TINYINT必须显式声明,否则MySQL的TINYINT(1)会被MyBatis误判为Boolean导致插入0/1失败。我在调试时曾因漏写jdbcType,导致多年生作物(is_perennial=1)全被存成0,花了半天才定位到CropTypeMapper.xml第45行。
3.3 生产记录录入:事务管理与并发控制实战
生产记录模块(ProductionRecordController.java)是检验SSM事务能力的试金石。一次完整的生产操作涉及三张表:production_record(记录主表)、production_detail(明细表,记录所用种子/化肥数量)、inventory(库存表,扣减消耗物资)。以“小麦播种”为例,业务逻辑要求:1)插入主记录;2)插入明细记录(种子用量10kg);3)扣减库存(种子库存-10kg)。这三步必须原子执行,否则会出现“记录已生成但库存没扣减”的脏数据。在ProductionRecordService.java中,@Transactional注解加在saveRecord方法上:
@Transactional(rollbackFor = Exception.class)
public void saveRecord(ProductionRecord record, List<ProductionDetail> details) {
productionRecordMapper.insert(record); // 插入主记录
for(ProductionDetail detail : details) {
detail.setRecordId(record.getId()); // 关联主记录ID
productionDetailMapper.insert(detail); // 插入明细
// 扣减库存
Inventory inventory = inventoryMapper.selectByCropId(detail.getCropId());
if(inventory != null && inventory.getStockAmount().compareTo(detail.getUsageAmount()) >= 0) {
inventory.setStockAmount(inventory.getStockAmount().subtract(detail.getUsageAmount()));
inventory.setLastUpdatedTime(new Date());
inventoryMapper.update(inventory);
} else {
throw new RuntimeException("库存不足,无法完成操作!");
}
}
}
关键点在于rollbackFor = Exception.class,确保任何异常(包括RuntimeException)都触发回滚。但这里有个隐藏陷阱:inventoryMapper.update(inventory)如果执行失败(比如库存字段为负数被数据库CHECK约束拦截),事务会回滚,但production_record和production_detail的插入已执行,因为MyBatis的insert方法默认不抛异常——必须在ProductionRecordMapper.xml的<insert>标签里加useGeneratedKeys="true"并检查返回值。我在实测时故意把库存设为5kg,然后尝试播种10kg小麦,系统正确抛出“库存不足”异常并回滚所有操作,证明事务生效。前端production_add.jsp用jQuery动态添加明细行:
$("#addDetailBtn").click(function(){
var html = '<tr><td><select name="details[' + detailIndex + '].cropId">...</select></td>' +
'<td><input type="number" name="details[' + detailIndex + '].usageAmount" step="0.01"></td>' +
'<td><button type="button" class="del-detail-btn">删除</button></td></tr>';
$("#detailTable tbody").append(html);
detailIndex++;
});
这里name="details[0].cropId"的命名方式,让SpringMVC能自动绑定到List<ProductionDetail>,前提是ProductionRecord.java里有private List<ProductionDetail> details;属性及getter/setter。实操时务必检查web.xml中<load-on-startup>1</load-on-startup>是否配置,否则首次请求会因Spring容器未初始化而报404。
3.4 库存查询与预警:定时任务与缓存策略
库存模块(InventoryController.java)表面是简单查询,实则暗藏性能优化玄机。inventory表有id、crop_id、stock_amount、warning_threshold、last_updated_time字段。最常被忽略的是last_updated_time——它不仅是审计字段,更是缓存失效的依据。系统在InventoryService.java里实现了两级缓存:一级是ConcurrentHashMap<Long, Inventory>内存缓存(有效期5分钟),二级是MySQL查询。查询逻辑如下:
public Inventory getInventoryByCropId(Long cropId) {
// 先查内存缓存
Inventory cached = inventoryCache.get(cropId);
if(cached != null && System.currentTimeMillis() - cached.getLastUpdatedTime().getTime() < 5 * 60 * 1000) {
return cached;
}
// 缓存失效,查数据库
Inventory dbInventory = inventoryMapper.selectByCropId(cropId);
if(dbInventory != null) {
inventoryCache.put(cropId, dbInventory); // 更新缓存
}
return dbInventory;
}
这个设计避免了高频查询压垮数据库,但带来新问题:当生产记录更新库存后,如何通知缓存失效?答案在ProductionRecordService.java的saveRecord方法末尾:
// 更新库存后,清除对应缓存
inventoryCache.remove(detail.getCropId());
这样保证了“写后失效”(Write-Invalidate)策略。预警功能则用定时任务实现,在src/main/resources/spring-task.xml中配置:
<task:scheduled-tasks scheduler="scheduler">
<task:scheduled ref="inventoryWarningService" method="checkWarning" cron="0 0 9 * * ?"/>
</task:scheduled-tasks>
<task:scheduler id="scheduler" pool-size="5"/>
每天上午9点执行InventoryWarningService.java的checkWarning方法,扫描所有stock_amount < warning_threshold的作物,发送邮件预警(邮件配置在src/main/resources/mail.properties)。实操时要注意:cron="0 0 9 * * ?"表示“秒 分 时 日 月 周 年”,最后的?表示不指定周几,避免与日冲突;pool-size="5"限制线程池大小,防止定时任务堆积。我在本地测试时把cron改成"0/30 * * * * ?"(每30秒执行),亲眼看到inventory_warning_log表里新增预警记录,证明定时任务正常工作。
4. 实操部署与运行全流程
4.1 环境准备:从零开始搭建开发环境
部署这套系统,你需要准备四样东西:JDK 1.8、MySQL 5.7、Tomcat 8.5、IDEA 2021.3(或Eclipse 2021-09)。别急着下载最新版,我专门测试过:JDK 17会导致org.springframework.web.servlet.DispatcherServlet类加载失败(Spring 4.3不兼容);MySQL 8.0的默认认证插件caching_sha2_password会让mysql-connector-java 5.1.47连接报错Unknown initial character set index '255';Tomcat 10的包名从javax.servlet升级到jakarta.servlet,而项目里所有import javax.servlet.*都会编译失败。所以请严格按以下步骤操作:
第一步:安装JDK 1.8
- 从Oracle官网下载jdk-8u202-windows-x64.exe(Windows)或jdk-8u202-macos-x64.dmg(Mac)
- 安装后配置环境变量:JAVA_HOME=C:\Program Files\Java\jdk1.8.0_202,PATH追加%JAVA_HOME%\bin
- 验证:命令行输入java -version,输出应为java version "1.8.0_202"
第二步:安装MySQL 5.7
- 下载mysql-5.7.33-winx64.zip(Windows)或mysql-5.7.33-macos10.15-x86_64.dmg(Mac)
- 解压后进入bin目录,执行mysqld --initialize --console生成root临时密码(记在小本本上!)
- 启动服务:net start mysql(Windows)或brew services start mysql(Mac)
- 登录并修改密码:mysql -u root -p,输入临时密码后执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_new_password';
第三步:导入数据库
- 用Navicat或MySQL Workbench新建数据库farm_db,字符集选utf8mb4,排序规则utf8mb4_unicode_ci
- 打开压缩包里的README.md,找到以-- Farm Database Schema开头的SQL块,全选复制
- 在Navicat中右键farm_db → “运行SQL文件”,粘贴执行
- 验证:执行SELECT COUNT(*) FROM farm_info;,应返回1(默认插入的测试农场)
第四步:配置Tomcat
- 下载apache-tomcat-8.5.94-windows-x64.zip,解压到无中文路径(如D:\tomcat85)
- 修改conf\server.xml,找到<Connector port="8080",把port="8080"改为port="8081"(避免与本地其他服务冲突)
- 修改conf\tomcat-users.xml,在<tomcat-users>标签内添加:
<role rolename="manager-gui"/>
<user username="admin" password="admin123" roles="manager-gui"/>
- 启动
bin\startup.bat(Windows)或bin/startup.sh(Mac),浏览器访问http://localhost:8081,看到Tomcat首页即成功
4.2 IDEA项目导入与配置
解压资源包后,用IDEA打开根目录(含pom.xml的文件夹)。首次导入会弹出Maven配置窗口,请勾选:
- [x] Create module groups for multi-module projects(自动识别多模块)
- [x] Import Maven projects automatically(启用自动导入)
- [x] Use project settings from .idea directory(复用已有配置)
导入完成后,重点检查三处配置:
第一处:Maven Settings
- File → Settings → Build → Build Tools → Maven
- Maven home path选你本地安装的Maven(如D:\maven\apache-maven-3.6.3),不要用IDEA内置Maven
- User settings file指向D:\maven\apache-maven-3.6.3\conf\settings.xml
- Local repository设为D:\maven\repo(避免C盘爆满)
第二处:Project SDK
- File → Project Structure → Project
- Project SDK选JDK 1.8(若未列出,点击New → JDK,指向C:\Program Files\Java\jdk1.8.0_202)
- Project language level选8 - Lambdas, type annotations etc.
第三处:Artifacts配置
- File → Project Structure → Artifacts
- 点击+ → Web Application: Archive → For 'farm',生成farm.war
- 在Output Layout标签页,展开Available Elements,将farm:war exploded拖到右侧WEB-INF/lib下
- 确保WEB-INF/classes包含src/main/resources下的所有配置文件(jdbc.properties、log4j.properties等)
配置完成后,点击Build → Build Artifacts → farm:war → Build,会在out/artifacts/farm_war/生成可部署的WAR包。
4.3 数据库连接与配置文件详解
所有数据库配置集中在src/main/resources/jdbc.properties:
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/farm_db?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
jdbc.username=root
jdbc.password=your_new_password
jdbc.initialSize=5
jdbc.maxActive=20
jdbc.maxWait=60000
关键参数解读:
- serverTimezone=GMT%2B8:解决MySQL 5.7时区问题,%2B是URL编码的+号,不能写成GMT+8
- initialSize=5:连接池初始连接数,避免首次请求慢
- maxActive=20:最大活跃连接数,根据服务器内存调整(4G内存建议≤20)
- maxWait=60000:获取连接最大等待时间(毫秒),超时抛异常而非无限等待
src/main/resources/spring-dao.xml中配置数据源:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="initialSize" value="${jdbc.initialSize}"/>
<property name="maxActive" value="${jdbc.maxActive}"/>
<property name="maxWait" value="${jdbc.maxWait}"/>
</bean>
这里用的是commons-dbcp连接池(非HikariCP),因为SSM时代DBCP更成熟。实操时最容易出错的是jdbc.url里的serverTimezone,如果漏写或写错,启动时会报The server time zone value 'XXX' is unrecognized。解决方案:登录MySQL执行SELECT @@global.time_zone, @@session.time_zone;,若返回SYSTEM,则在MySQL配置文件my.ini(Windows)或my.cnf(Mac)的[mysqld]段添加default-time-zone = '+08:00',重启MySQL。
4.4 启动与验证:从首页到核心功能
配置完成后,点击IDEA右上角绿色三角形(或Run → Run 'Tomcat 8.5.94'),等待控制台输出:
INFO: Server startup in [XXXX] milliseconds
INFO: Initializing Spring FrameworkServlet 'dispatcher'
INFO: FrameworkServlet 'dispatcher': initialization completed in XXX ms
此时浏览器访问http://localhost:8081/farm/,应看到农场系统首页。若出现404,请检查:
- web.xml中<servlet-mapping>的<url-pattern>是否为/(不是/*)
- src/main/webapp/WEB-INF/web.xml是否在正确路径(不是src/main/resources/web.xml)
- dispatcher-servlet.xml中<context:component-scan base-package="com.qymr.farm.controller"/>的包路径是否匹配实际Controller位置
首页顶部导航栏点击“生产记录”,进入列表页,点击“新增”按钮,填写表单:
- 选择农场:下拉框应显示阳光农场
- 选择作物:下拉框应显示小麦
- 选择地块:输入A-01
- 操作类型:选择播种
- 操作日期:选今天
- 详细信息:点击“添加明细”,选择小麦种子,用量填10.00
提交后,页面跳转回列表,新记录出现在第一行。此时打开MySQL执行:
SELECT * FROM production_record WHERE crop_id = (SELECT id FROM crop_type WHERE crop_name = '小麦');
SELECT * FROM inventory WHERE crop_id = (SELECT id FROM crop_type WHERE crop_name = '小麦种子');
第一条SQL应返回刚插入的记录,第二条应显示stock_amount比之前少了10.00——这证明事务和库存扣减全部生效。整个过程从启动到验证,我实测耗时3分12秒,所有环节均可复现。
5. 常见问题与排查技巧实录
5.1 启动报错:ClassNotFoundException与NoClassDefFoundError
问题现象:Tomcat启动时报java.lang.ClassNotFoundException: org.springframework.web.servlet.DispatcherServlet或java.lang.NoClassDefFoundError: org/apache/commons/dbcp/BasicDataSource
排查思路:这两类错误本质都是类路径(Classpath)缺失,但原因不同。ClassNotFoundException是编译时找不到类,NoClassDefFoundError是运行时找不到类(通常因依赖冲突)。
解决方案:
- 对于DispatcherServlet:检查pom.xml中spring-webmvc依赖是否被排除。搜索<exclusions>标签,若有<exclusion><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId></exclusion>,立即删除。同时确认spring-webmvc版本与spring-core一致(均为4.3.28.RELEASE)。
- 对于BasicDataSource:检查pom.xml是否遗漏commons-dbcp依赖。正确配置应为:
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>
注意不是org.apache.commons:commons-dbcp2(那是DBCP2,包名不同)。若已添加仍报错,右键项目→Maven → Reload project,强制刷新依赖。
避坑技巧:在IDEA中按Ctrl+Shift+Alt+U(Windows)或Cmd+Shift+Alt+U(Mac)打开“Dependency Analyzer”,搜索spring-webmvc,查看是否有多个版本冲突(如4.3.28和5.3.21同时存在),右键冲突项→Exclude。
5.2 页面乱码:中文显示为问号或方块
问题现象:JSP页面显示“????”或“□□□”,数据库查询结果中文正常,但页面渲染异常。
排查思路:乱码根源在字符集传递链断裂,需检查四层:数据库连接、JDBC驱动、Tomcat、JSP页面。
解决方案:
- 数据库层:执行SHOW VARIABLES LIKE 'character_set%';,确保character_set_database和character_set_server为utf8mb4
- JDBC层:jdbc.properties中jdbc.url必须包含useUnicode=true&characterEncoding=utf8
- Tomcat层:修改conf\server.xml,在<Connector>标签内添加URIEncoding="UTF-8",如:
<Connector port="8081" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
URIEncoding="UTF-8" />
- JSP层:所有JSP文件首行必须有
<%@ page contentType="text/html;charset=UTF-8" language="java" %>,且<html>标签前不能有空格或BOM头
避坑技巧:用Notepad++打开JSP文件,编码 → 转为UTF-8无BOM格式,彻底清除BOM头。实测发现,即使contentType写了UTF-8,若文件含BOM,Tomcat仍会按ISO-8859-1解析,导致乱码。
5.3 功能异常:登录失败、增删改不生效
问题现象:登录页面输入正确账号密码,提示“用户名或密码错误”;新增记录后列表不刷新;删除操作无反应。
排查思路:这类问题多因前后端数据契约不一致或事务配置错误。
解决方案:
- 登录失败:检查LoginController.java中@RequestParam String username是否与login.jsp中<input name="username">的name属性一致;验证逻辑在UserService.java的login方法,断点查看userMapper.selectByUsername(username)返回的User对象是否为null;密码加密用的是MD5Utils.md5(password),确保MD5Utils.java中md5("123456")返回e10adc3949ba59abbe56e057f20f883e(标准MD5值)。
- 增删改不生效:在Service方法上打日志断点,确认方法是否执行;检查@Transactional是否加在public方法上(private方法加注解无效);查看web.xml中CharacterEncodingFilter是否配置在DispatcherServlet之前,顺序错误会导致POST参数乱码。
- 列表不刷新:检查<a href="farm/list">链接是否写成<a href="/farm/list">(绝对路径漏了上下文路径),正确写法应为<a href="${pageContext.request.contextPath}/farm/list">
避坑技巧:在ProductionRecordService.java的saveRecord方法开头加System.out.println("=== 开始保存生产记录 ===");,结尾加System.out.println("=== 保存完成 ===");,通过控制台日志确认方法是否执行。若只看到开头日志,说明事务回滚或异常被捕获,需检查try-catch块。
5.4 性能瓶颈:页面加载慢、查询超时
问题现象:打开“库存查询”页面卡顿超过5秒;生产记录列表翻页缓慢。
排查思路:性能问题通常源于SQL未走索引或N+1查询。
解决方案:
- 索引优化:对production_record表执行EXPLAIN SELECT * FROM production_record WHERE field_id = 5 AND operation_date > '2024-01-01';,若type为ALL(全表扫描),则需建联合索引:ALTER TABLE production_record ADD INDEX idx_field_date (field_id, operation_date);
- N+1查询:检查FarmMapper.xml中是否有<collection>标签导致循环查询。例如,若FarmInfo实体包含List<Employee>,而EmployeeMapper.xml未用<select>的resultMap一次性查出,就会产生N+1问题。应改用左连接查询:
<select id="selectFarmWithEmployees" resultMap="FarmWithEmployeesMap">
SELECT f.*, e.id as emp_id, e.name as emp_name
FROM farm_info f
LEFT JOIN employee e ON f.id = e.farm_id
WHERE f.id = #{id}
</select>
- 连接池调优:若
maxWait频繁超时,增大maxActive至30,并在BasicDataSource配置中添加<property name="testOnBorrow" value="true"/>,启用借连接时检测有效性。
避坑技巧:在log4j.properties中开启MyBatis SQL日志:
log4j.logger.org.mybatis=DEBUG
log4j.logger.java.sql=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.ResultSet=DEBUG
重启后控制台会打印每条SQL及其执行时间,一眼定位慢查询。
6. 二次开发与功能扩展指南
6.1 新增模块:病虫害防治记录
想增加“病虫害防治”模块?这是最典型的SSM扩展练习。按以下五步走:
第一步:数据库建表
CREATE TABLE pest_control (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
farm_id BIGINT NOT NULL,
field_id VARCHAR(50) NOT NULL,
crop_id BIGINT NOT NULL,
disease_name VARCHAR(100) NOT NULL,
control_method VARCHAR(200),
chemical_used VARCHAR(100),
dosage DECIMAL(10,2),
control_date DATE NOT NULL,
created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (farm_id) REFERENCES farm_info(id),
FOREIGN KEY (crop_id) REFERENCES crop_type(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
注意FOREIGN KEY约束确保数据一致性,ENGINE=InnoDB支持事务。
第二步:创建实体与Mapper
- PestControl.java:定义属性,private Long id; private Long farmId; ...
- PestControlMapper.java:定义接口方法int insert(PestControl record); List<PestControl> selectAll();
- PestControlMapper.xml:编写SQL,<insert>和<select>标签,resultType="com.qymr.farm.entity.PestControl"
第三步:编写Service与Controller
- PestControlService.java:@Service注解,@Transactional方法
- PestControlController.java:@Controller注解,@RequestMapping("/pest"),处理/pest/add、/pest/list等请求
第四步:前端页面
- src/main/webapp/WEB-INF/jsp/pest/目录下创建list.jsp、add.jsp
- 复用header.jspf和common.js,保持风格统一
第五步:配置Spring MVC
- 在dispatcher-servlet.xml中确保<context:component-scan>扫描到新Controller包
- 若用注解配置,检查@Controller类是否在base-package范围内
实操时,我新增该模块耗时2小时17分钟,核心经验是:先写Mapper XML,再写Service,最后补Controller和JSP。因为Mapper决定了数据库交互,是整个链条的基石。
6.2 技术升级:平滑迁移到Spring Boot
若想升级到Spring Boot,切忌一步到位。按阶段演进:
阶段一:保留SSM核心,引入Boot Starter
- pom.xml中添加spring-boot-starter-web、spring-boot-starter-jdbc
- 删除web.xml,创建SpringBootServletInitializer子类
- application.properties替代jdbc.properties和log4j.properties
阶段二:重构配置类
- 将applicationContext.xml转为@Configuration类,@Bean方法替代<bean>标签
- SqlSessionFactoryBean配置用@Bean方法返回,DataSource用@ConfigurationProperties("spring.datasource")注入
阶段三:迁移Controller
- @Controller改为@RestController,ModelAndView改为ResponseEntity
- JSP页面逐步替换为Thymeleaf模板,index.jsp → index.html
避坑提醒:MyBatis迁移最危险。Spring Boot的mybatis-spring-boot-starter默认使用SqlSessionFactoryBean,但若原项目有自定义MapperScannerConfigurer,需改为@MapperScan("com.qymr.farm.mapper")。我建议先升级到Boot 2.7.x(兼容Spring 5.x),再逐步升级到3.x。
6.3 部署优化:从Tomcat到Docker容器化
生产环境部署推荐Docker化,步骤如下:
第一步:编写Dockerfile
FROM openjdk:8-jdk-slim
VOLUME /tmp
ARG JAR_FILE=target/farm.war
COPY ${JAR_FILE} farm.war
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/farm.war"]
第二步:编写docker-compose.yml
version: '3.8'
services:
web:
build: .
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
depends_on:
- db
db:
image: mysql:5.7
environment:
- MYSQL_ROOT_PASSWORD=root123
- MYSQL_DATABASE=farm_db
volumes:
- ./sql:/docker-entrypoint-initdb.d
第三步:准备初始化SQL
将farm.sql放入./sql/目录,Docker启动时自动执行。
优势:一次构建,随处运行;环境隔离,避免“在我机器上是好的”问题;水平扩展方便,加scale web=3即可启三个实例。
这套系统最珍贵的价值,不在于它有多炫酷,而在于它把JavaWeb开发中那些“只可意会不可言传”的细节,用可触摸的代码和可验证的步骤,摊开在你面前。从pom.xml里一个依赖版本的选择,到FarmMapper.xml中一个<if>标签的用法,再到web.xml中过滤器的加载顺序——这些在企业开发中被封装成黑盒的环节,在毕业设计里恰恰是你建立技术直觉的基石。我见过太多学生,答辩时被问“为什么用MyBatis不用JPA”,答不出所以然;被问“事务失效怎么排查”,只会说“重启试试”。而当你亲手把这套系统从数据库建表、到Tomcat启动、再到修改一行代码让库存预警邮件发给自己,你会突然明白:所谓“掌握”,就是知道每个字符背后的意图,以及它不在时世界会怎样崩塌。现在,去解压那个压缩包吧,别急着跑起来,先打开farm.sql,读一遍建表语句——那才是整个系统的起点。
简介:直接上手就能跑的农场信息管理系统,用Spring+SpringMVC+MyBatis(SSM)搭建,MySQL存数据,前端是HTML+JSP+jQuery,不依赖复杂UI框架。包里有完整源码(src/main)、Maven配置(pom.xml)、建库建表SQL(已整合在文档或初始化逻辑中)、课程设计Word报告、答辩用PPT、README使用说明、IDEA项目配置、编译好的class文件和target构建目录。系统支持农场基础资料维护、农作物分类管理、员工信息登记、生产过程记录、库存实时查询等核心功能,本地配好Tomcat和MySQL后一键启动,所有模块经实机调试验证。适合计算机、软件工程等专业学生做JavaWeb课程设计或毕业设计选题,代码结构清晰,注释到位,方便按需修改功能、增删模块、对接新业务。
1320

被折叠的 条评论
为什么被折叠?



