简介:面向Java Web课程设计与初学者的轻量级实战项目,基于Jsp+Bean+Servlet经典三层架构实现一个简单的网上购书系统,功能覆盖用户注册、登录、图书信息查询与浏览,适合用较短周期理解B/S结构下请求处理与业务分层。资源包共55个文件,包含15个java源码与15个class编译文件、6个jsp页面,另有sql数据库脚本、xml/properties配置以及PDF版课程综合训练文档,压缩包仅1.21MB,部署与阅读门槛低。项目按标准Web工程组织,src存放JavaBean和Servlet,WebRoot下为JSP视图,并附带项目配置文件与数据库脚本,便于直接导入IDE后对照学习。资料已有170人学习,适合需要快速完成课程设计或希望掌握Servlet+JSP协作机制的开发者。通过源码可梳理登录校验、流程跳转、数据查询等典型写法,配套PDF和README还可提供整体设计思路与运行说明。
1. 为什么课程设计都爱用 Jsp+Bean+Servlet
课程设计周,被分到“网上购书系统”这种题目的同学,大概率会在 Spring Boot 和纯 JSP 之间纠结。Spring Boot 初始化一个项目就要下载一堆依赖,答辩时一句“为什么多了一层”就能把你问住;纯 Servlet 写页面又回到了 PrintWriter 拼接 HTML 的原始社会。
Jsp+Bean+Servlet 这个三位一体组合,恰好卡在中间:JSP 负责把书单、个人信息渲染成页面,Servlet 负责接收注册、登录、查询请求并决定跳转,JavaBean 负责把用户、图书数据封装成内存对象。
这套系统实现了用户注册、登录、信息查询、信息浏览四个功能,不依赖任何重量级框架,既能让课程设计的 B/S 结构成立,又保留了往后迁移到 SSM 或 Spring Boot 时的分层直觉。适合刚学完 Java Web 的初学者,也适合想快速理清动态请求链路的人。
2. 模型层与数据链路:Bean 设计、JDBC 连接与三层结构划分
2.1 项目结构里 Bean 和 Servlet 怎么分目录
拿到源码包,解压后看到的目录结构是典型的 Java Web 工程。WebRoot 是 Web 根目录,里面放着 JSP 和 WEB-INF/web.xml;src 下按类的职责拆包。常见做法是分为 bean、dao、servlet、util 四个包,实体类和数据访问分开,控制器独立。下面是一个课程设计里常用的骨架:
src ├── com/example/bean # 实体类:User、Book ├── com/example/dao # 数据访问层:操作用户表和图书表 ├── com/example/servlet # 控制器:注册、登录、图书列表 ├── com/example/util # 工具类:DBUtil、EncodingFilter WebRoot ├── login.jsp ├── register.jsp ├── bookList.jsp ├── userInfo.jsp └── WEB-INF └── web.xml这样分的目的不是凑目录,而是让每条请求的走向一眼能看出来。浏览器把表单提交给 Servlet,Servlet 调 DAO 里的方法,DAO 通过 DBUtil 拿到 Connection,再把结果封装成 Bean,最后 JSP 用 EL 表达式把 Bean 的属性读出来。任何一个环节出了问题,只需要打开对应包里的类,不需要在几百行的页面里翻 Java 代码。
DAO 层在不少课程设计里会被直接省掉,把 JDBC 代码写在 Servlet 里,这样也能跑。但后续如果要从 MySQL 换成 SQL Server,或者加一个 Redis 缓存,所有 Servlet 都要跟着改。我一般会保留这一层,哪怕只是做一个多余的封装。
2.2 数据库表:user 和 book 的字段设计
网上购书系统的核心数据就是用户和图书,最少需要两张表。以 MySQL 5.7 为例,建表 SQL 如下:
CREATE DATABASE bookshop DEFAULT CHARSET utf8mb4; USE bookshop; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, email VARCHAR(100), phone VARCHAR(20), regdate DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, bookname VARCHAR(100) NOT NULL, author VARCHAR(50), price DECIMAL(10,2), publisher VARCHAR(100), stock INT DEFAULT 0, description TEXT ) ENGINE=InnoDB;字段名称不要直接用name、password虽然没问题,但desc、group这类是 MySQL 保留字,容易在查询时报语法错误。这里把书名定为bookname而不是name,是为了避免后续写select name from book时和函数名混淆。password字段设成 VARCHAR(64) 是预留散列宽度,课程设计阶段通常直接存明文,但至少留下改造空间。regdate用 DATETIME 存储注册时间,DEFAULT CURRENT_TIMESTAMP 可以在插入时省掉一个参数。
表与 Bean 的对应关系如下:
| 表名 | Bean | 主键 | 常用查询字段 |
|---|---|---|---|
| user | User | id | username, email |
| book | Book | id | bookname, author, price |
2.3 User Bean:为什么必须有 setter/getter
JavaBean 的规范是提供无参构造和属性对应的 getter/setter,Servlet 里new User()然后setXxx,JSP 里${user.username}调的就是getUsername()。如果少了 setter,反射会把数据塞不进去,这是初学者最容易遇到的 500 错误。
package com.example.bean; import java.util.Date; public class User { private int id; private String username; private String password; private String email; private String phone; private Date regdate; public User() { } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public Date getRegdate() { return regdate; } public void setRegdate(Date regdate) { this.regdate = regdate; } }这里没有让 User 实现 Serializable,因为只在 Servlet 和 JSP 之间传递,不涉及远程调用。但如果你把 User 放进了 HttpSession,并且后面要集群部署,就需要实现 Serializable,否则序列化时会抛异常。这也是很多人提到的 bean 生命周期问题的一个侧面:在纯 Jsp+Bean+Servlet 里,Bean 由 JVM 直接管理和回收,不存在 Spring 容器里的 init/destroy 回调,理解这一点对排查空指针有帮助。
2.4 JDBC 工具类与 Connection 参数
无论注册还是查询,第一步都是拿到数据库连接。我习惯写一个 DBUtil 静态工具类,把 URL、用户名、密码集中管理,默认不对外开放。
package com.example.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/bookshop?useUnicode=true&characterEncoding=utf8&useSSL=false"; private static final String USERNAME = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs != null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt != null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } } }URL 里的characterEncoding=utf8解决中文乱码传输的问题,useSSL=false是避免本机开发时出现 SSL 警告刷屏。驱动类名在 MySQL 5.x 之后是com.mysql.cj.jdbc.Driver,如果你还在用老版本驱动,要写com.mysql.jdbc.Driver,否则会报ClassNotFoundException。Class.forName放在静态块里,保证类第一次被加载时只执行一次,这是 Java Web 项目里初始化驱动最常见的写法,不会把驱动注册多次。
2.5 UserDao:用 PreparedStatement 操作数据表
DAO 层把 SQL 和业务逻辑隔离开。注册功能对应 insert,登录对应 select。下面是一个注册方法:
package com.example.dao; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import com.example.bean.User; import com.example.util.DBUtil; public class UserDao { public boolean insertUser(User user) { String sql = "INSERT INTO user(username, password, email, phone) VALUES(?,?,?,?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, user.getUsername()); ps.setString(2, user.getPassword()); ps.setString(3, user.getEmail()); ps.setString(4, user.getPhone()); return ps.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } } public User findByUsernameAndPassword(String username, String password) { String sql = "SELECT * FROM user WHERE username=? AND password=?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User user = new User(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); user.setEmail(rs.getString("email")); user.setPhone(rs.getString("phone")); // 不把 password 放回 user,避免在页面上暴露 return user; } } } catch (SQLException e) { e.printStackTrace(); } return null; } }这里选PreparedStatement而不是Statement,理由有两个:一是 SQL 语句可以先用?占位,参数通过 setString 绑定,可读性更好;二是能有效避免经典的单引号拼接注入,比如用户名传' or '1'='1时不会拼接进 WHERE 条件。注意 try-with-resources 写在 JDK 7 以后才行,Eclipse 里如果编译报错,先检查项目的 Java 版本是否调到 1.8 以上。插入成功返回executeUpdate()受影响的条数,注册时还需要在 Servlet 里对用户名做唯一性校验,否则数据库会抛 DuplicateKeyException。
3. Servlet 控制器:注册、登录、查询与浏览的请求流转
3.1 请求映射:从 form 到 Servlet
JSP 表单的action指向的是 Servlet 的映射路径。下面这张表是这套系统里最常见的四组映射:
| 请求路径 | Servlet | 功能 | 常用方法 |
|---|---|---|---|
| /register | RegServlet | 用户注册 | POST |
| /login | LoginServlet | 用户登录 | POST |
| /book/list | BookListServlet | 浏览全部图书 | GET |
| /book/search | BookSearchServlet | 按关键字查询图书 | GET |
映射方式有两种:一是给 Servlet 加@WebServlet("/login")注解,二是在web.xml里写<servlet>和<servlet-mapping>。课程设计里如果项目带有 config 和 web.xml,优先用 web.xml 方式,因为 Tomcat 7 或更早版本对注解支持不完整,而且答辩时可以直接翻配置文件讲路由。后面代码示例采用注解,方便看清路径,实际使用时把注解换成 web.xml 配置即可。
3.2 RegServlet:注册流程的 doPost 写法
注册页提交的数据是 username、password、email、phone,Servlet 要做三件事:接收参数、调用 DAO、根据结果跳转。
package com.example.servlet; import java.io.IOException; 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 com.example.bean.User; import com.example.dao.UserDao; @WebServlet("/register") public class RegServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); String email = request.getParameter("email"); String phone = request.getParameter("phone"); if (username == null || username.trim().isEmpty()) { request.setAttribute("error", "用户名不能为空"); request.getRequestDispatcher("/register.jsp").forward(request, response); return; } User user = new User(); user.setUsername(username.trim()); user.setPassword(password); user.setEmail(email); user.setPhone(phone); UserDao dao = new UserDao(); boolean ok = dao.insertUser(user); if (ok) { response.sendRedirect(request.getContextPath() + "/login.jsp"); } else { request.setAttribute("error", "注册失败,用户名可能已存在"); request.getRequestDispatcher("/register.jsp").forward(request, response); } } }第一行的request.setCharacterEncoding("UTF-8")必须在读取参数之前调用,否则 POST 过来的中文会变成乱码。response.setContentType设置响应给浏览器时的编码,两个编码不一致是 JSP 页面出现“???”最常见的原因。注册失败时用forward回到注册页,并带上 error 属性,成功时用sendRedirect跳到登录页。为什么失败用 forward,成功用重定向?因为 forward 是服务端内部跳转,浏览器地址栏不变,刷新页面会再次提交表单,容易出现重复注册;重定向让浏览器重新发起一个 GET 请求到 login.jsp,刷新不会重复提交。
3.3 LoginServlet:Session 保存登录状态
登录成功后必须把用户信息放到 Session,后续 JSP 页面才能识别“当前是谁”。LoginServlet 的核心位置:
@WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); User user = new UserDao().findByUsernameAndPassword(username, password); if (user == null) { request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); return; } HttpSession session = request.getSession(); session.setAttribute("user", user); response.sendRedirect(request.getContextPath() + "/book/list"); } }request.getSession()不传参数,表示“没有 Session 就创建”,适合登录成功场景。session.setAttribute("user", user)把 User 对象按引用存进 Session 容器,Tomcat 默认用内存存储,所以对象不需要序列化;但如果你把session.setAttribute的 bean 放到 Spring 容器里管理,生命周期会变得复杂,这里是显式 set/get,不存在 Spring 容器那种作用域问题。查到user为空时,说明登录名或密码不对,业务上不要提示“用户不存在”,避免被扫出有效账号。
3.4 BookListServlet:图书浏览与关键字查询
图书列表需要支持全部浏览和按书名查询两个入口。一个 Servlet 可以同时处理,通过keyword参数区分:
package com.example.servlet; import java.io.IOException; import java.util.List; 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 com.example.bean.Book; import com.example.dao.BookDao; @WebServlet("/book/list") public class BookListServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String keyword = request.getParameter("keyword"); BookDao dao = new BookDao(); // 参数为空则查全部,否则按书名关键字模糊查询 List<Book> books = (keyword == null || keyword.trim().isEmpty()) ? dao.findAll() : dao.search(keyword.trim()); request.setAttribute("books", books); request.getRequestDispatcher("/bookList.jsp").forward(request, response); } }列表页用doGet处理,因为图书浏览是只读操作,用 GET 还可以让用户方便地把页面收藏或分享。keyword == null || keyword.trim().isEmpty()这个判断是必须的:只访问/book/list时不带参数,如果直接调dao.search(null),SQL 里的LIKE就会传入一个 null,导致结果为空或被数据库拒绝。查询完把books放进 request 作用域,再用forward跳到 JSP,JSP 里用${books}就能拿到这个集合。
3.5 web.xml 映射:当注解不生效时
有些版本的 Tomcat 不扫描注解,或者项目配置了metadata-complete="true",会导致@WebServlet被忽略,页面提交后变成 404。这是 Tomcat 部署 Web 项目时最常见的坑之一。建议在 web.xml 里这样显式声明:
<servlet> <servlet-name>loginServlet</servlet-name> <servlet-class>com.example.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>loginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping>servlet-name只是个唯一标识,可以任意取,跟类名无关。url-pattern就是表单 action 里写的路径。一个 Servlet 可以配置多个 url-pattern,比如图书列表和图书搜索可以都指向BookListServlet,然后通过参数区分。配置完 web.xml 后要重新部署项目,只刷新页面不会重新加载配置文件。
4. JSP 表现层与表单会话:把 Bean 渲染成可见页面
4.1 JSP 页面最终也是 Servlet
JSP 不是浏览器执行的,它由 Tomcat 先翻译成.java,再编译成.class,本质上是一个以页面模板形式存在的 Servlet。很多同学会问“jsp编译的class文件保存在哪里”,其实跑过 Tomcat 后去看这个目录就能找到:
tomcat/work/Catalina/localhost/你的项目名/org/apache/jsp/里面会有login_jsp.java和login_jsp.class。理解了这一点,就能解释为什么 JSP 页面上可以直接使用request、response、session、out、application这些内置对象,因为它们正是_jspService方法签名里预先声明的局部变量。页面顶部的page指令决定了 JSP 内容的类型:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>contentType影响响应头的Content-Type,charset=UTF-8通知浏览器按 UTF-8 解码。如果 JSP 文件和这里声明的编码不一致,页面上所有中文都会显示成乱码,这是 JSP 个人信息展示页面、学生信息管理系统这些动态页面最容易出现的问题。
4.2 注册页面的表单绑定
register.jsp 是最典型的表单页,action 指向 Servlet 映射,method 用 post,避免密码出现在地址栏。配合后端返回的 error 属性显示错误信息:
<%@ page contentType="text/html;charset=UTF-8" %> <html> <head><title>用户注册</title></head> <body> <h3>注册新用户</h3> <% if (request.getAttribute("error") != null) { %> <p style="color:red"><%= request.getAttribute("error") %></p> <% } %> <form action="<%= request.getContextPath() %>/register" method="post"> 用户名:<input type="text" name="username" required/><br/> 密码:<input type="password" name="password" required/><br/> 邮箱:<input type="text" name="email"/><br/> 电话:<input type="text" name="phone"/><br/> <input type="submit" value="注册"/> </form> </body> </html>action="<%= request.getContextPath() %>/register"中的getContextPath()会动态补上项目上下文路径,这样部署到任意名字的项目下都不会 404。required属性是 HTML5 原生校验,能拦截空提交,但服务端仍然要再校验一次,因为浏览器校验可以被人为绕过。JSP 小脚本<% %>只用于简单的错误展示,复杂的循环和条件应尽量交给 EL 和 JSTL。
4.3 图书列表:JSTL 遍历 Bean 集合
图书列表页是这套系统里信息量最大的页面,建议用 JSTL 的c:forEach遍历 request 里的 books 集合。JSP 内置对象常见有:
| 内置对象 | 类型 | 说明 |
|---|---|---|
| request | HttpServletRequest | 一次请求内有效,存查询结果 |
| response | HttpServletResponse | 给浏览器返回响应 |
| session | HttpSession | 同一用户多次请求有效,存登录信息 |
| application | ServletContext | 整个应用全局共享,慎用 |
bookList.jsp 的列表部分:
<%@ page contentType="text/html;charset=UTF-8" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <body> <h3>图书列表</h3> <form action="<%= request.getContextPath() %>/book/list" method="get"> <input type="text" name="keyword" placeholder="输入书名关键字"/> <input type="submit" value="查询"/> </form> <table border="1"> <tr> <th>书名</th><th>作者</th><th>价格</th><th>出版社</th> </tr> <c:forEach items="${books}" var="book"> <tr> <td>${book.bookname}</td> <td>${book.author}</td> <td>${book.price}</td> <td>${book.publisher}</td> </tr> </c:forEach> <c:if test="${empty books}"> <tr><td colspan="4">没有找到任何图书</td></tr> </c:if> </table> </body> </html>${books}会调用request.getAttribute("books"),实际返回的是 List ,items属性接收任何 Collection 类型。${book.bookname}会调用getBookname(),所以 Bean 的属性名必须和数据库字段映射好,否则 500 或者输出空。JSTL 标签库需要在 Web 工程的WEB-INF/lib下放入jstl-1.2.jar,如果缺失,页面会报Unable to find taglib错误。
4.4 个人信息展示:Session 中取 Bean
登录成功后,userInfo.jsp 需要判断 Session 里有没有 user 对象,有就展示个人信息,没有就跳转登录页。这是“jsp个人信息展示页面”的标准结构:
<c:choose> <c:when test="${sessionScope.user != null}"> <p>欢迎,${sessionScope.user.username}</p> <p>邮箱:${sessionScope.user.email}</p> <p>电话:${sessionScope.user.phone}</p> </c:when> <c:otherwise> <p>请先 <a href="<%= request.getContextPath() %>/login.jsp">登录</a></p> </c:otherwise> </c:choose>${sessionScope.user}明确指定从 session 作用域取值,而不是用${user}让容器在 page/request/session/application 四个作用域里依次查找。后者在同一个页面里如果 request 也存了同名属性,就会取到错误的值。Session 里存的 User 如果在登录后又被修改,由于是引用,原对象也会变,所以不要轻易在跨页面流程里直接改sessionScope.user的字段。
4.5 全局编码 Filter:一个过滤器解决所有页面乱码
JSP 页面和 Servlet 各写一遍编码设置很容易漏,通常用一个 Filter 把全局请求和响应编码统一处理:
package com.example.util; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebFilter; @WebFilter("/*") public class EncodingFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); chain.doFilter(request, response); } }@WebFilter("/*")表示拦截所有请求路径,doFilter里设置编码后调用chain.doFilter放行,这样每个 Servlet 和 JSP 都不需要再单独设置 request 编码。response.setCharacterEncoding("UTF-8")只影响字符流,如果页面要返回 HTML,最好在 JSP 的 page 指令里再显式写出 content type。Filter 的注册顺序只影响多个过滤器之间的顺序,单过滤器场景下不需要关心。
5. 部署、排错与防注入技巧:从能跑到跑稳
5.1 Eclipse + Tomcat 三步部署
拿到源码后,最常见的问题是找不到怎么运行。在 Eclipse 里导入这个 Web 项目:
- File → Import → Existing Projects into Workspace,选择项目根目录。
- 确保项目上右键 Properties → Targeted Runtimes 勾选了 Tomcat 版本。
- 右键项目 → Run As → Run on Server,选择已配置的 Tomcat 实例。
启动后访问http://localhost:8080/项目名/login.jsp。如果直接用Run as Java Application当然会报“找不到或无法加载主类”,因为这本来就不是一个普通 Java 工程,必须交给 Tomcat 运行。部署完毕后再去查tomcat/work/Catalina/localhost/项目名/org/apache/jsp/,这时能看到 jsp 翻译后的.java和.class文件,这就是 JSP 编译产物的实际存储位置。
5.2 高频异常排查表
| 现象 | 可能原因 | 处理方向 |
|---|---|---|
| 404 | 表单 action 或 url-pattern 不一致 | 检查 web.xml 或 @WebServlet 路径 |
| 500 | Bean 属性不存在、DAO 返回 null | 看 Tomcat 日志里的 Caused by |
| ClassNotFoundException | 缺 mysql-connector-java.jar | 把 jar 放到 WEB-INF/lib |
| 中文乱码 | Filter 未拦截、JSP 编码不一致 | 统一 UTF-8,页面和数据库字符集对齐 |
| 端口占用 | Tomcat 8080 被占用 | 修改 conf/server.xml 端口或杀掉占用进程 |
排查 500 时不要只看页面上那几行,直接打开tomcat/logs/localhost.yyyy-MM-dd.log,几乎所有异常原因都写在 Caused by 里。最常见的org.apache.jasper.JasperException往往是 JSP 引用了不存在的 getter,或者 JSTL 标签没有引入而导致解析失败。
5.3 用 PreparedStatement 封死 SQL 注入
这个网上购书系统如果用到关键字查询,最容易写出这样的拼接 SQL:
// 错误示例:直接把参数拼进 SQL String sql = "SELECT * FROM book WHERE bookname LIKE '%" + keyword + "%'"; Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql);当 keyword 传入' OR '1'='1时,SQL 会变成WHERE bookname LIKE '%%' OR '1'='1%',条件恒成立,绕过查询边界。改成占位符就可以解决:
String sql = "SELECT * FROM book WHERE bookname LIKE ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, "%" + keyword + "%"); ResultSet rs = ps.executeQuery();LIKE的模糊匹配是在占位符参数值里手动拼上%,SQL 本身不包含用户输入。我一般会再写一个可复用的search方法,对keyword做两次转义:一次用trim()去掉首尾空格,一次把%和_这两个通配符转义掉,避免用户输入%时把全表查出来。这个改动不增加多少代码,但对于公开部署的 Web 项目,是必须守住的一层底线。
本文还有配套的精品资源,点击获取