告别JSP火葬场:JavaBean+Servlet+MVC分层开发实战指南

发布时间:2026/10/3 17:57:55
告别JSP火葬场:JavaBean+Servlet+MVC分层开发实战指南
前端也好、后端也好干过一阵子的人应该都有体会JSP 页面写起来一时爽维护起来火葬场。尤其是刚接触 Java Web 那会儿很多人习惯在一个 JSP 文件里把数据库查询、业务判断、HTML 标签全塞进去页面里密密麻麻全是% %脚本片段。当时觉得这不挺好吗一页搞定可等项目功能多起来改一个需求要在十几个 JSP 文件里翻来找去那种痛苦只有经历过的人才懂。所以从JSP 页面开发走向规范项目核心就是引入JavaBean、Servlet和MVC这套组合拳。这篇文章我想从自己实际踩坑和重构的经验出发把这三者到底解决什么问题、怎么配合、以及一个完整的改造案例掰开揉碎讲清楚希望能帮正在学 Java Web 或者刚进项目组被老代码折磨的朋友少走点弯路。1. 为什么说纯 JSP 开发是一条走不远的路1.1 JSP 最初的定位其实不是万能页面JSPJavaServer Pages刚出来的时候设计意图是让写页面的人能方便地把动态数据输出到 HTML 里本质上是 Servlet 的一种简化形式——JSP 最终会被容器编译成 Servlet 来执行。所以 JSP 擅长的是展示这件事它内置了out、request、response、session这些隐式对象写起输出来确实顺手。问题也出在这里因为太顺手了很多人就把什么活都往 JSP 里堆。我见过最离谱的一个老项目首页一个 JSP 文件两千多行开头连数据库驱动中间拼 HTML 拼到一半又查一次库底部还藏着一段写文件的逻辑。这种代码不是说不能跑而是没人敢动动一行可能带崩三个功能。1.2 纯 JSP 开发的五大典型痛点我总结了一下纯 JSP 开发的项目通常会遇到这几个问题职责完全混乱页面展示、业务校验、数据访问全都耦合在一个文件里。想改个页面布局得先看懂几十行业务代码想改个业务规则又得在 HTML 标签里找逻辑。复用性差一段公共的业务逻辑比如获取当前登录用户在 A 页面写了一遍B 页面只能复制粘贴。后面逻辑变了所有页面都要跟着改漏一个就是 bug。无法分工协作前端开发人员看到满屏 Java 代码头皮发麻后端开发人员调样式也无从下手。一个人同时搞前后端效率低下。测试成本高业务逻辑埋在 JSP 里想单元测试根本没门只能启动容器、用浏览器点出了问题只能靠肉眼盯。调试困难JSP 编译报错信息绕来绕去一个空指针异常要翻到第几百行才知道哪里出了事。1.3 规范化的本质把该谁干的活分清楚既然痛点在于乱那开出的药方自然就是分。Java 世界里解决这个问题的成熟方案就是MVC 分层Model 管数据和业务View 管展示Controller 管请求调度。而 JavaBean 和 Servlet 正是 Java Web 技术栈里承担这两个角色的标准组件。简单说这套组合让项目从一堆人挤在 JSP 文件里混战变成各司其职、各管一段的流水线作业。这也是为什么后来 Spring MVC、Struts 这些框架虽然封装程度更高但底层的思路依然是从 JavaBean、Servlet、MVC 这套基础长出来的。2. JavaBean数据封装这件事没那么随便2.1 JavaBean 的硬性规范与设计意图JavaBean 本质上就是一个普通 Java 类但它必须遵守一些约定俗称的规范否则很多工具和框架没法自动识别它。具体来说有几点类必须 public提供一个无参构造方法有参构造可以有但无参必须有。属性用 private 修饰通过 getter/setter 方法访问。属性名首字母小写getter 叫getXxxsetter 叫setXxx。实现java.io.Serializable接口可选但涉及跨进程传输或状态保存时建议实现。为什么要卡得这么死因为 JSP 里的jsp:useBean、jsp:getProperty、jsp:setProperty标签以及 EL 表达式${user.name}都是靠反射去调用这些 getter/setter 的。你不按规矩来它就找不到属性直接报错。另一个实际好处是JavaBean 这种属性私有 方法公开的结构天然符合封装思想setter 里可以加校验逻辑getter 里可以做格式转换而不是让数据裸奔。2.2 JSP 中使用 JavaBean 的三种动作标签在 JSP 规范里操作 JavaBean 有专门的标签虽然现在主流开发中大家更多直接配合 EL 使用但了解它们依然有价值很多老项目里还在用。标签作用示例jsp:useBean在指定作用域里查找或创建 JavaBean 实例jsp:useBean iduser classcom.demo.bean.User scoperequest/jsp:setProperty给 Bean 的属性赋值支持从请求参数自动匹配jsp:setProperty nameuser property*/jsp:getProperty读取 Bean 的属性值并输出jsp:getProperty nameuser propertyname/这里有个很常用的骚操作property*会自动把请求参数里跟 Bean 属性同名的那批数据全塞进去。比如表单提交了name张三age25而User类里恰好有name和age两个属性这一行标签就相当于帮你完成了手动request.getParametersetter的重复劳动。不过要注意类型转换如果失败比如age传了个非数字容器会抛异常所以生产环境我一般还是建议手动取值或者用工具类做转换别太依赖这个自动。2.3 作用域JavaBean 的生命周期管理JavaBean 在 JSP 中并不是随便一 new 就完事它存在于四个不同的作用域搞不清楚就容易出现页面能取到值换一个页面就没了的诡异问题page只在当前页面有效页面跳转或刷新就销毁。request一次请求范围内有效forward跳转后依然能取到。session同一个用户整个会话周期内有效适合放登录用户信息、购物车这类数据。application整个应用全局共享本质上就是所有用户共用的缓存适合放配置信息、全局计数等但要注意并发安全问题。3. Servlet真正干活的调度中心3.1 Servlet 生命周期从创建到销毁的完整流程Servlet 是运行在服务器端的小程序它比 JSP 更接近底层。一个 Servlet 的一生要经历三个阶段init()容器加载 Servlet 时执行一次适合做初始化操作比如读取配置、建立数据库连接池。默认是第一次被请求时懒加载也可以通过load-on-startup配置成容器启动时就初始化。service()每次请求都会走到这里容器会根据 HTTP 方法帮你分发到doGet或doPost。你也可以直接重写service方法自己接管但一般情况下别这么干保持分发逻辑标准。destroy()容器卸载 Servlet 时执行一次适合释放资源。注意它不是每次请求结束都跑所以别把请求结束该做的事放这里。我之前踩过一个坑把修改用户最后登录时间的逻辑放在了destroy()里结果开发环境一切正常因为容器关闭才触发。后来部署到生产环境服务器跑了几个月这个时间字段一直没更新排查了好久才发现这个低级错误。destroy 是 Servlet 的临终回调不是你请求的收尾动作。3.2 请求转发与重定向两个必须分清的兄弟Servlet 处理完业务之后怎么把活儿交给下一个环节两种方式方式方法浏览器地址栏能否带 request 数据本质请求转发request.getRequestDispatcher(xxx.jsp).forward(req, resp)不变能服务器内部跳转一次请求重定向resp.sendRedirect(xxx.jsp)变成新地址不能除非通过 URL 参数或 session浏览器发起第二次新请求这个选择直接影响 MVC 的写法。转发适合 Servlet 处理完数据后直接交到 JSP 展示数据用request.setAttribute传过去重定向适合表单处理完后跳转到另一个页面的场景能避免刷新重复提交但数据传递就没那么方便了要么拼 URL 要么丢 session。3.3 注解配置还是 web.xml两条路都行早期 Servlet 必须在web.xml里配置映射代码和配置分离略显繁琐。Servlet 3.0 以后支持注解方式直接在类上写一行就行WebServlet(name userServlet, urlPatterns /user)我个人在中小型项目里偏向注解省事、直观类文件和映射关系一目了然但在大型项目里统一用 web.xml 集中管理也很有必要方便 ops 同学在不改代码的情况下调整映射路径。还有一点urlPatterns的写法要小心/user和/user/*是完全不同的匹配策略前者是精确匹配后者是路径匹配写错会导致请求打不到对应 Servlet。4. MVCJavaBean、Servlet、JSP 各就各位4.1 一个请求在 MVC 里走完的完整路线MVC 在 Java Web 里的落地分工非常清晰Model模型层JavaBean 负责承载数据配合 Service/Dao 完成业务逻辑和数据库访问。View视图层JSP 只负责从 request 或 session 里取数据并渲染展示不再掺杂 Java 业务代码。Controller控制层Servlet 接收请求、解析参数、调用业务处理、根据结果选择转发还是重定向。一个典型的请求流程是这样的浏览器发起/user/detail?id1→ Tomcat 根据映射找到UserServlet.doGet→ doGet 从 request 里取id→ 调用 Service 层查询 → 得到User对象JavaBean→ 放进 request 域 → forward 到userDetail.jsp→ JSP 用 EL 表达式或 JSTL 标签输出。这套流程下来每个环节职责单一出了问题也能快速定位是在控制层、业务层还是展示层。4.2 为什么这样分层会香有人可能会问多写了 Servlet 和 JavaBean感觉代码量变多了图啥我实际重构过一个功能后才真正体会到分层的好处改展示不动逻辑页面从表格改成卡片只动 JSP跟 Java 代码无关。改逻辑不动页面计费规则从按次数改成按时长只改 Service/JavaBean页面一行不用碰。并发分工有人专门写前端页面有人专心写业务逻辑互不干扰。可测试JavaBean 和 Service 可以脱离容器单独写单元测试JSP 只能在容器里跑把逻辑抽出来之后测试成本直线下降。4.3 MVC 不是银弹过度拆分同样痛苦分层虽好但别走极端。我见过有人为了规范连一个简单的取当前时间都要搞一个 Controller、Service、Dao 三层结果项目里类文件爆炸维护成本反而上升。MVC 的核心思想是按需分层简单的查询展示Servlet 直接 new 一个 JavaBean 查库也可以接受逻辑复杂的场景才需要引入 Service 层进一步拆解。规范和灵活之间的度得靠项目规模来判断小项目硬套重设计是自找麻烦。5. 实操用 JavaBean Servlet MVC 改造一个个人信息展示页面5.1 改造前的反面教材一个典型的 JSP 页面直接撸假设现在要做一个个人信息展示页面很多入门教程教的写法是这样的% page importjava.sql.* % % Class.forName(com.mysql.jdbc.Driver); Connection conn DriverManager.getConnection(jdbc:mysql://localhost:3306/demo, root, 123456); Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(select * from user where id1); rs.next(); String name rs.getString(name); String email rs.getString(email); rs.close(); stmt.close(); conn.close(); % html body h1个人信息/h1 p姓名% name %/p p邮箱% email %/p /body /html这段代码能跑但问题一箩筐数据库驱动加载写死在页面里无法复用SQL 暴露在页面层安全性差每次请求都手动获取连接、关闭连接性能堪忧页面改版时这些 Java 代码还得跟着挪。这就是典型的能跑但不好维护。5.2 第一步定义 User 实体JavaBean首先把数据封装成 JavaBean这是所有后续操作的基础package com.demo.bean; import java.io.Serializable; public class User implements Serializable { private Integer id; private String name; private String email; public User() { } public User(Integer id, String name, String email) { this.id id; this.name name; this.email email; } public Integer getId() { return id; } public void setId(Integer id) { this.id id; } public String getName() { return name; } public void setName(String name) { this.name name; } public String getEmail() { return email; } public void setEmail(String email) { this.email email; } }这里有几个细节值得说无参构造方法必须保留很多框架和标签要依赖它implements Serializable加上没坏处尤其是 session 存储需要序列化时不然报NotSerializableException才是真的开始慌如果后续要做参数校验可以在 setter 里加逻辑比如email格式不对就直接抛异常让错误在源头被发现而不是在页面上渲染一堆null。5.3 第二步写一个 UserServlet 当控制器package com.demo.servlet; import com.demo.bean.User; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; WebServlet(name userServlet, urlPatterns /user/detail) public class UserServlet extends HttpServlet { Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 处理乱码 request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); // 模拟从数据库查询实际项目这里会调用 Service 层方法 String idParam request.getParameter(id); Integer id Integer.parseInt(idParam); User user new User(id, 张三, zhangsanexample.com); // 把 JavaBean 存入 request 作用域 request.setAttribute(user, user); // 转发到 JSP 视图层展示 request.getRequestDispatcher(/userInfo.jsp).forward(request, response); } Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }这里体现了几个 MVC 的关键点Servlet 拿到了原始 HTTP 请求做参数解析、编码处理调用业务逻辑这里简化为模拟数据然后把数据交给 JSP。注意我没有在 Servlet 里拼 HTML因为那是视图层的活。至于doPost里为什么也调doGet很多表单提交都是 POST如果只写doGet不写doPost表单一提交就是一个 405 错误写了doPost统一下发到doGet是省事儿的做法。5.4 第三步改造 JSP 视图层% page contentTypetext/html;charsetUTF-8 % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title个人信息/title /head body h1个人信息/h1 p姓名${user.name}/p p邮箱${user.email}/p c:if test${empty user.name} p暂无数据/p /c:if /body /html跟原来的版本比这个 JSP 文件里没有一个 Java 脚本片段全靠 EL 表达式和 JSTL 标签取数据。${user.name}实际上是调用 User 对象 getName 方法返回的结果跟% user.getName() %等价但写法干净得多而且当user为 null 时不会直接抛空指针而是输出空字符串这对页面健壮性很有帮助。5.5 完整改造后的请求链路复盘整个请求走一遍是这样的浏览器请求/user/detail?id1→ 容器根据WebServlet注解找到UserServlet→ 执行doGet→ 解析参数、构造 User → 放入 request 域 →forward到userInfo.jsp→ JSP 通过 EL 表达式展示 → 响应回浏览器。这个过程里Servlet 不产生 HTMLJSP 不访问数据库JavaBean 只充当数据容器各守一摊。以后再想加修改个人信息功能只需要在 UserServlet 里加一个doPost分支页面加一个表单逻辑改动非常局部。6. 常见问题与排查技巧实录6.1 中文乱码绕不开的老大难乱码是 Java Web 开发里最常见的坑而且往往不止一个环节。我自己的排查套路是三件套页面文件 keep 成 UTF-8、请求编码设置request.setCharacterEncoding(UTF-8)、响应编码设置response.setContentType(text/html;charsetUTF-8)。这里首先要明确一个点GET 请求和 POST 请求的编码处理逻辑不一样。POST 可以通过request.setCharacterEncoding解决GET 请求的参数在 URL 里Tomcat 8 之前默认是用 ISO-8859-1 解码的Tomcat 8 之后默认改成了 UTF-8所以老项目迁移到新 Tomcat 上面同样一套代码可能 GET 乱码就自动好了。还有一个容易漏的点数据库连接串里必须加编码参数比如jdbc:mysql://localhost:3306/demo?useUnicodetruecharacterEncodingUTF-8不然数据库读写照样乱。有时候页面上看到的是问号或者方块不是显示问题而是从数据库取出来的时候编码就是错的这种排查起来特别费劲因为前端改多少次都没用。6.2 404 还是 500先分清是哪一层的问题请求打不到 Servlet或者 Servlet 转发到 JSP 报错新手经常分不清 404 和 500 的区别。404 的本质是资源找不到可能是 URL 映射写错了也可能是 JSP 文件路径不对。比如你明明写了request.getRequestDispatcher(/userInfo.jsp)但实际文件在/WEB-INF/views/userInfo.jsp那就会 404。注意/WEB-INF目录下的 JSP 是不能直接通过浏览器地址栏访问的只能通过容器内转发这是安全设计得习惯这个设定。500 的本质是服务器内部错误通常是代码运行时报异常比如空指针、类型转换失败、数据库连接超时。遇到 500 别急着猜先把完整堆栈日志揪出来。Tomcat 控制台会打印完整堆栈IDE 里也能看到。很多新手看异常只看第一行其实最有价值的信息是Caused by那一串真正的根因往往藏在底部。6.3 刷新页面重复提交一个被忽略的交互陷阱这是真实开发里踩过的一个坑也是jsp页面让加载完后刷新一次这类需求背后的痛点。表单提交走doPost处理完后如果响应的是一个页面那用户按 F5 刷新浏览器会提示确认重新发送表单数据一确认就又提交了一次这会导致重复下单或者重复插入数据。解决方案有几个思路一是提交后重定向也就是Post/Redirect/GetPRG模式Servlet 处理完表单后不直接 forward 到结果页面而是sendRedirect到一个新的 GET 请求 URL。比如注册成功后跳转到/user/detail?idxx这时候用户刷新只是在刷新的 GET 页面不会重复提交表单。二是用 session 做一次性 token 防重提交这在秒杀场景里是标配但一般项目里 PRG 模式已经足够解决痛点。6.4 扩展思考MVC 思想如何延续到现代框架你可能已经发现JavaBean、Servlet、MVC 这套东西和现在的 Spring Boot、Spring MVC 有着明显的血缘关系。Spring MVC 的核心就是一个大 Servlet——DispatcherServlet它负责把请求分发给各种带注解的Controller方法ModelAndView承担了数据传递和视图选择的职责Service 层的Service注解加上实体类本质上还是 JavaBean 那一套思想。甚至RequestMapping(/user/detail)这种写法映射的还是 Servlet 时代urlPatterns的路径概念。所以我一直建议基础阶段的同学先把 JavaBean、Servlet 和手动 MVC 玩熟再去上手框架。因为框架帮你封装好的东西底层原理就是这些最原始的组件。当你理解了DispatcherServlet其实就是一个大号的 Servlet 做了统一调度看 Spring MVC 的源码就不会觉得神秘了。同理像 ASP.NET Core MVC、Gin 脚手架里的 MVC 模式思想都是同源的你负责把请求、业务、展示三层理清框架负责让你写得更快。7. 一些个人体会和实战建议课讲完了说点实在的。我这些年看过的 Java Web 项目凡是能长期维护、面试能拿出来讲的基本都遵循了 MVC 分层凡是写着写着变成一坨的大概率是从 JSP 里塞代码开始坏掉的。规范不是别人逼你而是代码量大到一定程度时你自然而然会做出的选择。给正在学习的朋友三个具体建议第一多写多练哪怕是一个五十行的学生管理系统也强行要求自己用 JavaBean Servlet JSP 做出来做完你会对分层有肌肉记忆第二每次写完代码后想想如果这个需求要改我要动几个文件如果超过了你觉得合理的数量那就是该重构的信号第三遇到报错不要急着改代码先看堆栈先定位是控制层、业务层还是视图层的问题这决定了你的排查方向。从 JSP 页面开发走向规范项目改变的不仅仅是代码组织形式更是一种思维习惯把复杂问题拆成边界清晰的小部分各个击破。这套思路在 Java Web 里适用放到任何领域的工程化实践里也依然成立。