1. 从课程设计到个人名片:这个项目到底在做什么
很多同学拿到“JavaWeb课程设计”这个题目时,第一反应是去网上找一套现成的后台管理系统模板,改改标题就交差。但“展示和管理自己和家乡的信息”这个题目,其实比想象中更有发挥空间——它本质上是一个个人主页与家乡档案的结合体,既要能对外展示,又要能对内管理。说白了,就是给自己和家乡做一个“数字名片”,别人打开浏览器就能看到你的基本信息、家乡的风土人情,而你自己登录后台就能随时修改这些内容。
这个项目适合谁?如果你是刚学完Servlet、JSP、JDBC这一套JavaWeb基础,正愁没有一个完整项目练手的学生,那这个题目再合适不过。它不像电商系统那样复杂,但该有的东西一样不少:前端页面展示、后端数据管理、数据库增删改查、用户登录验证、文件上传(比如家乡照片)。麻雀虽小,五脏俱全。做完这一套,你对JavaWeb的整个请求响应流程、MVC分层思想、数据库连接池这些概念会有完全不一样的理解。
我见过太多同学把课程设计做成了“一次性代码”——能跑就行,答辩完就扔。但如果你愿意多花两天时间,把这个项目打磨成一个真正能用的个人站点,它完全可以成为你简历上的一个亮点。面试官看到你有一个自己部署上线的个人主页,比看到十个“仿某某商城”要有兴趣得多。所以这篇文章,我会从零开始,把整个项目的设计思路、技术选型、核心代码、踩坑经验全部摊开来讲,你跟着做,最后拿到的不只是一个课程设计,而是一个真正属于你自己的作品。
2. 技术选型:为什么不用Spring Boot,偏要手写Servlet
2.1 课程设计的“限制”反而是优势
如果你去问一个已经工作两年的Java开发,让他做一个个人主页,他大概率会直接上Spring Boot,半小时搭完框架,剩下的时间全在写业务。但课程设计不一样,很多学校的评分标准里明确要求“不能使用高级框架”,必须用原生Servlet和JSP。这不是老师故意为难你,而是因为框架帮你屏蔽了太多细节,而课程设计的目的恰恰是让你理解这些细节。
我举个很简单的例子:在Spring Boot里,你写一个@RestController,加一个@GetMapping,浏览器就能访问到数据。但你知道这背后发生了什么吗?请求是怎么从浏览器到Tomcat,Tomcat怎么找到你的Servlet,Servlet怎么把数据写回响应,这一整套流程,如果你只用过Spring Boot,大概率是说不清楚的。而手写Servlet,你就必须自己配置web.xml或者用注解,自己处理doGet和doPost,自己设置response.setContentType。这些看似繁琐的步骤,恰恰是JavaWeb的核心。
所以我的建议很明确:课程设计阶段,老老实实用Servlet + JSP + JDBC。等你把这套东西吃透了,再去学Spring Boot,你会发现一切都顺理成章。反过来,如果你直接上框架,遇到问题你连从哪里排查都不知道。
2.2 具体技术栈与工具版本
下面是我实际用的技术栈,都是稳定且兼容性好的版本,你可以直接抄作业:
| 技术/工具 | 版本 | 说明 |
|---|---|---|
| JDK | 1.8 或 11 | 1.8最稳,学校机房大概率是这个 |
| Tomcat | 8.5 或 9.0 | 不要用Tomcat 10,包名从javax变成jakarta,很多老代码跑不起来 |
| MySQL | 5.7 或 8.0 | 8.0需要改驱动类名和时区配置,下面会讲 |
| Servlet | 3.1+ | 用注解方式配置,不用web.xml |
| JSP | 2.3+ | 配合JSTL使用 |
| JDBC | 原生 | 不用ORM框架,自己写DAO |
| 前端 | HTML + CSS + JS + Bootstrap | Bootstrap用CDN引入,省事 |
| IDE | IntelliJ IDEA 或 Eclipse | IDEA对JavaWeb支持更好,但Eclipse更轻量 |
| 数据库连接池 | Druid 或 HikariCP | 不用也行,但用了更专业 |
注意:如果你用的是MySQL 8.0,JDBC URL必须加时区参数,否则会报
The server time zone value '???ú±ê×??±??' is unrecognized这个经典错误。具体写法在后面的数据库章节会详细说。
2.3 项目分层结构:别把所有代码塞进一个Servlet
很多同学写Servlet,喜欢在一个doGet方法里从数据库查数据、拼HTML、写响应,一个方法两三百行。这样写能跑,但后期改一个字段你就要翻半天。正确的做法是分层,哪怕是小项目也要分层。我的分层是这样的:
- entity层:放实体类,比如
PersonalInfo、HometownInfo、User,每个类对应数据库一张表,字段一一对应。 - dao层:放数据库操作类,比如
PersonalInfoDao、HometownInfoDao,里面写增删改查方法,只负责和数据库打交道。 - service层:放业务逻辑,比如
PersonalInfoService,调用dao层的方法,做一些参数校验、事务控制。小项目里这一层可以很薄,但一定要有。 - servlet层:放Servlet类,负责接收请求、调用service、把结果存到request域、转发到JSP页面。
- util层:放工具类,比如
DBUtil(数据库连接)、FileUploadUtil(文件上传)。
这样分层之后,你的代码结构会非常清晰。答辩的时候老师问你“你的项目怎么分层的”,你能说出来,印象分直接拉满。
3. 数据库设计:两张核心表加一张用户表就够了
3.1 表结构设计思路
这个项目的数据模型其实很简单,核心就是“谁的信息”和“哪里的信息”。我设计了三个表:
第一张表:user(用户表)
这张表用来做后台登录验证。课程设计不需要太复杂的权限系统,一个管理员账号就够了。
CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `password` varchar(100) NOT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;密码我建议存MD5或者SHA-256的哈希值,不要存明文。虽然课程设计没人会攻击你,但这是一个习惯问题。你可以在UserDao里写一个md5()方法,注册和登录的时候都转一下。
第二张表:personal_info(个人信息表)
这张表存你自己的信息,因为只有一个人,所以实际上只会有一条记录。
CREATE TABLE `personal_info` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(50) DEFAULT NULL, `age` int(11) DEFAULT NULL, `gender` varchar(10) DEFAULT NULL, `major` varchar(100) DEFAULT NULL, `school` varchar(100) DEFAULT NULL, `hobby` varchar(255) DEFAULT NULL, `intro` text, `avatar` varchar(255) DEFAULT NULL, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;注意avatar字段存的是图片的路径,不是图片本身。图片文件存在服务器磁盘上,数据库只存路径,这是标准做法。
第三张表:hometown_info(家乡信息表)
这张表存家乡的信息,可以有多条,比如家乡的简介、特产、景点、美食,每条一个分类。
CREATE TABLE `hometown_info` ( `id` int(11) NOT NULL AUTO_INCREMENT, `category` varchar(50) DEFAULT NULL, `title` varchar(100) DEFAULT NULL, `content` text, `image` varchar(255) DEFAULT NULL, `sort_order` int(11) DEFAULT '0', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;category字段用来区分是“特产”还是“景点”还是“美食”,前端展示的时候可以按分类筛选。sort_order用来控制显示顺序,数字越小越靠前。
3.2 数据库连接工具类:一个坑点一个坑点地填
数据库连接是JavaWeb里最容易出问题的地方。我见过太多同学卡在ClassNotFoundException: com.mysql.jdbc.Driver这个错误上。下面是我写的DBUtil类,把常见的坑都处理了:
public class DBUtil { private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; private static final String URL = "jdbc:mysql://localhost:3306/personal_site?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"; private static final String USERNAME = "root"; private static final String PASSWORD = "你的密码"; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { try { if (rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (ps != null) ps.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }这里有几个关键点:
- 驱动类名:MySQL 5.x用
com.mysql.jdbc.Driver,MySQL 8.x用com.mysql.cj.jdbc.Driver。写错了直接报ClassNotFoundException。 - URL参数:
useSSL=false关掉SSL警告,serverTimezone=Asia/Shanghai解决时区问题,allowPublicKeyRetrieval=true解决MySQL 8的公共密钥检索问题。这三个参数缺一不可。 - 字符编码:
characterEncoding=utf8保证中文不乱码。数据库建表的时候也要用utf8mb4。
提示:如果你不想每次都手动关闭连接,可以用
try-with-resources语法,Java 7以上都支持。但课程设计里为了展示你对资源关闭的理解,手动关闭反而更好。
4. 后端核心功能实现:从登录到增删改查
4.1 登录功能:Session和Filter的配合
登录功能看起来简单,但要做好也不容易。我的实现思路是:
- 用户访问
login.jsp,输入用户名和密码。 - 表单提交到
LoginServlet,Servlet调用UserService.login()方法。 - Service调用
UserDao.findByUsername()查出用户,比对密码哈希。 - 如果匹配成功,把用户对象存到
session.setAttribute("user", user),然后重定向到后台首页。 - 如果失败,把错误信息存到
request域,转发回login.jsp显示。
关键点在于密码比对。不要直接比明文,而是把用户输入的密码也做一次哈希,再和数据库里的哈希值比。这样即使数据库泄露,攻击者也拿不到原始密码。
// LoginServlet核心代码 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); User user = userService.login(username, password); if (user != null) { request.getSession().setAttribute("user", user); response.sendRedirect(request.getContextPath() + "/admin/index.jsp"); } else { request.setAttribute("msg", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); } }光有登录还不够,还要防止未登录用户直接访问后台页面。这时候就需要一个登录拦截Filter:
@WebFilter("/admin/*") public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; Object user = request.getSession().getAttribute("user"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); } }这个Filter拦截所有/admin/下的请求,检查Session里有没有user对象。没有就踢回登录页。这样你就不用在每个后台Servlet里都写一遍登录检查了。
4.2 个人信息管理:单条记录的更新逻辑
个人信息因为只有一条记录,所以逻辑比较特殊。我的做法是:
- 页面加载时,先查数据库里有没有记录。如果有,就显示出来供编辑;如果没有,就显示空表单。
- 提交时,判断是新增还是更新。如果数据库里已经有记录,就执行UPDATE;如果没有,就执行INSERT。
这里有一个容易忽略的点:如果用户只改了部分字段,其他字段怎么办?比如用户只改了爱好,没改名字。如果你用UPDATE personal_info SET hobby=? WHERE id=?,那名字字段不会被覆盖,没问题。但如果你用UPDATE personal_info SET name=?, age=?, ... WHERE id=?,而表单里没传name,那name就会被更新成null。
所以我的建议是:表单里所有字段都要有对应的input,即使不修改也要提交原值。或者用动态SQL,只更新非空字段。课程设计里用第一种方案更简单。
// PersonalInfoServlet核心代码 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); PersonalInfo info = new PersonalInfo(); info.setName(request.getParameter("name")); info.setAge(Integer.parseInt(request.getParameter("age"))); info.setGender(request.getParameter("gender")); info.setMajor(request.getParameter("major")); info.setSchool(request.getParameter("school")); info.setHobby(request.getParameter("hobby")); info.setIntro(request.getParameter("intro")); // 处理头像上传 Part part = request.getPart("avatar"); if (part != null && part.getSize() > 0) { String fileName = System.currentTimeMillis() + "_" + part.getSubmittedFileName(); String savePath = request.getServletContext().getRealPath("/uploads/"); part.write(savePath + File.separator + fileName); info.setAvatar("uploads/" + fileName); } personalInfoService.saveOrUpdate(info); response.sendRedirect("personalInfo?action=edit"); }文件上传这里用的是Servlet 3.0的Part接口,需要在Servlet上加@MultipartConfig注解。注意getRealPath在打包成war后路径会变,生产环境建议用外部目录,但课程设计里用getRealPath没问题。
4.3 家乡信息管理:一对多的增删改查
家乡信息是一对多的关系,一个人可以有多个家乡条目。所以后台需要一个列表页,显示所有条目,每条后面有编辑和删除按钮。
列表页的实现很简单,HometownDao.findAll()返回一个List,Servlet把它存到request域,JSP用JSTL的<c:forEach>遍历显示。
新增和编辑共用一个表单页面。如果是新增,表单action指向hometown?action=add;如果是编辑,action指向hometown?action=update&id=xxx。编辑的时候要先根据id查出原数据,回填到表单里。
删除操作要注意级联删除。如果家乡条目有对应的图片文件,删除记录的时候也要把图片文件删掉,否则服务器上会堆积一堆没用的图片。这个逻辑可以放在Service层:
public void delete(int id) { HometownInfo info = hometownDao.findById(id); if (info != null && info.getImage() != null) { File file = new File(uploadPath + info.getImage()); if (file.exists()) { file.delete(); } } hometownDao.deleteById(id); }4.4 前台展示页面:JSP + JSTL的经典组合
前台页面是给访客看的,所以不需要登录。我的设计是:
index.jsp:首页,展示个人信息概览和家乡信息的前几条。personal.jsp:个人详细信息页。hometown.jsp:家乡信息列表页,按分类展示。detail.jsp:家乡信息详情页。
这些页面都用JSP + JSTL来渲染数据。比如在hometown.jsp里:
<c:forEach items="${hometownList}" var="item"> <div class="card"> <img src="${pageContext.request.contextPath}/${item.image}" alt="${item.title}"> <h3>${item.title}</h3> <p>${item.content}</p> <a href="hometown?action=detail&id=${item.id}">查看详情</a> </div> </c:forEach>注意${pageContext.request.contextPath}这个写法,它保证你的路径在项目部署到不同context下都能正确。很多同学直接写/uploads/xxx.jpg,结果项目名不是ROOT的时候就404了。
5. 前端页面:不用太花哨,但要干净整洁
5.1 页面布局的取舍
课程设计的前端不需要多炫酷,但一定要干净、整齐、能看。我见过有的同学用了一堆花哨的动画和渐变,结果文字都看不清,老师打开第一眼就皱眉。我的建议是:
- 用Bootstrap 5的CDN,直接引入,不用自己写太多CSS。
- 主色调选一个舒服的颜色,比如深蓝或墨绿,不要大红大紫。
- 字体用系统默认的,不要引入奇怪的中文字体,加载慢还容易乱码。
- 图片统一尺寸,不要一张大一张小,看起来像拼凑的。
首页的布局我推荐“上中下”结构:顶部导航栏,中间个人信息卡片,下面家乡信息网格。导航栏放“首页”“我的家乡”“后台管理”三个链接。个人信息卡片左边放头像,右边放姓名、专业、学校、爱好。家乡信息用卡片网格展示,每行三个,响应式布局。
5.2 后台管理页面的交互细节
后台页面是给自己用的,所以效率优先。我的做法是:
- 左侧固定一个侧边栏,放“个人信息管理”“家乡信息管理”“退出登录”三个菜单。
- 右侧是内容区,根据菜单切换显示不同的管理面板。
- 表格列表页加一个“新增”按钮,放在右上角。
- 每条记录后面放“编辑”和“删除”两个按钮,删除按钮加一个
onclick确认框。
function confirmDelete(id) { if (confirm("确定要删除这条记录吗?删除后不可恢复。")) { window.location.href = "hometown?action=delete&id=" + id; } }这个确认框很重要。我测试的时候手滑点了一次删除,直接把一条数据删了,还好是测试数据。如果没有确认框,正式数据被误删就麻烦了。
5.3 中文乱码问题的终极解决方案
中文乱码是JavaWeb的经典问题,我把它拆成三个层面来解决:
第一层:数据库层面。建库建表的时候用utf8mb4字符集,连接URL里加characterEncoding=utf8。
第二层:请求层面。在Servlet里,doPost方法第一行写request.setCharacterEncoding("UTF-8")。如果是doGet请求,Tomcat 8以后默认是UTF-8,不用额外处理。但如果你用的是Tomcat 7,需要在server.xml的Connector里加URIEncoding="UTF-8"。
第三层:响应层面。在Servlet里写response.setContentType("text/html;charset=UTF-8")。如果是JSP页面,在页面顶部写<%@ page contentType="text/html;charset=UTF-8" language="java" %>。
这三层都设置好,中文乱码基本就不会出现了。如果还有问题,检查一下IDE的文件编码设置,确保所有文件都是UTF-8。
6. 部署与测试:从本地跑通到上线访问
6.1 本地Tomcat部署的注意事项
在IDEA里配置Tomcat运行项目,有几个地方容易出问题:
- Artifact配置:确保把项目的编译输出和依赖库都加到Artifact里,否则运行时会报
ClassNotFoundException。 - Application context:建议设置成
/personal,这样访问路径就是http://localhost:8080/personal/,比默认的/更清晰。 - 端口冲突:如果8080被占用,改成8081或别的端口。改完之后记得同步改URL。
- 热部署:IDEA默认是热部署,改了JSP不用重启,但改了Java代码需要重启。如果发现改了代码没生效,先检查是不是没重启。
6.2 常见报错与排查思路
下面是我在开发过程中实际遇到过的报错,以及排查过程:
| 报错信息 | 原因 | 解决方案 |
|---|---|---|
ClassNotFoundException: com.mysql.jdbc.Driver | 驱动类名写错或驱动包没导入 | MySQL 8用com.mysql.cj.jdbc.Driver,检查lib目录下有没有mysql-connector-java.jar |
Access denied for user 'root'@'localhost' | 数据库密码错误 | 检查DBUtil里的密码,确认MySQL服务已启动 |
The server time zone value ... is unrecognized | MySQL 8时区问题 | URL加serverTimezone=Asia/Shanghai |
404 Not Found | 路径写错或Servlet没配置 | 检查@WebServlet注解的路径,检查JSP里的链接 |
500 Internal Server Error | Java代码抛异常 | 看Tomcat日志,定位到具体行号 |
| 中文乱码 | 编码没设置 | 按上面三层方案逐一检查 |
| 图片上传后不显示 | 路径错误 | 检查getRealPath返回的路径,检查JSP里的${pageContext.request.contextPath} |
排查报错的核心思路是:先看日志,再看代码,最后看配置。Tomcat的日志在logs/catalina.out里,IDEA的控制台也会输出。不要凭感觉猜,日志里写得清清楚楚。
6.3 项目打包与部署到服务器
如果你想让别人也能访问你的个人主页,可以把项目打包成war,部署到一台有公网IP的服务器上。步骤是:
- 在IDEA里用Maven的
package命令打包,生成personal.war。 - 把war上传到服务器的Tomcat的
webapps目录。 - 启动Tomcat,war会自动解压。
- 浏览器访问
http://服务器IP:8080/personal/。
注意服务器上的MySQL要单独安装和配置,数据库要手动导入。另外服务器防火墙要开放8080端口,否则外网访问不了。
提示:如果只是课程设计答辩,本地跑通就够了,不需要真的上线。但如果你想让这个项目成为简历亮点,上线是值得的。
7. 我在这个项目里踩过的坑和总结的经验
7.1 文件上传的路径陷阱
文件上传是我踩坑最多的地方。最开始我用request.getRealPath("/uploads")获取上传目录,本地跑没问题。但后来我把项目打包成war部署到另一台机器上,发现上传的图片全都不见了。原因是war包每次重启都会重新解压,之前上传的文件被覆盖了。
解决方案是:把上传目录设在项目外部,比如/data/uploads/,然后在Tomcat里配置一个虚拟路径映射到这个目录。这样重启项目不会丢文件。课程设计里如果只在本地跑,用getRealPath没问题,但你要知道这个坑的存在。
7.2 Session过期导致的“莫名其妙退出登录”
Tomcat默认的Session过期时间是30分钟。如果你在后台编辑一条很长的信息,超过30分钟才提交,Session已经过期了,提交的时候就会被Filter踢回登录页,之前编辑的内容全丢了。这个体验非常糟糕。
我的解决方案是:在web.xml里把Session过期时间设长一点,比如60分钟。同时在后台页面加一个定时器,每25分钟发一个AJAX请求刷新Session。这样只要页面开着,Session就不会过期。
<session-config> <session-timeout>60</session-timeout> </session-config>7.3 数据库连接未关闭导致的“连接数耗尽”
这个问题在课程设计里不容易发现,因为访问量小。但如果你反复刷新页面,每次请求都创建一个新连接而不关闭,很快MySQL就会报Too many connections。我一开始写DAO的时候,只在方法最后关闭了ResultSet和Statement,忘了关Connection。后来加了一个finally块统一关闭,问题就解决了。
所以我的经验是:只要用了Connection,就必须在finally里关闭。不管有没有抛异常,都要关。这是铁律。
7.4 关于代码注释和文档
最后说一个容易被忽略的点:注释和文档。课程设计答辩的时候,老师很可能会翻你的代码。如果你的代码里一个注释都没有,变量名全是a、b、c,老师的第一印象就会很差。我的建议是:
- 每个类头部写一段注释,说明这个类是干什么的。
- 每个方法上面写注释,说明参数和返回值。
- 关键逻辑行写行内注释,解释为什么这么写。
- 项目根目录放一个
README.md,写清楚项目怎么部署、数据库怎么导入、默认账号密码是什么。
这些花不了多少时间,但能让你的项目看起来专业很多。我见过一个同学,代码写得一般,但注释和文档非常完整,最后拿了优秀。另一个同学代码功能更多,但注释几乎没有,只拿了中等。这个差距就是这么来的。
7.5 后续可以扩展的方向
如果你做完基础功能还有时间,可以考虑加这几个扩展:
- 富文本编辑器:把家乡介绍的textarea换成富文本编辑器,支持加粗、插入图片、调整格式。
- 评论功能:访客可以在家乡信息下面留言,你可以在后台审核和回复。
- 数据统计:用ECharts做一个简单的访问量统计图表,展示每天有多少人访问了你的主页。
- 多用户支持:把个人信息表加上user_id字段,支持多个用户各自管理自己的信息。
这些扩展不需要全部做完,选一两个你觉得有意思的加进去,就能让你的项目在答辩时脱颖而出。但前提是基础功能要先跑通,不要本末倒置。
我在实际做这个项目的过程中,最大的体会是:不要怕麻烦,该写的代码一行都不要省。很多同学为了省事,把该分层的代码全塞在一起,该关的连接不关,该做的校验不做。结果就是项目能跑但经不起细看,答辩的时候被老师一问就露馅。JavaWeb这套东西虽然老,但它是理解Web开发本质的最好途径。你把这个项目认真做一遍,以后学任何框架都会觉得轻松很多。