JavaWeb课程设计实战:个人信息与家乡信息展示管理全攻略
1. 这个课程设计到底在做什么1.1 从标题拆解真实需求“JavaWeb课程设计——制作一个页面展示和管理自己和家乡的信息”这个标题看起来像是一道普通的期末大作业但如果你真的动手做过就会发现它其实是一个麻雀虽小、五脏俱全的完整Web应用。它要求你同时搞定三件事信息展示、信息管理、前后端数据打通。我见过太多同学拿到这个题目之后第一反应就是打开IDE新建一个项目然后开始写实体类。写到一半发现数据库还没建建完数据库发现前端页面不知道用什么写最后交上去的东西是一个能跑但自己都说不清楚逻辑的“缝合怪”。问题出在哪儿出在没有在动手之前把需求拆清楚。这个课程设计的核心需求可以拆成四个层次数据层你需要存储两类信息——你自己的个人信息姓名、学号、专业、兴趣爱好、个人简介等和你家乡的信息地名、人口、特产、风景名胜、历史沿革等。这两类信息之间可能存在关联比如“我来自某个地方”这就是一个典型的一对一或一对多关系。展示层需要一个页面把这些信息以友好的方式呈现出来。不是把数据库里的字段直接堆在页面上而是要有布局、有层次、有视觉引导。管理层需要提供增删改查的操作入口。课程设计里通常不会要求多用户权限体系但至少要有一个管理界面能对信息进行编辑。交互层前端和后端之间通过HTTP请求交互数据格式通常是JSON页面可能是服务端渲染也可能是前后端分离。把这四层想清楚了技术选型的方向也就基本确定了。1.2 为什么选择JavaWeb技术栈很多同学会问现在都什么年代了为什么还要用JavaWeb做课程设计用Python Flask或者Node.js不是更快吗这个问题我在带低年级同学做课设的时候被问过无数次。答案其实很现实JavaWeb技术栈是企业级开发中最成熟、资料最丰富、就业面最广的选择。你学完Servlet、JSP、JDBC这一套再去学Spring Boot会发现很多东西是相通的。而且Java的强类型特性在课程设计这种需要严谨逻辑的场景下反而能帮你少犯低级错误。具体到技术选型我建议的方案是这样的层次推荐技术备选方案选择理由前端HTML CSS JavaScriptJSP JSTL前后端分离更清晰调试方便后端Servlet JDBCSpring Boot课程设计用原生Servlet更能体现理解数据库MySQLH2 / SQLite免费、资料多、学校机房通常已安装服务器TomcatJetty与Servlet规范兼容性最好构建工具MavenGradle依赖管理清晰IDE支持好这个选型方案的核心逻辑是用最基础的技术栈把原理跑通而不是用最时髦的框架把功能堆出来。课程设计的评分标准里老师看的是你对请求响应流程的理解、对数据库操作的理解、对页面渲染的理解而不是你用了多少个高级注解。注意如果你所在的课程明确要求使用Spring Boot或者SSM框架那就按照课程要求来。但即使是用框架底层的Servlet原理和JDBC操作逻辑仍然是必须理解的否则遇到问题你连排查的方向都没有。1.3 适合谁来参考这篇内容这篇内容适合以下几类读者正在做JavaWeb课程设计的同学你可以直接参考这里的技术方案和代码结构但建议不要照抄理解之后自己写一遍。刚学完Servlet和JSP想找个练手项目的初学者这个项目规模适中不会让你望而生畏也不会简单到学不到东西。需要快速回顾JavaWeb核心知识点的开发者项目虽小但涵盖了请求处理、数据库连接、页面渲染、会话管理等核心概念。如果你已经能熟练使用Spring Boot MyBatis Vue做完整项目那这篇内容对你来说可能偏基础。但如果你连“一个HTTP请求从浏览器发出到收到响应中间经历了什么”都说不清楚那这个项目就是很好的补课材料。2. 动手之前先把这些想清楚2.1 数据库设计别急着写代码我见过太多同学一上来就打开IDEA新建项目然后写到一半发现数据库表结构不对回头改代码改得痛不欲生。数据库设计是地基地基没打好上面盖什么都是歪的。这个项目至少需要两张表一张存个人信息一张存家乡信息。但这里有一个关键决策点个人信息和家乡信息是一对一还是一对多如果你只展示一个家乡那就是一对一可以在个人信息表里直接加一个家乡ID字段。但如果你想展示“我出生在A地成长在B地祖籍在C地”这种多家乡场景那就需要一对多需要一张中间表或者在外键上做文章。考虑到课程设计的实际需求我建议采用一对多的设计因为这样更能体现数据库设计能力而且扩展性更好。具体表结构如下-- 个人信息表 CREATE TABLE personal_info ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 姓名, student_id VARCHAR(20) UNIQUE COMMENT 学号, major VARCHAR(100) COMMENT 专业, hobby VARCHAR(200) COMMENT 兴趣爱好, bio TEXT COMMENT 个人简介, avatar_url VARCHAR(255) COMMENT 头像地址, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 家乡信息表 CREATE TABLE hometown_info ( id INT PRIMARY KEY AUTO_INCREMENT, person_id INT NOT NULL COMMENT 关联的个人信息ID, city_name VARCHAR(100) NOT NULL COMMENT 城市名称, province VARCHAR(50) COMMENT 省份, population VARCHAR(50) COMMENT 人口数量, specialty VARCHAR(200) COMMENT 特产, scenic_spot TEXT COMMENT 风景名胜, history TEXT COMMENT 历史沿革, description TEXT COMMENT 补充描述, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (person_id) REFERENCES personal_info(id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有几个设计细节值得展开说字符集选择utf8mb4而不是utf8。很多同学建表的时候随手写个utf8结果存emoji或者某些特殊汉字的时候报错。utf8mb4是真正的“完整版UTF-8”能存4字节的字符现在MySQL 8.0默认就是utf8mb4但如果你用的是5.7或者更早版本一定要手动指定。外键约束用ON DELETE CASCADE。这意味着删除个人信息的时候关联的家乡信息会自动删除。这在课程设计里是一个加分项体现了你对数据完整性的理解。但要注意有些学校的数据库课程可能不教外键如果你的课程没讲过可以不加改成在应用层手动维护关联关系。时间戳字段用DEFAULT CURRENT_TIMESTAMP。这样插入数据的时候不用手动写时间数据库会自动填。updated_at字段加上ON UPDATE CURRENT_TIMESTAMP之后每次更新记录时间戳都会自动刷新省去了在代码里手动设置的麻烦。2.2 项目结构Maven标准目录用Maven管理项目的好处是目录结构标准化依赖管理清晰。一个典型的JavaWeb项目结构应该是这样的javaweb-hometown/ ├── pom.xml ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ └── hometown/ │ │ │ ├── dao/ -- 数据访问层 │ │ │ │ ├── PersonalInfoDao.java │ │ │ │ └── HometownInfoDao.java │ │ │ ├── model/ -- 实体类 │ │ │ │ ├── PersonalInfo.java │ │ │ │ └── HometownInfo.java │ │ │ ├── servlet/ -- 控制器层 │ │ │ │ ├── IndexServlet.java │ │ │ │ ├── PersonalInfoServlet.java │ │ │ │ └── HometownInfoServlet.java │ │ │ └── util/ -- 工具类 │ │ │ └── DBUtil.java │ │ ├── resources/ │ │ │ └── db.properties -- 数据库配置 │ │ └── webapp/ │ │ ├── WEB-INF/ │ │ │ └── web.xml -- 部署描述符 │ │ ├── css/ │ │ │ └── style.css │ │ ├── js/ │ │ │ └── main.js │ │ ├── index.jsp -- 首页 │ │ ├── edit.jsp -- 编辑页 │ │ └── detail.jsp -- 详情页 │ └── test/ │ └── java/ -- 测试代码这个结构的关键在于分层。DAO层只管数据库操作Servlet层只管请求处理和页面跳转Model层只管数据结构。分层的好处是当你需要把MySQL换成其他数据库的时候只需要改DAO层当你需要把JSP换成Thymeleaf的时候只需要改视图层。各层之间通过接口交互互不干扰。提示如果你的项目不需要这么复杂可以把DAO和Servlet合并但我不建议这么做。课程设计的评分标准里代码结构清晰度往往占很大比重。分层清晰的项目即使功能少一点得分也通常比功能多但代码乱成一团的项目高。2.3 连接池别再用DriverManager了很多教程里教JDBC连接数据库的写法是这样的Connection conn DriverManager.getConnection(url, username, password);这种写法在课程设计里能用但有一个致命问题每次请求都新建一个数据库连接用完就关频繁创建和销毁连接的开销非常大。如果页面上有多个数据库操作性能问题会非常明显。正确的做法是使用连接池。连接池的原理很简单预先创建一批数据库连接放在池子里需要的时候从池子里借一个用完还回去而不是每次都新建。常用的连接池有HikariCP、Druid、C3P0等。我推荐用HikariCP因为它是Spring Boot默认的连接池性能好、配置简单。在pom.xml里加一个依赖dependency groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId version5.0.1/version /dependency然后写一个DBUtil工具类来管理连接池public class DBUtil { private static HikariDataSource dataSource; static { Properties props new Properties(); try (InputStream is DBUtil.class.getClassLoader() .getResourceAsStream(db.properties)) { props.load(is); } catch (IOException e) { throw new RuntimeException(数据库配置加载失败, e); } HikariConfig config new HikariConfig(); config.setJdbcUrl(props.getProperty(jdbc.url)); config.setUsername(props.getProperty(jdbc.username)); config.setPassword(props.getProperty(jdbc.password)); config.setMaximumPoolSize(10); config.setMinimumIdle(2); config.setConnectionTimeout(30000); config.setIdleTimeout(600000); config.setMaxLifetime(1800000); dataSource new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(AutoCloseable... resources) { for (AutoCloseable resource : resources) { if (resource ! null) { try { resource.close(); } catch (Exception e) { // 记录日志不抛出 } } } } }db.properties文件放在resources目录下jdbc.urljdbc:mysql://localhost:3306/hometown_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4 jdbc.usernameroot jdbc.passwordyour_password这里有几个参数需要解释一下maximumPoolSize10最大连接数。课程设计这种并发量极低的场景10个连接绰绰有余。设太大了反而浪费资源。minimumIdle2最小空闲连接数。保持至少2个连接随时可用避免每次请求都要新建连接。connectionTimeout30000获取连接的超时时间30秒。超过这个时间还没拿到连接就抛异常。maxLifetime1800000连接的最大存活时间30分钟。超过这个时间连接会被回收重建防止数据库端主动断开导致的无效连接。注意连接池的close()方法和Connection的close()方法语义不同。连接池的close()是把连接归还给池子而不是真正关闭物理连接。所以你在代码里调用conn.close()的时候实际上连接并没有断只是回到了池子里等待下一次借用。3. 核心功能模块的实操落地3.1 实体类与DAO层数据操作的基石实体类Model是数据库表在Java中的映射。以PersonalInfo为例public class PersonalInfo { private int id; private String name; private String studentId; private String major; private String hobby; private String bio; private String avatarUrl; private Timestamp createdAt; private Timestamp updatedAt; private ListHometownInfo hometowns; // 一对多关联 // 省略getter和setter方法 // 建议用IDE自动生成或者用Lombok的Data注解 }这里有一个设计决策要不要在PersonalInfo里直接放一个List 这取决于你的查询需求。如果首页需要同时展示个人信息和家乡信息那放一个List会方便很多一次查询就能拿到所有数据。但如果你的页面是分开的个人信息页只展示个人信息家乡信息页只展示家乡信息那就不需要这个List避免不必要的关联查询。DAO层的核心方法是增删改查。以PersonalInfoDao为例public class PersonalInfoDao { public PersonalInfo getById(int id) { String sql SELECT * FROM personal_info WHERE id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, id); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { return mapRow(rs); } } } catch (SQLException e) { throw new RuntimeException(查询个人信息失败, e); } return null; } public int insert(PersonalInfo info) { String sql INSERT INTO personal_info (name, student_id, major, hobby, bio, avatar_url) VALUES (?, ?, ?, ?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, info.getName()); ps.setString(2, info.getStudentId()); ps.setString(3, info.getMajor()); ps.setString(4, info.getHobby()); ps.setString(5, info.getBio()); ps.setString(6, info.getAvatarUrl()); ps.executeUpdate(); try (ResultSet rs ps.getGeneratedKeys()) { if (rs.next()) { return rs.getInt(1); } } } catch (SQLException e) { throw new RuntimeException(插入个人信息失败, e); } return -1; } public void update(PersonalInfo info) { String sql UPDATE personal_info SET name?, student_id?, major?, hobby?, bio?, avatar_url? WHERE id?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, info.getName()); ps.setString(2, info.getStudentId()); ps.setString(3, info.getMajor()); ps.setString(4, info.getHobby()); ps.setString(5, info.getBio()); ps.setString(6, info.getAvatarUrl()); ps.setInt(7, info.getId()); ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(更新个人信息失败, e); } } public void delete(int id) { String sql DELETE FROM personal_info WHERE id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, id); ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(删除个人信息失败, e); } } private PersonalInfo mapRow(ResultSet rs) throws SQLException { PersonalInfo info new PersonalInfo(); info.setId(rs.getInt(id)); info.setName(rs.getString(name)); info.setStudentId(rs.getString(student_id)); info.setMajor(rs.getString(major)); info.setHobby(rs.getString(hobby)); info.setBio(rs.getString(bio)); info.setAvatarUrl(rs.getString(avatar_url)); info.setCreatedAt(rs.getTimestamp(created_at)); info.setUpdatedAt(rs.getTimestamp(updated_at)); return info; } }这段代码里有几个值得注意的点用PreparedStatement而不是Statement。PreparedStatement会预编译SQL能有效防止SQL注入。比如如果用户输入的名字是; DROP TABLE personal_info; --用Statement拼接SQL的话表就被删了用PreparedStatement则会把整个字符串当作一个普通参数值处理。用try-with-resources自动关闭资源。Java 7引入的try-with-resources语法会在代码块结束时自动调用close()方法不用手动写finally块。这比传统的try-catch-finally写法简洁很多而且不容易漏关资源。插入时获取自增主键。Statement.RETURN_GENERATED_KEYS参数配合getGeneratedKeys()方法可以拿到刚插入记录的自增ID。这个ID在后续关联插入家乡信息的时候会用到。3.2 Servlet层请求处理与页面跳转Servlet是JavaWeb的核心它负责接收HTTP请求、调用业务逻辑、返回响应。在这个项目里至少需要三个Servlet一个处理首页展示一个处理个人信息的管理一个处理家乡信息的管理。以IndexServlet为例WebServlet(/index) public class IndexServlet extends HttpServlet { private PersonalInfoDao personalInfoDao new PersonalInfoDao(); private HometownInfoDao hometownInfoDao new HometownInfoDao(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 设置编码防止中文乱码 req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); // 查询个人信息假设只有一条记录取第一条 PersonalInfo personalInfo personalInfoDao.getFirst(); if (personalInfo ! null) { // 查询关联的家乡信息 ListHometownInfo hometowns hometownInfoDao.getByPersonId(personalInfo.getId()); personalInfo.setHometowns(hometowns); } // 把数据放入请求作用域 req.setAttribute(personalInfo, personalInfo); // 转发到JSP页面渲染 req.getRequestDispatcher(/index.jsp).forward(req, resp); } }这里有几个关键点编码设置必须在获取任何参数之前。req.setCharacterEncoding(UTF-8)这行代码要放在doGet或doPost方法的第一行。如果放在后面参数已经按照默认编码解析了再设置就来不及了。响应端的resp.setContentType(text/html;charsetUTF-8)也要在获取Writer之前设置。用forward而不是redirect。forward是服务器内部跳转浏览器地址栏不变请求作用域里的数据可以传递到目标页面。redirect是客户端跳转浏览器会发起一个新的请求之前的请求作用域数据全部丢失。首页展示这种场景用forward更合适。Servlet的线程安全问题。Servlet默认是单例的多个请求会共享同一个Servlet实例。所以Servlet里不要定义可变的实例变量。上面代码里的DAO对象虽然是实例变量但DAO本身没有状态不存储请求相关的数据所以是线程安全的。如果你在Servlet里定义了private int count这样的变量多个用户同时访问就会出现数据错乱。3.3 前端页面用JSP还是纯HTML这是一个很多同学纠结的问题。JSP是JavaWeb的传统视图技术可以在HTML里嵌入Java代码。但现在更推荐的做法是前后端分离后端只返回JSON数据前端用纯HTMLJavaScript渲染。两种方案的对比对比项JSP方案前后端分离方案学习成本低会HTML就能写需要学AJAX和DOM操作调试难度低直接在页面看结果需要看Network面板代码可维护性差Java和HTML混在一起好职责清晰课程设计评分中规中矩通常更高体现综合能力部署复杂度低一个war包搞定需要处理跨域等问题我的建议是如果你的课程没有明确要求用JSP那就用前后端分离。虽然前期配置麻烦一点但代码结构更清晰而且更贴近实际开发场景。用纯HTMLJavaScript的方案后端Servlet返回JSONWebServlet(/api/personal) public class PersonalInfoApiServlet extends HttpServlet { private PersonalInfoDao dao new PersonalInfoDao(); private ObjectMapper objectMapper new ObjectMapper(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType(application/json;charsetUTF-8); PersonalInfo info dao.getFirst(); if (info ! null) { ListHometownInfo hometowns new HometownInfoDao().getByPersonId(info.getId()); info.setHometowns(hometowns); } objectMapper.writeValue(resp.getWriter(), info); } Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); resp.setContentType(application/json;charsetUTF-8); // 从请求体读取JSON PersonalInfo info objectMapper.readValue(req.getInputStream(), PersonalInfo.class); dao.update(info); MapString, Object result new HashMap(); result.put(code, 200); result.put(message, 更新成功); objectMapper.writeValue(resp.getWriter(), result); } }前端页面用fetch API调用// 加载个人信息 async function loadPersonalInfo() { const resp await fetch(/api/personal); const data await resp.json(); renderPersonalInfo(data); } // 保存个人信息 async function savePersonalInfo() { const info { name: document.getElementById(name).value, major: document.getElementById(major).value, hobby: document.getElementById(hobby).value, bio: document.getElementById(bio).value }; const resp await fetch(/api/personal, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(info) }); const result await resp.json(); if (result.code 200) { alert(保存成功); } }提示用ObjectMapperJackson库处理JSON的时候需要在pom.xml里加依赖。如果不想引入额外依赖也可以手动拼接JSON字符串但那样容易出错不推荐。3.4 页面布局与样式别让页面看起来像上个世纪的产物课程设计的评分里页面美观度往往占10%到20%。很多同学功能都实现了但页面丑得让人不想看第二眼最后得分不高。其实不需要多高深的设计功底掌握几个基本原则就能让页面看起来专业很多。原则一用卡片式布局代替表格布局。表格布局是上个世纪的产物现在更流行卡片式设计。每个信息模块用一个带圆角和阴影的卡片包裹视觉上更舒适。.card { background: #fff; border-radius: 12px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); padding: 24px; margin-bottom: 20px; transition: box-shadow 0.3s ease; } .card:hover { box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12); }原则二用系统字体栈别用花哨的字体。系统字体栈能保证在不同操作系统上都有良好的显示效果body { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, Microsoft YaHei, sans-serif; line-height: 1.6; color: #333; background: #f5f7fa; }原则三用Flexbox或Grid做布局别用float。float布局需要清除浮动容易出问题。Flexbox和Grid是现代CSS布局的标准方案.container { max-width: 960px; margin: 0 auto; padding: 20px; } .info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; }原则四加一点微交互。按钮hover变色、卡片hover浮起、页面加载淡入这些小细节能让页面看起来更有质感.btn { padding: 10px 24px; border: none; border-radius: 8px; background: #4a90d9; color: #fff; cursor: pointer; transition: background 0.2s ease, transform 0.1s ease; } .btn:hover { background: #3a7bc8; } .btn:active { transform: scale(0.97); }4. 踩过的坑和排查经验4.1 中文乱码问题一个字符集引发的血案中文乱码是JavaWeb开发中最常见的问题没有之一。乱码可能出现在三个环节请求参数乱码、响应内容乱码、数据库存储乱码。请求参数乱码的表现是表单提交的中文到了Servlet里变成了问号或者乱码。原因是Tomcat默认用ISO-8859-1解析请求体。解决方法是在获取参数之前设置编码req.setCharacterEncoding(UTF-8);如果是GET请求的参数乱码还需要检查Tomcat的server.xml配置。在Connector标签上加URIEncodingUTF-8Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 URIEncodingUTF-8 /响应内容乱码的表现是Servlet返回的中文在浏览器里显示为乱码。解决方法是设置响应类型resp.setContentType(text/html;charsetUTF-8);数据库存储乱码的表现是数据存进去的时候是好的取出来变成乱码或者存进去就是乱码。解决方法是确保数据库、表、连接三个层面的字符集都是utf8mb4-- 建库时指定 CREATE DATABASE hometown_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 建表时指定 CREATE TABLE personal_info (...) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 连接URL指定 jdbc:mysql://localhost:3306/hometown_db?characterEncodingutf8mb4注意如果以上三处都设置了还是乱码检查一下MySQL的my.ini配置文件里的character-set-server参数。有些MySQL安装的时候默认不是utf8mb4需要手动改配置文件然后重启服务。4.2 数据库连接失败从报错信息定位问题数据库连接失败是新手最容易卡住的地方。常见的报错和排查思路如下报错信息可能原因排查方法Communications link failureMySQL服务没启动检查服务是否运行端口是否被占用Access denied for user用户名或密码错误用命令行登录验证检查配置文件Unknown database数据库不存在用SHOW DATABASES确认检查URL拼写No suitable driver驱动依赖没引入检查pom.xml里有没有mysql-connector-javaPublic Key Retrieval is not allowedMySQL 8.0安全设置URL加allowPublicKeyRetrievaltrueThe server time zone value is unrecognized时区配置问题URL加serverTimezoneAsia/Shanghai我个人的排查习惯是先确认MySQL服务在运行再用命令行登录验证账号密码最后检查Java代码里的连接配置。这三步能解决90%的连接问题。4.3 页面404和500看懂报错信息404是路径问题500是代码问题。这两个错误的排查思路完全不同。404 Not Found的常见原因Servlet的WebServlet注解路径和请求路径不一致。比如注解写的是/personal但前端请求的是/personalInfo。web.xml里配置的url-pattern和实际请求不匹配。项目部署路径不对。比如项目部署在/hometown下但请求的时候没加这个前缀。JSP文件放错了位置。放在WEB-INF下的JSP不能直接通过URL访问必须通过Servlet转发。500 Internal Server Error的常见原因空指针异常。比如DAO返回null但代码里直接调用了返回对象的方法。类型转换异常。比如把字符串直接转成int但字符串内容不是数字。SQL语法错误。比如表名写错、字段名写错、SQL语句少了个逗号。类找不到异常。比如pom.xml里加了依赖但没刷新Maven。排查500错误的关键是看服务器日志。Tomcat的日志在logs目录下catalina.out文件里会有完整的异常堆栈信息。根据堆栈信息定位到具体的代码行问题就迎刃而解了。4.4 前端请求跨域一个让人头疼的问题如果你采用前后端分离方案前端页面和后端Servlet不在同一个端口下比如前端用Live Server跑在5500端口后端Tomcat跑在8080端口浏览器会因为同源策略拦截请求报CORS错误。解决跨域有三种方案方案一后端设置响应头。在Servlet里加resp.setHeader(Access-Control-Allow-Origin, *); resp.setHeader(Access-Control-Allow-Methods, GET, POST, PUT, DELETE, OPTIONS); resp.setHeader(Access-Control-Allow-Headers, Content-Type);如果是OPTIONS预检请求直接返回200if (OPTIONS.equalsIgnoreCase(req.getMethod())) { resp.setStatus(HttpServletResponse.SC_OK); return; }方案二用代理服务器。在开发阶段用Nginx或者webpack-dev-server的proxy功能把API请求转发到后端。这样前端请求的还是同源地址不存在跨域问题。方案三把前端页面也部署到Tomcat里。这样前后端同源自然没有跨域问题。这是课程设计里最简单的方案推荐使用。提示Access-Control-Allow-Origin设置为*表示允许任何来源访问这在开发阶段没问题但生产环境不建议这么用。课程设计里为了省事可以这么写但要知道这不是最佳实践。5. 让课程设计脱颖而出的加分项5.1 数据可视化用图表展示家乡数据如果家乡信息里有“人口数量”“面积”这类数值数据可以用图表展示出来比纯文字直观得多。用ECharts这个库几行代码就能画出漂亮的图表// 引入ECharts后 const chart echarts.init(document.getElementById(populationChart)); chart.setOption({ title: { text: 家乡人口变化趋势 }, tooltip: { trigger: axis }, xAxis: { type: category, data: [2010, 2015, 2020, 2023] }, yAxis: { type: value }, series: [{ data: [520000, 580000, 610000, 630000], type: line, smooth: true, areaStyle: { opacity: 0.3 } }] });ECharts的引入方式很简单在HTML里加一个script标签script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script注意如果学校机房不能访问外网需要把echarts.min.js下载到本地放到js目录下引用。5.2 图片上传让页面不再单调纯文字的个人信息页面看起来很枯燥如果能上传头像和家乡风景照页面会生动很多。图片上传的核心逻辑是前端用input typefile选择文件用FormData包装后发送。后端用req.getPart()获取上传的文件保存到服务器磁盘。把文件路径存到数据库页面渲染时用img标签展示。MultipartConfig(maxFileSize 5 * 1024 * 1024) WebServlet(/api/upload) public class UploadServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Part part req.getPart(file); String fileName System.currentTimeMillis() _ part.getSubmittedFileName(); String uploadPath getServletContext().getRealPath(/uploads); File uploadDir new File(uploadPath); if (!uploadDir.exists()) { uploadDir.mkdirs(); } part.write(uploadPath File.separator fileName); resp.setContentType(application/json;charsetUTF-8); resp.getWriter().write({\url\:\uploads/ fileName \}); } }MultipartConfig注解是必须的没有这个注解req.getPart()会返回null。maxFileSize限制上传文件大小防止用户上传超大文件把服务器磁盘撑爆。5.3 响应式设计手机也能正常看现在很多老师会用手机打开你的项目看效果。如果页面在手机上显示错乱印象分会大打折扣。加一个viewport meta标签就能解决大部分问题meta nameviewport contentwidthdevice-width, initial-scale1.0然后用媒体查询针对小屏幕做适配media (max-width: 768px) { .container { padding: 10px; } .info-grid { grid-template-columns: 1fr; } .card { padding: 16px; } }这样在手机上多列布局会自动变成单列内边距也会缩小阅读体验会好很多。6. 一些个人体会做这个课程设计的过程中我最大的感受是看起来简单的东西真正动手做的时候才会发现到处都是细节。数据库字符集、连接池配置、编码设置、跨域处理每一个点单独拿出来都不难但组合在一起就容易顾此失彼。我的建议是不要等到所有代码都写完再测试。写完DAO层就写一个main方法测一下能不能查到数据写完Servlet就用浏览器访问一下看能不能返回正确的页面写完前端就打开控制台看有没有报错。每完成一个模块就验证一个模块问题越早发现越好解决。另外注释和文档真的很重要。课程设计通常需要提交设计文档如果你代码里注释写得清楚文档写起来会轻松很多。而且老师在验收的时候看到你代码里清晰的注释印象分也会高一些。最后分享一个实用技巧用Postman或者浏览器开发者工具测试接口。不要每次都通过页面操作来验证后端逻辑那样效率太低了。直接用Postman发请求看返回的JSON数据对不对能节省大量时间。浏览器开发者工具的Network面板也能看到每个请求的详细信息包括请求头、响应头、请求体、响应体排查问题非常方便。