SSM+JSP网上购物商城项目实战:从数据库设计到打包部署 每年到这个时间点我收到的私信里有一半是同一个问题SSM加JSP的网上购物商城怎么做。做毕设的、做课程设计的、想拿个完整项目充实简历的全都扎堆在这个经典组合上。这个项目说难不难说简单也不简单——它不是一个纯增删改查的CRUD而是一套完整的Java Web全链路实践包含用户体系、商品管理、购物车、订单流程还牵扯到会话管理、事务控制、分页、文件上传、权限校验这些高频面试考点。这篇文章我会站在带过很多人从头改这个项目的角度把整件事拆开讲为什么选SSMJSP、系统怎么分层、数据库表怎么设计、SSM三个核心配置文件怎么整合、JSP页面怎么组织、最后怎么打包部署。所有代码都是可以直接抄走跑起来的踩过的坑我也会一并列出来。如果你正准备拿这个项目练手建议完整跟着走一遍你会比单纯背八股文收获大得多。1. 项目定位这套网上购物商城到底在做什么1.1 角色划分与功能边界先明确一件事商城系统不是给一个人用的。至少要有两个角色普通用户和管理员两者的功能边界完全不同。这也是这个项目在功能设计上比“学生管理系统”值钱的地方。普通用户端是前台面向消费者功能围绕“逛”和“买”展开注册、登录、商品分类浏览、关键字搜索、商品详情、加入购物车、提交订单、查看自己的订单列表。管理端是后台面向运营人员功能围绕“管”展开商品分类管理、商品上下架与库存修改、订单状态处理、基本的用户查看。两者之间的权限控制是必做项。商城不是所有页面都放开的后台管理页面必须做登录拦截普通用户的购物车和订单必须和当前登录人绑定。我见过不少毕设把后台地址直接写成/admin/index.jsp不做任何校验这属于典型的逻辑漏洞答辩时被问到会很尴尬。角色核心功能对应JSP页面普通用户注册、登录、商品浏览、搜索、购物车、下单、订单查询login.jsp、register.jsp、index.jsp、product_list.jsp、product_detail.jsp、cart.jsp、order_list.jsp管理员分类管理、商品管理、订单处理admin_product_list.jsp、admin_product_edit.jsp、admin_order_list.jsp页面不需要做得很华丽但功能闭环必须完整。一个商城如果只能浏览商品却不能下单或者下了单看不到订单状态这都叫功能不完整。从用户注册到最终生成订单这条主线是系统的命脉。1.2 2025年了为什么还在用SSMJSP这套组合很多同学一上来就问现在新项目都用SpringBoot加前后端分离了学SSMJSP是不是过时了我的回答是用这套组合练手恰恰能帮你把Java Web的地基打牢。SSM是SpringSpringMVCMyBatis的缩写它的每个组件都能对应到一个明确的职责。Spring管理对象和事务SpringMVC管请求分发和视图跳转MyBatis管SQL映射。整套链路中从浏览器发出请求到页面渲染返回每一步都是程序员亲手配置出来的没有任何“自动配置魔法”。相比之下SpringBoot把几乎一切都封装好了初学者很容易出现“放个依赖跑起来但不知道为什么能跑”的状态。JSP的价值也在这里。它让你看得见动态页面是怎么生成的Tomcat把JSP编译成Servlet再执行输出HTML。你会在JSP里直接感受到request、session、application这些内置对象的存在EL表达式和JSTL标签库的操作也是实实在在的。等以后你转向后端渲染模板Thymeleaf或者干脆做前后端分离你会感谢这段和JSP打交道的日子。面试维度多说一句。网上流传的java八股文里SpringMVC执行流程、Spring容器与SpringMVC容器的父子关系、MyBatis的映射原理、事务传播行为这些考点在这套代码里全都能找到对应场景。与其死记硬背不如指着你自己的项目讲一遍流程说服力完全不一样。1.3 一次完整请求的流转过程理解了定位再看技术实现就顺了。我用最经典的“用户登录”来演示一次完整请求流转这也是面试时经常被要求现场讲的链路。浏览器向/user/login.do提交用户名和密码请求先到达DispatcherServlet。DispatcherServlet是SpringMVC的前端控制器它通过HandlerMapping找到能处理这个请求的UserController.login()方法完成参数绑定。Controller调用UserService.login()Service层在需要时开启事务调用UserMapper.selectByUsername()。MyBatis根据Mapper接口的namespace找到对应XML里的SQL执行查询把结果集映射成User对象一层层返回。Controller拿到User之后校验密码把用户对象放进session然后返回一个重定向指令redirect:/index。浏览器收到302后重新发请求IndexController查出商品列表放进ModelInternalResourceViewResolver把逻辑视图名index拼成物理路径/WEB-INF/views/index.jsp。Tomcat执行这个JSP把动态内容渲染成HTML最终响应给浏览器。这中间有几个细节值得反复看。第一Controller不直接操作数据库这是分层的基本纪律。第二Service层是事务边界不是Mapper层因为一个业务动作往往涉及多张表。第三重定向和请求转发是两回事登录成功后用重定向可以防止刷新页面重复提交表单。把这两三个点想明白了整个SSM架构你就算真正入门了。2. 模块拆解与数据库设计六张表撑起一个商城2.1 五大核心模块的职责清单商城系统的功能再多归纳起来就是五个模块。每个模块的边界必须清晰否则代码写到后面会乱成一锅粥。用户模块管账号生命周期和登录会话。注册、登录、退出、密码加密、session管理都在这个模块。商品模块管前台展示和后台维护需要支持分类筛选和关键字搜索。购物车模块比较特殊它既依赖用户登录状态又要有独立的增删改查接口。订单模块是业务核心涉及事务、状态流转、订单号生成。后台模块复用前三者的Service但Controller和页面单独管理权限也要单独拦截。刚开始写这个项目的人最容易犯的错是把业务逻辑全堆在Controller里。我曾经看到有同学在Controller里直接调Mapper接口Service层形同虚设。这会导致后期想加事务控制、想复用逻辑时非常痛苦。正确的分层是Controller只做参数接收和结果返回Service负责业务规则Mapper只做数据访问。你哪怕代码少一点架构不能塌。2.2 数据库表设计核心表结构完全拆解数据库是商城项目的地基。设计阶段多花半小时后面写代码能少踩一半的坑。最低需要六张表用户表、分类表、商品表、购物车表、订单主表、订单明细表。表名核心作用关键字段user用户账号与权限id、username、password、email、rolecategory商品分类id、name、parent_id、sort_orderproduct商品信息id、category_id、name、price、stock、status、main_imagecart购物车条目id、user_id、product_id、quantity、checkedorders订单主表id、order_no、user_id、total_price、status、receiver_infoorder_item订单明细id、order_id、product_id、product_name、cur_price、quantity用户表的密码字段不要用明文存储哪怕这是个课程设计也建议用MD5或者加盐哈希存一下这是基本的职业习惯。商品表里的category_id是外键逻辑建表时可以不加物理外键约束但应用层必须保证插入的分类存在。价格字段用DECIMAL(10,2)不要用FLOAT或DOUBLE浮点数算钱会有精度问题。订单为什么要拆成主表和明细表因为一个订单可能包含多个商品。如果只建一张订单表三件商品就得插三行每行都要冗余订单号、收货人、总价查询和统计都会很别扭。拆成两张表后orders表管订单整体信息order_item表管每一件商品通过order_id关联。这是典型的一对多设计也是面试容易考到的点。订单明细表里特意加了product_name和cur_price两个“冗余”字段。原因是商品名称和价格随时可能被管理员修改但用户已经下的订单必须保留下单那一刻的快照。如果你直接去关联product表历史订单显示的商品名和价格会被改掉这在真实商城系统里是不能接受的。这种“快照冗余”的思路以后你做其他业务也会经常用到。购物车表也有人问要不要建很多教程用session存购物车。我的建议是建表。session购物车换浏览器就丢而且无法做数据统计表结构也不复杂。唯一要注意的是(user_id, product_id)要加唯一约束防止同一个用户把同一件商品插入多条记录代码里用“存在就更新数量不存在就插入”的upsert逻辑去处理。下面是核心建表SQLMySQL 8环境直接跑没有问题。CREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, email VARCHAR(100), phone VARCHAR(20), role TINYINT DEFAULT 0 COMMENT 0-普通用户 1-管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE product ( id INT AUTO_INCREMENT PRIMARY KEY, category_id INT NOT NULL, name VARCHAR(100) NOT NULL, subtitle VARCHAR(200), main_image VARCHAR(255), price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, status TINYINT DEFAULT 1 COMMENT 1-上架 0-下架, sale_count INT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE cart ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, checked TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_product (user_id, product_id) ); CREATE TABLE orders ( id INT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0-待付款 1-已付款 2-已发货 3-已完成 4-已取消, receiver_name VARCHAR(50), receiver_phone VARCHAR(20), receiver_address VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE order_item ( id INT AUTO_INCREMENT PRIMARY KEY, order_id INT NOT NULL, product_id INT NOT NULL, product_name VARCHAR(100), product_image VARCHAR(255), cur_price DECIMAL(10,2), quantity INT NOT NULL );2.3 订单状态字段的经验之谈订单状态是这个项目里最有业务味道的地方。网上购物流程大家都熟悉下单后待付款付款后待发货发货后待收货确认后完成中间还可以取消。代码层面这个字段建议用TINYINT整型不要用字符串“待付款”“已发货”去存。用整型的好处有三个。一是省空间、查询快二是状态流转可以用比较大小来控制比如只有status1的订单才能发货三是前端页面可以通过JSTL的c:if标签根据数值渲染不同的中文文案展示逻辑完全可控。千万不要做成“用varchar存中文描述”后期改一个状态名称要去update整张历史表非常痛苦。状态流转还需要在Service层加校验。比如“待付款”的订单才能取消“已付款”的订单才能发货。如果你不加限制用户直接调接口把订单改成已完成后台完全无感这种逻辑漏洞必须提前堵上。最稳妥的方式是在Service方法里先根据订单号查出当前状态再判断当前状态是否允许跳到目标状态。这也是事务和业务规则最典型的结合场景。3. SSM整合实操从依赖到代码的核心配置3.1 Maven依赖与项目目录规划SSM整合的第一步是建Maven工程把需要的依赖一次性引全。核心依赖有四个Spring相关的spring-webmvc、spring-jdbc、spring-txMyBatis本体MyBatis和Spring的桥接包mybatis-springMySQL驱动。JSP和JSTL相关的依赖要用provided或手动放到Tomcat里因为Servlet容器本身就带JSP支持。dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.20/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version5.3.20/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-tx/artifactId version5.3.20/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.11/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.7/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/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项目目录建议按下图组织。不要贪图省事把所有Java文件扔在同一个包下分层包名本身就是可读性。src/main/java/com/shop/ ├── controller/ # SpringMVC控制层 ├── service/ # 业务接口与实现 └── impl/ ├── mapper/ # MyBatis Mapper接口 ├── pojo/ # 实体类 └── util/ # 工具类如订单号生成 src/main/resources/ ├── jdbc.properties ├── spring-dao.xml ├── spring-service.xml ├── spring-mvc.xml └── mapper/ # UserMapper.xml等 src/main/webapp/ ├── WEB-INF/web.xml ├── WEB-INF/views/ # JSP页面 └── static/ # css/js/images3.2 三份Spring配置文件的分工与合作SSM整合通常是三份XML配置文件这是老SSM项目约定俗成的结构。web.xml负责启动和拦截规则spring-dao.xml负责数据源和MyBatisspring-service.xml负责Service和事务spring-mvc.xml负责Controller和视图解析。严格来说容器分两个spring-dao和spring-service合并成根容器spring-mvc是子容器。web.xml里最关键的有三件事。第一配置DispatcherServlet指定spring-mvc.xml的路径和拦截规则/。第二配置ContextLoaderListener加载spring-dao和spring-service两张根容器配置。第三配置编码过滤器直接用Spring自带的CharacterEncodingFilter强制UTF-8编码。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 context-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-dao.xml,classpath:spring-service.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mappingspring-dao.xml负责组装数据访问层。读取jdbc.properties配置数据源创建SqlSessionFactory最后用MapperScannerConfigurer自动扫描Mapper接口。这一步配置好之后你在Java代码里直接Autowired注入Mapper接口就能用不需要手动写Dao实现类。context:property-placeholder locationclasspath:jdbc.properties/ bean iddataSource classorg.springframework.jdbc.datasource.DriverManagerDataSource 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 nametypeAliasesPackage valuecom.shop.pojo/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.shop.mapper/ /beanspring-service.xml里最值得关注的是事务配置。Spring支持注解式和XML式两种事务传统SSM项目更常见的是XML配置DataSourceTransactionManager再用aop切点把Service包全部纳入事务管理。之所以要把事务放在Service层是因为“下单”这个动作可能涉及生成订单、写入订单明细、扣库存、清购物车四条SQL任何一条失败都要全部回滚。这直接回答了“Java怎么保证数据一致性”这个问题。context:component-scan base-packagecom.shop.service/ bean idtxManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:advice idtxAdvice transaction-managertxManager tx:attributes tx:method name* propagationREQUIRED/ /tx:attributes /tx:advice aop:config aop:pointcut idserviceMethod expressionexecution(* com.shop.service.*.*(..))/ aop:advisor advice-reftxAdvice pointcut-refserviceMethod/ /aop:configspring-mvc.xml是子容器的配置只扫描controller包这一点非常重要。如果你把com.shop整个包都放进来扫描Controller可以跑但Service会同时被根容器和子容器创建两份事务代理就会出问题。视图解析器配置成InternalResourceViewResolver前缀后缀一拼Controller里return index就会到/WEB-INF/views/index.jsp。mvc:annotation-driven/ mvc:default-servlet-handler/ context:component-scan base-packagecom.shop.controller/ bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /bean这里单独标一下mvc:default-servlet-handler/。它的作用是放行CSS、JS、图片这些静态资源让它们交给容器默认Servlet处理。很多同学跑通项目后页面样式全丢打开控制台全是404十有八九就是缺了这行配置。3.3 从Controller到Mapper的完整实现先看一个最典型的Controller方法。以购物车添加为例前端用Ajax提交后端返回JSON数据这比传统的表单跳转体验好得多。Controller RequestMapping(/cart) public class CartController { Autowired private CartService cartService; RequestMapping(/add) ResponseBody public MapString, Object add(HttpSession session, Integer productId, Integer count) { MapString, Object result new HashMap(); User user (User) session.getAttribute(user); if (user null) { result.put(code, 401); result.put(msg, 请先登录); return result; } if (productId null || count null || count 0) { result.put(code, 500); result.put(msg, 参数错误); return result; } cartService.addItem(user.getId(), productId, count); result.put(code, 200); result.put(msg, 添加成功); return result; } }Service层要体现出业务规则。购物车添加的规则是购物车中已有该商品则增加数量没有则插入新记录。这个判断和写库的逻辑合在一起加上事务注解才能保证中间出错时不产生脏数据。Service public class CartServiceImpl implements CartService { Autowired private CartMapper cartMapper; Autowired private ProductMapper productMapper; Transactional Override public void addItem(Integer userId, Integer productId, Integer count) { Product product productMapper.selectById(productId); if (product null || product.getStatus() ! 1) { throw new RuntimeException(商品不存在或已下架); } Cart cart cartMapper.selectByUserIdAndProductId(userId, productId); if (cart null) { cart new Cart(); cart.setUserId(userId); cart.setProductId(productId); cart.setQuantity(count); cartMapper.insert(cart); } else { cart.setQuantity(cart.getQuantity() count); cartMapper.updateQuantity(cart); } } }Mapper接口和XML的对应关系是MyBatis最容易出错的地方。接口的全类名必须等于XML的namespace方法名必须等于SQL标签的id。下面这个XML是一个带条件分页的商品查询where和if的搭配是为了在关键字为空时自动去掉条件。select idselectPage resultTypecom.shop.pojo.Product select id, category_id, name, price, stock, main_image, status from product where if testcategoryId ! null and category_id #{categoryId} /if if testkeyword ! null and keyword ! and name like concat(%, #{keyword}, %) /if /where order by id desc limit #{offset}, #{pageSize} /select如果出现Invalid bound statement (not found)先检查三处接口全类名和XML namespace是否一致接口方法名是否和SQL标签id一致mapperLocations是否指向了XML所在目录。这个报错几乎占据SSM项目报错量的三分之一记住这个排查顺序能省很多时间。4. JSP页面开发与前端联动让商品真正展示出来4.1 页面布局复用的正确姿势JSP页面之间大量重复的导航栏、页脚如果每个页面都复制粘贴后期改一个链接要改十几个文件。正确做法是把公共部分抽成单独文件用include指令引入。在/WEB-INF/views/common/header.jsp里放导航栏商品列表页、详情页、购物车页各自用一句% include filecommon/header.jsp %把它引进来。include指令是静态包含编译时直接把文件内容嵌进去虽然多人协作时容易产生变量污染但在小项目里它的简单高效是压倒性的。另一种动态包含jsp:include是运行时执行适合每次都需要重新计算内容的场景购物车右上角的数量角标这类需求可以考虑。页面里还要特别注意路径问题。浏览器地址栏的层级一变相对路径就乱了图片、CSS全变404。最稳的做法是在所有JSP的头部统一引入basePath标签之后所有资源引用都用绝对路径。% String path request.getContextPath(); String basePath request.getScheme() :// request.getServerName() : request.getServerPort() path /; % base href%basePath%这段代码要放在head最前面。有了它你在JSP里写link hrefstatic/css/index.css不管当前页面在哪个目录层级都能正确找到资源文件。4.2 EL表达式与JSTLJSP页面的核心武器JSP里不要出现大段Java代码更不要用% for %去循环。页面展示统一用EL表达式读数据用JSTL标签库做循环和判断。在JSP头部引入JSTL核心标签库和格式化标签库然后就可以用c:forEach遍历商品列表。Controller里设置model.addAttribute(pageBean, pageBean)页面上这样渲染% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % c:forEach items${pageBean.list} varp div classproduct-card a href${pageContext.request.contextPath}/product/detail?id${p.id} img src${pageContext.request.contextPath}${p.mainImage} alt${p.name} /a h4${p.name}/h4 span classprice fmt:formatNumber value${p.price} pattern0.00/ /span c:if test${p.stock 0} span classsold-out暂时缺货/span /c:if /div /c:forEach注意fmt:formatNumber的使用。数据库里存的是DECIMAL(10,2)撅到页面上可能就是99.0、88这种样子用格式化标签强制显示两位小数价格展示立即专业起来。c:if则是做条件判断的主力库存不足、登录状态、订单状态的呈现都靠它。4.3 Ajax加入购物车与分页交互购物车操作不要整页刷新用jQuery的$.post配合Controller里的ResponseBody返回JSON即可。前端拿到返回code后弹出提示语再更新右上角购物车数量整个过程一气呵成。function addToCart(productId, count) { $.post(ctx /cart/add, {productId: productId, count: count}, function (data) { if (data.code 200) { alert(添加成功); // 刷新购物车数量 loadCartCount(); } else if (data.code 401) { alert(请先登录); window.location.href ctx /user/login; } else { alert(data.msg || 操作失败); } }); }分页是商城项目的必备交互。商品数量一多不可能一次查全部必须做分页。最基础的分页参数是pageNo和pageSize后台封装一个PageBean对象包含当前页数据、总条数、总页数、当前页码。页面底部渲染页码链接时用c:forEach配合begin和end属性生成带页码的URL字符串。分页还有一个常见问题搜索条件下翻页时关键字不能丢。比如用户搜索“手机”后点了第2页URL里必须保留keywordmobile参数。如果你分页链接只生成了?pageNo2第二页就查不到搜索结果了。这个细节我见过太多次翻车写分页的时候一定要带上原查询条件。5. 部署上线与高频报错排查实录5.1 从IDEA本地运行到Tomcat的war包部署先说本地跑通的最快路径。IDEA里配置好Tomcat之后Deployment选项卡里把当前项目加到Artifact里Application context建议填/shop。这个值决定了浏览器访问路径前缀和JSP里request.getContextPath()取出来的是同一个值。如果你填了/那contextPath就是空字符串项目里的路径拼接逻辑也要相应调整。传统JSP项目打包部署走的是war包路线。在项目根目录执行mvn clean packagetarget目录下会生成shop.war把这个war包直接扔到Tomcat的webapps目录里启动Tomcat后它会自动解压部署。访问地址就是http://localhost:8080/shop/。这种方式在真实服务器环境里非常常见也正好是很多企业里老项目的部署方式。顺带回答一个热搜问题Nginx支持JSP吗不支持。Nginx本身只能处理静态资源和反向代理JSP必须由Tomcat这类Servlet容器来执行。常见部署架构是Nginx监听80端口负责静态文件和负载均衡把以.jsp结尾的请求通过proxy_pass转发给后端的Tomcat。这个架构在传统Java Web项目里很典型值得了解。5.2 高频问题速查表从404到乱码一次说清我在这个项目里见过的报错翻来覆去就是下面这几种直接整理成速查表。每一个都是实测高频对照着排查比瞎猜快得多。现象可能原因解决办法页面样式全丢CSS为404DispatcherServlet拦截了静态资源在spring-mvc.xml加mvc:default-servlet-handler/访问Controller报404请求路径拼错或Controller没被扫描检查RequestMapping确认spring-mvc扫描的是controller包Invalid bound statement (not found)Mapper接口和XML对应不上检查namespace全类名、方法id、mapperLocations路径页面中文全是问号请求/响应编码不一致或数据库连接不带编码参数配置CharacterEncodingFilter数据库URL加characterEncodingutf8EL表达式原样输出不解析JSP缺少taglib引用或容器配置了isELIgnored页面顶部正确引入core标签库401/登录后购物车仍为空购物车未按user_id关联或session中没存用户后端统一从session取userId去查购物车Mapped Statements collection already contains valueMapper XML文件重复加载检查mapperLocations是否包含重复路径清理target后重新编译数据源连接失败 Communications link failureMySQL没启动或url端口/database写错检查jdbc.properties确认mysql服务可用第四行的中文乱码是重灾区。它的坑往往不在单点而是“请求编码、Tomcat连接编码、JSP页面编码、数据库连接编码”四层都必须一致。JSP文件本身要UTF-8Tomcat的server.xml里连接器加上URIEncodingUTF-8JDBC的url加上characterEncodingutf8四层全对齐才能彻底根治。不要只改一处要四层一起检查。5.3 实测排障案例一次“环境能跑但一导入就报错”的完整复盘最后分享一个真实排障过程。有位同学从网上down了一个SSM商城项目源码在自己电脑上一启动Tomcat就报ClassNotFoundException: org.springframework.web.context.ContextLoaderListener。这个报错非常典型原因不是代码有问题而是Maven依赖根本没编译进部署包里。打开IDEA的Project Structure找到Artifacts的Output Layout展开后能看到部署包里的内容。正常的SSM项目部署包必须有WEB-INF/lib目录放所有Maven依赖的jar包。但网上很多项目导入后Artifact类型还是空的解决方案是在Available Elements里找到项目的WEB-INF/lib或者通过“”加入Library把全部依赖加入Output Layout。如果不加这一步本地Tomcat运行时只有classes和你手动写进去的源码Spring全家桶的jar全不在服务里。这个案例再次说明SSMJSP项目的坑很多时候不在业务代码而在部署形态。传统war包项目里“编译产物依赖jarweb.xml”三者缺一不可。我个人这几年最大的体会是SSMJSP这套组合并不新潮但它逼着你把每个环节都亲手配置一遍这对理解Java Web的价值是任何脚手架都替代不了的。以后你转SpringBoot再看那些自动配置的底层逻辑看到的全是这套老架构的影子。如果你正卡在这个项目上照着上面的分层、表和配置挨个走一遍跑通之后再把“用户登录后加购下单”这条链路捋清楚你会发现面试聊Java Web时心里特别有底。