ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

smbms超市账单管理系统:JavaWeb核心知识实战项目全解析

smbms超市账单管理系统:JavaWeb核心知识实战项目全解析

如果你正在学JavaWeb,那大概率绕不开smbms这个名字。smbms全称是Super Market Billing Management System,也就是超市账单管理系统,一套非常经典的JavaWeb练手项目。我第一次见到它是在一套流传很广的实战教程里,当时跟着视频把代码一行一行敲完,前前后后折腾了差不多三天,中间踩过的坑比项目本身的代码还多。但这套项目不复杂,它刚好把Servlet、JSP、Filter、JDBC、MySQL、Maven这些JavaWeb最核心的知识点串了起来,做完之后你会感觉整个HTTP请求在Web容器里走的那条路一下子通了。

这套项目适合两类人:一类是刚学完Java基础和Servlet/JSP基础、想找一个完整项目练手的初学者;另一类是准备实习面试、想快速刷一遍Web开发常考点的人。它不是企业级大型项目,没有Spring全家桶的加持,也正是因为不用框架,你才能看清楚一个Web应用从页面到数据库的完整链路到底是怎样工作的。这篇文章我把整个项目的结构、环境、数据库、核心模块、常见的坑全部梳理一遍,你照着走一遍就能跑起来。

1. smbms项目的整体骨架与业务范围

1.1 这套系统到底管理了什么业务

先弄明白smbms的业务边界。它的场景是超市的运营后台,管理员登录系统之后,可以管理三类核心数据:用户、供应商、订单(也就是账单)。拆开看是这样:

  • 用户管理:给系统创建登录账号,分配角色(系统管理员、经理、普通用户等),支持按用户名模糊搜索、按角色筛选、分页展示,还能新增、修改、删除用户。
  • 供应商管理:维护给超市供货的厂商信息,包括联系人、电话、地址、传真等基础资料。
  • 订单管理:记录每笔进货账单,包含商品名称、数量、总价、是否付款、对应供应商等字段,订单列表要与供应商做关联查询,显示的是供应商名称而不是供应商ID。

这其实就是一套典型的内部管理系统(Management System)的缩影。虽然业务不难,但没有一个功能是多余的——增删改查、关联查询、分页、筛选、登录拦截这些Web开发最常见的需求,它全占齐了。搞定这套项目,后面你去看很多企业里带后台管理的项目,会觉得模式都非常熟悉。

1.2 三层架构与MVC在项目里如何落地

项目采用的是教科书里最经典的JavaWeb三层架构:

  • 表示层(Web层):由JSP页面和Servlet组成,JSP负责展示,Servlet负责接收请求、调用业务逻辑、控制页面跳转。
  • 业务层(Service层):定义接口和实现类,处理业务规则。比如用户登录时要校验账号密码,新增用户时要做重名校验,这些都写在Service里而不是写在Servlet里。
  • 持久层(DAO层):负责和数据库打交道,所有SQL都封装在DAO里,返回的对象或集合交给Service使用。

再加上经典的MVC思想——JSP当View,Servlet当Controller,JavaBean当Model,整个项目层次非常清晰。很多人学框架之前没有认真写过这种分层的项目,我建议你哪怕只是照着结构敲一遍,也能理解为什么后来Spring要搞IoC——因为各层之间的依赖关系太多了,代码手写new太啰嗦,所以需要一个容器来统一管理对象的创建和注入。

1.3 技术栈选型的讲究之处

为什么这套项目不用Spring Boot?这正是它最大的价值所在。Spring Boot把Tomcat内置、依赖配置、自动装配全都帮你包好了,你写起来确实爽,但对底层机制的理解是模糊的。在smbms里,Tomcat是需要你手动配置的,JSP是需要打war包部署的,Servlet是需要在web.xml里注册映射的,数据库连接是需要手动获取和释放的——这些"麻烦"其实就是Web技术最原始的形态。把这一层补齐了,之后学Spring MVC的时候,你就会知道DispatcherServlet的前身就是一套自己写的Servlet映射规则。

技术栈选型小结:JDK 8 + Tomcat 8/9 + MySQL 5.7/8.0 + Maven + Servlet/JSP + JDBC + JSTL + Filter。这套组合虽然老,但是学习价值极高,你以后写各类接口项目、微服务项目时,底层的处理链路依然离不开这些基础。

2. 环境准备与数据库建模

2.1 开发环境的一揽子配置

配置环境是第一个容易劝退人的环节,我用的是这套组合,实测很稳:

  • JDK 1.8:大部分教程和代码都基于JDK8,版本太新反而容易出现兼容问题。
  • Tomcat 9.x:目前兼容Servlet 4.0规范,项目用到的Servlet、JSP、Filter都能正常跑。
  • MySQL 5.7或8.0:都行,但要特别注意驱动依赖版本的对应关系(后面会讲)。
  • Maven 3.6.x:用于管理依赖和打包。
  • IDEA:自带Tomcat集成插件,调试起来方便。

有一个容易被忽略的点:IDEA里配置Tomcat时,在"Deployment"选项卡要把项目部署路径(Application context)设置成一个明确的名称,比如/smbms。如果这里不设置,访问时路径会非常别扭,而且后面写跳转路径时容易错乱。我用的上下文路径就是/smbms,对应页面访问地址就是http://localhost:8080/smbms/。

2.2 数据库表结构设计分析

smbms的数据库表设计非常典型,总共核心表大概五张,我把字段梳理出来,你建表时可以对照:

用户表smbms_user:核心字段是id(主键)、userCode(登录账号,唯一)、userName(真实姓名)、userPassword(密码,通常存MD5加密后的值)、gender(性别)、birthday(出生日期)、phone(手机号)、address(地址)、userRole(角色ID,关联角色表)。

角色表smbms_role:id、roleCode、roleName。角色表单独拆出来的好处是权限扩展性强,以后想加一个"财务"角色,直接往表里插数据就行,不用改代码。

供应商表smbms_provider:id、proCode(供应商编码)、proName(名称)、proDesc(描述)、proContact(联系人)、proPhone(电话)、proAddress(地址)、proFax(传真)。这张表在订单查询时会被连表使用。

订单表smbms_bill:id、billCode(账单编码)、productName(商品名称)、productDesc(商品描述)、productCount(商品数量)、totalPrice(总价)、providerId(供应商ID,外键关联供应商表)、isPayment(是否付款,1代表已付,2代表未付)。

地址表smbms_address:有些版本里会有这张表,用于保存用户的收货地址信息,字段包括id、contact(联系人)、addressDesc(地址描述)、postCode(邮编)、tel(电话)、userId(归属用户)。

从设计上看,表之间没有建复杂的物理外键,而是通过逻辑外键(providerId、userRole、userId)来关联。这样做在业务层维护数据关系更灵活,也符合大多数实际项目的做法——物理外键约束过多会严重影响插入和删除的性能。

2.3 初始化脚本和测试账号

建完表之后要往库里插入基础数据,至少要有这几条:

  • 角色表:1-系统管理员、2-经理、3-普通用户。
  • 用户表:插入几条不同角色的用户,密码用MD5值。最典型的测试账号是admin / 123456,123456的MD5值是e10adc3949ba59abbe56e057f20f883e,初始化脚本里直接放这个加密后的字符串。
  • 供应商表:2到3家虚拟供应商,例如某食品公司、某日用百货公司。
  • 订单表:每个供应商对应几条账单,付款状态有1有2,方便后面测试筛选功能。

我建议你把数据库初始化脚本单独保存成db_smbms.sql,每次环境重新搭建时直接source执行一遍,不用手动重复造数据,能省很多时间。

3. 核心功能模块的实现链路

3.1 登录功能的完整处理流程

登录是这套项目最容易理解、也最适合用来理清请求链路的模块。一次登录请求经历的过程是这样的:

用户打开login.jsp,输入账号密码,点击提交,表单以POST方式提交到LoginServlet。Servlet在doPost方法中先设置request.setCharacterEncoding("UTF-8"),防止中文乱码,然后获取userCode和userPassword两个参数。拿到参数之后,Servlet不直接查数据库,而是调用UserService.login(userCode, userPassword)方法,Service层拿着userCode去调用UserDao.getLoginUser(userCode),DAO执行SELECT * FROM smbms_user WHERE userCode = ?,得到用户对象后,Service层再把用户传过来的密码做MD5加密,与数据库中取出的密码进行比对。

密码比对通过之后,Service返回用户对象,Servlet把用户存入Session:session.setAttribute("user", user),然后重定向到主页main.jsp。如果比对失败,Servlet会通过request.setAttribute("error", "用户名或密码错误")把错误信息带回登录页,并转发回login.jsp展示。这里注意转发和重定向的区别:错误场景如果用重定向,request里的error信息会丢失,必须用转发。

代码层面有一个值得注意的细节:加密比对这件事不应该在Servlet里做。有些人图省事直接把查出来的明文密码和表单密码做equals,这样写业务规则就渗透到了Web层,后面如果想换加密算法,改起来非常痛苦。老老实实把逻辑放在Service层,保持分层的干净。

3.2 Filter登录拦截与放行规则

有了登录功能之后,必须要做登录拦截——不能让用户跳过登录直接访问主页或用户列表的URL。项目里用一个LoginFilter实现javax.servlet.Filter接口,核心逻辑是:在doFilter方法中从Session里拿用户对象,如果用户不为空,说明已经登录,直接放行继续访问目标资源;如果用户为空,说明没有登录,重定向到登录页面。

这里最折磨新手的点是"放行路径"的设计。如果Filter把登录页本身也拦截了,就会造成重定向死循环:访问login.jsp被拦,重定向到login.jsp,又被拦,无限循环。我当时就卡在这里,浏览器直接报"重定向次数过多"。正确的放行名单应该至少包含:

  • /login.jsp(登录页面)
  • /LoginServlet(登录请求接口)
  • /css/*、/js/*、/images/*(静态资源)
  • 其他公共资源

一个稳妥的写法是在Filter里维护一个放行路径的List,循环判断请求的URL是否以这些路径开头,是就直接chain.doFilter,否则再走登录判断。注意filter-mapping的配置,一般用/*匹配所有请求,不然拦截不住子路径。

3.3 用户列表的条件查询与分页

用户列表是smbms里最实用的一个模块,它同时涵盖了模糊查询、条件筛选、分页三件事。页面上的查询条件有两个输入框:用户名(模糊搜索)和角色(下拉选择)。对应的SQL形态是:

SELECT * FROM smbms_user WHERE userName LIKE CONCAT('%', ?, '%') AND userRole = ? LIMIT ?, ?;

需要注意,模糊查询不能直接拼字符串去创建Statement,那样大概率会有SQL注入风险。一定要用JDBC的PreparedStatement占位符?来传入值。同时条件筛选不是必填的,如果用户没有选择角色,SQL里的AND userRole = ?就应该去掉,项目里的做法是动态拼接SQL和参数列表,用一个List<Object>收集参数,最后转成Object数组传给DAO。

分页的实现思路也很固定:前端页面通过请求参数带上pageNo(当前页码),页面上设置固定的pageSize(每页数量,通常10条)。DAO需要提供两个方法:一个执行count查询SELECT COUNT(1) FROM smbms_user ...计算总记录数;另一个执行LIMIT分页查询。Service层拿到总记录数后计算总页数:

totalPage = (totalCount % pageSize == 0) ? (totalCount / pageSize) : (totalCount / pageSize + 1)

然后把当前页的数据、总页数、当前页码、总记录数全部放进request域,JSP页面上用JSTL的c:forEach循环渲染表格,底部渲染上一页和下一页的按钮。这里要设置边界判断——当前已经是第一页时,"上一页"按钮应该禁用;已经是最后一页时,"下一页"按钮应该禁用。很多新手漏掉这个判断,导致越界查询返回空列表,页面瞬间空白。

3.4 新增与修改用户的参数处理细节

用户管理里的新增和修改模块,表面看是简单的表单提交,实际暗藏几个处理细节。新增用户时,表单提交的字段包括userCode、userName、userPassword、gender、birthday、phone、address、userRole。Servlet拿到参数后,Service层要先做一次唯一性检查——userCode在表里是否已存在。这一步很关键,如果不检查直接插入,数据库会报重复键异常,给用户的体验就是"新增失败但不知道为什么"。

birthday字段的处理也是经典考点。页面上<input type="date">提交到后台的是一个"2024-01-15"格式的字符串,而JavaBean里的birthday是Date类型,需要在Servlet或Service层做一次SimpleDateFormat转换。我建议定义一个统一的方法:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); Date birthday = sdf.parse(request.getParameter("birthday"));

这个方法最好放在一个工具类里,不要写在Servlet的业务代码中间,保持代码整洁。

修改用户和新增用户类似的坑是数据回显。点击"修改"按钮时,应该先把要修改的用户查出来,放进request域,再把页面跳转到userUpdate.jsp,页面里每个<input>的value属性要动态绑定数据,比如value="${user.userName}"。性别字段如果是<input type="radio">,需要根据当前用户的gender值判断哪个单选框被选中,这种判断最好用EL表达式的三元运算配合JSTL的c:if来做,简单又不冗余。

3.5 删除用户与前端确认

删除用户的功能逻辑比较简单,但涉及一个体验问题——误删。用户表里有真实数据时,点一下"删除"按钮直接执行DELETE,数据就没了,这非常危险。解决方式有两种:一种是后端转发到删除Servlet时,在页面上用onsubmit或onclick弹出一个confirm("确定要删除该用户吗?")确认框,确认后才提交。另一种是为了防止用户被当作系统管理员删除后系统没有人来管理,删除前还需要检查用户的角色和ID,至少要保证不能删除当前登录的超级管理员自己。

从学习项目的要求来说,做第一种前端确认就足够了。JDBC删除执行的是DELETE FROM smbms_user WHERE id = ?,传入的是拼接在URL上的uid参数。要注意获取参数时用Integer.parseInt(request.getParameter("uid")),如果参数为空或格式不对,页面会直接抛NumberFormatException,最好做一个参数异常判断,返回用户列表页并提示"参数不合法"。

3.6 订单管理的关联查询与付款状态显示

订单管理模块在DAO层体现的是一张连表SQL:因为页面上要展示供应商名称,而订单表里存的只有providerId,所以必须关联供应商表。

SELECT b.*, p.proName AS providerName FROM smbms_bill b LEFT JOIN smbms_provider p ON b.providerId = p.id WHERE 1 = 1 AND b.productName LIKE CONCAT('%', ?, '%') AND b.providerId = ? LIMIT ?, ?;

这里用了LEFT JOIN而不是INNER JOIN,原因是要保证即使某笔订单的供应商被删除(或不存在),订单记录依然能显示出来,供应商名称列为空而已。这是一个非常典型的统计分析场景的SQL经验。

订单模块特有的显示要求是付款状态。数据库里isPayment字段存的是数字1或2,但页面上要显示"已付款"或"未付款",不能把数字直接甩给用户看。处理方式有两个:一是在SQL查询时用CASE WHEN把数字直接转换为汉字别名;二是在JavaBean里增加一个paymentDesc属性,在Service层根据isPayment的值动态设置。我推荐第二种,逻辑直观,而且方便扩展成多状态管理,比如以后加一个"待审核"状态也不用改SQL。

3.7 密码加密与工具类设计

密码加密是这套项目里一个容易被忽略的细节。很多教程为了演示方便,直接明文存密码,这在实际工作中是不可接受的。smbms提供了一部分加密思路,我强烈建议把密码加密的代码单独做成一个工具类MD5Util,利用MessageDigest实现:

public final class MD5Util { public static String getMD5(String source) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] bytes = md.digest(source.getBytes("UTF-8")); return toHex(bytes); } catch (Exception e) { throw new RuntimeException(e); } } }

用户注册或新增时,Service层把原始密码加密后再存入数据库;登录时,把用户输入的明文密码加密后再和库里的密文比对。这样做的好处是即便数据库泄露,攻击者也无法直接拿到明文密码。

不过现代安全实践已经不建议单独使用MD5存储口令,因为彩虹表攻击成本很低。如果你想让项目更亮眼一点,可以升级成加盐的SHA-256或BCrypt算法。面试时能讲出这个演进思路,是明显的加分项。

4. 实操过程中踩过的坑与排查记录

4.1 Maven依赖版本不兼容

这个坑我几乎每次搭环境都会遇到一次。项目的pom.xml里需要引入servlet-api、jsp-api、jstl、mysql-connector-java等依赖。有一个非常容易踩的地方:servlet-api和jsp-api在Tomcat里已经内置了,如果Maven依赖里也引入同名的包,部署到Tomcat后会出现类冲突或方法找不到的报错。

解决办法是给这两个依赖加上<scope>provided</scope>,意思是由运行环境(Tomcat)提供,只在编译期内使用,打包时不打进去:

<dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency>

另一个版本坑是MySQL驱动。如果你本地的MySQL是8.0,再用旧的com.mysql.jdbc.Driver连接串,会直接报"Loading class is obsolete"或者连接超时。MySQL 8要使用新驱动和不同连接参数:

driverClass: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/smbms?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8

4.2 中文乱码的三层解法

中文乱码在JavaWeb项目里属于"野火烧不尽"的问题,smbms项目因为既有JSP又有Servlet,乱码出现的位置特别多。我总结了一套三层排查法:

第一层:JSP页面本身编码。每个JSP顶部要写上<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>,缺了pageEncoding就可能出现页面显示乱码。

第二层:请求参数编码。POST请求的参数会在Servlet里按默认编码解析,必须在读取任何参数之前调用request.setCharacterEncoding("UTF-8")。GET请求的编码解析在Tomcat的Connector配置里,需要在server.xml中添加URIEncoding="UTF-8":如果不设置,前端传来的中文搜索关键词会变成一串问号。

第三层:响应编码。Servlet输出内容或转发页面时,要设置响应类型response.setContentType("text/html;charset=UTF-8")。JSP页面则要确保<meta charset="UTF-8">。

如果你的项目用Maven的Tomcat插件而不是外部Tomcat,GET请求乱码的解决办法是在插件配置里追加<uriEncoding>UTF-8</uriEncoding>,不要和外部Tomcat的设置混在一起。

4.3 数据库连接资源没释放导致连接池耗尽

项目原版教程在基础JDBC阶段用的是手动获取连接的方式:DriverManager.getConnection()拿到Connection,使用完关闭。很多学习者包括我在内,最开始只在查询成功时关了连接,一旦查询条件不满足或者发生异常,close()没有执行,连接就一直挂着。跑几次页面后数据库连接数耗尽,报错"Too many connections"或者Connection refused。

规范的处理方式是无论正常执行还是抛出异常,都要在finally块中释放资源:

Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = JDBCUtil.getConnection(); ps = conn.prepareStatement(sql); ... } catch (SQLException e) { e.printStackTrace(); } finally { if (rs != null) { try { rs.close(); } catch (SQLException e) {} } if (ps != null) { try { ps.close(); } catch (SQLException e) {} } if (conn != null) { try { conn.close(); } catch (SQLException e) {} } }

这个代码虽然啰嗦,但是必须的。后来为了简化,我在项目里封装了一个BaseDao,把获取连接和释放资源的代码统一管理,所有DAO都继承它。这样既规范了代码,又减少了重复。

4.4 Service层事务控制不完整

smbms的账单模块中有一个典型的操作:新增订单的同时要更新供应商的账单汇总,如果第二句SQL执行失败,第一句已经插入的数据就变成了脏数据。原版项目有些代码会在DAO层写多句SQL但在Service层不开启事务,导致数据不一致。

要彻底解决这个问题,事务一定要在Service层控制:一个业务方法,要么全部成功,要么全部回滚。实现方式就是在Service层获取连接后手动关闭自动提交:

Connection conn = JDBCUtil.getConnection(); conn.setAutoCommit(false); try { billDao.insert(conn, bill); providerDao.updateStat(conn, providerId); conn.commit(); } catch (Exception e) { conn.rollback(); } finally { conn.setAutoCommit(true); conn.close(); }

注意一个细节:这里DAO方法要接收一个Connection参数,由Service层传入,而不是在DAO内部自己新开连接。因为JDBC的事务是绑定在单个Connection对象上的,如果每层各拿各的连接,事务根本不在同一个连接里,commit和rollback也就控制不住。

4.5 JS和CSS静态资源被Filter拦截

配置好LoginFilter之后出现了一个诡异现象:登录页能正常打开,但页面上的样式全部丢失、下拉框控件完全不渲染。控制台报错提示css或js资源请求返回的是登录页的HTML。

原因非常明显——Filter把/css/style.css这种静态资源请求也拦截了,因为Session里没有用户信息,所以重定向到登录页,前端解析js/css就全部失败了。解决办法在前面提到过,必须在Filter的放行名单里加上静态资源路径前缀。这里再补充一个细节:如果你的静态资源路径是以/statics/为前缀的,放行名单要写/statics/,而不是只写具体的文件名。用request.getRequestURI().startsWith(path)来判断前缀是最稳定的写法。

4.6 路径问题导致的404和死循环

开发smbms时路径是一个反复出错的点。request.getRequestDispatcher("jsp/userList.jsp").forward(...)和response.sendRedirect("jsp/userList.jsp")的区别,我第一次在这个项目里体会最深:转发是服务器内部跳转,路径以项目根路径为准;重定向是浏览器重新发起请求,路径必须带上下文路径。

最稳妥的办法是使用JSP内置对象获取项目根路径:

${pageContext.request.contextPath}

所有的Action链接、CSS引入、JS引入,前面都加上这个EL表达式。Servlet回跳页面时,如果使用的是重定向,也要用request.getContextPath()拼接目标地址。这样无论项目部署名是什么,路径都能自适应,不会出现部署后404的尴尬。

4.7 日期格式和空指针的防御

用户列表页如果直接显示birthday日期字段,默认格式可能是2024-01-15 00:00:00,非常难看。处理方式是在JSP页面用JSTL的fmt:formatDate格式化:

<fmt:formatDate value="${user.birthday}" pattern="yyyy-MM-dd" />

如果数据列表中有某条记录的手机号为null,页面上直接展示"null"字样非常粗糙。建议在JSP上用${empty user.phone ? '暂无' : user.phone}做兜底。类似的空值处理思路在所有列表页都适用,这也是一个代码习惯问题——不要假设数据库里的数据永远是完整的。

5. 功能验收清单与快速排错速查

5.1 模块验收对照清单

把整个项目跑完一遍之后,建议你不要光看页面能打开就完事,而是按功能清单挨个走一遍操作路径,确认每个细节是否达标。我自己用下面这张表做验收自测:

模块测试操作预期结果是否通过
登录输入admin/123456登录成功,跳转主页
登录输入错误密码登录页显示错误提示
注销点击注销按钮Session清空,跳回登录页
拦截未登录直接访问用户列表URL重定向到登录页
用户列表按用户名模糊搜索列表展示匹配记录
用户列表按角色筛选并分页页码和筛选条件保持正确
新增用户提交完整表单列表新增一条记录
新增用户使用已存在的userCode提示账号重复
修改用户修改用户名和角色列表显示最新数据
删除用户点击删除并确认记录删除,列表刷新
订单管理按供应商筛选订单只显示该供应商的账单
订单管理付款状态列显示"已付款/未付款"文本
编码所有中文展示无乱码

这张表打满勾,基本可以确定项目的核心链路是完整的。面试前把这张表里的逻辑在脑子里过一遍,不管是问你Filter原理、事务控制、PreparedStatement防注入还是JSTL的使用,你都能拿出实际的例子来说话。

5.2 常见问题快速排查表

根据我自己的经历和身边学弟学妹问过的问题,有些报错极高频出现,这里整理成一张速查表,遇到问题直接对号入座:

报错或现象可能原因解决方案
The superclass "javax.servlet.http.HttpServlet" was not found没有导入servlet-api依赖pom.xml引入servlet-api,scope设为provided
ClassNotFoundException: com.mysql.jdbc.DriverMySQL驱动版本过于老旧或未引入换成com.mysql.cj.jdbc.Driver,确认驱动已在pom中
Column 'xxx' not foundSQL里字段名和表列名不一致检查JavaBean的字段属性与数据库列名的映射
中文乱码且只在GET请求出现Tomcat连接器编码未配置server.xml的Connector加URIEncoding="UTF-8"
重定向次数过多Filter拦截了登录页或静态资源在Filter放行名单中添加登录页和静态资源前缀
页面样式丢失CSS/JS被登录Filter拦截放行/css/、/js/、/statics/等路径
上一页下一页失效分页参数pageNo未从页面回传分页链接带上&pageNo=${pageNo}参数
新增用户后列表没有变化查询条件残留清除session或request中保存的搜索条件后再跳转
javax.el.ELExceptionJSTL标签库未引入pom.xml引入jstl依赖,JSP顶部引入tablib指令
表单日期值为空前端date控件格式和后端解析格式不一致统一使用yyyy-MM-dd格式解析

5.3 项目跑通后的扩展建议

这个项目做完之后,不要急着删掉。它是有很好的扩展空间的,我自己后续做过一次重构,把smbms改造成了Spring Boot + MyBatis版本,改造过程收获非常大。

首先是换掉Servlet和JSP,改成Spring MVC + Thymeleaf模板,原来手写的Filter被Spring MVC的Interceptor替代了,Servlet里的参数解析和跳转逻辑全部由框架接管。然后是数据访问层从手写JDBC改造成MyBatis,把以前用PreparedStatement拼的SQL迁移到Mapper.xml里,分页也换成了PageHelper。这一步改造下来,你会非常直观地感受到框架到底帮你做了哪些事——以前自己写的那些重复代码,原来都是可以在框架层消化的。

如果你时间比较充裕,还可以给项目加上前后端分离的REST风格接口,用Vue写一个简易的管理界面,替换掉原来的JSP。这种改造的核心不是炫技,而是让你理解Web开发从"一体化"到"前后端分离"的演进逻辑。

我个人体会最深的是:smbms最大的价值不在于功能本身多强大,而在于它把JavaWeb的核心知识点以"做一个真实系统"的方式串成了一条线。跟着敲完代码再回到最基础的Servlet生命周期、过滤器链、JDBC事务这些概念上,你会从"看过"变成"真正懂"。最后分享一个小技巧:把这套项目做完之后,合上教程,从零开始重写一遍,不要看原来的代码,卡住了再回头查。这样重复两遍,你对整个项目的理解和记忆会比单纯看十遍视频要牢固得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进