嘿,朋友。如果你现在正盯着满屏的问号 ???? 或者数据库连不上的报错抓耳挠腮,先别急,把咖啡放下,我们坐下来慢慢聊。我做这套学生信息管理系统(Student Information System, 简称 SIS)的时候,也被中文乱码和连接池搞得心态崩过几次。今天我把这套“血泪史”整理成文,咱们不整那些虚头巴脑的理论,直接上手干货。我会用最通俗的大白话,配合能直接跑起来的代码,带你把 JSP、Java Servlet、MySQL 这套经典组合技玩透。
一、 为什么我们还在学这套“老古董”?
你可能会问:“现在都 Spring Boot 了,谁还用 JSP?” 嘿,这话说得好。但在很多传统企业、老旧项目维护,以及理解 Web 请求完整生命周期的初学者眼里,JSP + Servlet + JDBC 依然是基石中的基石。
这就像你学开车,得先懂离合器怎么配合。学会了 JSP 的 request、response 和 Servlet 的生命周期,你再去看 Spring MVC,那就是降维打击。而且,当你面对一个只有 JSP 的老项目需要修 Bug 时,本文就是你的救命稻草。
我们的目标很明确:做一个能增删改查(CRUD)学生信息的系统。界面简洁,功能实用,关键是——中文不乱码,数据不丢失。
二、 环境准备:工欲善其事
在写第一行代码之前,我们先要把“地基”打牢。别小看这一步,80% 的奇怪问题都源于环境配置不对。
1. 技术栈清单
- 前端:JSP (JavaServer Pages),配合简单的 Bootstrap CSS(为了好看点)。
- 后端:Servlet (Java),Java 11 或更高版本。
- 数据库:MySQL 8.0。
- 驱动:
mysql-connector-j(MySQL 8.x 官方推荐驱动)。 - 服务器:Apache Tomcat 9.0 或 10.1。
2. 数据库设计
首先,打开你的 MySQL 客户端,创建一个数据库 sis_db,并建一张表。这里有个关键点,很多新人都在这里栽跟头。
-- 创建数据库,指定字符集为 utf8mb4,这是解决中文问题的核心
CREATE DATABASE IF NOT EXISTS sis_db
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE sis_db;
-- 创建学生表
DROP TABLE IF EXISTS students;
CREATE TABLE students (
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '学号ID',
name VARCHAR(50) NOT NULL COMMENT '姓名',
age INT NOT NULL COMMENT '年龄',
gender VARCHAR(10) COMMENT '性别',
major VARCHAR(100) COMMENT '专业',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生信息表';
-- 插入几条测试数据,确保有中文
INSERT INTO students (name, age, gender, major) VALUES
('张三', 20, '男', '计算机科学'),
('李四', 21, '女', '软件工程'),
('王五', 22, '男', '人工智能'),
('赵六', 19, '女', '数据科学');
注意:utf8mb4 不仅仅是 utf8,它能支持 Emoji 和更多生僻字,强烈建议用这个。如果你在 MySQL 配置里 (my.ini 或 my.cnf) 看到 default-character-set=utf8,最好改成 utf8mb4。
三、 项目结构:像搭积木一样清晰
不要把所有东西都堆在一个包里。让我们建立一个标准的 Maven 项目结构,这样既专业又方便管理依赖。
StudentSIS/
├── pom.xml <!-- Maven 依赖配置 -->
└── src/
└── main/
├── java/
│ └── com/example/sis/
│ ├── util/
│ │ └── DBUtil.java <!-- 数据库连接工具类 -->
│ ├── model/
│ │ └── Student.java <!-- 实体类 -->
│ ├── servlet/
│ │ ├── ListServlet.java <!-- 查询列表 -->
│ │ ├── AddServlet.java <!-- 添加学生 -->
│ │ └── DeleteServlet.java <!-- 删除学生 -->
│ └── filter/
│ └── EncodingFilter.java <!-- 全局编码过滤器 -->
├── webapp/
│ ├── WEB-INF/
│ │ └── web.xml <!-- 部署描述符 -->
│ ├── index.jsp <!-- 首页/列表页 -->
│ ├── add.jsp <!-- 添加表单页 -->
│ └── css/
│ └── style.css <!-- 简单样式 -->
└── resources/
└── db.properties <!-- 数据库配置信息 -->
四、 核心痛点一:彻底解决中文乱码
这是本文的重中之重。乱码问题通常出现在三个地方:
- 浏览器发给服务器的中文(POST 请求体)。
- 服务器发给浏览器的中文(响应体)。
- 数据库读写中文(JDBC 连接字符串和表结构)。
只要解决了这三点,你的系统就稳了。
4.1 配置 web.xml 和 JDBC URL
在 src/main/resources/db.properties 中配置数据库连接:
# db.properties
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/sis_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
jdbc.username=root
jdbc.password=your_password
解析:
useUnicode=true&characterEncoding=UTF-8:这告诉 JDBC 驱动,我要用 UTF-8 和数据库通信。serverTimezone=Asia/Shanghai:解决时区报错。useSSL=false:本地开发可以关掉 SSL,否则会报警。
4.2 编写全局编码过滤器 (EncodingFilter)
这是解决“浏览器→服务器”和“服务器→浏览器”乱码的终极武器。我们在 servlet 包下创建一个 EncodingFilter。
package com.example.sis.filter;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 全局编码过滤器
* 作用:
* 1. 设置请求体的编码为 UTF-8 (解决 POST 中文乱码)
* 2. 设置响应体的编码为 UTF-8 (解决页面显示乱码)
*/
public class EncodingFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 初始化
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 强转类型,因为 ServletRequest 是通用接口
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 1. 设置请求编码:只针对 POST 请求有效,GET 请求需要单独处理(或者在 Tomcat server.xml 配置)
// 注意:request.setCharacterEncoding("UTF-8") 必须在读取任何参数之前调用
if ("POST".equalsIgnoreCase(httpRequest.getMethod())) {
httpRequest.setCharacterEncoding("UTF-8");
}
// 2. 设置响应编码
httpResponse.setCharacterEncoding("UTF-8");
httpResponse.setContentType("text/html;charset=UTF-8");
// 将请求传递给下一个过滤器或 Servlet
chain.doFilter(request, response);
}
@Override
public void destroy() {
// 销毁
}
}
关键点:chain.doFilter(request, response) 这行代码不能省,否则请求就断掉,页面打不开。
然后在 web.xml 中注册这个过滤器,让它对所有请求生效:
<!-- WEB-INF/web.xml -->
<filter>
<filter-name>EncodingFilter</filter-name>
<filter-class>com.example.sis.filter.EncodingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>EncodingFilter</filter-name>
<url-pattern>/*</url-pattern> <!-- 拦截所有请求 -->
</filter-mapping>
五、 数据库连接工具类 (DBUtil)
为了代码整洁,我们不让每个 Servlet 都去写 DriverManager.getConnection。提取一个 DBUtil 工具类。
package com.example.sis.util;
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;
public class DBUtil {
private static String driver;
private static String url;
private static String username;
private static String password;
static {
// 加载配置文件
Properties props = new Properties();
try (InputStream is = DBUtil.class.getClassLoader().getResourceAsStream("db.properties")) {
if (is == null) {
throw new RuntimeException("找不到 db.properties 配置文件");
}
props.load(is);
driver = props.getProperty("jdbc.driver");
url = props.getProperty("jdbc.url");
username = props.getProperty("jdbc.username");
password = props.getProperty("jdbc.password");
// 加载驱动
Class.forName(driver);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("数据库配置失败,请检查 db.properties", e);
}
}
/**
* 获取数据库连接
*/
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(url, username, password);
}
/**
* 关闭资源
*/
public static void close(Connection conn, Statement stmt, ResultSet rs) {
try {
if (rs != null) rs.close();
if (stmt != null) stmt.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
为什么用静态块? 因为驱动加载和配置读取只需要做一次,放在 static {} 里,类加载时就会执行,高效且安全。
六、 实体类与 Servlet 开发
6.1 Student 实体类
package com.example.sis.model;
import java.time.LocalDateTime;
public class Student {
private Integer id;
private String name;
private Integer age;
private String gender;
private String major;
private LocalDateTime createTime;
// 省略 getter 和 setter,省略 toString()
// 为了简洁,建议在实际项目中用 Lombok 的 @Data 注解
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 Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
public String getGender() { return gender; }
public void setGender(String gender) { this.gender = gender; }
public String getMajor() { return major; }
public void setMajor(String major) { this.major = major; }
public LocalDateTime getCreateTime() { return createTime; }
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
}
6.2 ListServlet:查询所有学生
这个 Servlet 负责从数据库取数据,并存到 Request 里,然后转发给 JSP 页面展示。
package com.example.sis.servlet;
import com.example.sis.model.Student;
import com.example.sis.util.DBUtil;
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;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
@WebServlet("/list") // 访问路径:http://localhost:8080/SIS/list
public class ListServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
List<Student> studentList = new ArrayList<>();
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = DBUtil.getConnection();
// 使用 PreparedStatement 防止 SQL 注入,这是好习惯!
String sql = "SELECT id, name, age, gender, major, create_time FROM students ORDER BY id DESC";
pstmt = conn.prepareStatement(sql);
rs = pstmt.executeQuery();
while (rs.next()) {
Student student = new Student();
student.setId(rs.getInt("id"));
student.setName(rs.getString("name"));
student.setAge(rs.getInt("age"));
student.setGender(rs.getString("gender"));
student.setMajor(rs.getString("major"));
student.setCreateTime(rs.getTimestamp("create_time").toLocalDateTime());
studentList.add(student);
}
} catch (SQLException e) {
e.printStackTrace();
// 实际项目中应该记录日志,而不是直接 printStackTrace
response.getWriter().write("数据库查询错误,请稍后重试。");
return;
} finally {
DBUtil.close(conn, pstmt, rs);
}
// 将数据放入 request 域对象,转发到 JSP
request.setAttribute("students", studentList);
request.getRequestDispatcher("/index.jsp").forward(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
6.3 AddServlet:添加学生
package com.example.sis.servlet;
import com.example.sis.util.DBUtil;
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;
import java.sql.*;
@WebServlet("/add")
public class AddServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 注意:EncodingFilter 已经处理了 POST 请求的编码,这里直接 getParameter 即可
String name = request.getParameter("name");
String ageStr = request.getParameter("age");
String gender = request.getParameter("gender");
String major = request.getParameter("major");
// 简单校验
if (name == null || name.trim().isEmpty() || ageStr == null) {
response.getWriter().write("<script>alert('姓名和年龄不能为空');history.back();</script>");
return;
}
Connection conn = null;
PreparedStatement pstmt = null;
try {
conn = DBUtil.getConnection();
String sql = "INSERT INTO students (name, age, gender, major) VALUES (?, ?, ?, ?)";
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, name);
pstmt.setInt(2, Integer.parseInt(ageStr));
pstmt.setString(3, gender);
pstmt.setString(4, major);
int rows = pstmt.executeUpdate();
if (rows > 0) {
// 添加成功,重定向到列表页,避免重复提交
response.sendRedirect(request.getContextPath() + "/list");
} else {
response.getWriter().write("添加失败,请重试。");
}
} catch (Exception e) {
e.printStackTrace();
response.getWriter().write("系统错误:" + e.getMessage());
} finally {
DBUtil.close(conn, pstmt, null);
}
}
}
6.4 DeleteServlet:删除学生
package com.example.sis.servlet;
import com.example.sis.util.DBUtil;
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;
import java.sql.*;
@WebServlet("/delete")
public class DeleteServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String idStr = request.getParameter("id");
if (idStr == null) {
response.sendRedirect(request.getContextPath() + "/list");
return;
}
Connection conn = null;
PreparedStatement pstmt = null;
try {
conn = DBUtil.getConnection();
String sql = "DELETE FROM students WHERE id = ?";
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, Integer.parseInt(idStr));
pstmt.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
} finally {
DBUtil.close(conn, pstmt, null);
}
// 删除后重定向到列表
response.sendRedirect(request.getContextPath() + "/list");
}
}
七、 前端页面:JSP 的优雅呈现
现在是最有趣的部分,把数据展示出来。我们用 index.jsp 作为主页,同时展示列表和添加表单(或者你可以拆分成两个页面,这里为了演示方便合在一起)。
index.jsp:
”`jsp <%@ page contentType=“text/html;charset=UTF-8” language=“java” %> <%@ taglib prefix=“c” uri=”http://java.sun.com/jsp/jstl/core” %> <%@ page import=“java.util.List” %> <%@ page import=“com.example.sis.model.Student” %>
<!DOCTYPE html>
<meta charset="UTF-8">
<title>学生信息管理系统
