踩坑记录:Java Web 开发中的中文乱码问题解决
📋 项目概述
本项目是一个基于 Java Web 的中医药数据库管理系统,实现了对中成药和中药材的信息化管理。系统采用 MVC 架构,包含完整的前后端功能。
项目位置: d:\projects\project1\tcm-management
开发时间: 2026-03-21
🎯 项目成果
✅ 已完成的功能
-
中成药管理
-
✅ 查看中成药列表
-
✅ 添加新中成药
-
✅ 编辑中成药信息
-
✅ 删除中成药
-
✅ 查看中成药详情
-
-
中药材管理
-
✅ 查看中药材列表
-
✅ 添加新中药材
-
✅ 编辑中药材信息
-
✅ 删除中药材
-
✅ 查看中药材详情
-
-
数据库
-
✅ MySQL 8.0.40 安装配置
-
✅ 数据库
tcm_db创建 -
✅ 3 个数据表设计
-
✅ 示例数据初始化
-
-
环境配置
-
✅ Java SE 21 (JDK)
-
✅ Apache Maven 3.9.6
-
✅ Apache Tomcat 8.5.100
-
✅ 环境变量配置
-
🏗️ 系统架构
┌─────────────────────────────────────────────────────────┐
│ 用户浏览器 │
│ http://localhost:8080/tcm-management │
└────────────────────┬────────────────────────────────────┘
│ HTTP 请求
▼
┌─────────────────────────────────────────────────────────┐
│ Apache Tomcat 8.5.100 │
│ Web 容器 │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Java Servlet 层 │
│ ┌─────────────────┐ ┌──────────────────┐ │
│ │ MedicineServlet │ │ IngredientServlet│ │
│ │ /medicine │ │ /ingredient │ │
│ └─────────────────┘ └──────────────────┘ │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ DAO 层 (数据访问层) │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ MedicineDAO │ │ IngredientDAO │ │
│ └──────────────┘ └──────────────────┘ │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ MySQL 数据库 │
│ ┌──────────────────────────────────────────┐ │
│ │ tcm_db │ │
│ │ - medicine_names │ │
│ │ - ingredients │ │
│ │ - medicine_ingredients │ │
│ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
📁 项目结构
tcm-management/
├── src/
│ ├── main/
│ │ ├── java/com/tcm/
│ │ │ ├── servlet/ # 控制器层
│ │ │ │ ├── IndexServlet.java
│ │ │ │ ├── MedicineServlet.java
│ │ │ │ └── IngredientServlet.java
│ │ │ ├── dao/ # 数据访问层
│ │ │ │ ├── MedicineDAO.java
│ │ │ │ └── IngredientDAO.java
│ │ │ ├── entity/ # 实体类
│ │ │ │ ├── Medicine.java
│ │ │ │ └── Ingredient.java
│ │ │ └── util/ # 工具类
│ │ │ └── DBUtil.java
│ │ └── webapp/
│ │ ├── WEB-INF/
│ │ │ ├── jsp/ # 视图层
│ │ │ │ ├── index.jsp
│ │ │ │ ├── medicine_list.jsp
│ │ │ │ ├── medicine_add.jsp
│ │ │ │ ├── medicine_edit.jsp
│ │ │ │ ├── medicine_view.jsp
│ │ │ │ ├── ingredient_list.jsp
│ │ │ │ ├── ingredient_add.jsp
│ │ │ │ ├── ingredient_edit.jsp
│ │ │ │ ├── ingredient_view.jsp
│ │ │ │ └── error.jsp
│ │ │ └── web.xml
│ │ └── css/ # 样式文件
│ └── test/
├── target/ # 编译输出
│ └── tcm-management.war
├── pom.xml # Maven 配置
├── 一键启动.bat
├── 部署并启动.bat
├── 修复中文乱码.bat
├── 修复添加错误.bat
└── README.md
💻 关键代码
1. 数据库连接工具类
文件: src/main/java/com/tcm/util/DBUtil.java
public class DBUtil {
// 数据库连接配置
private static final String URL = "jdbc:mysql://localhost:3306/tcm_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true";
private static final String USERNAME = "root";
private static final String PASSWORD = "Root123456!";
private static final String DRIVER = "com.mysql.cj.jdbc.Driver";
static {
try {
Class.forName(DRIVER);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(URL, USERNAME, PASSWORD);
}
}
说明:
-
使用 UTF-8 编码解决中文乱码问题
-
禁用 SSL 提高连接速度
-
设置时区为 Asia/Shanghai
2. Servlet 控制器 (带字符编码修复)
文件: src/main/java/com/tcm/servlet/MedicineServlet.java
@WebServlet("/medicine")
public class MedicineServlet extends HttpServlet {
private MedicineDAO medicineDAO = new MedicineDAO();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 🔧 关键修复:设置字符编码,防止中文乱码
req.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=UTF-8");
String action = req.getParameter("action");
if ("list".equals(action)) {
listMedicines(req, resp);
} else if ("add".equals(action)) {
req.getRequestDispatcher("/WEB-INF/jsp/medicine_add.jsp").forward(req, resp);
} else if ("edit".equals(action)) {
editMedicine(req, resp);
} else if ("delete".equals(action)) {
deleteMedicine(req, resp);
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 🔧 关键修复:设置字符编码
req.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=UTF-8");
String action = req.getParameter("action");
if ("add".equals(action)) {
addMedicine(req, resp);
} else if ("edit".equals(action)) {
updateMedicine(req, resp);
}
}
// 🔧 关键修复:优化添加逻辑,防止 NullPointerException
private void addMedicine(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
try {
Medicine medicine = new Medicine();
medicine.setMedicineName(req.getParameter("medicineName"));
medicine.setPinyinName(req.getParameter("pinyinName"));
medicine.setCategory(req.getParameter("category"));
medicine.setMainEffects(req.getParameter("mainEffects"));
medicine.setIndications(req.getParameter("indications"));
medicine.setUsageDosage(req.getParameter("usageDosage"));
// 优化:先保存结果,再判断
boolean success = medicineDAO.addMedicine(medicine);
if (success) {
resp.sendRedirect(req.getContextPath() + "/medicine?action=list");
} else {
req.setAttribute("error", "添加失败");
req.getRequestDispatcher("/WEB-INF/jsp/medicine_add.jsp").forward(req, resp);
}
} catch (SQLException e) {
e.printStackTrace();
req.setAttribute("error", "添加失败:" + e.getMessage());
req.getRequestDispatcher("/WEB-INF/jsp/medicine_add.jsp").forward(req, resp);
}
}
}
关键修复点:
-
✅ 设置请求编码:
req.setCharacterEncoding("UTF-8") -
✅ 设置响应编码:
resp.setContentType("text/html;charset=UTF-8") -
✅ 优化添加逻辑,避免响应提交后转发
3. 数据访问层 DAO
文件: src/main/java/com/tcm/dao/IngredientDAO.java
public class IngredientDAO {
/**
* 获取所有中药材
* @return 中药材列表
*/
public List<Ingredient> getAllIngredients() throws SQLException {
List<Ingredient> list = new ArrayList<>();
String sql = "SELECT * FROM ingredients ORDER BY id";
try (Connection conn = DBUtil.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
Ingredient ingredient = new Ingredient();
ingredient.setId(rs.getInt("id"));
ingredient.setIngredientName(rs.getString("ingredient_name"));
ingredient.setCategory(rs.getString("category"));
ingredient.setEffects(rs.getString("effects"));
ingredient.setSource(rs.getString("source"));
list.add(ingredient);
}
}
return list;
}
/**
* 添加中药材
* @param ingredient 中药材对象
* @return 是否添加成功
*/
public boolean addIngredient(Ingredient ingredient) throws SQLException {
String sql = "INSERT INTO ingredients (ingredient_name, category, effects, source) VALUES (?, ?, ?, ?)";
try (Connection conn = DBUtil.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, ingredient.getIngredientName());
pstmt.setString(2, ingredient.getCategory());
pstmt.setString(3, ingredient.getEffects());
pstmt.setString(4, ingredient.getSource());
return pstmt.executeUpdate() > 0;
}
}
}
说明:
-
使用 PreparedStatement 防止 SQL 注入
-
使用 try-with-resources 自动关闭资源
-
返回布尔值表示操作结果
4. 实体类
文件: src/main/java/com/tcm/entity/Ingredient.java
public class Ingredient {
private Integer id;
private String ingredientName; // 药材名称
private String category; // 类别
private String effects; // 功效
private String source; // 来源
// Getter 和 Setter 方法
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getIngredientName() { return ingredientName; }
public void setIngredientName(String ingredientName) {
this.ingredientName = ingredientName;
}
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public String getEffects() { return effects; }
public void setEffects(String effects) { this.effects = effects; }
public String getSource() { return source; }
public void setSource(String source) { this.source = source; }
}
5. JSP 视图页面
文件: src/main/webapp/WEB-INF/jsp/medicine_list.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<html>
<head>
<title>中成药列表 - 中医药数据库管理系统</title>
<style>
/* 渐变色设计 */
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
text-align: center;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
display: inline-block;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-success {
background-color: #28a745;
color: white;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
border: 1px solid #ddd;
padding: 12px;
text-align: left;
}
th {
background-color: #f8f9fa;
font-weight: bold;
}
</style>
</head>
<body>
<div class="header">
<h1>💊 中成药管理</h1>
</div>
<div class="container">
<h2>📋 中成药列表</h2>
<a href="${pageContext.request.contextPath}/medicine?action=add" class="btn btn-success">
➕ 添加中成药
</a>
<table>
<thead>
<tr>
<th>ID</th>
<th>药品名称</th>
<th>拼音</th>
<th>类别</th>
<th>主要功效</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<c:forEach items="${medicines}" var="medicine">
<tr>
<td>${medicine.id}</td>
<td>${medicine.medicineName}</td>
<td>${medicine.pinyinName}</td>
<td>${medicine.category}</td>
<td>${medicine.mainEffects}</td>
<td>
<a href="${pageContext.request.contextPath}/medicine?action=view&id=${medicine.id}" class="btn btn-primary">查看</a>
<a href="${pageContext.request.contextPath}/medicine?action=edit&id=${medicine.id}" class="btn btn-primary">编辑</a>
<a href="${pageContext.request.contextPath}/medicine?action=delete&id=${medicine.id}" class="btn" style="background-color: #dc3545; color: white;" onclick="return confirm('确定要删除吗?')">删除</a>
</td>
</tr>
</c:forEach>
</tbody>
</table>
</div>
</body>
</html>
说明:
-
使用 JSTL 标签库遍历数据
-
响应式表格设计
-
渐变色主题
-
操作按钮颜色区分
🗄️ 数据库设计
数据库表结构
-- 1. 中成药名称表
CREATE TABLE medicine_names (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键 ID',
medicine_name VARCHAR(100) NOT NULL COMMENT '药品名称',
pinyin_name VARCHAR(100) COMMENT '拼音名称',
category VARCHAR(50) COMMENT '类别',
main_effects TEXT COMMENT '主要功效',
indications TEXT COMMENT '适应症',
usage_dosage TEXT COMMENT '用法用量'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='中成药名称表';
-- 2. 中药材表
CREATE TABLE ingredients (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键 ID',
ingredient_name VARCHAR(100) NOT NULL COMMENT '药材名称',
latin_name VARCHAR(100) COMMENT '拉丁名',
part_used VARCHAR(50) COMMENT '药用部位',
properties VARCHAR(100) COMMENT '药性'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='中药材表';
-- 3. 中成药 - 中药材关系表
CREATE TABLE medicine_ingredients (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键 ID',
medicine_id INT NOT NULL COMMENT '中成药 ID',
ingredient_id INT NOT NULL COMMENT '中药材 ID',
dosage VARCHAR(50) COMMENT '用量',
role VARCHAR(50) COMMENT '角色 (君/臣/佐/使)',
FOREIGN KEY (medicine_id) REFERENCES medicine_names(id),
FOREIGN KEY (ingredient_id) REFERENCES ingredients(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='中成药 - 中药材关系表';
数据库 ER 图
┌─────────────────────────┐
│ medicine_names │
│ (中成药名称表) │
├─────────────────────────┤
│ PK id │
│ medicine_name │
│ pinyin_name │
│ category │
│ main_effects │
│ indications │
│ usage_dosage │
└───────────┬─────────────┘
│ 1
│
│ N
┌───────────┴─────────────┐
│ medicine_ingredients │
│ (关系表) │
├─────────────────────────┤
│ PK id │
│ FK medicine_id │───┐
│ FK ingredient_id │───┼─── 多对多关系
│ dosage │ │
│ role │ │
└───────────┬─────────────┘ │
│ N │
│ │ N
└─────────────────┘
│
│ 1
┌───────────────────┴─────────────┐
│ ingredients │
│ (中药材表) │
├─────────────────────────────────┤
│ PK id │
│ ingredient_name │
│ latin_name │
│ part_used │
│ properties │
└─────────────────────────────────┘
示例数据
中成药表 (medicine_names):
|
ID |
药品名称 |
拼音 |
类别 |
主要功效 |
|---|---|---|---|---|
|
1 |
感冒灵颗粒 |
Ganmaoling Keli |
感冒药 |
疏风解表,清热解毒 |
|
2 |
板蓝根颗粒 |
Banlangen Keli |
感冒药 |
清热解毒,凉血利咽 |
|
3 |
藿香正气水 |
Huoxiang Zhengqi Shui |
祛暑药 |
解表化湿,理气和中 |
|
4 |
六味地黄丸 |
Liuwei Dihuang Wan |
补益药 |
滋阴补肾 |
|
5 |
逍遥丸 |
Xiaoyao Wan |
理气药 |
疏肝健脾,养血调经 |
中药材表 (ingredients):
|
ID |
药材名称 |
拉丁名 |
药用部位 |
药性 |
|---|---|---|---|---|
|
1 |
金银花 |
Lonicerae Japonicae Flos |
花 |
甘,寒。清热解毒 |
|
2 |
连翘 |
Forsythiae Fructus |
果实 |
苦,微寒。清热解毒 |
|
3 |
板蓝根 |
Isatidis Radix |
根 |
苦,寒。清热解毒 |
|
4 |
甘草 |
Glycyrrhizae Radix |
根茎 |
甘,平。补脾益气 |
|
5 |
茯苓 |
Poria |
菌核 |
甘、淡,平。利水渗湿 |
🔧 修复的关键问题
问题 1: 中文乱码问题
现象: 页面显示中文为问号 (?????)
原因: Servlet 未设置字符编码
修复代码:
// 在每个 Servlet 的 doGet 和 doPost 方法开头添加
req.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=UTF-8");
影响文件:
-
MedicineServlet.java -
IngredientServlet.java -
IndexServlet.java
问题 2: 添加失败 NullPointerException
现象: 添加操作时报错 NullPointerException
错误日志:
java.lang.NullPointerException: Cannot invoke "javax.servlet.RequestDispatcher.forward(...)"
because the return value of "javax.servlet.http.HttpServletRequest.getRequestDispatcher(String)" is null
原因: 响应已提交后尝试转发
修复代码:
// 修复前
if (ingredientDAO.addIngredient(ingredient)) {
resp.sendRedirect(...);
} else {
req.setAttribute("error", "添加失败");
req.getRequestDispatcher("/WEB-INF/jsp/ingredient_add.jsp").forward(req, resp);
}
// 修复后
boolean success = ingredientDAO.addIngredient(ingredient);
if (success) {
resp.sendRedirect(...);
} else {
req.setAttribute("error", "添加失败");
req.getRequestDispatcher("/WEB-INF/jsp/ingredient_add.jsp").forward(req, resp);
}
问题 3: 数据库标识符过长
现象: Identifier name '中医药汇总?...' is too long
原因: 数据库名使用中文导致标识符过长
解决方案: 使用英文数据库名 tcm_db
// DBUtil.java
private static final String URL = "jdbc:mysql://localhost:3306/tcm_db?...";
📦 部署说明
环境要求
-
JDK: 21 或更高版本
-
Maven: 3.9.6 或更高版本
-
Tomcat: 8.5.100 或更高版本
-
MySQL: 8.0.40 或更高版本
快速部署
方法一:一键启动 (推荐)
# 双击运行
一键启动.bat
方法二:手动部署
1. 编译项目
cd d:\projects\project1\tcm-management
mvn clean package
2. 部署 WAR 包
# 复制 WAR 包到 Tomcat
copy target\tcm-management.war D:\apache-tomcat-8.5.100\webapps\
3. 启动 Tomcat
D:\apache-tomcat-8.5.100\bin\startup.bat
4. 访问系统
http://localhost:8080/tcm-management/
环境变量配置
JAVA_HOME=C:\Java\jdk-21
MAVEN_HOME=D:\apache-maven-3.9.6
CATALINA_HOME=D:\apache-tomcat-8.5.100
Path=%JAVA_HOME%\bin;%MAVEN_HOME%\bin
🎨 系统界面
首页
┌────────────────────────────────────────────┐
│ 🏠 中医药数据库管理系统 │
├────────────────────────────────────────────┤
│ 📋 中成药列表 │
│ │
│ [➕ 添加中成药] [🏠 返回首页] │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ ID | 药品名称 | 类别 | 操作 │ │
│ ├──────────────────────────────────────┤ │
│ │ 1 | 感冒灵颗粒 | 感冒药 | 查看 编辑│ │
│ │ 2 | 板蓝根颗粒 | 感冒药 | 查看 编辑│ │
│ │ ... │ │
│ └──────────────────────────────────────┘ │
└────────────────────────────────────────────┘
添加页面
┌────────────────────────────────────────────┐
│ ➕ 添加中成药 │
├────────────────────────────────────────────┤
│ 药品名称:[________________] │
│ 拼 音:[________________] │
│ 类 别:[________________] │
│ 主要功效:[________________] │
│ 适应症: [________________] │
│ 用法用量:[________________] │
│ │
│ [提交] [取消] │
└────────────────────────────────────────────┘
📊 项目统计
代码统计
|
类型 |
文件数 |
代码行数 |
|---|---|---|
|
Java Servlet |
3 |
~400 行 |
|
Java DAO |
2 |
~200 行 |
|
Java Entity |
2 |
~100 行 |
|
JSP 视图 |
9 |
~900 行 |
|
SQL 脚本 |
3 |
~200 行 |
|
批处理脚本 |
5 |
~200 行 |
|
总计 |
24 |
~2000 行 |
功能模块
|
模块 |
功能点 |
状态 |
|---|---|---|
|
中成药管理 |
增删改查 |
✅ 完成 |
|
中药材管理 |
增删改查 |
✅ 完成 |
|
数据库连接 |
UTF-8 编码 |
✅ 完成 |
|
字符编码 |
防止乱码 |
✅ 完成 |
|
错误处理 |
异常捕获 |
✅ 完成 |
🚀 访问地址
-
中成药列表: http://localhost:8080/tcm-management/medicine?action=list
-
中药材列表: http://localhost:8080/tcm-management/ingredient?action=list
-
Tomcat 管理: http://localhost:8080/
📝 技术栈
后端技术
-
Java SE 21: 编程语言
-
Java Servlet 4.0: Web 框架
-
JSP (JavaServer Pages): 视图技术
-
JSTL 1.2: 标签库
-
JDBC: 数据库连接
-
Maven 3.9.6: 构建工具
前端技术
-
HTML5: 页面结构
-
CSS3: 样式设计 (渐变色、响应式)
-
JavaScript: 交互逻辑
数据库
-
MySQL 8.0.40: 关系型数据库
-
utf8mb4: 字符集 (支持中文)
服务器
-
Apache Tomcat 8.5.100: Web 应用服务器
📖 相关文档
|
文档名称 |
说明 |
|---|---|
|
|
项目说明 |
|
|
中文乱码修复脚本 |
|
|
添加错误修复脚本 |
|
|
一键启动脚本 |
|
|
部署脚本 |
|
|
环境配置指南 |
|
|
Tomcat 重启指南 |
|
|
错误修复说明 |
✅ 项目亮点
-
完整的 MVC 架构: 清晰的层次分离
-
字符编码处理: 完美解决中文乱码
-
异常处理机制: 完善的错误处理
-
渐变色设计: 现代化的 UI 界面
-
一键部署脚本: 简化部署流程
-
详细文档: 完整的开发和部署文档
🎓 学习价值
本项目涵盖了 Java Web 开发的核心技术:
-
✅ Servlet 编程
-
✅ JSP 页面开发
-
✅ JDBC 数据库操作
-
✅ Maven 项目管理
-
✅ Tomcat 服务器部署
-
✅ 字符编码处理
-
✅ 异常处理机制
-
✅ MVC 设计模式
项目完成时间: 2026-03-21
项目状态: ✅ 已完成并部署
最后更新: 2026-03-21 12:20
更多推荐



所有评论(0)