
从SSM做电商这件事说起很多人可能会觉得现在都是Spring Boot的天下了再来弄一套SSM是不是有点过时了。但说实话我在实际工作和带新人的过程中发现SSM这套组合恰恰是最能让人把Java Web底层逻辑吃透的。Spring的IOC和AOP、SpringMVC的请求流转、MyBatis的SqlSession生命周期这些东西在Spring Boot里全被自动配置给“藏”起来了出了问题你根本不知道它内部是怎么协作的。而电商平台又是一个非常典型的业务场景——商品、购物车、订单、库存、支付这些模块之间有着天然的事务边界和状态流转用SSM手写一套你会被逼着去思考数据库表要怎么设计才能支撑这些业务Service层的事务边界切在哪里DAO层的一条SQL在大流量下会不会是性能瓶颈这些问题才是做Java后端真正值钱的经验。所以我一直建议如果你想深入理解Java服务端开发或者你在准备面试、打算做一个能写到简历上的实战项目用JavaSSM手写一个电商平台是性价比非常高的选择。网上关于这个组合的课程和源码其实不少但很多要么只给了代码让人看不懂要么讲得稀碎部署起来各种报错。这一篇我打算把整个项目的骨架、核心模块的代码逻辑、数据库设计思路以及从本地跑通到服务器部署的完整过程从头到尾拆一遍。包括了我在实际部署中踩过的坑、那些文档里不会写的注意点。如果你正打算基于这个项目做二次开发或者作为毕业设计、面试项目那这篇应该能帮你省下很多瞎折腾的时间。1. 从零开始梳理电商平台的功能边界与模块划分1.1 一个SSM电商项目到底要具备哪些功能模块才完整很多新手拿到一个电商项目源码第一反应是看代码跑起来什么样却忽略了一个最关键的问题这套系统到底有哪些功能每个功能对应着数据库里的哪几张表代码里哪些类是核心如果这些想不清楚你后面改Bug、加功能都会很痛苦。这里我按前台用户视角和后台管理视角把功能拆开这也是电商系统最通用的划分方式。前台核心功能包括用户注册与登录、商品分类展示与搜索、商品详情、购物车管理加入购物车、修改数量、删除、勾选结算、订单确认与生成、订单列表与详情查看、收货地址管理。后台管理端则包括商品分类管理、商品上下架与库存管理、订单状态处理发货、取消、用户列表查看以及最基础的统计报表这部分如果做简化版可以只展示订单量和销售额汇总。从开发量来评估以上功能全部实现并且做到前端页面能正常走通完整购买流程大概需要15张左右的数据表、20个左右的Controller、对应数量的Service和Mapper。这个体量对个人开发者来说是比较合适的——既不会简单到没有含金量也不会大到半年做不完。1.2 技术栈清单与版本选型要稳定不要追新SSM意味着Spring SpringMVC MyBatis那版本怎么选这是我特别想强调的一点网上能下载到的源码大多数是2018到2021年之间的产物用的JDK版本、Tomcat版本、Maven插件版本都可能和你本机环境不一致这是绝大多数部署失败的根源。给你一套我实测稳定的组合照着这个配能省很多事组件版本推荐理由JDK1.88u202SSM项目的主流编译目标太高版本可能出现反射相关警告Maven3.6.3对旧项目的依赖解析兼容性最好Tomcat8.5.x支持Servlet 3.1兼容Spring 5.xSpring5.2.x生命周期稳定不会要求jakarta命名空间MyBatis3.5.x配合mybatis-spring 2.0.xMySQL5.7 或 8.0注意驱动不同见下面的说明数据库连接池Druid 1.1.x自带监控页面排查问题方便一个小提醒如果用的是MySQL 8.0驱动类名要写com.mysql.cj.jdbc.Driver并且连接串里必须带serverTimezoneAsia/Shanghai否则时间字段全部会偏移很多新人在这上面卡半天。如果是5.7用com.mysql.jdbc.Driver即可。1.3 项目目录结构先看懂源码再动手改代码拿到手的源码不要急着点运行先把包结构捋一遍。标准的SSM工程分两种组织方式一种是按层分包controller/service/dao/po另一种是按模块分包user/product/order/cart。电商项目我建议用后者因为业务模块之间的边界清晰改一个功能时你知道去哪个包下面找。一个比较合理的目录结构长这样src/main/java/ ├── com.mall.controller # 所有Controller按模块再分子包 │ ├── portal/ # 前台接口商品、购物车、订单、用户 │ └── manage/ # 后台接口管理端相关 ├── com.mall.service # 业务接口 ├── com.mall.service.impl # 业务实现事务注解在这一层 ├── com.mall.dao # MyBatis Mapper接口 ├── com.mall.pojo # 数据库实体类 ├── com.mall.common # 公共类常量、异常、响应封装 ├── com.mall.util # 工具类日期、字符串、金额 ├── com.mall.vo # 视图对象返回给前端的组合数据 src/main/resources/ ├── spring/ # Spring与SpringMVC配置文件 │ ├── applicationContext.xml │ ├── spring-mvc.xml │ └── mybatis-config.xml ├── mapper/ # MyBatis的XML映射文件 └── jdbc.properties # 数据库连接配置其中你最先要看的应该是common包里的统一响应类比如ServerResponse这个类决定了前端拿到的是什么样的JSON结构。一般约定就是status/code msg data三段式。很多源码里的ResponseCode枚举也在这里什么情况返回0什么情况返回1看这个类就全明白了。2. 数据库设计电商平台的地基到底怎么打2.1 核心表结构拆解15张表怎么布局数据库设计是源码分析中最重要的一环因为所有业务逻辑归根到底都是对表的增删改查。电商系统的核心表围绕着两条主线商品线和订单线。商品线的表有分类表、商品表、商品图片表如果图片不多也可以直接冗余在商品表中。订单线的表有购物车表、订单表、订单明细表、收货地址表。再加上用户表就构成一个最小可运行的电商闭环。我拿最核心的几张表来说用户表t_user关键字段就是id, username, password, phone, email, create_time。密码字段这里多提一嘴做项目练手可以用MD5但如果正式上线或者写进简历里一定要说清楚实际项目中会用BCrypt这类加盐哈希至少得体现出你懂这个安全问题。商品表t_product字段要包括id, category_id, name, subtitle, main_image, sub_images, detail, price, stock, status, create_time, update_time。这里有两个细节值得注意price字段数据库里用decimal(10,2)存千万不要用float或double不然金额会出现精度丢失status字段是上架/下架状态取值为1和0所有前台查询都要带条件status1这个字段设计得好后台上下架功能就直接改这一个值就行不用动别的地方。购物车表t_cart字段包含id, user_id, product_id, quantity, checked。这个checked字段是给“勾选结算”功能用的——用户可以在购物车里勾选一部分商品去结算。如果你看过一些不完整的源码会发现它没有这个字段那购物车就只能全选全结算这就是功能覆盖度的差别。订单表t_order是整张数据库里设计上最讲究的一张。推荐字段id, order_no, user_id, shipping_id, payment, payment_type, status, create_time, close_time, end_time。order_no是订单编号要唯一一般用时间戳加随机数生成比如20250101123000123456绝不能用自增id直接当订单号暴露给用户会泄露销量。status是订单状态用整数表示常见的状态机是10待支付、20已支付待发货、30已发货、40已完成、50已取消。用数字而不是直接用字符串能方便后续状态流转的判断。2.2 订单表和商品表之间为什么要加一张明细表这是我在被问到项目时最常被追问的一个点。订单表为什么不能直接存商品名称和价格反而要多搞一张t_order_item订单明细表答案是因为一个订单里可能包含多种商品如果直接在订单表里存死商品信息那这个表就无法满足“一个订单多个商品”的场景。所以需要把订单和商品做成一对多关系订单表存公共信息订单号、用户、总金额、状态订单明细表存每个商品的下单快照。这里“快照”两个字很关键。订单明细表里必须冗余一份商品名称、商品图片、下单时的单价。为什么不能等查询时再通过product_id去商品表里拿名称和价格因为商品可能会改价、改名、甚至下架删除如果关联查询当时的商品信息历史订单显示的数据就和用户下单时不一致了。电商里的订单是合同凭证必须以交易那一刻的快照为准。这个设计体现了对业务的理解深度面试中能把这个讲清楚非常加分。下面的SQL展示了订单明细表的关键字段CREATE TABLE t_order_item ( id int(11) NOT NULL AUTO_INCREMENT, order_no varchar(64) NOT NULL COMMENT 订单号, user_id int(11) DEFAULT NULL, product_id int(11) DEFAULT NULL, product_name varchar(100) DEFAULT NULL COMMENT 商品名称快照, product_image varchar(500) DEFAULT NULL COMMENT 商品图片快照, current_unit_price decimal(10,2) DEFAULT NULL COMMENT 下单时单价快照, quantity int(11) DEFAULT NULL COMMENT 数量, total_price decimal(10,2) DEFAULT NULL COMMENT 总价快照, PRIMARY KEY (id), KEY idx_order_no (order_no) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4;2.3 订单号生成规则与状态字段的设计逻辑订单号生成看起来是个小功能但写得好不好直接体现代码功底。我见过的方案有UUID直接当订单号唯一性没问题但是太长而且不是递增的在数据库索引上性能一般用户看到也懵。时间戳 用户id 随机数长度可以控制在20位以内信息量足可以反推下单时间、哪个用户排查问题时特别方便。用Redis生成自增序列性能最好但SSM这种练手项目再引入Redis会增加部署复杂度不太建议。我推荐这种生成规则yyyyMMddHHmmss 用户id后四位 4位随机数。这样订单号在同一个用户下不太容易重复而且整型排序后新订单永远排后面分页查询不紊乱。订单状态流转这块代码里一般会有一个常量类里面把状态值定义成常量public interface OrderStatus { int CANCELED 0; // 已取消 int NO_PAY 10; // 未付款 int PAID 20; // 已付款 int SHIPPED 30; // 已发货 int SUCCESS 40; // 交易成功 int CLOSED 50; // 已关闭 }有了这个常量类Service层里每次状态变更的判断就清晰多了。比如取消订单的逻辑是只有NO_PAY10状态才能取消取消后要改status0同时把占用的库存释放回去。这一步如果忘了释放库存就是一个典型的数据一致性问题属于电商系统里非常严重的事故。2.4 初始化数据为什么源码里要配一个SQL文件绝大多数SSM电商源码都会附带一个init.sql或者mall.sql之类的脚本里面除了建表语句还预置了一些商品分类和测试商品。这个文件的意义不仅是为了让你能跑起来看到效果——它同时也是一份“系统里有哪些硬编码配置”的清单。比如商品分类如果在代码里写死了“手机数码”“家用电器”这些分类名那后台想加一个分类就得改代码重新发布这是很糟糕的设计。正确的做法是分类在数据库里动态维护页面和接口通过category_id关联查询初始化数据只是给你演示用的。看源码的时候注意区分哪些数据是演示数据、哪些是系统必要的初始数据比如后台管理员的账号密码一般加密后存在t_user表里role字段标记为管理员。把这两类数据分清楚你就对这套系统的数据流有了整体认知。3. 三层架构下的核心代码实现从Controller到Mapper逐层拆解3.1 Controller层参数接收、校验与统一响应SSM项目的请求入口是SpringMVC的Controller。很多人初学的时候喜欢在Controller里写一大坨业务逻辑这其实是我们要刻意避免的。Controller只做三件事接收参数、做最基础的格式校验、调用Service并返回统一结果。以用户注册接口为例Controller RequestMapping(/user) public class UserController { Autowired private IUserService userService; RequestMapping(value /register.do, method RequestMethod.POST) ResponseBody public ServerResponseString register(String username, String password, String email) { if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) { return ServerResponse.createByErrorMessage(用户名或密码不能为空); } return userService.register(username, password, email); } }注意几个细节。第一接口路径用了.do后缀这是SSM项目的常见命名习惯不是必须的但是很多老项目就是这样约定的配合SpringMVC的url-pattern配置成*.do可以避免静态资源被拦截这个后面部署的时候会讲到。第二这里的业务逻辑只有一行userService.register(...)因为真正的校验——比如用户名是否已存在、密码加密——都收在Service层里做了。这样拆分的好处是如果将来要支持手机号注册Controller改动很小业务复用度高。响应类ServerResponse的设计也是SSM项目里很值得看的点。它的结构长这样public class ServerResponseT implements Serializable { private int status; // 状态码0成功1失败 private String msg; // 提示信息 private T data; // 业务数据 private ServerResponse(int status, String msg, T data) { this.status status; this.msg msg; this.data data; } public static T ServerResponseT createBySuccess() { ... } public static T ServerResponseT createBySuccess(String msg, T data) { ... } public static T ServerResponseT createByErrorMessage(String msg) { ... } }为什么要把成功和失败都统一成这一种结构因为前端不管是ajax还是后续的Vue、小程序只要约定好了后端永远返回{status, msg, data}这个格式那么前端就只需要写一套拦截器统一处理错误码。这个习惯延续到Spring Boot里也是一样的套路。在面试里就算你介绍其他项目这个统一响应设计也是可以说上一两句的亮点。3.2 Service层下单接口的事务边界应该怎么切Service层是SSM项目里业务逻辑最重的一层也是事务控制的关键。Spring声明式事务通过Transactional注解来标记但注解加在哪里、方法内部怎么拆直接影响系统的正确性。以创建订单为例完整的创建订单逻辑至少包括七步校验收货地址属于当前用户从购物车中查询勾选的商品列表遍历商品校验库存是否足够计算订单总金额生成订单号插入订单表扣减每个商品对应的库存清空购物车中已下单的商品这七步要么全部成功要么全部失败。比如说库存扣了但订单创建失败那用户没下单成功库存却少了这就是事故。所以在createOrder方法上必须加Transactional注解让Spring帮我们管理事务边界Override Transactional public ServerResponseString createOrder(Integer userId, Integer shippingId) { // step 1: 校验地址 // step 2: 从购物车查已勾选商品 // step 3: 遍历商品校验库存 // step 4: 计算总价 // step 5: 插入订单 // step 6: 扣库存 // step 7: 清购物车 }这里有一个很多新手容易忽略的坑Transactional默认只对RuntimeException回滚对受检异常Exception的子类不会回滚。如果你在Service里用try-catch把异常吞掉了或者抛出了一个自定义的受检异常那事务是不会帮你回滚的库存照样会扣。所以在SSM项目里我个人的习惯是Service层业务逻辑中不主动catch异常让异常自然抛出传给上层事务才能正确感知。像上面这七步如果中间某一步库存不足我直接抛一个RuntimeException或者返回一个带错误码的失败结果并主动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()这两种方式都能确保事务回滚。3.3 DAO层MyBatis的SQL映射与动态SQL实战MyBatis的Mapper接口和XML配置文件是SSM项目里数据访问的核心。一个最常见的误区是把所有SQL都写死在注解里这样代码确实简洁但一旦遇到动态条件查询比如商品列表要根据分类和价格区间筛选注解方式就非常难受了。所以正规的SSM项目SQL都写在resources/mapper/*.xml里通过if、where、foreach这些动态SQL标签实现灵活的查询条件组合。商品列表的分页查询是面试必问点。如果用原生的LIMIT offset, pageSize手写分页每页偏移量都要自己算而且还得再写一条SELECT count(*)来查总数。这个重复性劳动完全可以用PageHelper来简化。在MyBatis的配置里引入分页插件plugins plugin interceptorcom.github.pagehelper.PageInterceptor property namehelperDialect valuemysql/ property namereasonable valuetrue/ /plugin /plugins然后在Service层里任何一次查询之前先启动分页PageHelper.startPage(pageNum, pageSize); ListProduct productList productMapper.selectProductList(categoryId, keyword); PageInfoProduct pageInfo new PageInfo(productList);PageHelper的实现原理其实是给MyBatis的Executor加了拦截器在执行你的SQL之前自动拼接一条count查询和分页条件。这里有一个非常重要的使用细节PageHelper.startPage()是线程本地变量它只对接下来执行的第一条查询语句生效。如果你在 startPage 之后先执行了别的查询那么这条分页条件就串到别的SQL上去了查出来的结果就会莫名其妙。这个问题在同一个方法里有多条数据库操作时特别容易出现。再来看一个动态SQL的典型案例——商品列表的条件查询select idselectProductList resultTypecom.mall.pojo.Product SELECT id, category_id AS categoryId, name, subtitle, price, stock, status FROM t_product where if testcategoryId ! null AND category_id #{categoryId} /if if testkeyword ! null and keyword ! AND name LIKE CONCAT(%, #{keyword}, %) /if AND status 1 /where ORDER BY id DESC /select这里有三个细节值得展开。第一where标签会自动去掉第一个多余的AND这样你不需要担心每个if前面要不要加AND的问题。第二LIKE查询不要直接写%#{keyword}%MyBatis的#{}预编译不适用于这种场景得用CONCAT拼接。第三resultType里面的下划线转驼峰一般靠mybatis-config.xml里的mapUnderscoreToCamelCase设置打开或者在SQL里手动AS categoryId两种方式选一种就行不要混用否则容易被字段映射不上的Bug迷惑。3.4 购物车与商品详情页面逻辑和接口联调的配合这一节聊聊购物车模块和商品详情页面因为这两块是前台交互最频繁的部分也是前后端联调时最容易出问题的环节。购物车操作在数据库中体现为对t_cart表的增删改。加入购物车接口的逻辑是先查这个用户的购物车中是否已经存在该商品如果存在数量加一如果不存在插入一条新记录。这个判断如果用代码来实现步骤是“先select再判断再insert或update”有那么一丁点并发风险但练手项目里影响不大。如果你想在细节上加分可以给(user_id, product_id)建一个唯一索引然后利用数据库的ON DUPLICATE KEY UPDATE一步搞定这样既避免了并发下的重复数据代码也简洁了。商品详情页通常需要把商品基础信息、图片列表、富文本详情、以及当前库存一次性返回给前端。数据库里商品表已经包含这些字段了但是图片列表和富文本详情在存储上可能有JSON格式或者逗号分隔的格式。设计一个ProductDetailVo视图对象把散落的数据组装成一个前端友好的结构也是Service层的一个常见练习。这也是为什么项目里会单独有一个vo包的原因——它和数据库表不是一一对应的而是为了满足前端页面展示而组装出来的对象。4. 部署上线的完整流程从本地环境到服务器运行4.1 本地环境搭建JDK、Maven、Tomcat、MySQL的配置细节拿到源码之后第一步不是立即导入IDE而是先把本地环境核对一遍。我遇到过太多因为环境不一致导致项目启动失败的情况其实大部分问题都可以在导入IDE之前就规避掉。首先装好JDK 1.8并配置JAVA_HOME环境变量。在命令行里输入java -version能正常输出版本号说明OK。然后是Maven下载3.6.3版本解压后配置MAVEN_HOME同时修改settings.xml里的本地仓库路径和阿里云镜像。镜像这个配置非常关键因为国内直接访问Maven中央仓库下载依赖非常慢一个Spring项目拉下来可能要十几分钟甚至更久配置了阿里云镜像后基本一两分钟内就能把依赖全拉完。mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror然后是MySQL。建议用5.7版本因为大多数SSM源码编写时用的就是5.7兼容性最稳。安装完成后用Navicat或者命令行工具创建数据库然后把源码自带的初始化SQL文件导入。导入之前先检查一下SQL文件里的建库语句——很多源码的SQL里有CREATE DATABASE mall;这一句如果你在图形化工具里已经先建好了库再导入就会因为重复建库报错这里需要稍微留意一下你的操作顺序。数据库准备好后打开源码里的jdbc.properties文件核对连接串、用户名、密码是否和本机一致。实际经验是这里90%的启动失败都来源于用户名密码写错、数据库没创建成功、连接串里漏了serverTimezone参数。4.2 导入IDEA与Tomcat配置一步步跑起来用IDEA导入SSM项目比较简单选择File - New - Project from Existing Sources选中项目的pom.xml文件以Maven项目方式导入。首次导入时Maven会自动下载依赖这时候你可以在IDEA的Preferences - Build Tools - Maven里确认一下User settings file指向的是你刚配置好的settings.xml确保用的是阿里云镜像。项目导入后在IDEA里配置Tomcat。点开右上角的运行配置新增一个Tomcat Server - Local。在Deployment选项卡里点加号选择Artifact选中项目名字带:war exploded的那个——这是解压模式热部署效果更好改完Java代码后CtrlShiftF9编译一下就能立即生效。在Server选项卡里On frame deactivation可以选Update classes and resources这样修改前端静态资源文件后只要切换窗口就会自动同步到Tomcat里不用手动重启开发效率高很多。启动Tomcat后观察控制台日志。如果看到类似下面的信息说明Spring容器初始化成功信息: Initializing Spring FrameworkServlet dispatcherServlet如果启动过程中抛异常优先看Caused by部分。常见的错误有ClassNotFoundException说明某个jar包没拉下来在IDEA的Maven面板里点击刷新按钮重新导入。Access denied for user rootlocalhost数据库连接配置错误回去改jdbc.properties。Table xxx doesnt exist初始化SQL没有导入成功或者连接串里的库名和建库名称不一致。Connection refusedMySQL服务没启动或者端口不是3306。跑通一个接口后在浏览器访问http://localhost:8080/项目名/商品列表接口路径能看到JSON数据说明项目已经成功运行了。接下来才是真正有价值的步骤把项目部署到远程服务器上。4.3 打包发布Maven打war包与Linux服务器部署SSM项目打的是war包不是Spring Boot那种可执行jar。在IDEA右侧Maven面板里执行Lifecycle - clean然后执行package。如果没有配置跳过测试Maven会先跑一遍单元测试万一测试类里连了数据库而服务器上没库可能直接构建失败。我建议在pom.xml里加一个跳过测试的配置或者打包时使用命令mvn package -Dmaven.test.skiptrue。打包成功后target/目录下会生成一个.war文件。将这个war包通过scp命令或者FinalShell等工具上传到服务器的Tomcatwebapps目录下scp target/mall.war root你的服务器IP:/usr/local/tomcat/webapps/然后启动或重启Tomcatcd /usr/local/tomcat/bin ./shutdown.sh ./startup.shTomcat在启动时会自动解压war包生成同名的目录。访问时通过http://服务器IP:8080/mall/就能看到系统首页。如果希望不带项目名直接访问也就是用根路径访问可以把war包改名为ROOT.war并清掉webapps下原有的ROOT目录这样访问http://服务器IP:8080/即是你的项目。Linux服务器的部署过程中有几个特别容易踩的坑。第一是防火墙没放行8080端口你在服务器上明明看到Tomcat进程已经启动但外网访问不通十有八九是防火墙的问题。用systemctl stop firewalld临时关闭或者用firewall-cmd --permanent --add-port8080/tcp放行端口。第二是云安全组规则也需要放行端口这个在云服务商的控制台里操作。第三是服务器上MySQL的账号权限如果连接串里用了root账号得确认root账号允许从任意主机连接因为本地的localhost和服务器上的localhost不是一个概念很多人在这一步被绊住。4.4 服务器上数据库初始化与配置文件分离在本地开发的jdbc.properties里面是本地数据库密码上传到服务器后不能直接用了。有些不做区分的人会把服务器数据库密码也改成和本地一样这在练手阶段没问题但如果你要让项目上线给别人演示这种做法有安全隐患。我建议在部署阶段把配置文件的修改作为一个独立步骤来对待。把jdbc.properties里的连接地址从localhost:3306改成服务器的内网IP或公网IP同时更新用户名和密码。为了不每次打包都去改源码里的配置可以维护一份“服务器专用”配置或者干脆把配置文件放到Tomcat的lib目录外面通过Spring的PropertyPlaceholderConfigurer的locations参数去读取外部配置。这样代码仓库里只保留本地开发配置部署到服务器上时用外部文件覆盖配置和代码分离后续维护也更容易。数据库初始化方面在服务器上执行init.sql时优先用命令行mysql -u root -p init.sql的方式而不是用Navicat手动复制执行。因为SQL文件较大时图形化工具有可能卡死或者部分语句执行失败而你察觉不到。导入完成后可以用show tables;快速核对一下表数量是否和预期一致。5. 上线前必须处理的细节连接池、日志、静态资源与安全检查5.1 数据库连接池配置Druid监控帮你定位慢SQLSSM项目里我强烈推荐使用Druid连接池不只是因为它性能好更因为它自带的监控页面能在你调试项目时提供极大的帮助。Spring配置Druid的步骤分两步数据源Bean的配置和监控Servlet的注册。在applicationContext.xml里配置Druid数据源bean iddataSource classcom.alibaba.druid.pool.DruidDataSource init-methodinit destroy-methodclose property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ !-- 初始化时建立物理连接的个数 -- property nameinitialSize value5/ !-- 最大连接池数量 -- property namemaxActive value30/ !-- 最小空闲连接数 -- property nameminIdle value5/ !-- 获取连接时最大等待时间单位毫秒 -- property namemaxWait value60000/ !-- 检测连接是否有效的SQL -- property namevalidationQuery valueSELECT 1/ property nametestWhileIdle valuetrue/ /bean然后在web.xml里注册Druid的监控Servletservlet servlet-nameDruidStatViewServlet/servlet-name servlet-classcom.alibaba.druid.support.http.StatViewServlet/servlet-class /servlet servlet-mapping servlet-nameDruidStatViewServlet/servlet-name url-pattern/druid/*/url-pattern /servlet-mapping启动后访问http://localhost:8080/项目名/druid就能看到监控面板里面能看到当前连接池的使用情况、每个SQL的执行次数、耗时。这个页面在你排查系统卡顿时非常管用——如果发现某条SQL执行了上千次、平均耗时几十毫秒你就知道该给它加索引或者优化了。5.2 Log4j日志配置项目排查问题的第一抓手很多SSM源码自带log4j.properties但里面配置得很粗暴直接输出到控制台。开发阶段没问题到了服务器上你不可能时刻盯着控制台看日志必须落盘并且要按天切分。log4j.properties的配置建议按这样的思路来log4j.rootCategoryINFO, stdout, file log4j.appender.fileorg.apache.log4j.DailyRollingFileAppender log4j.appender.file.File/var/log/mall/mall.log log4j.appender.file.DatePattern.yyyy-MM-dd log4j.appender.file.layoutorg.apache.log4j.PatternLayout log4j.appender.file.layout.ConversionPattern%d{yyyy-MM-dd HH:mm:ss} [%p] %c{1} - %m%n在Linux上一定要记得提前创建/var/log/mall目录并设置好写权限否则Tomcat启动时日志文件创建失败不会报启动错误但运行时信息全都丢了排查问题像没头苍蝇一样。在Service层里关键的业务节点要打日志。比如下单成功后打一条logger.info(userId{} create order success, orderNo{}, userId, orderNo)库存扣减时打logger.info(productId{} stock deducted, current stock{}, productId, newStock)。这些日志在排查线上问题时有多重要只有真正经历过事故的人才能体会。5.3 静态资源放行与请求拦截SpringMVC配置里的小细节SSM项目中SpringMVC的spring-mvc.xml里有一个核心配置静态资源放行。因为前端的CSS、JS、图片如果都被DispatcherServlet拦截页面会全部变成裸HTML样式排版全无。常见的两种配置方式!-- 方式一放行指定目录的静态资源 -- mvc:resources mapping/resources/** location/resources// !-- 方式二使用defaultServlet兜底处理所有未被HandlerMapping匹配的请求 -- mvc:default-servlet-handler/对于电商项目因为会有商品图片上传通常会把图片单独存到一个目录下比如/upload而不是混在resources里。部署时这个/upload目录要在Tomcat的webapps中单独创建否则上传的图片重启后可能丢失。更稳妥的做法是把上传路径配置成服务器上的绝对路径比如/data/mall/upload然后通过Tomcat的虚拟目录映射到外部路径。这个属于上线部署进阶操作很多SSM源码没有处理这个问题但你要意识到这个短板在哪上线前需要自行补齐。另外spring-mvc.xml里还需要开启SpringMVC注解驱动mvc:annotation-driven/没有这一行Controller里面的ResponseBody、RequestMapping这些注解都无法生效。这个配置对老手来说是标配但自查时还是要确认一下少这一行项目也能启动但所有接口全部404而且控制台基本没有任何异常提示很容易让人一头雾水。5.4 简单的安全防护密码加密与SQL注入预防SSM项目的安全防护在这个体量下我建议至少做两件事密码加密存储和SQL注入预防。密码加密在注册时用MD5加盐的方式处理。单纯的MD5很容易被彩虹表攻击加盐的做法是MD5(password username)因为username对每个用户来说都是唯一的这样每个用户的盐值都不同即便两个用户密码相同密文也不同。不过这仍然是比较初级的做法你在简历上和面试中最好能提到生产环境更推荐BCrypt或SCrypt这类专门为密码设计的哈希算法内部自带盐值工作因子可调节能抵抗暴力破解。能说出这个层次面试官会觉得你有安全意识而不只是会调API。SQL注入的预防只要你写了下面的代码隐患就非常大String sql SELECT * FROM t_user WHERE username username ;这种字符串拼接SQL是绝对禁止的。MyBatis的#{}语法会自动生成预编译的PreparedStatement传进去的参数都是安全的。只有${}才会做字符串替换有注入风险一般在排序字段名、表名动态拼入等少数场景才会使用而且要严格校验参数内容。如果项目里所有SQL都规范地写在XML里、用#{}取参数SQL注入基本就被挡在门外了。6. 常见问题排查实录这些坑我都替你踩过6.1 启动时报错无法找到Spring配置文件这类报错常见于Tomcat启动阶段错误信息像是这样Error creating bean with name userController: Injection of autowired dependencies failed导致这个问题的原因很多但最常见的一种是Spring的web.xml里配置的上下文加载路径不对。检查web.xml里的这段context-param param-namecontextConfigLocation/param-name param-valueclasspath:spring/applicationContext.xml/param-value /context-paramclasspath:前缀说明Spring会去classpath根目录找这个文件。如果你的配置文件放在src/main/resources/spring/目录下那这个路径就是对的但如果源项目的目录结构不同比如有的源码把配置文件直接放在resources根目录路径匹配不上Spring容器就无法初始化。排查方法就是打开war包解压后的WEB-INF/classes目录看看里面有没有spring/applicationContext.xml没有的话说明目录结构不对需要调整路径。6.2 404问题访问接口返回404但项目启动正常项目能启动但访问任何接口都返回404这种情况十有八九是URL没有匹配到Handler。排查步骤按照下面的顺序来一是检查请求路径和RequestMapping里的值是否完全一致包括大小写、斜杠、.do后缀。二是检查SpringMVC的配置里context:component-scan的包扫描范围。如果base-package写的是com.mall.controller而你的Controller实际在com.mall.controller.portal子包下默认情况下子包是会被扫描到的这是没问题的。但如果包名写错了一个字母比如com.mall.controllers那所有Controller都注册不进去自然全部404。三是检查web.xml里DispatcherServlet的url-pattern配置。如果是/表示所有请求都交给SpringMVC处理需要配合静态资源放行如果是*.do那请求路径必须带.do后缀才能匹配进来。四是检查Tomcat部署的Artifact是不是最新的。IDEA有时候会同步不及时页面改了没生效接口加了没生效点一下Build - Rebuild Project再重启Tomcat往往就好了。6.3 中文乱码问题从请求参数到数据库全链路排查乱码问题的本质是编码不一致。在SSM项目里乱码可能出现在三个环节第一请求参数乱码。POST请求可以通过在web.xml里配置Spring的CharacterEncodingFilter来解决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第二数据库连接串没有指定编码。在jdbc.properties里URL要加上useUnicodetruecharacterEncodingutf8否则数据库连接是ISO-8859-1写入的中文就变成问号了。第三数据库表和字段的字符集。建表时要指定DEFAULT CHARSETutf8mb4MySQL 5.7以上建议用utf8mb4它和utf8都支持中文但utf8mb4还能存储emoji表情和生僻字。如果你新建的表没有显式指定字符集就会继承数据库的默认字符集很可能不是UTF-8存中文就会出现乱码。乱码问题从页面到数据库全链路排查最佳顺序是先看数据库里存的对不对如果存错了问题出在连接串或表字符集如果数据库存的正确页面显示乱码那问题出在响应头或页面本身的meta声明。用Chrome开发者工具看响应头里的Content-Type: text/html; charsetUTF-8是否正常就能定位到到底是哪一层出了问题。6.4 部署后接口可以访问但页面显示异常这个问题的典型表现是接口直接访问返回JSON数据正常但打开首页时样式错乱、JavaScript不执行。原因几乎都是静态资源被DispatcherServlet拦截了。还记得前面说的url-pattern是/的情况吗这种配置下所有请求都会先进DispatcherServlet如果你的spring-mvc.xml没有配置mvc:default-servlet-handler/那么浏览器请求css/style.css时SpringMVC找不到对应的Controller直接返回404页面自然就没有样式。解决办法有两个一是配置mvc:default-servlet-handler/让SpringMVC对匹配不到Controller的请求交给Tomcat默认的Servlet处理由它来返回静态资源二是把静态资源放到指定的目录下用mvc:resources映射出来。两种方式都可以我个人更推荐第二种因为更明确也更符合大项目的开发习惯——静态资源集中管理后面如果要上CDN也方便做域名分离。7. 从SSM电商项目中学到的底层思维这个项目带来的额外收获7.1 事务与幂等电商系统最核心的两个概念如果你把这个SSM电商项目完整地做了一遍或者跟了一遍源码你对“事务”和“幂等”这两个词的理解一定比死记八股文要深刻得多。在下单这个场景里整个方法是一个事务要么七步全部成功要么全部回滚这是ACID里的原子性。但是事务只能保证一次操作的正确性如果网络波动导致用户多点了两次“提交订单”或者支付回调重试了多次系统是否还能保证不产生重复订单这就是幂等性的问题。SSM练手项目里通常的解决方案是在用户提交订单时前端用按钮置灰的方式阻止重复点击后端在生成订单号之前先查一下这个用户是否有一个相同金额、相同地址、刚刚创建的待支付订单如果有则直接返回已有订单而不新建。这种“先查再插”的方案虽然简单但在高并发下有竞态条件。更稳妥的做法是在数据库层面给订单表的(user_id, order_no)建唯一索引或者利用Redis的分布式锁来做。理解这些方案的演进过程比直接背一个分布式锁的八股文有用得多——因为你清楚地知道它要解决什么问题以及什么时候该用、什么时候不该用。7.2 从SSM到Spring Boot为什么理解了SSM就不怕框架升级很多学SSM的人会担心我现在学的这套东西等到了Spring Boot项目里是不是就白费了其实完全不是。Spring Boot的底层还是SpringIOC容器还是那个IOC容器AOP代理还是那一套。Spring Boot只是把配置方式从XML换成了自动配置和注解把部署方式从war包换成了内嵌Tomcat的jar包。你在SSM里手动配置过DispatcherServlet所以你看Spring Boot时一眼就能看出spring-boot-starter-web里自动装配的DispatcherServletAutoConfiguration在干什么你手动配置过Druid数据源所以看spring-boot-starter-jdbc里的DataSourceAutoConfiguration也不会陌生。反过来讲如果你一上来就学Spring Boot很多项目莫名其妙就启动成功了出了问题根本不知道是哪一步配置错了。而经历过SSM项目的你会习惯先检查配置文件、再检查依赖、再查数据库连接这套排查思路在很多框架里是通用的。所以不要觉得SSM“过时”了它更像是一条带你真正进入Java Web世界的路路况复杂一点但沿途的地图你都会画了。7.3 这个项目在简历上和面试中应该怎么讲如果你是打算用它作为面试项目那有几个要点值得留意。不要只写“基于SpringSpringMVCMyBatis的电商平台”写清楚你在里面承担的具体模块和核心难点。比如实现了订单创建的事务控制处理了库存扣减与订单生成的一致性基于MyBatis动态SQL完成了商品多条件分页查询通过Druid监控定位慢SQL并优化了列表查询接口的响应时间使用MD5加盐实现用户密码加密存储防止明文密码泄露面试官通常的追问点会集中在这些地方你的订单和购物车表关系是怎么设计的事务加在了哪一层、为什么高并发下库存会不会超卖你的密码加密方案有没有考虑更安全的做法MyBatis的#{}和${}有什么区别这些问题只要你从头到尾自己跟过一遍源码、甚至改过代码都能回答得比较从容。怕就怕项目是别人跑通的、你只是看着视频敲了一遍遇到追问就露馅了。我个人在实际操作中的体会是这个SSM电商项目最重要的不是你最终跑起来那一下的成就感而是你在这中间无数次“为什么不行”的追问。为什么库存没扣成功为什么页面样式加载不出来为什么MySQL连接报错每一个问题逼着你去读源码、查资料的过程才是真正让你成长的部分。所以拿到一份源码别急着删注释、改包名、提交到GitHub先把它跑通再试着改一个功能再想想如果是我设计这套系统我会怎么做。这三步走完你收获的就不只是一份能跑的项目而是实打实的后端开发能力。