ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SSM+JSP农场供销一体化系统源码实战:从环境搭建到避坑指南

SSM+JSP农场供销一体化系统源码实战:从环境搭建到避坑指南 简介面向Java Web开发学习者的一套农场供销一体化系统完整源码并配有说明文档适用于课程设计、毕业设计以及想通过完整项目提升实战能力的初学者。系统聚焦农场后台管理和前台会员订购两大场景覆盖农产品信息管理、分类管理、订单处理、会员管理、配送等功能模块整体采用SSMJSP技术栈以MySQL存储数据并用Maven统一管理依赖和构建流程。资源压缩包大小约23.55MB除项目源码外还包含说明文档代码与文档搭配便于对照阅读导入IDEA或Eclipse即可开展运行调试与二次开发。当前已有105人学习浏览适合希望系统理解Java Web分层架构、SSM框架整合、前后端数据交互及数据库表设计的读者也能借此掌握供销类管理系统的完整业务流程与项目组织方式。1. 农场供销一体化系统源码拿到手先想清楚它是哪一代 Java Web把“java-ssmjsp”的农场供销一体化系统源码拿到手很多人第一反应是赶紧配环境跑起来但跑起来只解决今天的问题真正值钱的是搞清楚它为什么分成 Spring、SpringMVC、MyBatis、JSP 四层以及明天加一个需求要动哪些文件。这个标题背后是一个典型的 Java Web 单体应用Spring 管理对象SpringMVC 接收页面请求MyBatis 操作 MySQLJSP 负责最后把数据渲染成网页。业务上覆盖农产品和农资的采购、入库、销售、出库、客户与供应商台账适合三类人做毕设和课设的学生、刚准备接手老项目的新工程师、想拿一套可扩展的内网管理后台当起步框架的团队。下面直接按“环境选型、建表、一条业务链路、避坑、验证进阶”的顺序拆每一步都可以照着抄。2. 环境选型与依赖闭环Spring 5.3 MyBatis 3.5 JSP先把版本定死2.1 版本组合先定死后面少一半折腾SSMJSP 项目开头最容易翻车的不是代码而是 JDK、Tomcat、MySQL、Maven 之间版本打架。我一般默认用 JDK 8、Tomcat 8.5 或 9.0、MySQL 5.7 或 8.0、Maven 3.6 以上框架版本固定在 Spring 5.3.x、MyBatis 3.5.x、mybatis-spring 2.0.x。这套组合在 Maven 仓库里沉淀了很多年官方文档和论坛问答能对上遇到问题搜索关键词能直接命中不需要自己当小白鼠。不建议在旧源码上强行升 Spring 6 和 JDK 17。Spring 6 把 javax 命名空间换成了 jakarta项目里所有 Servlet API、JSP 标签、依赖坐标都要跟着改对一套课程设计源码来说这种迁移比业务开发还费时间。如果你是从零开始做新项目完全可以直接用 Spring Boot但标题既然写明 SSMJSP我们就按这套老组合落地保留那份代码的原始结构。pom.xml 里最小依赖集合通常长这样properties java.version1.8/java.version spring.version5.3.24/spring.version mybatis.version3.5.9/mybatis.version mybatis.spring.version2.0.7/mybatis.spring.version /properties dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version${mybatis.spring.version}/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.29/version /dependency dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies几个参数要注意javax.servlet-api 的 scope 是 provided意思是编译时需要、打包时排除因为 Tomcat 自带 Servlet 容器实现如果把 Tomcat 自带的 jakarta 或旧版 servlet-api 再塞进 WEB-INF/lib启动时反而冲突。mysql-connector-java 8.0.x 新坐标也叫 com.mysql:mysql-connector-j两个都可以拉但驱动类名必须写成 com.mysql.cj.jdbc.Driver下文第 5 章会专门讲这个坑。注意如果你的源码不是 Maven 工程而是直接把 jar 放在 WEB-INF/lib 里第一件事是核对 lib 下有没有 spring-webmvc、mybatis-spring、jstl 这三个关键包。缺 mybatis-spring 桥接包时Spring 容器里配置的 Mapper 扫描会直接报找不到 Bean。2.2 web.xml 把请求交给 SpringMVC再把乱码问题按死SSM 的老项目入口在 web.xml。常见做法是配置两套容器ContextLoaderListener 加载 Spring 的 applicationContext.xmlDispatcherServlet 加载 springmvc.xml。监听器在前Servlet 在后这样业务 Bean 先被 Spring 管理再被 SpringMVC 组装。?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 context-param param-namecontextConfigLocation/param-name param-valueclasspath:applicationContext.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener 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-mapping servlet servlet-namespringmvc/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:springmvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namespringmvc/servlet-name url-pattern//url-pattern /servlet-mapping /web-app这段配置里最容易忽略的是 CharacterEncodingFilter 的 forceEncodingtrue。如果不加request 的编码会被设置但 response 的编码不一定生效中文乱码会从 JSP 一路传到浏览器。另一个重点是 DispatcherServlet 的 url-pattern 用 /不是 // 只拦截 Controller 映射的请求/会把 .jsp 请求也拦截掉造成页面白屏或二次转发这一点在第 5 章还会展开。2.3 视图解析器与静态资源把 JSP 藏到 /WEB-INF 下springmvc.xml 里最关键的是组件扫描、注解驱动和视图解析器三件套context:component-scan base-packagecom.xxfarm.controller/ mvc:annotation-driven/ mvc:resources mapping/static/** location/static// bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/jsp// property namesuffix value.jsp/ /bean这样 Controller 里写 return sale/listSpringMVC 会解析成 /WEB-INF/jsp/sale/list.jsp。JSP 放在 /WEB-INF 下的好处是浏览器无法直接访问这个目录页面只能经过 Controller 再转发避免有人绕过登录直接拿 JSP 的原始路径打开内部页面。静态资源单独放到 webapp/static 下用 mvc:resources 映射否则会被 DispatcherServlet 当作普通请求处理引用 CSS、JS 时全部 404。如果源码里的 applicationContext.xml 只配置数据源、事务、Mapper 扫描springmvc.xml 只配置 Controller 和视图分层是合理的如果两份配置文件内容混在一起不影响运行但改起来容易互相踩踏。我的做法是保持这个分工后面扩展定时任务、邮件发送时往 applicationContext.xml 里加页面相关配置一律进 springmvc.xml。3. 数据库设计先行把“供销一体化”拆成六张表再动手写代码3.1 业务域梳理谁在买、谁在卖、谁在记账写 SSM 项目最容易犯的错是一上来就写 Controller表结构边写边改。农场供销一体化系统的核心是“供”和“销”两条线中间用库存表衔接。先按业务角色拆实体至少要有用户、商品、供应商、客户、库存、销售单六类。角色和表的关系可以这样看表名角色关键字段一句话职责sys_user系统登录者username, password, role区分管理员、采购员、销售员goods商品主数据goods_name, price, unit, threshold描述卖什么supplier供应商supplier_name, contact_phone采购来源customer客户customer_name, contact_phone销售对象stock当前库存goods_id, qty供销两端的平衡点sale_order销售单order_no, customer_id, total_amount记录一次销售采购订单这类表要不要单独建取决于源码的 bd设计。我的经验是一个课设项目至少保留 purchase_order 和 sale_order_item否则“供销一体化”在答辩时会被问住。采购单证明货是哪里进来的销售明细证明一张单子卖了哪些商品只靠几张主表撑不起对账逻辑。3.2 建表 SQL从建库到外键一次跑通MySQL 下面这套建表 SQL 是按课程设计常见的“少外键、多注释”风格写的CREATE DATABASE IF NOT EXISTS xx_farm DEFAULT CHARACTER SET utf8mb4; USE xx_farm; CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT 密码建议MD5或BCrypt, role VARCHAR(20) NOT NULL COMMENT admin/purchase/sale, real_name VARCHAR(50) COMMENT 员工姓名, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB COMMENT系统用户表; CREATE TABLE goods ( id INT PRIMARY KEY AUTO_INCREMENT, goods_name VARCHAR(100) NOT NULL COMMENT 商品名称, category VARCHAR(50) COMMENT 分类如蔬菜/粮油/农资, unit VARCHAR(10) DEFAULT 斤 COMMENT 计量单位, price DECIMAL(10,2) NOT NULL COMMENT 销售单价, threshold INT DEFAULT 10 COMMENT 库存告警阈值 ) ENGINEInnoDB COMMENT商品表; CREATE TABLE supplier ( id INT PRIMARY KEY AUTO_INCREMENT, supplier_name VARCHAR(100) NOT NULL COMMENT 供应商名称, contact_user VARCHAR(50) COMMENT 联系人, contact_phone VARCHAR(20) COMMENT 联系电话, address VARCHAR(255) COMMENT 地址 ) ENGINEInnoDB COMMENT供应商表; CREATE TABLE customer ( id INT PRIMARY KEY AUTO_INCREMENT, customer_name VARCHAR(100) NOT NULL COMMENT 客户名称, contact_phone VARCHAR(20) COMMENT 联系电话, address VARCHAR(255) COMMENT 地址 ) ENGINEInnoDB COMMENT客户表; CREATE TABLE stock ( id INT PRIMARY KEY AUTO_INCREMENT, goods_id INT NOT NULL COMMENT 商品ID, qty INT NOT NULL DEFAULT 0 COMMENT 当前库存数量, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_goods (goods_id) ) ENGINEInnoDB COMMENT库存表; CREATE TABLE sale_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 销售单号如SO20250101, customer_id INT NOT NULL COMMENT 客户ID, total_amount DECIMAL(12,2) NOT NULL COMMENT 销售总额, status TINYINT DEFAULT 0 COMMENT 0待出库 1已出库, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 下单时间 ) ENGINEInnoDB COMMENT销售主表;这里要解释几个取舍。商品价格用 DECIMAL(10,2) 而不是 float/double避免金额在小数运算时出现 0.30000000000000004 这种黑匣子问题。库存表对 goods_id 建唯一键强制一个商品只能有一条库存记录这是防脏数据最关键的一步。我没有建物理外键只保留逻辑外键。原因很现实课程设计项目经常要手动删数据、改主键物理外键会让 DELETE 顺序变得脆弱应用层 Service 方法加事务校验后逻辑外键完全够用。3.3 采购入库与销售出库的数据闭环“供销一体化”在数据库层面其实只有两个动作采购入库让库存加销售出库让库存减。落到 SQL 上就是这样-- 采购入库库存记录存在时直接加 UPDATE stock SET qty qty #{qty}, update_time NOW() WHERE goods_id #{goodsId}; -- 库存记录不存在时先插入 INSERT INTO stock(goods_id, qty) VALUES(#{goodsId}, #{qty}); -- 销售出库扣减前判断足够扣 UPDATE stock SET qty qty - #{qty}, update_time NOW() WHERE goods_id #{goodsId} AND qty #{qty};第三条 SQL 的 qty #{qty} 条件非常关键。它把“库存够不够”的判断直接压到数据库层面即使两次请求同时到来也不会出现负库存。MyBatis 执行 UPDATE 后返回的影响行数如果等于 0说明当前商品库存不够Service 层就可以中断后续流程。常见做法是先调用 select 查一次库存再执行 update但中间隔了一个网络往返并发时仍可能超卖直接把判断写进 UPDATE 是更稳妥的老手方案。为什么不建议在数据库里写触发器来做库存扣减原因一是触发器不好调试出问题要翻 MySQL 日志二是这套源码将来可能迁移到别的数据库触发器是强绑定三是项目本身已经在用 Spring 事务Service 层就能解决没必要引入一套黑盒机制。把事务边界放在 Service 方法上代码谁都能看懂。4. 从 Mapper 到 JSP 的完整链路用一个“采购入库”功能把 SSM 串起来4.1 实体与 Mapper 接口MyBatis 的 SQL 写在 XML 里而不是 Java 里SSM 工程里的代码组织一般按 entity、mapper、service、controller、jsp 五层铺开。先看商品实体和 Mapper 接口的最小写法public class Goods { private Integer id; private String goodsName; private String category; private String unit; private BigDecimal price; private Integer threshold; // 省略 getter / setter }public interface GoodsMapper { ListGoods selectGoodsList(Goods query); Goods selectById(Integer id); int updateGoods(Goods goods); Stock selectStockByGoodsId(Integer goodsId); int insertStock(Stock stock); int updateStockQty(Integer goodsId, Integer qty); int deductStockQty(Integer goodsId, Integer qty); }为什么不直接在 Mapper 接口上用 Select 注解写 SQL因为真实业务的 SQL 大概率会拼接动态条件比如按商品名称模糊查询、按分类过滤注解里写长 SQL 可读性很差。SSM 项目更常见的做法是 XML 里写 SQL保持接口和映射文件同名同包价值在于 SQL 可以作为独立资源维护修改时不用重新编译 Java 类。对应 GoodsMapper.xmlmapper namespacecom.xxfarm.mapper.GoodsMapper select idselectGoodsList resultTypecom.xxfarm.entity.Goods SELECT id, goods_name AS goodsName, category, unit, price, threshold FROM goods where if testgoodsName ! null and goodsName ! AND goods_name LIKE CONCAT(%, #{goodsName}, %) /if if testcategory ! null and category ! AND category #{category} /if /where ORDER BY id DESC /select update idupdateStockQty UPDATE stock SET qty qty #{qty}, update_time NOW() WHERE goods_id #{goodsId} /update update iddeductStockQty UPDATE stock SET qty qty - #{qty}, update_time NOW() WHERE goods_id #{goodsId} AND qty #{qty} /update /mapper这里 resultType 直接写了实体全限定名没有用 resultMap。只要 mybatis-config 里开启驼峰映射 mapUnderscoreToCamelCasetruegoods_name 就能自动映射成 goodsName少写一堆列映射。动态 SQL 里的 标签会自动去掉多余的 AND新手如果手写 WHERE 11 也能跑但维护性差不推荐。4.2 Service 层事务库存扣减为什么不能只写一条 UPDATEController 不能直接调 Mapper中间要经过 Service 层这是老项目被问得最多的一句话。原因很简单一个业务动作往往要跨多张表操作Service 层能保证事务而 Controller 层只负责接收参数和返回视图。以销售出库为例Service Transactional public class GoodsServiceImpl implements GoodsService { private final GoodsMapper goodsMapper; public GoodsServiceImpl(GoodsMapper goodsMapper) { this.goodsMapper goodsMapper; } Override public boolean purchaseIn(Integer goodsId, Integer qty) { Stock stock goodsMapper.selectStockByGoodsId(goodsId); if (stock null) { Stock newStock new Stock(); newStock.setGoodsId(goodsId); newStock.setQty(qty); return goodsMapper.insertStock(newStock) 0; } return goodsMapper.updateStockQty(goodsId, qty) 0; } Override public boolean saleOut(Integer goodsId, Integer qty) { int rows goodsMapper.deductStockQty(goodsId, qty); if (rows 0) { throw new RuntimeException(库存不足扣减失败); } // 这里还可以继续插入 sale_order、sale_order_item return true; } }setter 注入在旧工程里很常见但 newer 项目用构造器注入更明确如果源码里都是 Autowired 字段注入照源码风格来也没问题别大改。Transactional 注解保证整个方法里所有数据库操作同生共死扣库存成功但插入销售明细失败时扣库存这条 UPDATE 也会回滚不会出现单子没建、库存却少了的怪状态。saleOut 里故意在行数为 0 时抛 RuntimeException而不是返回 false是为了让 Spring 事务感知到异常并回滚。如果捕获异常后返回一个错误码事务管理器默认不会回滚已提交的 UPDATE这是很多“库存还是被扣了”问题的根源。4.3 Controller 转发与重定向别把数据又塞回 JSP 里Controller 层在 SSM 里承担的是“参数校验 调 Service 决定响应路径”。以销售单创建为例Controller RequestMapping(/sale) public class SaleController { private final SaleOrderService saleOrderService; public SaleController(SaleOrderService saleOrderService) { this.saleOrderService saleOrderService; } PostMapping(/create) public String create(SaleForm form, RedirectAttributes ra) { try { boolean ok saleOrderService.createSale(form); if (!ok) { ra.addFlashAttribute(error, 库存不足请检查库存数量); } } catch (Exception e) { ra.addFlashAttribute(error, 下单失败 e.getMessage()); } return redirect:/sale/list; } }这里的细节是“失败也要重定向”不是 forward。原因是 POST 提交后如果 forward 回 JSP用户刷新页面会再次提交同一个表单销售单重复创建。RedirectAttributes 的 addFlashAttribute 把错误信息放到 session 里重定向后的下一次请求读取一次就消失正好适合这种一次性提示。return redirect:/sale/list 中的 redirect: 前缀会被视图解析器识别为重定向指令不会去拼 /WEB-INF/jsp/sale/list.jsp。老源码里如果有人在 Controller 里写 return sale/list 又非要带数据回显常见的做法是把查询结果通过 Model 传过去如果发现页面数据是上一次的残留多半是 Model 和 RedirectAttributes 用混了。4.4 JSP 渲染列表JSTL/EL 完成最后一次数据搬运JSP 在整个 SSM 链路里的唯一职责是展示。Controller 把数据放进 ModelJSP 用 EL 表达式 ${xxx} 和 JSTL 标签读取不在页面里写 Java 脚本片段这是老 Java Web 的基本底线。销售单列表页的核心片段% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % table classtable table-hover thead tr th销售单号/th th客户/th th金额/th th下单时间/th th状态/th /tr /thead tbody c:forEach items${orderList} varorder tr td${order.orderNo}/td td${order.customerName}/td tdfmt:formatNumber value${order.totalAmount} pattern#,##0.00//td tdfmt:formatDate value${order.createTime} patternyyyy-MM-dd HH:mm//td td c:choose c:when test${order.status 1}已出库/c:when c:otherwise待出库/c:otherwise /c:choose /td /tr /c:forEach c:if test${empty orderList} trtd colspan5暂无销售单/td/tr /c:if /tbody /tablefmt:formatDate 格式化日期是 JSP 页面最常见的需求如果不做格式化Date 对象会被 toString 打出一长串看不懂的时间。c:choose/c:when 是 java 的 switch 替代品比写一串 c:if 干净。JSP 页面里如果要提交表单通常是 jQuery 监听 submit先做前端数量校验再 AJAX 提交给 Controller注意 Controller 接收的是表单对象字段名要和 JSP 里 input 的 name 对应上对不上时后端拿到全是 null这种问题最玄学查的时候先打印参数。5. 避坑记录从启动失败到页面白屏你一定会遇到的五个坑5.1 Maven 打出的 war 缺 jarTomcat 启动直接 NoClassDefFoundError现象Tomcat 启动到一半抛 java.lang.NoClassDefFoundError最常见的报错类是 ContextLoaderListener 或者 DispatcherServlet更惨的是启动正常但访问首页时 500。原因依赖没有打进 war 包或者 lib 目录里漏了 mybatis-spring 这类桥接包。用 IDEA 直接 Run 时 IDE 会把依赖临时带进去一旦改成打包部署到 Tomcat缺依赖的问题立刻暴露。解决在项目根目录执行 mvn clean package然后解压 target 下的 war重点看 WEB-INF/lib 里有没有 spring-webmvc、mybatis-spring、jstl。哪几个缺就回去核对 pom.xml 的依赖坐标。顺手检查 Tomcat 的 lib 目录千万别把 spring-webmvc 旧版本和项目里的新版本同时放进去双份 jar 会造成类加载混乱报错信息会让人以为是代码问题。5.2 MySQL 8 连不上驱动类名与时区联合翻车现象启动时报 java.lang.ClassNotFoundException: com.mysql.jdbc.Driver或者报 Communications link failure又或者日志里出现 time zone 关键词数据库连接直接失败。原因MySQL 8 之后驱动类名从 com.mysql.jdbc.Driver 改成了 com.mysql.cj.jdbc.Driver。老源码的 jdbc.properties 里用的多半是老驱动名。另外 MySQL 8 要求连接 URL 里指定 serverTimezone不指定就报时区错误报错内容在不同系统里还是乱码。解决jdbc.properties 里改成下面这样jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/xx_farm?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.passwordyour_password如果这段配置写在 XML 里而不是 properties 文件URL 里的 符号要写成 否则 XML 解析会报错。密码出现中文乱码时先确认 properties 文件本身是 UTF-8 保存服务器系统编码也要一致。5.3 DispatcherServlet 的 url-pattern 写成 /*JSP 页面全部白屏现象Controller 日志显示正常返回视图浏览器地址也对但页面一片空白或者 HTML 源码变成一串看不懂的内容。原因web.xml 里 DispatcherServlet 的 url-pattern 被写成了 /*。这个通配符会拦截所有请求包括 .jsp 结尾的请求。Tomcat 的 JSP 引擎还没来得及编译请求就被 DispatcherServlet 接管视图解析器再把 JSP 转给 JSP 引擎时链路已经乱了结果就是白屏或内容被二次输出。解决把 url-pattern 改成 /让普通 JSP 请求交给 Tomcat 本身处理Controller 返回的 forward 由 InternalResourceViewResolver 转发到 JSP链路才通。这里再强调一遍/ 和 /* 在 Servlet 里完全是两回事/ 只匹配非 JSP 的路径。SSM 老项目里凡是页面白屏的先查 web.xml别急着翻代码。5.4 JSTL 标签库失效taglib 报错还是显示原样 EL 表达式现象JSP 页面顶部 taglib 指令直接报错或者浏览器里显示 ${order.orderNo} 这串原文而不是订单号。原因分两种情况。taglib 报错说明工程里没有 jstl.jar显示 EL 原文说明 web.xml 版本过低EL 默认关闭或者页面里被设置了 isELIgnoredtrue。解决pom.xml 里加入 javax.servlet:jstl:1.2如果是手动部署就把 jstl.jar 放进 WEB-INF/lib非 Maven 项目还要注意 jsp-api.jar 不要打进 lib它会造成 JSP 编译冲突。web.xml 的根节点版本提到 3.1 或 4.0这样 JSP 2.3 默认启用 EL。实在不行在 JSP 页面顶部加 % page isELIgnoredfalse %这是兜底手段但最好还是把配置文件修正。5.5 MyBatis 绑定异常XML 文件被 Maven 打包“吃掉”了现象启动时或第一次调用 Mapper 方法时抛 org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)报错接口和 XML 看起来都在。原因最常见的是 Mapper 接口和 XML 不同名不同 namespace或者方法 id 对不上但还有一个人人踩过的坑是 Maven 工程里 XML 放在了 src/main/java 目录下默认打包不会把非 Java 资源放进 classpathtarget/classes 里根本没有这个 XML。解决把 Mapper XML 统一放到 src/main/resources/mapper 目录下在 Spring 配置里指定 mapperLocations 为 classpath:mapper/*.xml。然后检查三处一致性XML 文件名与 Mapper 接口名一致、namespace 是接口全限定名、statement id 是方法名。改完执行 mvn clean把 target 清掉重新编译再确认 target/classes 下能看到 XML看不到就说明资源路径配置还有问题。6. 验证与进阶给它加一个“库存告警”旁路功能三十分钟看出源码值不值得继续投入拿到源码跑通主流程之后我一般不会急着看每个文件而是先塞一个不伤主链路的旁路需求进去用这个动作验证它的可维护性。最适合练手的是“库存告警”库存低于商品表里设置的 threshold 阈值时在首页销售单列表旁边显示缺货商品清单。这个需求会串联 Mapper、Service、Controller、JSP 四层改动却不会影响现有采购和销售流程。先加一条查询select idselectAlarmGoods resultTypecom.xxfarm.entity.Goods SELECT g.id, g.goods_name AS goodsName, g.unit, g.threshold, IFNULL(s.qty, 0) AS stockQty FROM goods g LEFT JOIN stock s ON s.goods_id g.id WHERE IFNULL(s.qty, 0) lt; g.threshold ORDER BY g.threshold - IFNULL(s.qty, 0) DESC /select这段 SQL 故意用 LEFT JOIN 而不是 INNER JOIN因为新商品可能还没有库存记录IFNULL 把空库存当成 0再和 threshold 比较。实体里需要新增一个 stockQty 字段Controller 加一个 /goods/alarm 方法把告警列表放进 ModelJSP 页面用 c:forEach 渲染成一个小表格低于阈值的行显示红色整个改动不需要动任何现有接口。做完这个功能要立刻复盘如果只是加一个查询、加一个方法、加一段页面渲染说明这套 SSM 源码的分层是通的可以继续投入面向答辩时还能把这个“库存告警”包装成业务闭环亮点如果你发现改一个字段要动五层文件、copy 大量重复代码或者 Controller 里直接写 SQL那这套源码更适合当作业交付不适合当长期底座。我的习惯是拿到一套老系统先找最小旁路需求验证边界而不是一头扎进主流程里慢慢读。一次二十多分钟的验证省掉的是后面三天改不动代码的血压。希望帮到你。本文还有配套的精品资源点击获取
返回列表