尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Java Web综合案例实战:从Servlet到JDBC的用户管理系统
1. 从零散知识点到完整项目为什么需要一个综合案例很多人学Java Web的经历都差不多Servlet学一遍、JSP学一遍、JDBC学一遍、Filter和Listener再各学一遍每个知识点单独看都能理解但一旦要把它们串起来做一个完整的东西脑子就空了。这不是你一个人的问题而是因为大部分教程都是按“技术点”来组织的而不是按“项目”来组织的。技术点像是一颗颗散落的珠子综合案例就是那根把它们串起来的线。这个综合案例要解决的问题很明确把前面学的Servlet、JSP、JDBC、Session、Filter、MVC分层这些东西揉进一个能跑起来的Web应用里。它适合已经学完Java Web基础语法、但还没有独立做过完整项目的同学。做完这个案例你应该能清楚地知道一个请求从浏览器发出到数据库返回数据、再到页面渲染中间到底经过了哪些环节每个环节由谁负责。我当初学完Servlet之后觉得自己什么都会了结果第一次动手做项目连“登录功能该写几个类”都搞不清楚。后来带过几个新人发现大家卡住的地方高度一致不是某个技术点不会而是不知道代码该往哪里放。所以这篇内容不会再去重复讲Servlet的API怎么用而是重点讲一个综合案例从设计到落地的完整思路以及那些教程里不会写的坑。2. 案例选型为什么是“用户管理系统”而不是别的2.1 选一个“麻雀虽小五脏俱全”的业务场景综合案例的业务不能太复杂否则你会把精力耗在业务逻辑上而不是技术上但也不能太简单否则Filter、Session、分页这些技术点根本用不上。我对比过几种常见的案例选题最后认为“用户管理系统”是最合适的。它天然包含登录、注册、列表展示、增删改查、分页、权限控制这些功能每一个都对应着Java Web的核心技术点。登录对应Session和Filter列表对应JDBC查询和JSP渲染增删改查对应Servlet的请求分发分页对应SQL的limit和页码计算。业务本身你闭着眼睛都能理解这样注意力就能全部放在技术实现上。2.2 功能清单与技术点映射在动手写代码之前先把功能和技术点的对应关系列清楚这一步非常关键。很多人上来就建表写代码写到一半发现某个功能需要用到Filter又回头改结构返工成本极高。功能模块涉及技术点难度用户登录Servlet、Session、Filter基础用户注册Servlet、JDBC、表单验证基础用户列表JDBC查询、JSP、EL表达式基础分页展示SQL limit、页码计算中等修改用户Servlet请求分发、JDBC更新中等删除用户批量删除、事务处理中等权限拦截Filter、Session校验中等中文乱码处理CharacterEncodingFilter基础这张表建议你自己也画一遍画完之后你对整个项目的全貌就清楚了。我每次带新人做项目第一步都是让他们先画这张表而不是急着写代码。2.3 技术栈的取舍为什么不用框架有人会问都什么年代了还用原生Servlet和JSP直接用Spring Boot不香吗这个问题要分两面看。如果你是为了快速做一个能上线的产品那确实应该用框架。但如果你是为了理解Web开发的底层原理原生Servlet和JSP是最好的教材。框架帮你屏蔽了大量细节比如请求参数怎么解析、Session怎么管理、数据库连接怎么池化。这些细节在你用框架的时候感觉不到但一旦出了问题你连排查的方向都没有。我见过太多人用Spring Boot写了两年的接口却不知道一个HTTP请求在Servlet容器里是怎么被处理的。所以这个综合案例的价值不在于做出一个多牛的产品而在于让你把Web开发的每个环节都亲手摸一遍。3. 项目骨架搭建目录结构和分层设计3.1 包结构的划分逻辑一个清晰的包结构能让你在写代码的时候不用思考“这个类该放哪”。我推荐的分层方式是按照MVC的思路来但比教科书上的分层要更实用一些。com.example.usermanager ├── controller // Servlet层负责接收请求和分发 ├── service // 业务逻辑层处理业务规则 ├── dao // 数据访问层只负责和数据库打交道 ├── entity // 实体类和数据库表一一对应 ├── util // 工具类如数据库连接、字符串处理 └── filter // 过滤器如编码过滤、登录校验这个结构看起来简单但每个包都有明确的职责边界。controller只负责拿参数、调service、跳页面不写业务逻辑。service负责业务规则比如“用户名不能重复”这种判断就放在这里。dao只负责SQL的执行不关心业务。这样分层的好处是当你需要改数据库的时候只需要动dao层当你需要改业务规则的时候只需要动service层。3.2 数据库连接的管理方式数据库连接是新手最容易出问题的地方。很多人写代码是这样的每次需要查数据库就DriverManager.getConnection()一下用完就close。这种写法在功能上没问题但性能极差因为每次建立连接都要经过TCP握手和身份验证。正确的做法是用连接池。在综合案例阶段我建议用Druid或者HikariCP配置简单性能也好。如果你不想引入第三方库至少也要自己写一个简单的连接池工具类把连接缓存起来复用。public class DBUtil { private static DataSource dataSource; static { DruidDataSource ds new DruidDataSource(); ds.setUrl(jdbc:mysql://localhost:3306/user_manager?useSSLfalseserverTimezoneUTC); ds.setUsername(root); ds.setPassword(your_password); ds.setInitialSize(5); ds.setMaxActive(20); ds.setMinIdle(5); dataSource ds; } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection conn, Statement stmt, ResultSet rs) { // 依次关闭注意异常处理 } }这里有个细节要注意serverTimezoneUTC这个参数在MySQL 8.0以上是必须的否则会报时区错误。我当初被这个问题卡了整整一个下午一直以为是代码写错了最后才发现是连接字符串少了这个参数。3.3 实体类的设计原则实体类就是和数据库表一一对应的Java类。设计的时候有一个原则实体类的属性名和数据库字段名要能对应上但命名风格可以不同。数据库用下划线命名如user_nameJava用驼峰命名如userName。这样在写SQL的时候需要做映射但代码可读性更好。public class User { private Integer id; private String userName; private String password; private String email; private Date createTime; // getter和setter省略 }实体类不要加任何业务逻辑它就是一个数据容器。有些人喜欢在实体类里写一些计算方法比如getDisplayName()之类的这在简单项目里没问题但在复杂项目里会导致实体类和业务逻辑耦合后期很难维护。4. 核心功能实现从登录到分页的完整链路4.1 登录功能Session和Filter的配合登录功能看起来简单但它是理解Session和Filter的最佳入口。整个流程是这样的用户在登录页面输入用户名和密码Servlet接收参数调用service层验证验证通过后把用户信息存入Session然后跳转到首页。同时一个登录校验Filter会拦截所有需要登录才能访问的页面检查Session中是否有用户信息。// LoginServlet的核心逻辑 protected void doPost(HttpServletRequest request, HttpServletResponse response) { String userName request.getParameter(userName); String password request.getParameter(password); User user userService.login(userName, password); if (user ! null) { request.getSession().setAttribute(currentUser, user); response.sendRedirect(request.getContextPath() /user/list); } else { request.setAttribute(errorMsg, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } }这里有一个新手经常踩的坑登录失败后用的是forward而不是redirect。为什么因为forward是在服务器内部跳转request域中的errorMsg还能被JSP取到如果用redirectrequest域就失效了错误信息传不过去。这个细节很多教程不会专门讲但实际开发中非常关键。Filter的写法也有讲究。不是所有请求都需要登录校验登录页面本身、注册页面、静态资源CSS、JS、图片都应该放行。我见过有人在Filter里判断URL是否包含“login”这种写法很脆弱一旦URL变了就失效。更好的做法是在web.xml或者注解里配置Filter的拦截路径把不需要拦截的路径排除掉。4.2 用户列表与分页SQL limit的正确用法分页是综合案例里最有技术含量的部分之一。它的核心是两个SQL一个查总数一个查当前页的数据。-- 查总数 SELECT COUNT(*) FROM user; -- 查当前页数据 SELECT * FROM user LIMIT #{offset}, #{pageSize};offset的计算方式是(当前页码 - 1) * 每页条数。比如每页10条第3页的offset就是20。这个计算看起来简单但页码的边界处理很容易出错。如果用户手动把URL里的page参数改成0或者负数怎么办如果改成999怎么办这些都需要在代码里做校验。public PageResultUser getUserPage(int pageNum, int pageSize) { // 页码校验 if (pageNum 1) pageNum 1; int totalCount userDao.countAll(); int totalPage (int) Math.ceil((double) totalCount / pageSize); if (pageNum totalPage) pageNum totalPage; int offset (pageNum - 1) * pageSize; ListUser list userDao.findByPage(offset, pageSize); PageResultUser result new PageResult(); result.setList(list); result.setPageNum(pageNum); result.setTotalPage(totalPage); result.setTotalCount(totalCount); return result; }分页还有一个容易被忽略的点排序。如果不加ORDER BY数据库返回的数据顺序是不确定的可能导致同一页数据在不同请求中重复或遗漏。所以分页查询一定要加ORDER BY通常按id排序。4.3 增删改查的请求分发策略一个模块的增删改查通常有5个操作列表、新增页面、新增保存、修改页面、修改保存、删除。如果每个操作都写一个Servlet那一个用户模块就要写6个Servlet类会非常多。常见的做法是用一个Servlet处理一个模块的所有请求通过一个参数来区分具体操作。比如在URL后面加一个method参数// /user?methodlist // /user?methodadd // /user?methodedit // /user?methoddelete在Servlet的service()方法里根据method参数分发到不同的处理方法。这种写法减少了Servlet的数量但缺点是随着功能增加这个Servlet会越来越臃肿。另一种做法是用反射根据method参数直接调用同名方法代码更简洁但调试的时候稍微麻烦一点。我个人的建议是在综合案例阶段用if-else分发就够了简单直接容易调试。等你以后用Spring MVC的时候框架会自动帮你做请求映射那时候你自然就理解为什么需要框架了。4.4 中文乱码一个看似简单却反复出现的问题中文乱码是Java Web开发中最经典的问题没有之一。它出现的原因是浏览器和服务器对字符集的编码方式不一致。浏览器默认用UTF-8发送数据但Tomcat 8之前默认用ISO-8859-1解码两边对不上就乱码了。解决方案分两种情况。对于POST请求在获取任何参数之前设置request.setCharacterEncoding(UTF-8)。对于GET请求Tomcat 8之后默认就是UTF-8不需要额外处理如果是Tomcat 7或更早版本需要在server.xml的Connector标签里加URIEncodingUTF-8。但最省事的做法是写一个CharacterEncodingFilter统一处理所有请求的编码public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) { req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); } }这个Filter要配置在所有Filter的最前面否则后面的Filter如果先获取了参数编码设置就无效了。这个顺序问题我踩过坑当时把编码Filter放在登录Filter后面结果登录Filter里获取的用户名全是乱码排查了半天才发现是顺序问题。5. 那些教程不会告诉你的踩坑实录5.1 ClassNotFoundException驱动包到底放哪里这个问题几乎每个新手都会遇到。代码写得没问题但一运行就报ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因通常只有一个MySQL驱动包没有放到正确的位置。在Web项目中驱动包必须放在WEB-INF/lib目录下而不是项目的classpath里。因为Web应用运行在Tomcat容器中容器有自己的类加载器体系WEB-INF/lib下的jar包才会被Web应用的类加载器加载。如果你只是把jar包加到IDE的build path里编译能通过但运行时会找不到。还有一个细节MySQL 5.x和8.x的驱动类名不一样。5.x是com.mysql.jdbc.Driver8.x是com.mysql.cj.jdbc.Driver。如果你用的驱动包版本和代码里写的类名不匹配也会报ClassNotFoundException。5.2 空指针异常那些你以为不会为null的地方在Web开发中空指针异常是最常见的运行时错误。有几个地方特别容易出问题第一个是request.getParameter()。如果前端没有传这个参数它返回的是null不是空字符串。如果你直接拿它去做equals判断或者trim操作就会空指针。正确的做法是先判断是否为null或者用工具类做统一处理。第二个是Session中取数据。用户如果直接访问一个需要登录的页面Session里可能没有用户信息这时候getAttribute返回null如果你直接调用它的方法就会空指针。这就是为什么需要登录Filter在Filter层就把未登录的请求拦截掉后面的代码就不用反复判空了。第三个是数据库查询结果。根据id查询用户如果id不存在返回的就是null。如果你不判断直接使用就会空指针。这种地方我建议用Optional来包装强制调用方处理null的情况。5.3 事务问题删除操作只成功了一半批量删除是一个典型的需要事务的场景。假设你要删除3个用户代码是这样的for (Integer id : ids) { userDao.deleteById(id); }如果删除第2个的时候数据库出错了第1个已经删了第3个还没删数据就不一致了。正确的做法是把这三个删除操作放在一个事务里要么全成功要么全回滚。Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); for (Integer id : ids) { userDao.deleteById(conn, id); } conn.commit(); } catch (Exception e) { conn.rollback(); throw new RuntimeException(e); } finally { conn.setAutoCommit(true); DBUtil.close(conn); }这里有一个关键点dao层的方法必须接收Connection参数而不能自己从连接池获取连接。因为事务是绑定在Connection上的如果dao层自己获取连接那就和service层的Connection不是同一个事务就失效了。这个设计模式叫做“事务传播”在Spring里是自动处理的但在原生JDBC里需要手动传递Connection。5.4 路径问题相对路径和绝对路径的坑在JSP和Servlet中跳转页面时路径问题非常容易出错。比如在/user/list这个URL下如果JSP里写 浏览器会解析成/user/edit?id1而不是你期望的/user/edit?id1。如果写 浏览器会解析成/edit?id1丢掉了应用上下文路径。正确的做法是用request.getContextPath()获取应用上下文路径然后拼接a href${pageContext.request.contextPath}/user?methodeditid${user.id}编辑/a在Servlet中做重定向时也一样response.sendRedirect()的路径是相对于服务器根路径的必须加上contextPath。而request.getRequestDispatcher()的路径是相对于Web应用根路径的不需要加contextPath。这两个的区别我当初混淆了很久后来总结了一句话sendRedirect是给浏览器看的要加contextPathforward是给服务器看的不加contextPath。6. 从能跑到好用几个值得做的优化6.1 把JDBC代码抽成BaseDao在写dao层的时候你会发现增删改查的代码结构高度相似获取连接、创建Statement、设置参数、执行、处理结果、关闭资源。如果每个方法都写一遍代码会非常冗余。解决办法是抽一个BaseDao把公共部分封装起来子类只需要提供SQL和参数即可。public abstract class BaseDao { protected int update(String sql, Object... params) { // 公共的更新逻辑 } protected T ListT query(String sql, RowMapperT mapper, Object... params) { // 公共的查询逻辑 } }RowMapper是一个接口负责把ResultSet的一行转换成一个对象。这样UserDao只需要写SQL和映射逻辑不用再关心连接和资源关闭。这个重构的价值不在于代码变短了而在于当你需要修改数据库连接方式或者加日志的时候只需要改BaseDao一个地方。这就是分层和抽象的意义。6.2 用Filter统一处理登录校验和编码前面已经提到了编码Filter登录校验Filter也是同样的思路。把这两个横切关注点从业务代码中抽离出来Servlet里就只需要关注业务逻辑代码会清爽很多。登录校验Filter的配置需要注意放行路径。登录页面、注册页面、登录接口、注册接口、静态资源这些都不需要拦截。我通常会在Filter的init方法里配置一个白名单private ListString whiteList Arrays.asList( /login.jsp, /register.jsp, /user/login, /user/register, /css/, /js/, /images/ );然后在doFilter里判断请求路径是否在白名单中如果在就放行不在就检查Session。6.3 前端页面的复用把公共部分抽成includeJSP有一个很实用的特性叫include指令可以把公共的页面片段如头部导航、底部版权、侧边栏抽成单独的JSP文件然后在其他页面中引入。这样修改导航栏的时候只需要改一个文件所有页面都会生效。% include file/common/header.jsp % !-- 页面内容 -- % include file/common/footer.jsp %include有两种写法% include file %是静态包含在编译期把文件内容合并进来jsp:include page /是动态包含在运行期分别编译再合并。静态包含效率更高但被包含的文件不能是动态生成的动态包含更灵活但效率稍低。在综合案例中用静态包含就够了。7. 项目做完之后你真正学到了什么做完这个综合案例你手里会有一个能跑的用户管理系统但比这个系统更重要的是你对Web开发整体流程的理解。你知道了一个请求从浏览器发出后先经过Filter再到达ServletServlet调用ServiceService调用DaoDao查数据库结果原路返回最后JSP渲染成HTML发给浏览器。这条链路在你脑子里是清晰的而不是一堆孤立的知识点。你也应该体会到了分层的价值。当需求变化时你只需要修改对应的层而不是到处改代码。这种思维方式在你以后学习任何框架的时候都会用到因为Spring MVC、Spring Boot本质上也是在帮你做分层和依赖管理只是它们做得更自动化、更优雅。最后说一个我自己的体会综合案例不要只做一遍。第一遍跟着教程做能跑起来就行第二遍关掉教程自己从头写遇到卡住的地方再查第三遍尝试加一些教程里没有的功能比如用Ajax做无刷新登录、用拦截器做权限控制。每做一遍你对这些技术的理解就会深一层。我当初把用户管理系统写了三遍第三遍的时候才真正觉得自己懂了Java Web。
RELATED

相关推荐

云服务器买一年还是三年?2026双11前把账算明白(附年限决策表)

云服务器买一年还是三年?2026双11前把账算明白(附年限决策表)

价格与权益信息为 2026-10-13 核验值(来源:阿里云 99 计划活动页 aliyun.com/activity/ecs/99program、官方价格表);下单前请以官方页面实时展示为准。一、为什么双11 前最纠结这个问题 每年 9-10 月,和"买不买&q…

📅 2026/10/15 19:35:01
如何搭建一套真正用得起来的参考资源库:从筛选到维护的完整方法论

如何搭建一套真正用得起来的参考资源库:从筛选到维护的完整方法论

在技术圈里摸爬滚打了这些年,我带过的团队、参与过的项目、写过的文档加起来,能塞满好几个硬盘。但真要问我什么东西最值钱,我会说是那套被我翻了无数遍的参考资源库。这次系列连载写到第32章,正好轮到"参考资源"这个主…

📅 2026/10/15 19:35:01
自学嵌入式Day19:数据结构基础与单链表

自学嵌入式Day19:数据结构基础与单链表

目录 一、数据结构基础概念 1. 逻辑结构(数据之间的逻辑关系) 2. 存储结构(数据在计算机中的存储方式) 3. 算法与复杂度 二、链表的核心概念 1. 链表相关术语 2. 链表 vs 数组 三、单链表的基本操作(C语言实现&…

📅 2026/10/15 19:30:01
MORE NEWS

更多资讯

📰

两节串联磷酸铁锂电池主动均衡方案:升压控制器+低功耗MCU实现能量转移

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

MyBatis-Plus自动填充实战:从时间戳到多租户的数据治理基石

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

C++工资个税计算器实战:累计预扣法、边界处理与参数化设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

降AI率工具实测:8款网站横向对比与避坑指南

写继续教育论文、学习心得,却被AI检测判了“疑似AI生成”?这半年我陆续测了8个主流的降AI率网站,把真实体验、踩过的坑、数据对比都整理出来了。这篇文章不吹不黑,只讲我实测下来的真实感受,给同样被继续教育写作困扰的…

📰

消灭星星自动求解:DFS回溯剪枝与蒙特卡洛实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

2串电池组均衡方案:基于MP2642GR-Z与STM32的备用电源设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬