web-第三次课后作业
本次作业是在之前Web网站的基础上,连接了数据库(SQL Server)。之前项目把注册信息存在 application 内置对象里(一个 List),本质是内存存储。一旦 Tomcat 重启,所有注册数据全部清空。所以必须用数据库持久化存储数据。
本次作业的核心:数据库、JDBC 与 CRUD
一、为什么需要数据库
之前项目把注册信息存在 application 内置对象里(一个 List),本质是内存存储。一旦 Tomcat 重启,所有注册数据全部清空。真实项目必须用数据库持久化存储数据。
本项目使用 SQL Server + JDBC 实现数据库操作。
二、JDBC ------ Java 连接数据库的统一接口
2.1 架构
JDBC(Java Database Connectivity)是 Java 定义的一套操作数据库的标准接口。不管底层是什么数据库(SQL Server、MySQL、Oracle),Java 代码都用同一套 API 操作,区别只在于加载不同的驱动 jar 包和写不同的连接字符串。
你的 Java 代码
│
▼
java.sql.DriverManager ←── 统一的 JDBC 接口
│
▼
SQLServerDriver (mssql-jdbc.jar) ←── 具体数据库的驱动实现
│
▼
SQL Server 数据库
2.2 JDBC 操作数据库的标准步骤
① Class.forName("驱动类全名") 加载驱动,注册到 DriverManager
② DriverManager.getConnection(url) 建立 TCP 连接到数据库
③ conn.prepareStatement(sql) 预编译 SQL 语句
④ pstmt.setXxx(index, value) 给占位符 ? 赋值
⑤ pstmt.executeQuery/Update() 执行查询 或 增删改
⑥ rs → pstmt → conn 逐级关闭 释放资源(先开后关)
注册写入示例:
// ① 加载 SQL Server 驱动
Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
// ② 建立连接
String dbUrl = "jdbc:sqlserver://localhost:1433;databaseName=DB1;encrypt=false";
Connection conn = DriverManager.getConnection(dbUrl, "sa", "你猜");
// ③ 预编译 SQL(占位符 ? 防注入)
String sql = "INSERT INTO [User] (Name, Password, Gender, Age, Hobbies) VALUES (?, ?, ?, ?, ?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
// ④ 给每个占位符赋值(下标从 1 开始)
pstmt.setString(1, name);
pstmt.setString(2, password);
pstmt.setString(3, gender);
pstmt.setInt(4, Integer.parseInt(ageStr));
pstmt.setString(5, hobbyStr);
// ⑤ 执行更新(INSERT / UPDATE / DELETE 用 executeUpdate)
pstmt.executeUpdate();
// ⑥ 释放资源
pstmt.close();
conn.close();
查询示例:
String sql = "SELECT Id FROM [User] WHERE Name = ? AND Password = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, username);
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
loginSuccess = true; // 查到匹配用户
}
// 关闭顺序:rs → pstmt → conn
rs.close();
pstmt.close();
conn.close();
三、CRUD 核心操作
3.1 查(Read)
String sql = "SELECT Id, Name, Gender, Age, Hobbies FROM [User] ORDER BY Id";
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
int id = rs.getInt("Id");
String name = rs.getString("Name");
String gender = rs.getString("Gender");
int age = rs.getInt("Age");
// getInt 对 NULL 返回 0,必须用 wasNull() 区分
String ageStr = rs.wasNull() ? "" : String.valueOf(age);
String hobbies = rs.getString("Hobbies");
hobbies = hobbies != null ? hobbies : "";
}
ResultSet 关键方法:
| 方法 | 说明 |
|---|---|
rs.next() |
光标移到下一行,无数据时返回 false |
rs.getString("列名") |
获取字符串值 |
rs.getInt("列名") |
获取整数值 |
rs.wasNull() |
上一次 get 的值是否为 NULL(getInt 对 NULL 返回 0,需此方法区分) |
模糊搜索(LIKE):
String keyword = request.getParameter("keyword");
if (keyword != null && !keyword.isEmpty()) {
String sql = "SELECT * FROM [User] WHERE Name LIKE ? ORDER BY Id";
pstmt.setString(1, "%" + keyword + "%"); // % 拼在 Java 侧,不拼在 SQL 字符串中
}
3.2 增(Create)
String sql = "INSERT INTO [User] (Name, Password, Gender, Age, Hobbies) VALUES (?, ?, ?, ?, ?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, name);
pstmt.setString(2, password);
pstmt.setString(3, gender);
if (ageStr != null && !ageStr.isEmpty()) {
pstmt.setInt(4, Integer.parseInt(ageStr));
} else {
pstmt.setNull(4, Types.INTEGER); // 空值插入 NULL
}
if (!hobbyStr.isEmpty()) {
pstmt.setString(5, hobbyStr);
} else {
pstmt.setNull(5, Types.NVARCHAR);
}
pstmt.executeUpdate();
3.3 改(Update)
String sql = "UPDATE [User] SET Name=?, Password=?, Gender=?, Age=?, Hobbies=? WHERE Id=?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, name);
pstmt.setString(2, password);
pstmt.setString(3, gender);
pstmt.setInt(4, Integer.parseInt(ageStr));
pstmt.setString(5, hobbyStr);
pstmt.setInt(6, Integer.parseInt(idStr)); // WHERE 条件用主键 Id
pstmt.executeUpdate();
关键:UPDATE 和 DELETE 的 WHERE 条件必须使用主键(Id),否则可能误改/误删多条记录。
可选字段处理:密码在编辑时选填(留空=不修改),通过判断是否为空选择不同的 SQL:
if (password != null && !password.isEmpty()) {
sql = "UPDATE [User] SET Name=?, Password=?, Gender=?, Age=?, Hobbies=? WHERE Id=?";
} else {
sql = "UPDATE [User] SET Name=?, Gender=?, Age=?, Hobbies=? WHERE Id=?";
}
3.4 删(Delete)
String sql = "DELETE FROM [User] WHERE Id=?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, Integer.parseInt(idStr));
pstmt.executeUpdate();
前端删除确认:
function delUser(id, name) {
if (confirm('确定要删除用户「' + name + '」吗?此操作不可恢复。')) {
document.getElementById('deleteId').value = id;
document.getElementById('deleteForm').submit();
}
}
3.5 请求处理流程(GET/POST 合一)
同一个 JSP 文件处理全部请求:
用户浏览器
│
├─ GET page4.jsp → 查询并展示用户列表(支持搜索)
├─ POST page4.jsp?action=add → 新增用户
├─ POST page4.jsp?action=update → 修改用户
└─ POST page4.jsp?action=delete → 删除用户
if ("POST".equalsIgnoreCase(request.getMethod())) {
// 处理增/删/改
String action = request.getParameter("action");
if ("add".equals(action)) { /* INSERT */ }
if ("update".equals(action)) { /* UPDATE */ }
if ("delete".equals(action)) { /* DELETE */ }
}
// GET 或 POST 处理完后,总是执行查询并渲染页面
核心思路:先处理写操作,再执行查询。POST 后页面立即展示最新数据。
四、PreparedStatement vs Statement(SQL 注入防护)
| 特性 | Statement | PreparedStatement |
|---|---|---|
| 写法 | 字符串拼接 SQL | 占位符 ? + setXxx() |
| SQL 注入防护 | ❌ 不安全 | ✅ 安全,参数和 SQL 结构分离 |
| 预编译 | 每次发送完整 SQL | 一次编译,多次执行,效率更高 |
| 可读性 | 引号拼接混乱 | 占位符清晰 |
SQL 注入攻击示例(永远不要这样做):
// 危险写法:字符串拼接
String sql = "SELECT * FROM [User] WHERE Name='" + username + "' AND Password='" + password + "'";
// 如果用户输入 username = "admin' --"
// 实际执行的 SQL 变成:
// SELECT * FROM [User] WHERE Name='admin' --' AND Password='xxx'
// 注释掉了密码校验部分 → 无需密码即可登录
// 安全写法:PreparedStatement 参数化
String sql = "SELECT * FROM [User] WHERE Name = ? AND Password = ?";
pstmt.setString(1, username); // 无论用户输入什么,都只当作"值",不会变成 SQL 的一部分
pstmt.setString(2, password);
原理:PreparedStatement 在数据库端先编译 SQL 模板,再将参数作为纯数据传入------参数值无论包含什么特殊字符('、--、;),都不会被当作 SQL 语法解析。
原则:所有来自用户输入的值,都必须通过 ? 占位符 + setXxx() 方法传入,绝不拼接到 SQL 字符串中。
五、常用方法速查
| 方法 | 用途 |
|---|---|
executeQuery() |
执行 SELECT,返回 ResultSet |
executeUpdate() |
执行 INSERT/UPDATE/DELETE,返回影响行数 |
setString(index, value) |
设置第 index 个 ? 为字符串 |
setInt(index, value) |
设置第 index 个 ? 为整数 |
setNull(index, java.sql.Types.XXX) |
设置第 index 个 ? 为数据库 NULL |
getParameter(name) |
获取单个请求参数(text/radio/select) |
getParameterValues(name) |
获取多个请求参数(checkbox 多选),无选中时返回 null |
? 占位符索引从 1 开始(不是 0)。
空字符串和 NULL 是不同的概念:用户没填的字段应插入 NULL。
Types.INTEGER / Types.NVARCHAR 需与数据库列类型匹配。
六、请求参数处理
request.setCharacterEncoding("UTF-8"); // 处理中文参数(仅 POST 有效)
String name = request.getParameter("name"); // 单值参数
String[] hobbies = request.getParameterValues("hobbies"); // 多值参数(checkbox)
搜索使用 GET 方法,与 CRUD 的 POST 互不干扰。搜索参数拼接在 URL 中(?keyword=张三),方便分享和书签。
七、数据表结构
CREATE TABLE [User] (
Id INT IDENTITY(1,1) PRIMARY KEY, -- 自增主键
Name NVARCHAR(50) NOT NULL, -- 姓名
Password NVARCHAR(50) NOT NULL, -- 密码
Gender NVARCHAR(2) NOT NULL, -- 男/女
Age INT NULL, -- 年龄(可空)
Hobbies NVARCHAR(200) NULL, -- 爱好(用、拼接)
CreatedAt DATETIME DEFAULT GETDATE() -- 创建时间
);
IDENTITY(1,1) 表示从 1 开始,每次自增 1。插入时不需要指定 Id 值。
NVARCHAR 的 N 表示 Unicode,能存中文。永远用 NVARCHAR 存可能有中文的字段。
八、SQL Server JDBC 连接字符串
jdbc:sqlserver://localhost:1433;databaseName=DB1;encrypt=false
│ │ │ │ │
协议标识 主机/IP 端口 数据库名 关闭 SSL 加密
| 参数 | 说明 | 生产环境建议 |
|---|---|---|
encrypt=false |
关闭 SSL 加密,本地开发用 | encrypt=true |
trustServerCertificate=true |
信任自签证书 | 用正式 CA 证书 |
integratedSecurity=true |
Windows 集成认证 | 视环境而定 |
不同数据库的连接字符串对比:
| 数据库 | 连接字符串格式 |
|---|---|
| SQL Server | jdbc:sqlserver://localhost:1433;databaseName=DB1 |
| MySQL | jdbc:mysql://localhost:3306/DB1?useSSL=false |
| PostgreSQL | jdbc:postgresql://localhost:5432/DB1 |
| Oracle | jdbc:oracle:thin:@localhost:1521:DB1 |
结构统一:jdbc:数据库类型://主机:端口/数据库名?参数
九、SQL Server 常用数据类型(与 Java 类型对照)
| SQL Server 类型 | Java 类型 | 说明 | 本项目使用 |
|---|---|---|---|
| INT | int / Integer | 整数 | Age |
| NVARCHAR(n) | String | 可变长 Unicode 字符串 | Name, Password, Gender, Hobbies |
| DATETIME | java.sql.Timestamp | 日期时间 | CreatedAt |
| BIT | boolean | 布尔值 | 未使用 |
| DECIMAL(p,s) | java.math.BigDecimal | 精确小数 | 未使用 |
十、常见错误与调试
| 问题 | 原因 | 解决 |
|---|---|---|
ClassNotFoundException |
未导入 JDBC 驱动 jar | 将 mssql-jdbc.jar 放入 WEB-INF/lib/ 或配置 Maven 依赖 |
SQLException: 拒绝连接 |
SQL Server 未启动或端口不对 | 检查 SQL Server 服务,确认端口 1433 |
| 此列名无效 | 表结构不匹配 | 核对 SQL 中的列名与数据库是否一致 |
getInt 返回 0 但数据库是 NULL |
getInt 对 NULL 返回 0 |
调用 rs.wasNull() 判断 |
| 中文乱码 | 字符编码不统一 | request.setCharacterEncoding("UTF-8") + 数据库列用 NVARCHAR |
| 删除操作没反应 | 隐藏表单的 action 或 id 未正确赋值 |
在 JS 中用 console.log 检查值 |
十一、关键类一览
| 类 / 接口 | 所属包 | 作用 | 获取方式 |
|---|---|---|---|
DriverManager |
java.sql |
管理驱动,创建数据库连接 | DriverManager.getConnection(url, user, password) |
Connection |
java.sql |
代表一个数据库 TCP 连接 | 由 DriverManager 创建 |
PreparedStatement |
java.sql |
预编译 SQL,安全传参 | conn.prepareStatement(sql) |
ResultSet |
java.sql |
查询结果集,逐行读取 | pstmt.executeQuery() |
SQLServerDriver |
com.microsoft.sqlserver.jdbc |
SQL Server 的 JDBC 驱动实现 | 通过 Class.forName() 加载 |
十二、当前架构的隐患与改进方向
| 问题 | 隐患 | 改进方向 |
|---|---|---|
| 每次请求新建连接 | 浪费资源 | 使用连接池(如 HikariCP、Druid) |
| 数据库密码明文写在代码里 | 泄露风险 | 外置到 .properties 配置文件 |
| 密码明文存储 | 数据库泄露则密码全暴露 | 改为哈希存储(BCrypt、SHA-256 + 盐) |
| 每个 JSP 重复写连接代码 | 维护困难 | 抽取 DBUtil 工具类统一管理 |
理想的目标架构
JSP 页面(只负责显示)
↓ 调用
Servlet(处理请求、校验参数、调用业务逻辑)
↓ 调用
DAO 类(封装 SQL,通过 DBUtil 获取连接)
↓ 使用
DBUtil 工具类(管理连接池、提供 getConnection())
↓ 读取
db.properties(数据库连接参数)
↓ 连接
SQL Server
当前阶段直接在 JSP 里写 JDBC 代码是可以接受的------代码量小,概念清晰,适合理解数据库操作的基本流程。后续项目变大再重构不迟。
十三、关键概念一句话总结
| 概念 | 一句话 |
|---|---|
| JDBC | Java 操作数据库的统一接口,一套 API 操作所有数据库 |
| DriverManager | 管理数据库驱动的"接线员",负责创建连接 |
| Connection | 一个数据库 TCP 连接,用完必须关闭 |
| PreparedStatement | 预编译 + 参数化的 SQL 执行器,防注入 |
| ResultSet | 查询结果集,像 Excel 表一样逐行读取 |
| mssql-jdbc | 微软官方提供的 SQL Server JDBC 驱动 |
| 连接池 | 预先创建好一批连接反复使用,避免频繁建立/销毁 |
| SQL 注入 | 用户输入被当成 SQL 代码执行,PreparedStatement 可以杜绝 |
| 硬编码 | 把配置直接写在代码里,应外置到配置文件 |
| CRUD | Create / Read / Update / Delete,数据库四大基本操作 |
| IDENTITY | SQL Server 的自增列,插入时不需手动指定值 |
| NVARCHAR | Unicode 可变长字符串,存中文必须用它 |
十四、具体效果
