最近在帮学弟学妹们看毕业设计,发现很多用 JavaWeb(JDBC+JSP+Servlet)做的项目,虽然功能实现了,但代码结构真是一言难尽。业务逻辑全写在 JSP 里,数据库连接随手创建随手关,SQL 语句直接拼接字符串…… 这不仅是代码“丑”的问题,更埋下了安全漏洞和维护噩梦的种子。今天,我就结合自己的经验,系统梳理一下如何用这套经典技术栈,做出一个结构清晰、安全可靠、易于维护的毕业设计项目。
1. 学生项目中的那些“典型”痛点
在开始讲“正确姿势”前,我们先看看常见的“踩坑”操作,你是不是也中招了?
- 脚本式编码:一个
Servlet或JSP文件里,从接收参数、校验数据、处理业务逻辑、操作数据库,到最终渲染页面,所有代码都堆在一起。这种代码读起来费劲,改起来更是牵一发而动全身。 - SQL 注入漏洞:这是最高频的安全问题。很多同学图省事,直接用字符串拼接的方式构造 SQL 语句,比如
"SELECT * FROM user WHERE name='" + username + "'"。如果用户输入admin' OR '1'='1,后果可想而知。 - XSS 跨站脚本攻击:在 JSP 页面上,直接使用
<%= request.getParameter("content") %>输出用户提交的内容。如果用户提交了一段<script>alert('xss')</script>,这段脚本就会被执行,窃取 cookie 或进行其他恶意操作。 - 资源泄露与无事务管理:在
Servlet的doGet/doPost方法里直接DriverManager.getConnection(),用完后可能忘记关闭Connection、Statement、ResultSet。更复杂的是,涉及到多个数据库操作时(比如转账:A账户扣钱,B账户加钱),没有事务概念,一旦中间出错,数据就处于不一致状态。 - 混乱的页面逻辑:JSP 页面中充斥着大量的
<% ... %>脚本片段,用于控制流程、查询数据。这导致前端美工无法介入,后端开发改页面也头疼,职责完全不清。
2. 为什么毕业设计还要学这套“老”技术?
现在 Spring Boot 这么火,为什么很多学校还要求用 JDBC+JSP+Servlet 呢?我认为这恰恰是教学的高明之处。
- 理解 Web 开发本质:Spring MVC 再强大,其底层依然是 Servlet 规范。亲手写一遍
Servlet,你才能真正理解一个 HTTP 请求是如何被接收、处理、响应的。理解了Request和Response对象,再看 Spring 的@RequestMapping、@RequestParam就会觉得豁然开朗。 - 掌握基础原理:JDBC 是 Java 操作数据库的基石。通过手写 JDBC 代码,你能深刻理解连接池为什么重要、事务是如何控制的、ORM 框架(如 MyBatis, Hibernate)到底帮我们做了什么。有了这个基础,学习任何上层框架都会事半功倍。
- 培养架构意识:在没有框架“约束”的情况下,如何自己组织代码结构(分层)、如何管理依赖、如何处理异常,这些思考能极大地锻炼你的软件设计能力。用框架是“开车”,而学这套是“造车”,虽然慢,但对发动机(原理)的理解更深。
3. 核心实现细节:构建清晰的三层架构
我们的目标是构建一个表现层(JSP)- 控制层(Servlet)- 数据访问层(DAO)的清晰结构。
3.1 Servlet 的生命周期与控制层设计
Servlet是单例的,它的init()、service()、destroy()方法由容器(如 Tomcat)管理。我们主要重写doGet()和doPost()。
关键点:一个功能对应一个 Servlet 是糟糕的设计。推荐一个模块对应一个 Servlet,通过请求参数(如action)来分发不同的处理方法。
// UserServlet.java - 处理所有用户相关请求 @WebServlet("/user") public class UserServlet extends HttpServlet { private UserService userService = new UserService(); protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); String action = request.getParameter("action"); if ("login".equals(action)) { login(request, response); } else if ("list".equals(action)) { listUsers(request, response); } // ... 其他action } // 登录处理方法 private void login(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); // 1. 调用Service层处理业务逻辑 User user = userService.login(username, password); if (user != null) { // 登录成功,将用户信息存入Session request.getSession().setAttribute("currentUser", user); // 2. 跳转到成功页面(重定向,避免重复提交) response.sendRedirect(request.getContextPath() + "/success.jsp"); } else { // 登录失败,设置错误信息并转发回登录页 request.setAttribute("msg", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); } } // ... 其他方法 }3.2 JDBC 工具类封装与 DAO 模式
绝对不能在每个需要数据库操作的地方都写一遍连接代码。我们需要一个JdbcUtil工具类来管理连接(这里引入连接池,如 Druid),并提供统一的获取连接和释放资源的方法。
// JdbcUtil.java public class JdbcUtil { // 使用Druid连接池 private static DataSource dataSource; static { try { Properties prop = new Properties(); prop.load(JdbcUtil.class.getClassLoader().getResourceAsStream("druid.properties")); dataSource = DruidDataSourceFactory.createDataSource(prop); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("初始化数据库连接池失败!"); } } // 获取连接 public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } // 释放资源 (重载方法,用于增删改) public static void close(Connection conn, Statement stmt) { close(conn, stmt, null); } // 释放资源 (重载方法,用于查询) public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace();} try { if (stmt != null) stmt.close(); } catch (SQLException e) { e.printStackTrace();} try { if (conn != null) conn.close(); } // 注意:这里是归还连接到池,不是关闭物理连接 } catch (SQLException e) { e.printStackTrace();} } }然后,我们为每个实体(如User)创建对应的DAO(Data Access Object) 接口和实现类,专门负责数据库操作。
// UserDao.java (接口) public interface UserDao { User findByUsernameAndPassword(String username, String password) throws SQLException; List<User> findAll() throws SQLException; } // UserDaoImpl.java public class UserDaoImpl implements UserDao { @Override public User findByUsernameAndPassword(String username, String password) throws SQLException { Connection conn = null; PreparedStatement pstmt = null; ResultSet rs = null; User user = null; try { conn = JdbcUtil.getConnection(); // 关键!使用PreparedStatement预编译,防止SQL注入 String sql = "SELECT id, username, nickname FROM t_user WHERE username = ? AND password = ?"; pstmt = conn.prepareStatement(sql); pstmt.setString(1, username); pstmt.setString(2, password); // 注意:密码应先加密再对比,这里仅为示例 rs = pstmt.executeQuery(); if (rs.next()) { user = new User(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); user.setNickname(rs.getString("nickname")); } } finally { JdbcUtil.close(conn, pstmt, rs); // 确保资源被释放 } return user; } }3.3 JSP 与 Java 逻辑分离
JSP 应该只负责显示数据,不要在里面写业务逻辑或数据库查询。数据由Servlet准备好,通过request.setAttribute()传递过来,JSP 使用JSTL 标签库和EL 表达式来渲染。
首先,在 JSP 头部引入 JSTL 核心库:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>然后,在页面中优雅地展示数据和控制流程:
<%-- login.jsp --%> <form action="${pageContext.request.contextPath}/user?action=login" method="post"> <input type="text" name="username" placeholder="用户名"> <input type="password" name="password" placeholder="密码"> <button type="submit">登录</button> <%-- 使用EL表达式安全地输出错误信息 --%> <c:if test="${not empty msg}"> <div style="color:red;">${msg}</div> </c:if> </form> <%-- userList.jsp --%> <table> <tr><th>ID</th><th>用户名</th><th>昵称</th></tr> <%-- 使用JSTL的forEach遍历Servlet传来的userList --%> <c:forEach items="${userList}" var="user"> <tr> <td>${user.id}</td> <%-- 使用JSTL的c:out防止XSS,它会自动转义HTML特殊字符 --%> <td><c:out value="${user.username}" /></td> <td><c:out value="${user.nickname}" /></td> </tr> </c:forEach> </table>4. 安全性与性能考量
- 密码加密存储:绝对不要用明文存密码!即使是毕业设计,也要养成好习惯。使用
BCrypt、MD5(加盐)等算法对密码进行哈希处理后再存入数据库。在UserService.login()方法中,对输入的密码用同样算法加密后,再与数据库中的密文对比。 - 会话超时与安全:在
web.xml中配置会话超时时间(如30分钟)。对于敏感操作(如支付、修改密码),除了检查 Session 中是否存在用户对象,还可以增加验证码、二次密码确认等。 - 避免 N+1 查询问题:在
UserDao.findAll()中,如果你还需要显示用户的订单信息,不要在循环里为每个用户单独查询一次数据库。应该使用 SQL 的JOIN语句一次查询出来,或者在后端进行适当的数据组装。 - 统一异常处理:不要在每个
Servlet方法里都写try-catch。可以创建一个实现Servlet的ExceptionHandler类,并在web.xml中配置<error-page>,将不同类型的异常(如SQLException,NullPointerException)统一导向友好的错误提示页面。
5. 生产环境避坑指南(毕业设计也适用)
- 绝对避免在 JSP 中写业务逻辑:这会让你的项目难以测试和维护。所有逻辑应放在
Servlet或Service层。 - 使用连接池:正如我们上面用 Druid 做的。直接
DriverManager.getConnection()在高并发下会拖垮数据库。 - 务必关闭资源:在
finally块或使用 try-with-resources 语句确保Connection、Statement、ResultSet被关闭,防止内存泄漏。 - 使用预编译 Statement (PreparedStatement):这是防止 SQL 注入最简单有效的方法,同时还能提升 SQL 执行效率。
- 输出内容做转义:在 JSP 中,使用
<c:out value="${content}">或JSTL的fn:escapeXml()函数对用户输入的内容进行转义,防范 XSS。
写在最后
通过以上这些实践,你的 JDBC+JSP+Servlet 毕业设计项目就能摆脱“学生气”,拥有一个清晰、健壮、可维护的骨架。这个过程虽然繁琐,但每一步都在加深你对 Web 开发底层原理的理解。
当你熟练掌握了这套“原始”技术栈后,再去看 Spring Boot,你会发现它做的很多事情——比如依赖注入、MVC 分发、事务管理、JDBC 封装——都是为了更优雅、更自动化地解决我们上面手动处理的问题。你的Servlet变成了@RestController,你的JdbcUtil和DAO被JdbcTemplate或MyBatis Mapper替代,你的web.xml配置变成了application.properties和注解。
这时,平滑迁移的思路就非常清晰了:将你手写的“轮子”,替换为 Spring 提供的成熟“组件”。你所积累的分层思想、面向接口编程、异常处理等良好习惯,在任何框架下都是通用的宝贵财富。希望这篇指南能帮助你不仅完成毕业设计,更能为未来的技术之路打下坚实的基础。