SSM框架农场信息管理系统实战:从配置到MyBatis动态查询
简介基于SSM框架SpringSpringMVCMyBatis实现的农场信息管理系统完整源码包面向JavaEE课程设计、毕业设计及全栈学习者。项目难度适中适合已掌握Java基础、希望通过真实项目串联后端业务逻辑、数据库映射与页面交互的中级开发者从包内持久层实体类可见系统覆盖卖家信息、种植记录、订单管理、田地信息、任务分配等农场核心业务模块划分清晰便于二次开发与答辩讲解。压缩包共567个文件、约50.13MB核心包括Java业务与持久层源码86个java、JSP视图页面32个jsp、MyBatis映射XML61个xml、依赖JAR包31个jar及SQL初始化脚本同时含有docx说明文档、pptx汇报材料、properties配置、CSS/JS静态资源等项目骨架完整、目录分工明确。源码均经本地编译验证可运行下载后按文档配置好环境即可启动开发中遇到问题可直接私信作者获得解答。目前已有355人学习下载适合需要快速落地课设/毕设并希望理解SSM整合细节的同学参考。1. 这套农场信息系统为什么还敢用 SSM 来做这套农场信息管理系统选型是 JavaEE 技术栈里的经典组合 SSMSpring SpringMVC MyBatis和现在清一色的 Spring Boot 项目不一样。很多同学能靠 Spring Boot 把接口跑起来但被问到“IOC 容器怎么管理 Bean”“事务在哪一层生效”“Mapper 怎么被注入”就答不上来SSM 项目的答辩恰好都落在这些点上。压缩包里的源码已经过本地编译按文档配好 JDK、MySQL、Tomcat 就能启动。数据模型覆盖卖家信息SellerInfo、作物Plant、订单Order、田地Field、农事任务Assignment五个业务对象完整覆盖增删改查、关联查询和状态流转适合 JavaEE 课程设计和毕业设计两种交付场景。2. SSM 三套配置的职责划分spring.xml、springmvc.xml、spring-mybatis.xmlSSM 项目文件看起来多配置梳理下来只有三件事Spring 管哪些 BeanSpringMVC 接管哪些请求MyBatis 怎么连数据库。本地跑不起来常见的原因一是组件扫描范围重叠二是 Mapper 没扫到三是视图解析器的前缀路径写错。这套系统把配置都放在 src/main/resources/spring 下导入 Maven 依赖、改好 jdbc.properties 里的数据库账号就能启动但配置不能跳过因为答辩第一问通常从 web.xml 开始。2.1 从 web.xml 看父容器与子容器的边界context-param param-namecontextConfigLocation/param-name param-valueclasspath:spring/spring.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener servlet servlet-namespringmvc/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring/springmvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namespringmvc/servlet-name url-pattern//url-pattern /servlet-mappingContextLoaderListener在 Web 应用启动时创建 Spring 根容器加载数据源、事务管理器、Service 和 MapperDispatcherServlet创建 SpringMVC 子容器加载 Controller 和视图解析器。两个容器是父子关系Controller 可以注入 ServiceService 不会反向依赖 Controller。url-pattern配成/后动态请求全部交给 Controller静态资源由后面的mvc:default-servlet-handler兜底。2.2 组件扫描范围与视图解析器!-- spring.xml只扫 Service、DAO 相关注解 -- context:component-scan base-packagecom.farm context:exclude-filter typeannotation expressionorg.springframework.stereotype.Controller/ /context:component-scan!-- springmvc.xml只扫 Controller -- mvc:annotation-driven/ context:component-scan base-packagecom.farm.controller/ bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/jsp// property namesuffix value.jsp/ /bean mvc:default-servlet-handler/把 Controller 排除在父容器之外是为了避免同一个 Controller 在父子容器里各实例化一次。如果两个容器都扫描 Controller启动日志里能看到同一个类出现两个 Bean 定义Transactional代理有时会挂在错误的实例上排查起来很费劲。视图解析器把 Controller 返回的字符串拼成/WEB-INF/jsp/xxx.jsp所以 JSP 必须放在 webapp/WEB-INF/jsp 目录下放错位置就会出现“Controller 正常访问、页面 404”的假象。2.3 数据源、SqlSessionFactory 与 Mapper 扫描context:property-placeholder locationclasspath:jdbc.properties/ bean iddataSource classorg.apache.commons.dbcp.BasicDataSource destroy-methodclose property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.farm.pojo/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.farm.mapper/ /bean bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/这个配置里有几个对应关系要记住。mapperLocations指向 classpath:mapper/*.xml意味着所有 SQL 映射文件必须放在 src/main/resources/mapper 下Maven 编译后才会进入 classpath否则运行时报 Invalid bound statement。typeAliasesPackage把 com.farm.pojo 里的实体类简写成别名在 XML 里写parameterTypePlant或resultTypePlant就能直接用。MapperScannerConfigurer扫描 com.farm.mapper 包接口并生成动态代理业务代码直接Autowired一个 PlantMapper 就能调方法。tx:annotation-driven开启Transactional默认只对 RuntimeException 回滚对受检异常不回滚业务代码里抛业务异常建议统一用 RuntimeException。三份配置加上一张 web.xml把分层边界定死Controller 只接参数和返视图Service 放业务和事务Mapper 只写 SQL。这套项目里后续新增模块都沿这条线走不容易破坏现状。配置与职责的对应关系如下表。配置负责对象常见内容出错时典型表现spring.xmlService、DAO、数据源、事务组件扫描、数据源 Bean、事务管理器启动时 Bean 找不到springmvc.xmlController、视图解析annotation-driven、视图解析器页面 404、静态资源丢失spring-mybatis.xmlSqlSessionFactory、MapperMapper 扫描、SQL 映射Invalid bound statement3. 从 Example 和 GeneratedCriteria 吃透 MyBatis 动态查询打开资源包你会看到SellerInfoExample$GeneratedCriteria.class、PlantExample$GeneratedCriteria.class、OrderExample$GeneratedCriteria.class、FieldExample$GeneratedCriteria.class、AssignmentExample$GeneratedCriteria.class。这些不是手写的测试类而是 MyBatis GeneratorMBG根据数据库表反向生成的代码也是理解这套项目查询逻辑的钥匙。3.1 为什么 MyBatis 要生成一整套 Example 类业务表动辄十几个字段每个字段都可能成为查询条件手写 if 判断会让 Mapper XML 膨胀到崩溃。MBG 的思路是Example类对应一张表的查询参数容器Criteria负责追加 and 条件GeneratedCriteria实现具体的条件拼装最终由 Mapper 的selectByExample把 Java 方法调用翻译成预编译 SQL。以作物表plant的stock字段为例生成的方法有andStockEqualTo、andStockNotEqualTo、andStockGreaterThan、andStockLessThan、andStockBetween等方法名就是 SQL 条件的镜像。方法名以 and 开头条件之间默认是 and 关系用 OR 时再调用or()方法开新分组。3.2 用 Criteria 组合查询条件的标准步骤// 1. 创建 Example作为动态 where 容器 PlantExample example new PlantExample(); // 2. 获取 Criteria所有 andXxx 条件都挂在它下面 PlantExample.Criteria criteria example.createCriteria(); // 3. 追加等值、比较和模糊条件 criteria.andCategoryEqualTo(叶菜); criteria.andStatusEqualTo(在售); criteria.andStockGreaterThan(0); criteria.andPlantNameLike(%生菜%); // 4. 排序字段单独指定 example.setOrderByClause(plant_time desc); // 5. 交给 Mapper 执行 ListPlant plantList plantMapper.selectByExample(example);这段代码最终拼出的 SQL 大致如下SELECT id, plant_name, category, price, stock, status, plant_time FROM plant WHERE category ? AND status ? AND stock ? AND plant_name LIKE ? ORDER BY plant_time DESC所有值都以#{}占位符形式传入值由 PreparedStatement 绑定不会出现字符串拼接式 SQL 注入。PlantNameLike的参数需要在 Java 端把百分号拼好MyBatis 负责传值。调用selectByExample时MyBatis 根据 Criteria 内部记录的判断条件决定哪些条件参与 SQL没有赋值的字段不会进 WHERE这段逻辑在GeneratedCriteria的isValid()和getCriteria()里能看到全貌。IDE 自动补全里能列出Criteria的一整套andXxxEqualTo、andXxxGreaterThan、andXxxLike、andXxxIn方法查条件和查字段名时先确认表结构因为方法名来自数据库列名改过表字段但没重新生成 MBG 代码的情况最常导致编译失败。3.3 OR 条件与 IN 条件的展开// 查询“叶菜”或“花卉”且两个分组都在售 PlantExample example new PlantExample(); example.createCriteria() .andCategoryEqualTo(叶菜) .andStatusEqualTo(在售); example.or() .andCategoryEqualTo(花卉) .andStatusEqualTo(在售); ListPlant list plantMapper.selectByExample(example);对应的 SQL 结构是(category ? AND status ?) OR (category ? AND status ?)。每次调用or()会开启一个新的 Criteria 分组分组内部还是 and 连接。筛选多个分类时用andCategoryIn(Arrays.asList(叶菜, 花卉))生成category IN (?, ?)参数太多时注意 MySQL 对 IN 列表长度有限制实际项目里超过一千个元素就分批次处理。Criteria 条件方法的对应关系如下表排错时可以按表里列对照。条件方法生成的 SQL 片段使用场景andStatusEqualTostatus ?等值筛选andStockGreaterThanstock ?库存预警andPlantNameLikeplant_name LIKE ?名称模糊搜索andCategoryIncategory IN (?, ?)批量分类筛选andFieldIdIsNullfield_id IS NULL未分配地块的作物3.4 Example 兜不住联表查询Example 适合单表动态条件一旦出现 JOIN比如订单列表要同时显示作物名和卖家名Example 没有表达能力。遇到这种情况常见做法是在 Mapper 接口里加自定义方法在对应 XML 里手写 SQL第 4 章会给出这套项目里典型的三表关联写法。4. 从订单联查到农事任务五张业务表的核心实现4.1 数据模型与字段设计五张表的字段和关联关系如下数据库建表脚本可以直接用资源包里的 SQL 文件导入。表名关键字段业务角色field 田地id, field_name, area, soil_type, manager地块基础信息plant 作物id, plant_name, category, price, stock, status种植与库存seller_info 卖家id, seller_name, phone, region销售渠道信息order 订单id, order_no, plant_id, seller_id, quantity, total_amount, status, create_time销售订单assignment 农事任务id, field_id, plant_id, worker, task_type, assign_date, status地块到人的任务订单通过 plant_id 关联作物、seller_id 关联卖家assignment 同时关联 field 和 plant表达“这块地种什么、由谁负责”。每张外键背后都有明确的业务含义答辩时顺着表结构讲业务比自己临时编一套流程更有说服力。4.2 农事任务分配的 Service 实现Service public class AssignmentServiceImpl implements AssignmentService { Autowired private AssignmentMapper assignmentMapper; Autowired private FieldMapper fieldMapper; Transactional(rollbackFor Exception.class) public int createAssignment(Assignment assignment) { // 1. 校验地块是否存在避免任务落到空地块 Field field fieldMapper.selectByPrimaryKey(assignment.getFieldId()); if (field null) { throw new RuntimeException(地块不存在无法下发任务); } // 2. 新任务默认置为待执行后续靠状态流转推进 assignment.setStatus(待执行); assignment.setCreateTime(new Date()); // 3. 只插入非 null 字段保留数据库默认值 return assignmentMapper.insertSelective(assignment); } }insertSelective和insert的区别在于insert 把所有字段按实体值写进 SQLnull 也会拼进去insertSelective 只拼非 null 字段。这里 createTime 在 Java 端显式赋值是为了保证插入后同一个事务里能拿到时间。Transactional(rollbackFor Exception.class)的含义是任何异常都回滚包括自定义 RuntimeException校验失败抛异常后任务表不会留下半截数据。事务边界放在 Service 层Controller 里只调方法不处理 Connection 和事务。4.3 订单列表的三表关联查询订单页面要展示订单编号、作物名、卖家名、数量和金额单表 Example 查不出来需要在 OrderMapper 里增加自定义方法。public interface OrderMapper { ListOrderInfoVO selectOrderList(Param(status) String status, Param(keyword) String keyword); }对应的 XML 写法select idselectOrderList resultTypecom.farm.vo.OrderInfoVO SELECT o.id, o.order_no, p.plant_name, s.seller_name, o.quantity, o.total_amount, o.order_date, o.status FROM order o LEFT JOIN plant p ON o.plant_id p.id LEFT JOIN seller_info s ON o.seller_id s.id where if testkeyword ! null and keyword ! AND (p.plant_name LIKE CONCAT(%, #{keyword}, %) OR s.seller_name LIKE CONCAT(%, #{keyword}, %)) /if if teststatus ! null and status ! AND o.status #{status} /if /where ORDER BY o.order_date DESC /selectorder是 MySQL 保留字所以表名加了反引号这个细节在自写 SQL 时很容易漏。where标签自动处理条件前缀关键字为空时不会残留多余的 AND。keyword 参数同时匹配作物名和卖家名用CONCAT(%, #{keyword}, %)拼接模糊查询条件避免了在 Java 代码里手工拼百分号。4.4 列表分页和状态流转Controller RequestMapping(/order) public class OrderController { Autowired private OrderService orderService; RequestMapping(/list) public String list(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, Model model) { PageHelper.startPage(pageNum, pageSize); ListOrderInfoVO orderList orderService.selectOrderList(null, null); PageInfoOrderInfoVO pageInfo new PageInfo(orderList); model.addAttribute(pageInfo, pageInfo); return order/list; } }PageHelper.startPage是拦截器机制实现的物理分页调用后 MyBatis 发起的下一条查询会被改写自动追加 LIMIT。PageInfo里封装了总记录数、总页数、当前页和导航页码JSP 页面用 c:forEach 遍历 pageInfo.list 即可。订单和任务都建议用状态字段控制页面操作列表页只有“待支付”的订单能点支付按钮“已完成”状态的任务不显示删除入口这样既简化 Controller 判断也方便答辩时演示业务流程。5. 部署复现与答辩前的自检点5.1 环境版本对照组件版本建议说明JDK1.8与 SSM 兼容性最稳定Maven3.6.x配置阿里云镜像后依赖下载快Tomcat8.5支持 Servlet 3.1MySQL5.7导入 SQL 建表脚本后直接使用5.2 运行前的最小检查项先把数据库建好导入资源包里的 SQL 脚本然后修改 src/main/resources/jdbc.properties 里的 url、username、password确认 Tomcat 的 context path 和部署名一致。Maven 执行 clean package 成功后再启动避免依赖缺漏影响排查。5.3 三个高频异常第一个是 Invalid bound statement (not found)。现象是 Service 调用 Mapper 方法时报绑定异常原因是 Mapper XML 没进入 target/classes。检查 mapper 目录是否在 src/main/resources 下再确认 XML 文件里 namespace 写的是接口全限定名。第二个是数据库驱动找不到。多数情况是 pom.xml 里 mysql-connector-java 的 scope 配置错误或版本和 MySQL 服务端不匹配5.7 配 5.1.x 驱动即可注意别引入高版本 JDBC 驱动到 Tomcat 的 lib 目录造成冲突。第三个是中文乱码。在 web.xml 里补一个字符编码过滤器filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping5.4 加分项用条件更新处理库存扣减演示订单流程时直接展示库存扣减。常见做法是先查询 stock 再计算更新但两次操作中间可能被并发请求覆盖。用一个条件更新可以兼顾正确性和简洁性UPDATE plant SET stock stock - #{quantity} WHERE id #{plantId} AND stock #{quantity}受影响行数为 0 时说明库存不够Service 直接抛异常事务回滚订单数据。这样既避免了超卖也省掉了一次查询。库存扣减成功后订单状态才从待支付切到已支付这两步必须放在同一个事务里状态流转的顺序就是事务里方法的调用顺序。答辩时把这个点讲清楚比描述重复的增删改查更有说服力。本文还有配套的精品资源点击获取