準備工作
- MySQL 8.0+ 已安裝並運行
- 下載
mysql-connector-j-8.0.x.jar 放入 WEB-INF/lib/ 或 Tomcat lib/
- 創建測試數據庫表
數據庫腳本
CREATE DATABASE IF NOT EXISTS jsp_demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE jsp_demo;
CREATE TABLE IF NOT EXISTS users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
INSERT INTO users (username, email, password_hash) VALUES
('zhangsan', 'zhang@example.com', 'hash1'),
('lisi', 'li@example.com', 'hash2');
基礎版:JSP 直接操作 JDBC (教學/原型用)
警告:生產環境嚴禁在 JSP 中寫 JDBC 代碼!應使用 DAO/Service 層 + 連接池。此處僅示範 API。
<%@ page import="java.sql.*" contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<%
// 1. 數據庫連接參數 (生產環境應讀取配置文件)
String url = "jdbc:mysql://localhost:3306/jsp_demo?" +
"useSSL=false&" +
"serverTimezone=Asia/Shanghai&" +
"allowPublicKeyRetrieval=true&" +
"characterEncoding=utf8&" +
"useUnicode=true";
String dbUser = "root";
String dbPwd = "your_password";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
// 2. 註冊驅動 (JDBC 4.0+ SPI 自動加載,可省略)
// Class.forName("com.mysql.cj.jdbc.Driver");
// 3. 獲取連接
conn = DriverManager.getConnection(url, dbUser, dbPwd);
// 4. 預編譯 SQL (防 SQL 注入)
String sql = "SELECT id, username, email, created_at FROM users WHERE username = ?";
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "zhangsan"); // 參數綁定
// 5. 執行查詢
rs = pstmt.executeQuery();
// 6. 處理結果集
%>
<table border="1" cellpadding="5">
<tr><th>ID</th><th>用戶名</th><th>郵箱</th><th>創建時間</th></tr>
<%
while (rs.next()) {
%>
<tr>
<td><%= rs.getLong("id") %></td>
<td><%= rs.getString("username") %></td>
<td><%= rs.getString("email") %></td>
<td><%= rs.getTimestamp("created_at") %></td>
</tr>
<% } %>
</table>
<%
} catch (SQLException e) {
e.printStackTrace();
out.println("<p style='color:red'>數據庫錯誤: " + e.getMessage() + "</p>");
} finally {
// 7. 逆序關閉資源 (關鍵!防連接洩漏)
try { if (rs != null) rs.close(); } catch (SQLException ignored) {}
try { if (pstmt != null) pstmt.close(); } catch (SQLException ignored) {}
try { if (conn != null) conn.close(); } catch (SQLException ignored) {}
}
%>
進階版:DAO 模式 + 連接池 (生產標準)
1. 連接池配置 (Tomcat context.xml + HikariCP)
<!-- $CATALINA_HOME/conf/context.xml 或 META-INF/context.xml -->
<Resource name="jdbc/jspDemoDS"
auth="Container"
type="javax.sql.DataSource"
factory="com.zaxxer.hikari.HikariJNDIFactory"
driverClassName="com.mysql.cj.jdbc.Driver"
jdbcUrl="jdbc:mysql://localhost:3306/jsp_demo?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&characterEncoding=utf8"
username="root"
password="your_password"
maximumPoolSize="20"
minimumIdle="5"
connectionTimeout="30000"
idleTimeout="600000"
maxLifetime="1800000"
connectionTestQuery="SELECT 1"
/>
依賴:HikariCP.jar + mysql-connector-j.jar 放入 Tomcat lib/。
2. DAO 類
package com.example.dao;
import javax.naming.InitialContext;
import javax.sql.DataSource;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public class UserDao {
private DataSource dataSource;
public UserDao() {
try {
InitialContext ctx = new InitialContext();
dataSource = (DataSource) ctx.lookup("java:comp/env/jdbc/jspDemoDS");
} catch (Exception e) {
throw new RuntimeException("DataSource 初始化失敗", e);
}
}
public List findAll() {
String sql = "SELECT id, username, email, created_at FROM users";
List list = new ArrayList<>();
// Try-with-resources 自動關閉 (Java 7+)
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
User u = new User();
u.setId(rs.getLong("id"));
u.setUsername(rs.getString("username"));
u.setEmail(rs.getString("email"));
u.setCreatedAt(rs.getTimestamp("created_at"));
list.add(u);
}
} catch (SQLException e) {
throw new RuntimeException("查詢用戶列表失敗", e);
}
return list;
}
public void insert(User user) {
String sql = "INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)";
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
pstmt.setString(1, user.getUsername());
pstmt.setString(2, user.getEmail());
pstmt.setString(3, user.getPasswordHash());
pstmt.executeUpdate();
try (ResultSet keys = pstmt.getGeneratedKeys()) {
if (keys.next()) user.setId(keys.getLong(1));
}
} catch (SQLException e) {
throw new RuntimeException("插入用戶失敗", e);
}
}
}
3. 實體類
package com.example.model;
import java.sql.Timestamp;
public class User {
private Long id;
private String username;
private String email;
private String passwordHash;
private Timestamp createdAt;
// Getter/Setter/ToString 省略
}
4. JSP 使用 DAO (極簡)
<%@ page import="com.example.dao.UserDao, com.example.model.User, java.util.List"
contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<%
UserDao dao = new UserDao();
List<User> users = dao.findAll();
request.setAttribute("users", users); // 供 EL 使用
%>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<table border="1">
<tr><th>ID</th><th>用戶名</th><th>郵箱</th><th>創建時間</th></tr>
<c:forEach items="${users}" var="u">
<tr>
<td>${u.id}</td>
<td><c:out value="${u.username}"/></td>
<td><c:out value="${u.email}"/></td>
<td>${u.createdAt}</td>
</tr>
</c:forEach>
</table>
關鍵最佳實踐總結
| 實踐 | 錯誤做法 | 正確做法 |
| SQL 注入防護 | 字符串拼接 "SELECT * FROM t WHERE name='" + name + "'" | PreparedStatement 參數綁定 pstmt.setString(1, name) |
| 資源關閉 | 無 finally / 順序錯誤 | Try-with-resources (Java 7+) 或 finally 逆序關閉 |
| 連接管理 | 每請求 new Connection / 靜態單例 | 連接池 (HikariCP/Druid) + JNDI |
| 異常處理 | catch 後不處理 / 吞沒異常 | 轉換為業務異常拋出,統一攔截器記錄日誌返回友好頁面 |
| 配置管理 | 硬編碼 URL/用戶/密碼 | 外部配置文件 / 環境變量 / 配置中心 |
| 事務控制 | 自動提交 / 手動 commit/rollback 分散 | Service 層 @Transactional (Spring) 或手動事務邊界 |
| 字符集 | 默認 latin1 導致中文亂碼 | URL 參數 characterEncoding=utf8&useUnicode=true,庫表 utf8mb4 |
常見錯誤排查
| 錯誤信息 | 原因 | 解決 |
| ClassNotFoundException: com.mysql.cj.jdbc.Driver | 驅動 jar 未放入 classpath | 放入 WEB-INF/lib 或 Tomcat lib,重啟 |
| Communications link failure | MySQL 未啟動 / IP/端口錯誤 / 防火牆 | 檢查服務狀態、bind-address、安全組/防火牆 3306 |
| Public Key Retrieval is not allowed | MySQL 8 缓存 sha2 密碼插件需公鑰 | URL 加 allowPublicKeyRetrieval=true |
| Server timezone value '...' unrecognized | 時區識別失敗 | URL 加 serverTimezone=Asia/Shanghai 或 GMT%2B8 |
| Data truncation: Incorrect string value | 字符集不支持 Emoji/生僻字 | 庫表改 utf8mb4,URL characterEncoding=utf8 |