简介:这是一份面向高校Java Web初学者与课程设计学生的期末大作业实战项目源码,聚焦Web应用开发全流程实践,助力学生高效完成课程设计、期末答辩及高分作业提交。资源包含124个文件,涵盖43个核心Java业务逻辑类、19个JSP页面模板、28张界面截图与图标素材(jpg/gif/png)、11个运行依赖jar包(含druid连接池、MySQL驱动、kaptcha验证码组件)、8个XML配置文件(web.xml、Spring/SpringMVC配置等)以及SQL建表脚本、CSS/JS前端样式脚本等,结构完整、模块清晰,5.43MB压缩包轻量易部署。已有957人学习下载,项目注释详尽、功能完备——含用户登录、后台管理、数据增删改查、验证码校验等典型Web模块,界面简洁美观,部署后即可本地运行演示,特别适合零基础学生快速理解MVC分层架构与Java Web技术栈整合逻辑。
1. 这不是“抄作业”,而是用 Java Web 技术栈完成一个可运行、可调试、可扩展的期末项目闭环
很多同学拿到“Java Web 期末大作业”这个任务时,第一反应是搜压缩包、解压、改个数据库连接就交差——结果部署时报ClassNotFoundException,登录页空白,控制台刷满NullPointerException,甚至连 Tomcat 启动都失败。其实,“完美大作业项目源码”里的“完美”,不指界面炫酷或功能堆砌,而在于:结构清晰可读、依赖明确可控、分层逻辑完整、HTTP 请求链路可追踪、数据库操作有事务边界、前后端交互符合 Servlet 规范。它适合刚学完 JSP/Servlet/MySQL/JDBC 的学生,也适合作为 Java Web 入门者验证 MVC 拆分是否合理的第一手实践样本。本文不提供任何 ZIP 下载链接,而是带你从零还原一个典型 Java Web 项目应有的骨架、关键配置和必调参数——所有代码基于 JDK 8+、Tomcat 9.0.x、MySQL 5.7+,不依赖 Spring Boot 自动装配,专注 Servlet 原生能力落地。
2. 用标准 Java Web 工程结构跑通最小可运行版本:从 web.xml 到 HelloServlet
2.1 标准目录结构与部署描述符 web.xml 的强制约定
Java Web 项目必须遵循 Servlet 规范定义的目录布局,否则 Tomcat 无法识别为合法 Web 应用。核心结构如下(路径以src/main/webapp为根):
WEB-INF/ ├── web.xml ← 必须存在,且 DTD 或 schema 版本需匹配 Tomcat ├── classes/ ← 编译后的 .class 文件存放处(非 Maven 默认,但原生项目常用) └── lib/ ← 所有第三方 JAR(如 mysql-connector-java.jar) index.jsp login.jsp提示:Maven 项目中
webapp目录默认对应src/main/webapp,但若直接用 IDE 新建 Dynamic Web Project,需确认WebContent是否被正确映射为部署根目录;否则WEB-INF不在正确位置会导致 404 或 500 错误。
web.xml是整个项目的入口契约。Tomcat 启动时首先解析它,注册 Servlet、Filter 和 Listener。以下是最小可用配置(Servlet 3.1+ 兼容,适用于 Tomcat 9):
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <servlet> <servlet-name>HelloServlet</servlet-name> <servlet-class>com.example.HelloServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>HelloServlet</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping> </web-app>该配置声明:当请求/hello时,由com.example.HelloServlet类处理。注意<servlet-class>必须是全限定类名,且.class文件必须位于WEB-INF/classes/com/example/HelloServlet.class路径下(即包路径转为文件夹结构)。
2.2 编写第一个 Servlet:理解 request/response 生命周期与中文乱码根源
HelloServlet.java示例(需编译后放入WEB-INF/classes/com/example/):
package com.example; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 关键:设置响应编码,否则浏览器显示乱码 resp.setContentType("text/html;charset=UTF-8"); PrintWriter out = resp.getWriter(); out.println("<h1>你好,Java Web!</h1>"); out.println("<p>当前时间:" + System.currentTimeMillis() + "</p>"); } }逻辑说明:
doGet()处理 GET 请求,doPost()处理 POST;若未重写service()方法,父类HttpServlet会根据 HTTP 方法自动分发。resp.setContentType("text/html;charset=UTF-8")必须在getWriter()之前调用,否则PrintWriter会按默认 ISO-8859-1 编码输出,中文变成??。req.setCharacterEncoding("UTF-8")用于处理 POST 请求体中的中文参数(如表单提交),需在req.getParameter()之前调用;GET 参数乱码则需在 Tomcat 的server.xml中为 Connector 添加URIEncoding="UTF-8"属性。
验证方式:启动 Tomcat,在浏览器访问http://localhost:8080/your-app-context/hello。若看到标题和时间戳,说明 Servlet 容器已正确加载并执行。
2.3 数据库连接池初始化:为什么不能在 Servlet 中 new Connection()
直接在doGet()里写DriverManager.getConnection(...)是典型反模式:每次请求新建连接,耗尽数据库资源,且无异常兜底。标准做法是在ServletContextListener中初始化连接池,并存入ServletContext全局作用域:
package com.example.listener; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.annotation.WebListener; import javax.sql.DataSource; import com.alibaba.druid.pool.DruidDataSource; @WebListener public class ContextLoaderListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { DruidDataSource dataSource = new DruidDataSource(); dataSource.setUrl("jdbc:mysql://localhost:3306/edu_db?useSSL=false&serverTimezone=Asia/Shanghai"); dataSource.setUsername("root"); dataSource.setPassword("123456"); dataSource.setInitialSize(5); dataSource.setMaxActive(20); dataSource.setMinIdle(2); // 设置连接测试 SQL(MySQL 用 SELECT 1) dataSource.setValidationQuery("SELECT 1"); dataSource.setTestOnBorrow(true); // 将数据源绑定到 ServletContext,供后续 Servlet 获取 sce.getServletContext().setAttribute("dataSource", dataSource); } @Override public void contextDestroyed(ServletContextEvent sce) { DataSource ds = (DataSource) sce.getServletContext().getAttribute("dataSource"); if (ds instanceof DruidDataSource) { ((DruidDataSource) ds).close(); } } }参数说明:
initialSize=5:应用启动时预创建 5 个连接,避免首请求延迟。maxActive=20:最大并发连接数,需根据 MySQLmax_connections配置调整,避免连接拒绝。validationQuery="SELECT 1":借出连接前执行该 SQL 检测有效性,防止 stale connection。testOnBorrow=true:开启借连接时检测,比testWhileIdle更及时,但略增开销。
注意:
DruidDataSource需引入druid-1.2.16.jar(兼容 JDK 8),不可用 HikariCP 1.0+(要求 JDK 11)。若不用连接池,至少应将Connection创建封装为静态工具类,但依然不推荐。
3. 实现用户登录模块:从 JDBC 查询到 Session 状态管理
3.1 用户实体与 DAO 层:分离 SQL 与业务逻辑
User.java(POJO,无框架依赖):
package com.example.model; public class User { private Long id; private String username; private String password; // 实际项目中应为加密后字符串 private String email; // getter/setter 省略 }UserDao.java(纯 JDBC 操作,使用try-with-resources自动关闭):
package com.example.dao; import com.example.model.User; import javax.sql.DataSource; import java.sql.*; import java.util.ArrayList; import java.util.List; public class UserDao { private final DataSource dataSource; public UserDao(DataSource dataSource) { this.dataSource = dataSource; } public User findByUsername(String username) throws SQLException { String sql = "SELECT id, username, password, email FROM users WHERE username = ?"; try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User user = new User(); user.setId(rs.getLong("id")); user.setUsername(rs.getString("username")); user.setPassword(rs.getString("password")); user.setEmail(rs.getString("email")); return user; } return null; } } } public List<User> findAll() throws SQLException { String sql = "SELECT id, username, email FROM users ORDER BY id DESC"; try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql)) { List<User> users = new ArrayList<>(); while (rs.next()) { User user = new User(); user.setId(rs.getLong("id")); user.setUsername(rs.getString("username")); user.setEmail(rs.getString("email")); users.add(user); } return users; } } }逻辑说明:
- 构造函数注入
DataSource,避免硬编码获取方式,便于单元测试 mock。 PreparedStatement防止 SQL 注入,?占位符由 JDBC 驱动转义。try-with-resources确保Connection、Statement、ResultSet在异常或正常执行后自动关闭,无需手动finally块。
3.2 登录 Servlet:校验密码、管理 Session、跳转控制
LoginServlet.java(处理/loginPOST 请求):
package com.example.servlet; import com.example.dao.UserDao; import com.example.model.User; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import javax.sql.DataSource; import java.io.IOException; public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 设置请求体编码(处理 POST 表单中文) req.setCharacterEncoding("UTF-8"); // 2. 获取参数 String username = req.getParameter("username"); String password = req.getParameter("password"); // 3. 从 ServletContext 获取数据源 DataSource ds = (DataSource) req.getServletContext().getAttribute("dataSource"); UserDao userDao = new UserDao(ds); try { User user = userDao.findByUsername(username); if (user != null && password.equals(user.getPassword())) { // 4. 登录成功:存入 Session 并重定向 HttpSession session = req.getSession(true); session.setAttribute("currentUser", user); session.setMaxInactiveInterval(1800); // 30 分钟无操作失效 resp.sendRedirect(req.getContextPath() + "/welcome.jsp"); } else { // 5. 登录失败:设置错误信息,转发回登录页 req.setAttribute("errorMsg", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } catch (Exception e) { e.printStackTrace(); req.setAttribute("errorMsg", "系统繁忙,请稍后再试"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } }关键点解析:
req.getSession(true):true表示如果 Session 不存在则创建新 Session;false则只获取不创建。session.setMaxInactiveInterval(1800):单位为秒,Tomcat 默认为 30 分钟,此处显式设置更清晰。resp.sendRedirect()是客户端重定向,URL 变化;req.getRequestDispatcher().forward()是服务器端转发,URL 不变,且能传递request属性。- 密码明文比较仅用于教学演示,真实项目必须使用 BCrypt 或 Argon2 加密存储与校验。
3.3 welcome.jsp:从 Session 读取用户信息并展示
welcome.jsp(放在webapp/根目录):
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head><title>欢迎页</title></head> <body> <h2>欢迎回来,<c:out value="${sessionScope.currentUser.username}"/>!</h2> <p>邮箱:<c:out value="${sessionScope.currentUser.email}"/></p> <a href="${pageContext.request.contextPath}/logout">退出登录</a> </body> </html>说明:
<c:out>是 JSTL 标签,自动 HTML 转义,防止 XSS;需引入jstl-1.2.jar和standard-1.1.2.jar(JDK 8 兼容)。sessionScope.currentUser直接从 Session 中取值,无需 Java 代码脚本片段(<% %>),更安全简洁。${pageContext.request.contextPath}动态获取应用上下文路径,避免硬编码/myapp。
4. 部署与排错:定位常见 404/500/空白页问题的三步法
4.1 404 Not Found 的 3 个高频原因与验证命令
| 现象 | 可能原因 | 验证方式 | 解决方案 |
|---|---|---|---|
访问/hello报 404 | web.xml中<url-pattern>与实际 URL 不匹配 | 查看 Tomcat 日志catalina.out是否有SRVE0242E: Error caught | 确保<url-pattern>/hello</url-pattern>与浏览器地址栏路径完全一致(不含多余斜杠) |
访问login.jsp报 404 | JSP 文件未放在webapp/目录下,或路径拼写错误 | 在 Tomcatwebapps/your-app/目录下执行ls -R | grep login.jsp | 将login.jsp放在webapp/login.jsp,而非webapp/WEB-INF/login.jsp(后者不可直接访问) |
访问/login报 404 | LoginServlet类未编译,或web.xml中<servlet-class>包名错误 | 进入webapps/your-app/WEB-INF/classes/com/example/,执行ls -l LoginServlet.class | 检查 Java 文件包声明package com.example.servlet;与web.xml中<servlet-class>是否一致 |
提示:Tomcat 启动后,检查
webapps/目录下是否生成了你的应用文件夹(如mywebapp),且其内部结构符合标准。若无,则可能是 IDE 部署配置错误(如 Eclipse 的 Deployment Assembly 未包含webapp)。
4.2 500 Internal Server Error 的日志定位技巧
500 错误本质是 Servlet 抛出未捕获异常。关键日志在logs/catalina.out,但需快速过滤:
# 实时跟踪最新错误(Linux/macOS) tail -f $CATALINA_HOME/logs/catalina.out | grep -A 5 -B 5 "Exception" # 查看最近 10 行异常堆栈 grep -A 10 "java.lang." $CATALINA_HOME/logs/catalina.out | tail -n 20常见 500 场景及修复:
ClassNotFoundException: com.mysql.jdbc.Driver→ MySQL 8+ 驱动类名改为com.mysql.cj.jdbc.Driver,且 URL 需加serverTimezone=Asia/Shanghai。SQLException: Access denied for user→ 检查web.xml或代码中数据库用户名密码是否与 MySQL 实际账号一致,注意空格。NullPointerException在UserDao.findByUsername()→ 确认dataSource是否成功注入(System.out.println(dataSource)),若为null,说明ContextLoaderListener未触发或web.xml缺少<listener>声明。
4.3 页面空白(无报错)的 DOM 与网络层双检法
页面加载后一片空白,控制台无 JS 错误,HTTP 状态码为 200,此时问题在服务端输出流或 JSP 编译:
- 检查响应内容:浏览器开发者工具 → Network → 点击请求 → Preview / Response 标签页,看返回 HTML 是否为空或含
JasperException。 - 强制重新编译 JSP:删除
work/Catalina/localhost/your-app/目录(Tomcat 缓存 JSP 编译结果),重启 Tomcat。 - 验证 JSP 语法:
<%= new java.util.Date() %>若报错,说明 JSP 引擎未启用,检查web.xml是否遗漏jsp-config或 Tomcatconf/web.xml中 JSP Servlet 映射是否被注释。
5. 进阶技巧:用 Filter 实现统一登录拦截与请求日志
5.1 登录状态拦截 Filter:避免每个 Servlet 重复校验
AuthFilter.java(实现javax.servlet.Filter):
package com.example.filter; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; @javax.servlet.annotation.WebFilter(urlPatterns = {"/admin/*", "/profile.jsp"}) public class AuthFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; HttpSession session = request.getSession(false); // 检查 Session 中是否存在 currentUser if (session == null || session.getAttribute("currentUser") == null) { // 未登录,重定向到登录页,并保存原始请求路径 String originalUrl = request.getRequestURI(); if (request.getQueryString() != null) { originalUrl += "?" + request.getQueryString(); } session.setAttribute("redirectAfterLogin", originalUrl); response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } // 已登录,放行 chain.doFilter(req, resp); } }说明:
@WebFilter(urlPatterns = {"/admin/*", "/profile.jsp"})声明拦截规则,/admin/*匹配所有/admin/xxx路径。session.getAttribute("currentUser") == null是核心判断,比检查username字段更可靠(避免字段名拼写错误)。session.setAttribute("redirectAfterLogin", originalUrl)记录原始请求,登录成功后可跳转回该页(需在LoginServlet中读取并重定向)。
5.2 请求日志 Filter:记录 IP、URL、耗时,替代 System.out
package com.example.filter; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.time.Instant; @javax.servlet.annotation.WebFilter(urlPatterns = "/*", asyncSupported = true) public class LoggingFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; String ip = getClientIP(request); String url = request.getRequestURL().toString(); long start = Instant.now().toEpochMilli(); try { chain.doFilter(req, resp); } finally { long end = Instant.now().toEpochMilli(); System.out.printf("[LOG] %s | %s | %dms%n", ip, url, (end - start)); } } private String getClientIP(HttpServletRequest request) { String xff = request.getHeader("X-Forwarded-For"); if (xff != null && !xff.isEmpty()) { return xff.split(",")[0].trim(); } return request.getRemoteAddr(); } }参数意义:
urlPatterns = "/*"拦截所有请求,包括静态资源(CSS/JS),生产环境建议排除*.js*.css。asyncSupported = true允许异步请求通过此 Filter,避免AsyncContext相关异常。X-Forwarded-For头用于 Nginx 反向代理场景,获取真实客户端 IP;若无代理,直接用getRemoteAddr()。
5.3 Filter 配置优先级与执行顺序控制
多个 Filter 时,执行顺序由@WebFilter的dispatcherTypes和urlPatterns决定,但更可靠的方式是使用web.xml显式排序:
<filter> <filter-name>LoggingFilter</filter-name> <filter-class>com.example.filter.LoggingFilter</filter-class> </filter> <filter> <filter-name>AuthFilter</filter-name> <filter-class>com.example.filter.AuthFilter</filter-class> </filter> <filter-mapping> <filter-name>LoggingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name>AuthFilter</filter-name> <url-pattern>/admin/*</url-pattern> </filter-mapping>规则:filter-mapping在web.xml中的声明顺序即执行顺序。上例中,请求/admin/user时,先执行LoggingFilter,再执行AuthFilter。若需AuthFilter优先(如防止未授权访问日志),则交换两个<filter-mapping>的位置。
提示:Filter 链中任意 Filter 调用
response.getWriter().write()后,后续 Filter 和 Servlet 将无法写入响应体,导致空白页。务必确保 Filter 只做拦截/记录,不直接输出 HTML。
本文还有配套的精品资源,点击获取