1. 为什么选择SSH框架开发电商平台
十年前我刚入行时,第一次接触电商项目用的是纯JSP+Servlet开发,光是处理一个简单的用户登录功能就写了200多行代码。后来接触到SSH框架后,开发效率直接提升了三倍不止。SSH框架(Struts2 + Spring + Hibernate)作为JavaEE开发的经典组合,特别适合中小型电商系统的快速开发。
Struts2负责前端交互,它的拦截器机制能优雅地处理表单验证、权限控制等通用逻辑。比如用户提交订单时,我们只需要在struts.xml中配置一个简单的拦截器栈,就能自动完成数据校验和防重复提交。
Spring的IOC容器让组件管理变得轻松。想象一下,当你的系统需要切换支付服务提供商时,传统方式可能要修改几十处代码,而用Spring只需要改一个配置文件的bean定义。AOP功能更是利器,我曾经用@Transactional注解轻松解决了订单支付时的分布式事务问题。
Hibernate的ORM功能让数据库操作变得直观。记得有一次需求变更要增加商品规格属性,传统JDBC需要重写大量SQL,而Hibernate只需在实体类添加几个字段,配置下映射关系就搞定了。
2. 开发环境搭建实战
2.1 工具选型避坑指南
我推荐使用IntelliJ IDEA而不是MyEclipse,前者对Maven的支持更友好。数据库建议MySQL 5.7+,千万别用8.0默认的caching_sha2_password认证方式,否则连接时会报错。Tomcat用8.5版本最稳定,记得在server.xml中配置URIEncoding="UTF-8"解决中文乱码问题。
2.2 Maven依赖配置
这是经过多次项目验证的最稳定依赖组合:
<!-- Struts2核心 --> <dependency> <groupId>org.apache.struts</groupId> <artifactId>struts2-core</artifactId> <version>2.5.30</version> </dependency> <!-- Spring全家桶 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>5.3.18</version> </dependency> <!-- Hibernate --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>5.6.5.Final</version> </dependency>2.3 数据库连接池优化
在bean.xml中配置DBCP连接池时,这几个参数直接影响系统性能:
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource"> <property name="maxTotal" value="50"/> <!-- 最大连接数 --> <property name="maxIdle" value="10"/> <!-- 最大空闲连接 --> <property name="minIdle" value="5"/> <!-- 最小空闲连接 --> <property name="testOnBorrow" value="true"/> <!-- 借出连接时验证 --> </bean>3. 电商核心模块设计
3.1 商品模块的巧妙设计
商品表设计要预留扩展字段,这是我的血泪教训:
CREATE TABLE `product` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '商品名称', `price` decimal(10,2) NOT NULL COMMENT '售价', `stock` int(11) NOT NULL COMMENT '库存', `spec_extra` json DEFAULT NULL COMMENT '规格扩展字段', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;用JSON类型存储规格参数,这样当需要新增颜色、尺寸等属性时,不需要频繁修改表结构。
3.2 购物车实现技巧
购物车数据建议同时存Session和数据库。用户未登录时存Session,登录后自动合并到数据库。关键代码:
// 购物车合并逻辑 public void mergeCart(HttpSession session, User user) { Cart sessionCart = (Cart)session.getAttribute("cart"); if(sessionCart != null) { Cart dbCart = cartService.getByUser(user.getId()); dbCart.mergeItems(sessionCart.getItems()); cartService.update(dbCart); session.removeAttribute("cart"); } }3.3 订单分布式事务处理
采用"预创建订单+MQ确认"的模式解决分布式事务问题:
- 先创建订单状态为"待支付"
- 调用支付接口
- 支付成功后通过消息队列更新订单状态
// 订单创建伪代码 @Transactional public Order createOrder(Cart cart) { Order order = new Order(); // 扣减库存 inventoryService.reduceStock(cart.getItems()); // 生成订单 orderDao.save(order); // 发送支付消息 mqProducer.sendPaymentMsg(order); return order; }4. 性能优化实战经验
4.1 缓存策略设计
采用多级缓存架构:
- 商品详情用Redis缓存,设置不同的过期时间
- 分类列表用Ehcache本地缓存
- 热点数据预加载
// 商品缓存示例 public Product getProductWithCache(Integer id) { String cacheKey = "product:" + id; Product product = redisTemplate.opsForValue().get(cacheKey); if(product == null) { product = productDao.getById(id); // 随机过期时间防止缓存雪崩 int expire = 3600 + new Random().nextInt(600); redisTemplate.opsForValue().set(cacheKey, product, expire, TimeUnit.SECONDS); } return product; }4.2 数据库优化技巧
- 商品表按分类做水平分表
- 订单表按用户ID哈希分库
- 建立组合索引时遵循最左前缀原则
-- 订单查询优化索引 ALTER TABLE orders ADD INDEX idx_user_status (user_id, status); ALTER TABLE order_items ADD INDEX idx_order_id (order_id);4.3 高并发应对方案
秒杀场景下的技术方案:
- 用Redis原子计数器做库存预扣减
- 订单队列采用RabbitMQ削峰填谷
- 前端加入验证码防刷
// 秒杀核心逻辑 public boolean seckill(Integer productId, Integer userId) { String lockKey = "seckill:" + productId; // Redis分布式锁 boolean locked = redisLock.tryLock(lockKey, 10, TimeUnit.SECONDS); if(locked) { try { Long remain = redisTemplate.opsForValue().decrement("stock:" + productId); if(remain >= 0) { mqProducer.sendSeckillMsg(productId, userId); return true; } } finally { redisLock.unlock(lockKey); } } return false; }5. 安全防护体系构建
5.1 常见攻击防护
- XSS防护:用HtmlUtils.htmlEscape转义输出
- CSRF防护:启用Struts2的token机制
- SQL注入:坚持使用预编译语句
<!-- Struts2安全配置 --> <interceptor-stack name="secureStack"> <interceptor-ref name="token"/> <interceptor-ref name="defaultStack"/> </interceptor-stack>5.2 支付安全方案
- 敏感信息加密存储:采用AES加密银行卡号
- 签名验证:所有支付回调都要验签
- 审计日志:记录完整操作轨迹
// 支付回调验证示例 public boolean verifyNotify(Map<String,String> params) { String sign = params.get("sign"); params.remove("sign"); String localSign = SignUtils.generate(params, SECRET_KEY); return sign.equals(localSign); }6. 项目部署与监控
6.1 自动化部署
用Jenkins搭建CI/CD流水线:
- 代码提交触发构建
- 自动运行单元测试
- 使用Docker容器化部署
# Dockerfile示例 FROM tomcat:8.5-jdk8 COPY target/*.war /usr/local/tomcat/webapps/ ENV SPRING_PROFILES_ACTIVE=prod EXPOSE 80806.2 监控方案
- 使用Prometheus采集JVM指标
- ELK收集分析日志
- 关键业务指标埋点
<!-- Spring监控配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>7. 源码结构与使用指南
项目采用标准Maven结构:
src/ ├── main/ │ ├── java/ │ │ ├── com/ │ │ │ └── example/ │ │ │ ├── controller/ # Struts2 Action │ │ │ ├── service/ # 业务逻辑 │ │ │ ├── dao/ # 数据访问 │ │ │ └── model/ # 实体类 │ ├── resources/ │ │ ├── struts.xml # Struts配置 │ │ ├── bean.xml # Spring配置 │ │ └── hibernate.cfg.xml # Hibernate配置 ├── webapp/ │ ├── WEB-INF/ │ │ └── web.xml │ └── static/ # 静态资源启动步骤:
- 导入数据库脚本(schema.sql)
- 修改bean.xml中的数据库配置
- 部署到Tomcat运行
- 访问http://localhost:8080
遇到性能问题时,建议先检查:
- 数据库连接池配置
- Hibernate的N+1查询问题
- Redis连接数是否足够
8. 常见问题解决方案
问题1:Hibernate懒加载报错解决:在bean.xml中配置OpenSessionInViewFilter
问题2:Struts2文件上传中文乱码解决:在struts.xml中配置:
<constant name="struts.i18n.encoding" value="UTF-8"/> <constant name="struts.multipart.saveDir" value="/tmp"/>问题3:Spring事务不生效检查:
- 是否配置了tx:annotation-driven
- 是否在Service类上加了@Transactional
- 异常类型是否配置回滚
9. 扩展功能建议
- 接入第三方登录(微信、支付宝)
- 实现智能推荐系统
- 增加客服机器人功能
- 开发移动端APP
// 微信登录示例 public String wechatLogin(String code) { WechatAuth auth = wechatService.getAuthToken(code); User user = userService.getByOpenid(auth.getOpenid()); if(user == null) { user = new User(); user.setOpenid(auth.getOpenid()); userService.create(user); } return tokenService.generateToken(user); }10. 项目演进方向
当系统用户量突破百万时,需要考虑:
- 微服务化改造(Spring Cloud)
- 读写分离架构
- 引入Elasticsearch做商品搜索
- 搭建大数据分析平台
技术选型建议:
- 服务注册中心:Nacos
- 配置中心:Apollo
- 分布式事务:Seata
- 监控:SkyWalking
// 分布式ID生成示例 public class SnowflakeIdGenerator { private long workerId; private long sequence = 0L; public synchronized long nextId() { long timestamp = System.currentTimeMillis(); if(timestamp < lastTimestamp) { throw new RuntimeException("时钟回拨异常"); } if(lastTimestamp == timestamp) { sequence = (sequence + 1) & sequenceMask; if(sequence == 0) { timestamp = tilNextMillis(lastTimestamp); } } else { sequence = 0L; } lastTimestamp = timestamp; return ((timestamp - twepoch) << timestampLeftShift) | (workerId << workerIdShift) | sequence; } }