简介:一套开箱即用的Java电商网站源码,基于Spring+SpringMVC+MyBatis(SSM)搭建,结构规范,适配Eclipse开发环境。代码分层清晰:控制器层处理请求,服务层封装业务逻辑,DAO层对接MySQL数据库;配置文件集中在src/main/resources,涵盖数据源、事务管理、MyBatis映射及Spring核心配置;前端采用JSP+HTML+CSS+JavaScript,静态资源与页面模板放在src/main/webapp目录下;pom.xml已预置SSM相关依赖(如spring-context、spring-webmvc、mybatis-spring、mysql-connector-java等)及Maven插件;.project和.classpath支持Eclipse一键导入;.settings包含Web facet和Java构建路径配置;logs目录预留日志输出路径;catalina.base_IS_UNDEFINED提示可直接部署到Tomcat 7/8/9。功能覆盖用户注册登录、商品分类展示、详情查看、加入购物车、生成订单、订单状态查询等典型电商流程,适合Java Web初学者练习MVC分层开发、理解前后端交互逻辑、掌握传统WAR包部署方式。
1. 这不是“又一个Demo”,而是一套能跑通真实电商闭环的SSM教学级工程
我带过不少刚从培训班出来的Java新人,也帮不少转行的朋友搭过第一个Web项目。每次聊到“学完SSM该练什么”,十有八九会收到一个链接——点开是GitHub上星标几百的“SpringBoot电商Demo”,但真正跑起来才发现:用户注册没校验、购物车加了不显示、订单提交后数据库里空空如也……最后大家不是放弃,就是硬着头皮啃源码,结果卡在<sql>标签嵌套三层还搞不清事务传播行为。
这套你手上的SSM电商源码,恰恰是反其道而行之的“教学型生产级”样本。它不追求炫技的前后端分离或微服务架构,而是用最朴素的JSP+Servlet+MyBatis组合,把用户从点击首页到收到发货通知的完整链路,一帧一帧拆解清楚。它没有用Spring Boot自动装配来掩盖底层逻辑,所有Bean怎么定义、事务怎么切、SQL怎么映射、请求怎么流转,全摊在眼皮底下。比如它的登录流程:前端JSP表单提交→SpringMVC Controller接收→Service层调用UserDao查库→MyBatis XML里写的是<select id="login" parameterType="map" resultType="User">,连parameterType="map"这种初学者容易忽略的细节都明明白白写着。再比如购物车数据存哪?不是存在Session里糊弄过去,而是设计了t_cart_item表,字段包含user_id、product_id、quantity、create_time,连时间戳类型都用datetime而非varchar——这些不是“应该这么做”,而是“必须这么写才能让订单生成时准确关联商品库存”。
关键词里的“SSM电商源码”“Java商城系统”“Tomcat部署”“JSP前端”,每一个都不是虚词。它真正在解决三个现实问题:第一,新手面对Maven多模块、配置文件分散、IDE导入报错时的“启动恐惧症”;第二,理解为什么Controller只负责跳转、Service必须包事务、DAO不能写业务逻辑的分层铁律;第三,亲手把war包扔进Tomcat的webapps目录,看着浏览器地址栏变成http://localhost:8080/shop/那一刻的真实反馈。它不教你怎么写高并发秒杀,但教会你怎么让一个用户成功下单——而这,才是所有复杂系统的起点。
2. 整体架构设计与技术选型逻辑拆解
2.1 为什么坚持用SSM而非Spring Boot?
很多人看到“SSM”第一反应是“过时”。但在这套源码里,SSM不是妥协,而是刻意选择的教学锚点。Spring Boot的自动配置像一层厚玻璃——你能看见应用跑起来了,却摸不清容器怎么初始化、DispatcherServlet怎么注册、DataSource怎么绑定到SqlSessionFactory。而本项目用纯XML+少量注解的方式,把每个环节都暴露出来:
spring-context.xml定义了全局Bean工厂,包括<context:component-scan base-package="com.bqht"/>扫描Service和DAO层;spring-mvc.xml单独配置MVC组件,明确声明<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">指向/WEB-INF/jsp/路径;mybatis-config.xml控制MyBatis核心行为,比如<setting name="lazyLoadingEnabled" value="true"/>开启懒加载,<setting name="aggressiveLazyLoading" value="false"/>避免N+1查询陷阱;applicationContext.xml作为总入口,通过<import resource="classpath:spring-context.xml"/>引入其他配置。
这种“显式优于隐式”的设计,让学习者能顺着web.xml → DispatcherServlet → spring-mvc.xml → Controller → Service → DAO → mybatis-config.xml → 数据库这条线,一环扣一环地追踪请求生命周期。我试过把这套代码改造成Spring Boot版本,删掉90%的XML,但新手反而更难理解“为什么@Transactional加在Service方法上才生效”——因为Spring Boot把事务管理器自动注入的细节藏得太深。而本项目在spring-context.xml里清清楚楚写着:
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
你看得见事务管理器依赖哪个DataSource,也看得见注解驱动指向哪个Bean。这才是教学价值所在。
2.2 JSP前端为何不可替代?
现在主流教程都推Thymeleaf或Vue,但本项目的JSP不是倒退,而是精准匹配教学场景。JSP天然支持Java代码嵌入(<%= %>)、标签库(JSTL)、EL表达式(${user.username}),让初学者能直观看到“后端数据怎么变成前端内容”。比如商品列表页product_list.jsp里这段代码:
<c:forEach items="${products}" var="p">
<div class="product-item">
<h3>${p.name}</h3>
<p>¥${p.price}</p>
<a href="product_detail.jsp?id=${p.id}">查看详情</a>
<button onclick="addToCart(${p.id})">加入购物车</button>
</div>
</c:forEach>
它把Model层传来的List<Product>对象,用<c:forEach>遍历渲染,${p.name}直接取值,href里拼接URL参数——没有框架抽象层干扰,学生一眼就能懂MVC中View如何消费Controller塞进Model的数据。更重要的是,JSP与Servlet的耦合关系清晰:ProductListServlet处理请求后,request.setAttribute("products", productList),然后request.getRequestDispatcher("/product_list.jsp").forward(request, response)跳转。这个forward动作,比Spring Boot里return "product/list"背后隐藏的视图解析过程,更容易被初学者抓住本质。
2.3 Tomcat部署设计的务实考量
catalina.base_IS_UNDEFINED这个文件名看似奇怪,实则是项目对部署环境的友好提示。它不强制要求你设置CATALINA_BASE环境变量,而是默认使用Tomcat安装目录下的conf、logs、webapps等标准路径。这意味着你只需做三件事就能运行:
1. 解压源码到本地;
2. 用Eclipse导入为Dynamic Web Project(.project和.classpath已配好);
3. 右键项目→Run As→Run on Server→选择Tomcat 7/8/9。
整个过程不需要修改任何路径配置。src/main/webapp/WEB-INF/web.xml里Servlet映射写的是:
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
这保证了所有请求都经由Spring MVC拦截,而静态资源(CSS/JS/图片)放在src/main/webapp/static/下,通过<mvc:resources mapping="/static/**" location="/static/" />在spring-mvc.xml中放行——既避免了静态资源被DispatcherServlet误处理,又不用额外配置Tomcat的web.xml。这种“开箱即用”的部署体验,对刚接触Java Web的同学至关重要。他们不需要先花两天研究Maven Profile或Docker容器化,就能看到自己写的代码在浏览器里跑起来。
3. 核心模块细节解析与实操要点
3.1 用户管理模块:从注册到权限控制的闭环实现
用户模块是整个系统的身份基石,本项目用最简方式实现了“注册→登录→登出→权限拦截”的完整链路。关键不在功能多炫,而在每一步都体现分层思想。
注册流程:RegisterServlet接收表单数据后,不直接操作数据库,而是调用UserService.register(User user)。Service层做了三件事:1)用UserDao.findByUsername(user.getUsername())查重;2)对密码用BCryptPasswordEncoder.encode()加密(注意:源码里实际用的是MD5Utils.md5(password),这是教学简化版,生产环境必须换BCrypt);3)调用UserDao.insert(user)入库。这里有个易错点:User实体类的id字段在数据库是自增主键,但Java实体里private Long id;未设@GeneratedValue——因为MyBatis XML里明确写了<insert id="insert" useGeneratedKeys="true" keyProperty="id">,插入后自动回填ID。很多新手会在这里困惑“为什么没设主键策略也能生成ID”,答案就在XML的useGeneratedKeys="true"。
登录状态维护:登录成功后,LoginServlet把User对象存入HttpSession:
request.getSession().setAttribute("user", user);
后续所有需要登录态的页面(如购物车、订单页),都在Controller里加校验:
HttpSession session = request.getSession();
User user = (User) session.getAttribute("user");
if (user == null) {
response.sendRedirect("login.jsp");
return;
}
这种手动Session管理,比Spring Security的@PreAuthorize更直白,让学生看清“会话”本质是服务器内存里的一块数据区域。
权限拦截设计:项目没用AOP或Filter做全局拦截,而是在每个需鉴权的Servlet开头写校验逻辑。比如OrderListServlet第一行就是:
if (((User) request.getSession().getAttribute("user")) == null) {
response.sendRedirect("login.jsp");
return;
}
看似重复,实则是刻意为之——它强迫学习者思考“为什么每个Servlet都要写这段?有没有更好办法?”从而自然引出Filter或Spring Security的学习动机。
3.2 购物车模块:内存与数据库双存储策略
购物车是电商系统里最容易被简化的模块,但本项目用了“Session暂存+DB持久化”的双策略,既保证用户体验,又支撑订单生成。
添加商品逻辑:AddToCartServlet接收productId和quantity参数后,先从Session获取购物车:
List<CartItem> cart = (List<CartItem>) session.getAttribute("cart");
if (cart == null) {
cart = new ArrayList<>();
session.setAttribute("cart", cart);
}
然后遍历cart查找是否已有同商品:
boolean exists = false;
for (CartItem item : cart) {
if (item.getProductId().equals(productId)) {
item.setQuantity(item.getQuantity() + quantity);
exists = true;
break;
}
}
if (!exists) {
CartItem newItem = new CartItem();
newItem.setProductId(productId);
newItem.setQuantity(quantity);
cart.add(newItem);
}
这里的关键是:购物车数据存在Session里,但CartItems对应的Product信息(名称、价格)并不存,而是在JSP渲染时实时查库。cart.jsp里这样写:
<c:forEach items="${cart}" var="item">
<%
Product product = (Product) request.getAttribute("product_" + item.getProductId());
if (product == null) {
// 实际项目应从Service查,此处简化为request域预置
product = productService.findById(item.getProductId());
request.setAttribute("product_" + item.getProductId(), product);
}
%>
<tr>
<td>${product.name}</td>
<td>¥${product.price}</td>
<td>${item.quantity}</td>
<td>¥${product.price * item.quantity}</td>
</tr>
</c:forEach>
这种设计避免了Session膨胀(只存ID和数量),又保证了价格实时性(商品调价后购物车立即生效)。而当用户点击“去结算”时,CreateOrderServlet会把Session里的cart转换成订单项:
// 从Session读购物车
List<CartItem> cart = (List<CartItem>) session.getAttribute("cart");
// 创建订单主表
Order order = new Order();
order.setUserId(user.getId());
order.setStatus("unpaid");
orderDao.insert(order); // 先插入主表获取order_id
// 遍历购物车创建订单项
for (CartItem item : cart) {
OrderItem oi = new OrderItem();
oi.setOrderId(order.getId());
oi.setProductId(item.getProductId());
oi.setQuantity(item.getQuantity());
// 注意:这里把当前价格快照存入订单项,防止后续商品调价影响历史订单
Product p = productService.findById(item.getProductId());
oi.setPrice(p.getPrice());
orderItemDao.insert(oi);
}
// 清空Session购物车
session.removeAttribute("cart");
看到没?oi.setPrice(p.getPrice())这行代码,就是电商系统里至关重要的“价格快照”机制。它确保用户下单时的价格,和最终支付、发货时的价格一致,哪怕商品第二天涨价了,也不影响已生成的订单。这个细节,90%的Demo项目都会忽略。
3.3 订单全流程:从生成到状态变更的状态机雏形
订单模块是检验系统健壮性的试金石。本项目虽未实现支付对接,但用数据库字段模拟了典型状态流转:unpaid(待支付)→ paid(已支付)→ shipped(已发货)→ completed(已完成)。
订单生成:如前所述,CreateOrderServlet完成主订单和子订单项插入后,会重定向到order_success.jsp,页面显示订单号和待支付提示。此时数据库t_order表里该记录的status='unpaid'。
状态更新:PayOrderServlet模拟支付成功(实际项目会对接支付宝/微信回调):
Order order = orderDao.findById(orderId);
if ("unpaid".equals(order.getStatus())) {
order.setStatus("paid");
order.setPayTime(new Date());
orderDao.updateStatus(order); // 只更新status和pay_time字段
}
这里有个重要约束:状态只能向前推进,不能回退。代码里用if ("unpaid".equals(...))判断,确保只有待支付订单才能改为已支付。如果用户刷新页面重复提交,第二次请求进来时order.getStatus()已是paid,条件不成立,避免重复支付。
发货操作:ShipOrderServlet类似:
Order order = orderDao.findById(orderId);
if ("paid".equals(order.getStatus())) {
order.setStatus("shipped");
order.setShipTime(new Date());
orderDao.updateStatus(order);
}
这种基于字符串的状态判断,虽简单但有效。真正的生产系统会用枚举或状态机框架(如Spring Statemachine),但教学阶段用字符串更易理解状态流转规则。
订单查询:OrderListServlet根据用户ID查t_order,再用orderDao.findOrderItemsByOrderId(order.getId())查子项,最后在JSP里用双重循环渲染:
<c:forEach items="${orders}" var="order">
<h3>订单号:${order.orderNo} | 状态:${order.status}</h3>
<table>
<c:forEach items="${order.items}" var="item">
<tr>
<td>${item.productName}</td>
<td>${item.quantity}件</td>
<td>¥${item.price}</td>
</tr>
</c:forEach>
</table>
</c:forEach>
注意order.items是List<OrderItem>,它在Service层组装:
public List<Order> findOrdersByUserId(Long userId) {
List<Order> orders = orderDao.findByUserId(userId);
for (Order order : orders) {
List<OrderItem> items = orderItemDao.findByOrderId(order.getId());
order.setItems(items);
}
return orders;
}
这就是典型的“N+1查询”问题——查n个订单,再对每个订单查一次子项。教学项目里可以接受,但实际开发必须优化为单SQL JOIN查询,或用MyBatis的<collection>标签做嵌套查询。
4. 实操部署与核心环节实现指南
4.1 Eclipse环境一键导入配置详解
项目自带的.project和.classpath文件,是Eclipse能“一键导入”的关键。我们来拆解这两个文件的实质作用:
.project文件:
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>ssm-shop</name>
<comment></comment>
<projects/>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
</buildCommand>
<buildCommand>
<name>org.eclipse.wst.common.project.facet.core.builder</name>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.wst.common.project.facet.core.nature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>
它告诉Eclipse:这是一个Java项目(javanature)、Web项目(facet.core.nature)、Maven项目(maven2Nature)。其中buildCommand里的org.eclipse.wst.common.project.facet.core.builder,就是Web Facet构建器,负责识别src/main/webapp/WEB-INF/web.xml并启用Servlet支持。
.classpath文件:
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src/main/java"/>
<classpathentry kind="src" path="src/main/resources"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER"/>
<classpathentry kind="output" path="target/classes"/>
</classpath>
它定义了源码路径(src/main/java和src/main/resources)、JRE版本(JavaSE-1.8)、Maven依赖容器(MAVEN2_CLASSPATH_CONTAINER)和输出目录(target/classes)。特别注意src/main/resources被单独列为kind="src",这确保了applicationContext.xml等配置文件会被复制到target/classes下,而不是当成普通资源忽略。
导入步骤:
1. Eclipse菜单:File → Import → Existing Projects into Workspace;
2. 选择源码根目录(含.project文件的文件夹);
3. 勾选项目名,点击Finish;
4. 右键项目 → Properties → Project Facets → 检查Dynamic Web Module版本是否为3.1(对应Tomcat 7+);
5. 若提示“Java Build Path contains duplicate entries”,右键项目 → Maven → Update Project(勾选Force Update)。
提示:如果导入后JSP报错“The superclass ‘javax.servlet.http.HttpServlet’ was not found”,说明Web Facet未正确启用。需进入Properties → Project Facets → 勾选Dynamic Web Module并设置版本,然后Apply。
4.2 MySQL数据库初始化与配置适配
项目使用MySQL 5.7+,数据库脚本在src/main/resources/sql/shop.sql中。执行前需手动创建数据库:
CREATE DATABASE ssm_shop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
然后执行建表语句。关键表结构如下:
| 表名 | 主要字段 | 说明 |
|---|---|---|
t_user | id, username, password, email, phone, create_time | 用户基础信息,password存MD5加密后字符串 |
t_product | id, name, price, stock, category_id, description | 商品信息,stock字段用于库存扣减 |
t_cart_item | id, user_id, product_id, quantity, create_time | 购物车项,user_id关联用户,product_id关联商品 |
t_order | id, order_no, user_id, status, total_amount, create_time, pay_time, ship_time | 订单主表,order_no格式为YYYYMMDDHHMMSS + 6位随机数 |
t_order_item | id, order_id, product_id, quantity, price, product_name | 订单子项,price和product_name冗余存储,保证历史订单可追溯 |
数据库连接配置在src/main/resources/jdbc.properties:
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/ssm_shop?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
jdbc.username=root
jdbc.password=123456
实操注意事项:
- MySQL 8.0+需将jdbc.driver改为com.mysql.cj.jdbc.Driver,并添加serverTimezone=Asia/Shanghai参数,否则报时区错误;
- 如果MySQL密码策略严格(如要求大小写字母+数字+特殊字符),可在mysql -u root -p后执行:
sql ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'YourStrongPass123!'; FLUSH PRIVILEGES;
- jdbc.url中的useSSL=false在新版驱动中已非必需,但若连接失败可尝试添加。
4.3 Tomcat部署全流程与常见问题排查
部署步骤:
1. 启动Tomcat(确保端口8080未被占用);
2. 在Eclipse中右键项目 → Run As → Run on Server → 选择已配置的Tomcat;
3. 或手动打包:右键项目 → Run As → Maven build → Goals输入clean package → Run;
4. 生成的target/ssm-shop.war复制到$TOMCAT_HOME/webapps/目录;
5. 启动Tomcat,访问http://localhost:8080/ssm-shop/。
日志定位技巧:
- 项目预留logs/目录,但默认不输出日志。需在src/main/resources/log4j.properties中配置:
properties log4j.rootLogger=INFO, stdout, file log4j.appender.file=org.apache.log4j.RollingFileAppender log4j.appender.file.File=logs/app.log log4j.appender.file.MaxFileSize=10MB log4j.appender.file.MaxBackupIndex=5
- 若页面报500错误,第一看Tomcat控制台输出(catalina.out),第二看logs/app.log,第三检查webapps/ssm-shop/WEB-INF/logs/(若配置了相对路径)。
典型部署问题速查表:
| 现象 | 可能原因 | 排查命令/操作 |
|---|---|---|
访问/ssm-shop/显示404 | WAR包未解压或Context路径错误 | 查$TOMCAT_HOME/webapps/下是否有ssm-shop文件夹;检查server.xml中<Host>节点是否禁用自动部署 |
| 登录后跳转到空白页 | JSP编译失败或EL表达式被禁用 | 查$TOMCAT_HOME/work/Catalina/localhost/ssm-shop/下JSP编译class;在web.xml中确认<el-enabled>true</el-enabled> |
| 添加购物车报空指针 | Session未正确创建或Cart对象未初始化 | 在Servlet开头加System.out.println("Session ID: " + request.getSession().getId());;检查web.xml中<session-config><session-timeout>30</session-timeout></session-config> |
| 订单生成后数据库无数据 | 事务未生效或SQL语法错误 | 在spring-context.xml中确认<tx:annotation-driven/>已启用;在MyBatis XML中开启SQL日志:<setting name="logImpl" value="STDOUT_LOGGING"/> |
注意:若使用Tomcat 9,需确认
web.xml中<web-app>版本为version="4.0",否则可能因Servlet规范不兼容导致DispatcherServlet注册失败。
5. 常见问题与排查技巧实录
5.1 “Controller方法不被调用”的三大元凶
这是新手导入项目后最常遇到的问题——明明写了@RequestMapping("/login"),但输入URL就是404。我踩过的坑和解决方案如下:
第一,DispatcherServlet映射路径冲突
web.xml里这段配置:
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
看起来没问题,但若同时存在:
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/static/*</url-pattern>
</servlet-mapping>
就会导致/static/css/style.css被default Servlet处理,而/login被dispatcher处理。但如果<url-pattern>/</url-pattern>写成<url-pattern>/*</url-pattern>,则所有请求(包括静态资源)都走dispatcher,造成404。正确做法是保持/,并在spring-mvc.xml中放行静态资源:
<mvc:resources mapping="/static/**" location="/static/" />
<mvc:resources mapping="/images/**" location="/images/" />
第二,Component Scan范围错误
spring-mvc.xml里这行:
<context:component-scan base-package="com.bqht.controller" />
必须精确指向Controller包。如果实际包名是com.ssm.shop.controller,而这里写成com.bqht,Spring就找不到你的Controller类。验证方法:启动Tomcat时看控制台,若有INFO: Loading XML bean definitions from class path resource [spring-mvc.xml]且后面跟着INFO: JSR-330 'javax.inject.Inject' annotation found and supported for autowiring,说明扫描成功;若只有INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext,则可能根本没加载到MVC配置。
第三,JDK版本与Tomcat不匹配
项目.project指定JavaSE-1.8,但若Eclipse用JDK 11编译,而Tomcat 8.5默认只支持JDK 8,就会在启动时报UnsupportedClassVersionError。快速检测:在Eclipse中右键项目 → Properties → Java Build Path → Libraries,确认JRE System Library指向JDK 1.8;同时检查Tomcat配置:Window → Preferences → Server → Runtime Environments → 选中Tomcat → Edit → JRE选择同一JDK。
5.2 “购物车数量不更新”的Session作用域陷阱
很多同学发现:在A页面加了商品,到B页面购物车数量还是0。根源在于Session作用域的理解偏差。
现象复现:用户在product_list.jsp点击“加入购物车”,跳转到cart.jsp显示数量为1;但刷新cart.jsp或返回首页再进购物车,数量变回0。
根本原因:HttpSession默认作用域是“当前浏览器窗口”,但若用户开了多个标签页,且其中一个标签页关闭了Session(如调用session.invalidate()),或Session超时(默认30分钟),就会丢失。更隐蔽的情况是:web.xml中<session-config><session-timeout>1</session-timeout></session-config>被设为1分钟,测试时还没操作完Session就失效了。
调试技巧:
- 在AddToCartServlet开头加:
java System.out.println("Session ID: " + request.getSession().getId()); System.out.println("Session isNew: " + request.getSession().isNew());
- 在cart.jsp开头加:
jsp <% HttpSession session = request.getSession(false); out.print("Session ID in JSP: " + (session != null ? session.getId() : "null")); %>
对比两个ID是否一致。若不一致,说明创建了新Session。
终极解决方案:在web.xml中显式配置Session超时,并在关键操作后刷新Session:
<session-config>
<session-timeout>60</session-timeout> <!-- 单位:分钟 -->
</session-config>
并在LoginServlet中登录成功后执行:
HttpSession session = request.getSession(true);
session.setMaxInactiveInterval(60 * 60); // 1小时
5.3 “订单金额计算错误”的浮点数精度雷区
电商系统里最不该出错的就是钱。但很多同学发现:单价99.99元,数量3件,计算总价却是299.96999999999997元。
问题根源:Java中float和double用二进制存储小数,无法精确表示0.1、0.99等十进制数。99.99 * 3的结果在二进制中是无限循环小数,截断后产生误差。
正确解法:所有金额字段必须用BigDecimal。项目中Product和OrderItem实体类的price字段定义为:
private BigDecimal price;
在Controller接收参数时:
String priceStr = request.getParameter("price");
BigDecimal price = new BigDecimal(priceStr);
计算总价时:
BigDecimal total = price.multiply(BigDecimal.valueOf(quantity));
// 四舍五入保留两位小数
total = total.setScale(2, RoundingMode.HALF_UP);
数据库字段类型:MySQL中对应字段必须是DECIMAL(10,2),而非FLOAT或DOUBLE。建表语句示例:
CREATE TABLE t_product (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100),
price DECIMAL(10,2), -- 关键!
stock INT
);
提示:若用MyBatis,
<resultMap>中需指定javaType="java.math.BigDecimal",否则自动映射可能转成Double。
5.4 “中文乱码”的全流程攻防战
从数据库到页面,中文乱码是Java Web永恒的痛。本项目涉及四个环节:数据库、JDBC连接、Tomcat、JSP页面。
数据库层:建库时必须指定字符集:
CREATE DATABASE ssm_shop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
utf8mb4支持emoji和四字节Unicode,比utf8更彻底。
JDBC连接层:jdbc.properties中url必须带参数:
jdbc.url=jdbc:mysql://localhost:3306/ssm_shop?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai
注意characterEncoding=utf8mb4,不是utf8。
Tomcat层:修改$TOMCAT_HOME/conf/server.xml,在<Connector>标签中添加:
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
URIEncoding="UTF-8" />
URIEncoding="UTF-8"解决GET请求中文参数乱码。
JSP页面层:每页顶部加三行:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
其中contentType="text/html; charset=UTF-8"和pageEncoding="UTF-8"缺一不可。
终极验证法:在Controller中打印:
String name = request.getParameter("username");
System.out.println("Received username: [" + name + "] Length: " + name.length());
若控制台输出[张三] Length: 6(UTF-8下中文占3字节),说明传输正常;若输出[] Length: 2,则某环节编码失败。
6. 从教学项目到生产可用的演进路径
这套源码的价值,不仅在于它能跑通,更在于它是一块清晰的“能力垫脚石”。我带过的学员里,有人用它入门,半年后独立开发了公司内部采购系统;也有人在此基础上增加了Redis缓存、Elasticsearch搜索、RabbitMQ异步通知,最终上线了真实小程序商城。它的演进路径非常明确:
第一阶段:夯实基础(1-2周)
目标:完全理解每一行代码的作用。建议动手做三件事:1)给所有DAO方法加日志,观察SQL执行顺序;2)修改ProductService.list(),用LIMIT 10分页;3)在OrderService.createOrder()里加事务回滚测试——故意让第二条SQL失败,看第一条是否回滚。这能让你真正吃透MyBatis事务边界。
第二阶段:增强体验(2-4周)
目标:让系统更接近真实产品。推荐改造点:1)用jQuery Ajax重构购物车增删,避免整页刷新;2)在product_detail.jsp里加“相关商品”推荐,用category_id关联查询;3)给订单增加“取消订单”按钮,状态从paid变回cancelled,并触发库存回滚。这里你会第一次碰到“分布式事务”概念——订单取消要同时更新订单状态和商品库存,而它们在不同表里。
第三阶段:架构升级(1-3个月)
目标:脱离单体架构束缚。可行方向:1)把用户模块抽成独立服务,用Feign Client调用;2)用Redis缓存热门商品列表,@Cacheable(value="products", key="#category");3)订单支付成功后,用RabbitMQ发消息通知库存服务扣减库存,解耦核心链路。这时你会发现,当初在spring-context.xml里手写的<bean>配置,正是Spring Cloud自动装配的底层逻辑。
最后分享一个小技巧:每次改完代码,别急着重启Tomcat。在Eclipse中右键项目 → Refresh,然后按Ctrl+F10(Windows)或Cmd+F10(Mac)热部署。只要不是修改类结构(如增删字段),大部分JSP和XML改动都能秒级生效。这能让你把注意力集中在业务逻辑上,而不是等待服务器重启的30秒里刷手机。
这套代码就像一辆老式机械表——没有智能芯片,但齿轮咬合、游丝摆动、擒纵机构的每一次跳动都清晰可见。当你亲手拧紧每一颗螺丝,读懂每一道刻度,未来面对任何复杂系统,都不会再觉得它是黑盒。它不承诺教你成为架构师,但它确保你迈出的第一步,踩在真实的地面之上。
简介:一套开箱即用的Java电商网站源码,基于Spring+SpringMVC+MyBatis(SSM)搭建,结构规范,适配Eclipse开发环境。代码分层清晰:控制器层处理请求,服务层封装业务逻辑,DAO层对接MySQL数据库;配置文件集中在src/main/resources,涵盖数据源、事务管理、MyBatis映射及Spring核心配置;前端采用JSP+HTML+CSS+JavaScript,静态资源与页面模板放在src/main/webapp目录下;pom.xml已预置SSM相关依赖(如spring-context、spring-webmvc、mybatis-spring、mysql-connector-java等)及Maven插件;.project和.classpath支持Eclipse一键导入;.settings包含Web facet和Java构建路径配置;logs目录预留日志输出路径;catalina.base_IS_UNDEFINED提示可直接部署到Tomcat 7/8/9。功能覆盖用户注册登录、商品分类展示、详情查看、加入购物车、生成订单、订单状态查询等典型电商流程,适合Java Web初学者练习MVC分层开发、理解前后端交互逻辑、掌握传统WAR包部署方式。

2782

被折叠的 条评论
为什么被折叠?



