简介:本资源是《Java Web数据库系统应用开发与实例》配套源码与教学视频合集,面向Java初学者及Web开发入门者,系统解决Servlet、JSP、JDBC与数据库协同开发中的核心实践问题,覆盖MVC架构搭建、Tomcat部署、SQL增删改查、事务处理及MyBatis等ORM技术衔接。压缩包共884个文件,含163个Java源码(含Servlet与DAO实现)、140个JSP页面(含登录、报表、用户管理等完整视图)、84个JS脚本(支持Ajax交互)、48个AVI教学视频(含系统演示、共通Servlet设计、报表打印等关键环节)、17个SQL建表与初始化脚本,以及XML配置、Jar依赖等工程必需文件,整体222.5MB,结构完整、即下即用。已有471人学习下载,提供从环境配置→代码分析→运行调试→功能验证的全链路学习支撑,特别适合通过真实项目案例理解Web层与数据库层的交互逻辑与常见排错路径。
1. 这不是又一个“Servlet+JSP+MySQL”教学Demo:它是一套能跑通真实业务闭环的Java Web数据库系统骨架
你手头可能已经攒了三四个“学生选课系统”“图书管理系统”的课程设计压缩包,解压进去全是index.jsp、web.xml、DBUtil.java,连个登录密码校验都写在 Servlet 里——这种结构根本撑不起一次像样的答辩,更别说拿去当毕设原型。而这份《Java Web数据库系统应用开发与实例》不一样:它从需求建模开始就按真实项目节奏走,用分层架构把表现层、业务逻辑层、数据访问层切得清清楚楚;数据库设计包含外键约束、索引优化和事务边界标注;所有 DAO 方法都封装了异常分类处理,不是简单 throw SQLException;连前端页面都做了基础 XSS 过滤和表单防重复提交。它适合两类人:一类是正在赶毕设 deadline 的同学,需要一份结构完整、代码可读、答辩时能讲出设计取舍的参考基线;另一类是刚转 Java Web 的初级开发者,想跳过“Hello World 式 CRUD”,直接看一个带用户权限、数据校验、操作日志、错误回滚的最小可行系统怎么组织。这不是教你怎么写 for 循环,而是告诉你:当需求文档里出现“管理员可导出近30天订单汇总(含状态统计)”这句话时,你的 Controller 层该接什么参数、Service 层该调几个 DAO、Mapper XML 里 GROUP BY 怎么写才不拖垮查询性能。
2. 总体架构设计:MVC 分层 + DAO 模式 + 基础安全加固的四层结构
这套系统没有堆砌 Spring Boot 或微服务概念,而是用原生 Java EE 技术栈(Servlet 3.1+JSP 2.3+JSTL 1.2)构建了一个清晰可控的四层结构:表现层(View)、控制层(Controller)、业务层(Service)、数据访问层(DAO)。每一层只依赖下一层,且严格禁止跨层调用——比如 JSP 页面不能直接 new Service 实例,Controller 不能手动拼 SQL 字符串。这种“笨办法”看似繁琐,但对理解 Web 请求生命周期、调试数据流转路径、以及后续升级到 Spring MVC 极其关键。整个系统围绕“用户-角色-权限-资源”主干展开,核心实体包括User、Role、Permission、Menu、Order、Product六张表,通过外键和中间表(如user_role、role_permission)实现 RBAC 权限模型。数据库采用 MySQL 5.7,字符集统一为 utf8mb4,所有文本字段均设 NOT NULL DEFAULT '',数值字段设 DEFAULT 0,避免空值引发的 NPE 和逻辑歧义。部署包中已预置init_db.sql脚本,含建库、建表、初始化管理员账号(admin/123456)及菜单树结构,执行后即可启动 Tomcat 访问/login.jsp。
2.1 表现层设计:JSP + JSTL + 自定义标签 + 基础前端防护
表现层不追求炫酷 UI,重点解决三个实际问题:模板复用、安全输出、状态反馈。所有页面继承自base.jsp,该文件定义<head>公共资源(jQuery 3.6、Bootstrap 4.6 CSS/JS)、顶部导航栏、左侧菜单栏(动态加载当前用户权限菜单),并通过<c:if test="${not empty errorMsg}">统一渲染错误提示。关键安全实践如下:
所有用户输入内容(如用户名、商品描述)在 JSP 中一律使用
<c:out value="${user.name}" />输出,而非${user.name},自动转义<,>,&,',";表单提交前通过 JavaScript 拦截空提交和重复点击:
<form onsubmit="return checkForm(this) && disableSubmit(this)"> <!-- input fields --> <button type="submit" id="submitBtn">提交</button> </form> <script> function disableSubmit(form) { const btn = form.querySelector('#submitBtn'); btn.disabled = true; btn.textContent = '提交中...'; return true; } function checkForm(form) { if (!form.username.value.trim()) { alert('用户名不能为空'); form.username.focus(); return false; } return true; } </script>提示:此 JS 逻辑放在每个表单页底部,不抽离为公共 JS,避免因缓存导致旧版本未更新而失效。
自定义标签
permission:has封装权限校验逻辑,用于控制按钮/链接可见性:<%@ taglib prefix="permission" uri="/WEB-INF/tlds/permission.tld" %> <permission:has permissionCode="order:export"> <a href="${pageContext.request.contextPath}/exportOrder.do?days=30">导出30天订单</a> </permission:has>标签处理器
HasPermissionTag从 Session 获取当前用户权限集合,在doStartTag()中判断是否包含指定权限码,决定是否跳过 body 内容。这比在 JSP 里写 Java 脚本(<% if(...) { %>)更规范、更易测试。
2.2 控制层设计:Servlet 路由 + 统一异常拦截 + 请求参数预处理
Controller 层由BaseServlet统一继承,所有具体 Servlet(如UserServlet、OrderServlet)只需重写doGet()/doPost()并调用super.handleRequest(request, response)即可。BaseServlet完成三件事:① 解析请求路径获取 action 名(如/user.do?action=save→save);② 通过反射调用对应doSave(HttpServletRequest, HttpServletResponse)方法;③ 捕获所有未处理异常,统一跳转至error.jsp并记录日志。关键代码如下:
// BaseServlet.java protected void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action = request.getParameter("action"); if (action == null || action.trim().isEmpty()) { action = "index"; // 默认入口 } try { Method method = this.getClass().getDeclaredMethod("do" + capitalize(action), HttpServletRequest.class, HttpServletResponse.class); method.setAccessible(true); method.invoke(this, request, response); } catch (Exception e) { logger.error("Action [{}] failed", action, e); request.setAttribute("errorMsg", "系统繁忙,请稍后再试"); request.getRequestDispatcher("/error.jsp").forward(request, response); } } private String capitalize(String str) { return str.substring(0, 1).toUpperCase() + str.substring(1); }注意:
doIndex()方法必须存在,否则默认路由会报 404。我们约定所有 action 方法名首字母大写(如doSave,doList),避免大小写混淆。
此外,BaseServlet在doPost()中强制设置请求编码:request.setCharacterEncoding("UTF-8"),并在doGet()中对 URL 参数做 ISO-8859-1 → UTF-8 转码(因 Tomcat 8+ 默认 GET 编码为 ISO),确保中文参数不乱码。所有请求参数均经ParamValidator类预校验:非空字段调用StringUtils.isNotBlank(),数字字段用NumberUtils.toInt()并捕获NumberFormatException,日期字段用SimpleDateFormat解析并捕获ParseException。校验失败则返回errorMsg到页面,不进入业务逻辑。
2.3 业务层设计:Service 接口 + 实现类 + 事务边界 + 领域对象封装
Service 层采用接口+实现类模式,如UserService接口定义save(User user)、findByUsername(String username)等方法,UserServiceImpl实现类负责具体逻辑。所有 Service 方法均以@Override显式标注,便于 IDE 导航和单元测试 Mock。事务管理通过TransactionManager工具类实现,不依赖容器(如 Tomcat JTA),而是手动控制 Connection 的setAutoCommit(false)和commit()/rollback()。典型事务场景代码如下:
// UserServiceImpl.java public boolean save(User user) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 插入用户基本信息 int userId = userDao.insert(conn, user); if (userId <= 0) throw new ServiceException("用户插入失败"); // 2. 插入用户角色关联 if (!userRoleDao.insert(conn, userId, user.getRoleId())) { throw new ServiceException("角色关联插入失败"); } conn.commit(); return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } throw new ServiceException("保存用户失败", e); } finally { DBUtil.closeConnection(conn); } }提示:事务边界必须包裹所有数据库操作,且 Connection 必须来自同一
DBUtil.getConnection()调用,否则 rollback 无效。我们禁用 ThreadLocal 存 Connection,因 Servlet 容器线程复用可能导致连接泄漏。
领域对象(如User)不直接暴露数据库字段,而是封装 getter/setter,并添加业务方法:User.isActive()返回status == 1,User.getFullname()返回lastName + firstName。避免在 JSP 或 Servlet 中写user.getStatus() == 1这类硬编码逻辑,提升可维护性。
2.4 数据访问层设计:DAO 接口 + JDBC Template 封装 + SQL 外置化
DAO 层彻底分离 SQL 与 Java 代码。每个实体对应一个 DAO 接口(如UserDao)和一个实现类(如UserDaoImpl),SQL 语句全部外置到sql/user.sql文件中,格式为:
# INSERT_USER INSERT INTO t_user (username, password, real_name, status, create_time) VALUES (?, ?, ?, ?, ?) # SELECT_USER_BY_USERNAME SELECT * FROM t_user WHERE username = ?SqlLoader工具类在应用启动时(ServletContextListener)读取所有.sql文件,解析为Map<String, String>缓存。DAO 实现类通过SqlLoader.getSql("INSERT_USER")获取 SQL,再交由JdbcTemplate执行。JdbcTemplate封装了PreparedStatement设置参数、ResultSet映射为对象、异常转换等重复逻辑:
// JdbcTemplate.java public <T> List<T> query(String sql, RowMapper<T> rowMapper, Object... params) { List<T> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { setParameters(ps, params); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { list.add(rowMapper.mapRow(rs)); } } } catch (SQLException e) { throw new DataAccessException("Query failed: " + sql, e); } return list; } private void setParameters(PreparedStatement ps, Object[] params) throws SQLException { for (int i = 0; i < params.length; i++) { if (params[i] == null) { ps.setNull(i + 1, Types.VARCHAR); } else if (params[i] instanceof String) { ps.setString(i + 1, (String) params[i]); } else if (params[i] instanceof Integer) { ps.setInt(i + 1, (Integer) params[i]); } // ... 其他类型 } }注意:
RowMapper是函数式接口,每个 DAO 查询都需提供其实现,如new UserRowMapper(),确保 ResultSet 到对象的映射逻辑集中、可测试。
3. 核心模块实现:用户管理、权限控制、订单处理与导出功能落地
本系统最常被答辩老师追问的四个模块——用户增删改查、角色权限分配、订单状态流转、Excel 导出——全部给出可运行、可讲解、可扩展的实现。每个模块均遵循前述分层原则,且关键逻辑附带注释说明设计意图。例如,权限控制不是简单“有权限就显示按钮”,而是贯穿请求链路:前端隐藏无权按钮 → Controller 校验权限码 → Service 层再次校验(防绕过)→ DAO 层 SQL 加权限过滤(如WHERE user_id IN (SELECT user_id FROM user_role WHERE role_id IN (?)))。这种纵深防御虽增加代码量,但让答辩时能清晰回答“如果黑客篡改 URL 怎么办”。
3.1 用户管理模块:注册、登录、信息修改与状态开关
用户管理是系统基石,其实现需兼顾安全性与用户体验。注册流程强制邮箱验证(模拟发送邮件,实际写入t_email_verify表并生成 24 小时有效 token),登录采用盐值哈希(SHA-256 + 随机 salt 存于数据库),密码重置需验证原密码或绑定手机。关键代码如下:
// UserServiceImpl.java public boolean register(User user, String verifyCode) { // 1. 校验验证码(从 Redis 或 DB 查) if (!verifyCodeService.validate(user.getEmail(), verifyCode)) { throw new ServiceException("验证码错误或已过期"); } // 2. 生成 salt 并哈希密码 String salt = RandomStringUtils.randomAlphanumeric(16); String hashedPwd = DigestUtils.sha256Hex(user.getPassword() + salt); user.setPassword(hashedPwd); user.setSalt(salt); // 3. 插入用户(含 salt 字段) return userDao.insertWithSalt(user) > 0; } // LoginServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) { String username = request.getParameter("username"); String password = request.getParameter("password"); User user = userService.findByUsername(username); if (user == null || !user.isActive()) { request.setAttribute("errorMsg", "用户名不存在或已被禁用"); request.getRequestDispatcher("/login.jsp").forward(request, response); return; } // 盐值哈希校验 String hashedInput = DigestUtils.sha256Hex(password + user.getSalt()); if (!hashedInput.equals(user.getPassword())) { request.setAttribute("errorMsg", "密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); return; } // 登录成功,存入 Session request.getSession().setAttribute("currentUser", user); response.sendRedirect(request.getContextPath() + "/index.jsp"); }提示:
verifyCodeService.validate()方法内部会检查验证码是否匹配且未超时(create_time > NOW() - INTERVAL 24 HOUR),避免暴力破解。盐值(salt)必须每用户独立,不可全局固定。
用户信息修改支持头像上传(<input type="file">),后端用 Apache Commons FileUpload 解析,限制文件大小 ≤2MB、类型为 JPG/PNG,保存路径为upload/avatar/,数据库仅存相对路径(如/avatar/u1001.jpg)。状态开关(启用/禁用用户)通过 AJAX 调用UserServlet?action=toggleStatus&userId=1001,返回 JSON{success:true, status:"disabled"},前端刷新表格行状态,避免整页刷新。
3.2 权限控制模块:RBAC 模型落地与动态菜单生成
权限模块基于标准 RBAC(Role-Based Access Control)模型,但做了轻量级适配:不引入复杂策略引擎,而是用四张表支撑——t_role(角色)、t_permission(权限点)、t_menu(菜单项)、t_role_permission(角色-权限关联)。关键设计点在于“菜单”与“权限”分离:t_menu定义左侧导航栏结构(如“订单管理”、“用户中心”),t_permission定义具体操作(如“order:list”、“user:delete”),一个菜单可对应多个权限,一个权限可被多个菜单引用。动态菜单生成逻辑在MenuService中:
// MenuService.java public List<Menu> buildUserMenu(Integer userId) { // 1. 获取用户所有角色ID List<Integer> roleIds = roleService.findRoleIdsByUserId(userId); // 2. 获取这些角色拥有的所有权限码(permission_code) Set<String> permCodes = permissionService.findPermCodesByRoleIds(roleIds); // 3. 查询所有一级菜单(parent_id = 0),按 sort_order 排序 List<Menu> rootMenus = menuDao.findAllRootMenus(); // 4. 为每个一级菜单,查询其子菜单(parent_id = root.id),并过滤掉用户无权限的 for (Menu root : rootMenus) { List<Menu> children = menuDao.findChildrenByParentId(root.getId()); children.removeIf(child -> !permCodes.contains(child.getPermCode())); root.setChildren(children); } return rootMenus; }Menu对象包含id,name,url,icon,sortOrder,permCode,children字段,JSP 中用 JSTL 递归渲染:
<c:forEach items="${menuList}" var="menu"> <li class="nav-item"> <a href="${menu.url}" class="nav-link"> <i class="${menu.icon}"></i> ${menu.name} </a> <c:if test="${not empty menu.children}"> <ul class="nav flex-column ml-3"> <c:forEach items="${menu.children}" var="child"> <li class="nav-item"><a href="${child.url}" class="nav-link">${child.name}</a></li> </c:forEach> </ul> </c:if> </li> </c:forEach>注意:
permCode字段在t_menu表中非空,意味着每个菜单项必须绑定至少一个权限码,否则无法控制可见性。这是 RBAC 实施的关键约束。
3.3 订单处理模块:状态机驱动与并发安全处理
订单模块是业务复杂度最高的部分,涉及状态流转(待支付 → 已支付 → 已发货 → 已完成 → 已取消)、库存扣减、优惠券核销、支付回调通知。本系统采用状态机思想,用OrderStatus枚举定义所有状态及合法转移:
public enum OrderStatus { PENDING_PAYMENT(1, "待支付", Collections.singleton(OrderStatus.PAID)), PAID(2, "已支付", Sets.newHashSet(OrderStatus.SHIPPED, OrderStatus.CANCELLED)), SHIPPED(3, "已发货", Collections.singleton(OrderStatus.COMPLETED)), COMPLETED(4, "已完成", Collections.emptySet()), CANCELLED(5, "已取消", Collections.emptySet()); private final int code; private final String desc; private final Set<OrderStatus> allowedNext; OrderStatus(int code, String desc, Set<OrderStatus> allowedNext) { this.code = code; this.desc = desc; this.allowedNext = allowedNext; } public boolean canTransitionTo(OrderStatus next) { return this.allowedNext.contains(next); } }订单状态更新必须加数据库行锁,防止并发超卖。例如“确认收货”操作:
// OrderServiceImpl.java @Transactional public boolean confirmReceipt(Integer orderId, Integer userId) { // 1. 查询订单(for update),确保同一订单不会被多次确认 Order order = orderDao.selectForUpdate(orderId); if (order == null || !order.getUserId().equals(userId)) { throw new ServiceException("订单不存在或不属于当前用户"); } if (!OrderStatus.SHIPPED.canTransitionTo(OrderStatus.COMPLETED)) { throw new ServiceException("当前状态不允许确认收货"); } // 2. 更新状态 order.setStatus(OrderStatus.COMPLETED.getCode()); order.setCompleteTime(new Date()); return orderDao.updateStatus(order) > 0; }selectForUpdate()方法在 SQL 中添加FOR UPDATE子句,使数据库对该行加写锁,其他事务读取该行时会阻塞直至锁释放。这是保障状态流转原子性的核心手段。
3.4 订单导出模块:POI SXSSFWorkbook 流式写入与内存控制
导出功能常因数据量大导致 OOM,本系统采用 Apache POI 的SXSSFWorkbook(流式 Excel 写入器),将内存占用控制在 10MB 以内。ExportService接收days参数(如 30),构造 SQL 查询近 N 天订单,并分批读取、分批写入:
// ExportService.java public void exportOrdersToExcel(HttpServletResponse response, int days) throws IOException { String filename = "orders_" + new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()) + ".xlsx"; response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename=" + filename); // 创建 SXSSFWorkbook,windowSize=100 表示内存中保留100行,超出则刷入磁盘临时文件 SXSSFWorkbook workbook = new SXSSFWorkbook(100); Sheet sheet = workbook.createSheet("订单汇总"); // 写入表头 Row headerRow = sheet.createRow(0); String[] headers = {"订单号", "用户姓名", "商品名称", "金额", "状态", "下单时间"}; for (int i = 0; i < headers.length; i++) { headerRow.createCell(i).setCellValue(headers[i]); } // 分页查询,每页1000条,避免单次查询耗尽内存 int pageNum = 1; int pageSize = 1000; List<OrderExportDTO> pageData; int rowIndex = 1; do { pageData = orderDao.findExportPage(days, pageNum, pageSize); for (OrderExportDTO dto : pageData) { Row row = sheet.createRow(rowIndex++); row.createCell(0).setCellValue(dto.getOrderNo()); row.createCell(1).setCellValue(dto.getRealName()); row.createCell(2).setCellValue(dto.getProductName()); row.createCell(3).setCellValue(dto.getAmount()); row.createCell(4).setCellValue(dto.getStatusDesc()); row.createCell(5).setCellValue( new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(dto.getCreateTime()) ); } pageNum++; } while (pageData.size() == pageSize); // 写入响应流 workbook.write(response.getOutputStream()); workbook.dispose(); // 必须调用,清理临时文件 }提示:
SXSSFWorkbook的windowSize不是总行数,而是内存中缓存的行数。设为 100 意味着最多在内存中存 100 行,第 101 行会自动刷入磁盘临时文件,极大降低 GC 压力。务必调用dispose(),否则临时文件不删除。
4. 关键配置与部署:Tomcat 适配、数据库连接池、日志分级与常见问题排查
系统开箱即用的前提是配置正确。本节不罗列所有配置项,而是聚焦四个最易出错的环节:Tomcat 版本兼容性、数据库连接池参数、日志输出控制、以及中文乱码根治。所有配置均经过实测(Tomcat 8.5.99 + MySQL 5.7.42 + JDK 1.8.0_381),避免“网上抄的配置本地跑不通”的尴尬。例如,很多同学照搬context.xml里的maxActive参数,却不知 DBCP2 已废弃该属性,应改用maxTotal;又如log4j2.xml中%d{yyyy-MM-dd HH:mm:ss.SSS}的毫秒格式若写成%d{yyyy-MM-dd HH:mm:ss.sss}(小写 s),会导致日志时间戳全为 000。
4.1 Tomcat 与 Servlet 容器配置要点
本系统基于 Servlet 3.1 规范,要求 Tomcat ≥ 8.0。关键配置在conf/server.xml和conf/web.xml中:
server.xml中<Connector>标签必须添加URIEncoding="UTF-8",解决 GET 请求中文参数乱码:<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" />web.xml中<web-app>标签必须声明version="3.1",否则@WebServlet注解不生效:<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_3_1.xsd" version="3.1">若使用 IDEA 部署,需在
Project Structure → Artifacts中勾选Include in project build,确保WEB-INF/lib下包含所有 JAR(如mysql-connector-java-5.1.49.jar,commons-dbcp2-2.9.0.jar,poi-ooxml-5.2.4.jar)。漏掉slf4j-api或log4j-core会导致启动报ClassNotFoundException。
4.2 数据库连接池配置:DBCP2 参数调优与连接泄漏检测
系统采用 Apache Commons DBCP2 作为连接池,配置文件src/main/resources/db.properties内容如下:
driverClassName=com.mysql.jdbc.Driver url=jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai username=root password=123456 # 连接池基础参数 initialSize=5 maxTotal=20 maxIdle=10 minIdle=5 # 连接有效性检测 testOnCreate=false testOnBorrow=true testOnReturn=false validationQuery=SELECT 1 validationQueryTimeout=3 # 连接回收 timeBetweenEvictionRunsMillis=30000 minEvictableIdleTimeMillis=60000 removeAbandonedOnBorrow=true removeAbandonedOnMaintenance=true removeAbandonedTimeout=60 # 性能监控(可选) logAbandonedOnBorrow=true注意:
removeAbandonedTimeout=60表示连接被借用超过 60 秒未归还,连接池将强制回收并记录警告日志。这是发现连接泄漏(如DBUtil.getConnection()后忘记close())的最有效手段。生产环境建议开启logAbandonedOnBorrow=true,便于定位哪行代码没关连接。
validationQuery=SELECT 1是轻量级心跳检测,比SELECT NOW()更高效。testOnBorrow=true确保每次从池中借出连接前都执行该 SQL,剔除失效连接,避免Communications link failure错误。
4.3 日志系统配置:Log4j2 分级输出与异步写入
日志采用 Log4j2(2.20.0),配置文件src/main/resources/log4j2.xml实现:① 控制台输出 DEBUG 级别(开发用);②logs/app.log输出 INFO 级别(运维用);③logs/error.log单独捕获 ERROR 级别(告警用);④ 异步 Appender 提升性能。关键配置如下:
<Configuration status="WARN"> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </Console> <RollingFile name="AppFile" fileName="logs/app.log" filePattern="logs/app-%d{yyyy-MM-dd}-%i.log.gz"> <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> <Policies> <TimeBasedTriggeringPolicy /> <SizeBasedTriggeringPolicy size="10 MB"/> </Policies> <DefaultRolloverStrategy max="30"/> </RollingFile> <RollingFile name="ErrorFile" fileName="logs/error.log" filePattern="logs/error-%d{yyyy-MM-dd}-%i.log.gz"> <ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/> <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> <Policies> <TimeBasedTriggeringPolicy /> <SizeBasedTriggeringPolicy size="10 MB"/> </Policies> <DefaultRolloverStrategy max="30"/> </RollingFile> </Appenders> <Loggers> <AsyncLogger name="com.example" level="debug" includeLocation="true" additivity="false"> <AppenderRef ref="Console"/> <AppenderRef ref="AppFile"/> <AppenderRef ref="ErrorFile"/> </AsyncLogger> <Root level="info"> <AppenderRef ref="Console"/> </Root> </Loggers> </Configuration>提示:
AsyncLogger必须配合includeLocation="true"才能打印行号,否则日志中看不到at com.example.UserServlet.doLogin(UserServlet.java:45)。但开启后有轻微性能损耗,生产环境可设为false。
4.4 常见问题排查:5 个高频翻车点与血泪解决方案
系统部署后最常见的 5 个问题,按发生频率排序,每条给出现象、根因、解决步骤:
4.4.1 现象:登录成功后跳转到空白页或 404,Session 中currentUser为空
原因:request.getSession().setAttribute("currentUser", user)执行了,但后续页面session.getAttribute("currentUser")取不到。根本原因是浏览器 Cookie 的JSESSIONID未正确传递,常见于:① 前端 AJAX 登录后未处理重定向,导致新请求无 Cookie;② Tomcat 配置了secure=true但访问的是 HTTP 非 HTTPS;③ 应用上下文路径(Context Path)不一致,如部署为/myapp,但前端请求/login.jsp(缺上下文)。
解决:① AJAX 登录成功后,用window.location.href = "${pageContext.request.contextPath}/index.jsp"强制跳转,而非response.sendRedirect()(Servlet 返回 302,AJAX 无法自动跟随);② 检查conf/server.xml中<Connector>是否有secure="true",如有则改为false或启用 HTTPS;③ 所有链接必须带${pageContext.request.contextPath},如<a href="${pageContext.request.contextPath}/user/list.do">。
4.4.2 现象:中文入库后变成???,或页面显示方块字
原因:字符集未全程统一。MySQL 服务端、数据库、表、连接、JDBC URL、JSP 页面、Tomcat Connector,任一环节漏设utf8mb4,都会导致乱码。
解决:① MySQL 命令行执行SHOW VARIABLES LIKE 'character_set_%';,确保character_set_server,collation_server为utf8mb4;② 创建数据库时指定CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;③ JDBC URL 必须含?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai;④ JSP 顶部加<%@ page contentType="text/html;charset=UTF-8" %>;⑤server.xmlConnector 加URIEncoding="UTF-8"。
4.4.3 现象:导出 Excel 时抛OutOfMemoryError: Java heap space
原因:未使用SXSSFWorkbook,或windowSize设得过大(如 10000),导致内存中缓存过多行。
解决:① 确认使用org.apache.poi.xssf.streaming.SXSSFWorkbook,非XSSFWorkbook;② 将windowSize设为 100~1000(根据服务器内存调整);③ 必须调用workbook.dispose();④ Tomcat 启动脚本bin/catalina.sh中添加-Xms512m -Xmx1024m。
4.4.4 现象:权限菜单不显示,或所有按钮都不可见
原因:t_role_permission表中未给管理员角色(role_id=1)分配任何权限码,或t_menu.perm_code字段为空,或MenuService.buildUserMenu()中permCodes集合为空。
解决:① 检查init_db.sql是否执行成功,特别是INSERT INTO t_role_permission语句;② 查询SELECT * FROM t_menu WHERE perm_code IS NULL OR perm_code = '',修复空值;③ 在buildUserMenu()方法开头加日志logger.debug("User {} has permissions: {}", userId, permCodes);,确认集合不为空。
4.4.5 现象:订单状态更新失败,日志报Lock wait timeout exceeded
原因:高并发下selectForUpdate行锁等待超时(默认 50 秒),常见于长时间事务未提交,或死锁。
解决:① 检查OrderServiceImpl中所有selectForUpdate方法,确保其后紧跟update操作,且update成功后立即commit;② 在DBUtil.getConnection()中设置connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);③ 数据库层面执行SHOW ENGINE INNODB STATUS\G查看最近死锁详情。
5. 毕设/课设答辩通关指南:从演示脚本到答辩话术的全流程准备
答辩不是代码朗诵,而是向评委证明:你理解这个系统为什么这样设计,知道它的边界在哪,能解释每一个技术选型背后的 trade-off。我
本文还有配套的精品资源,点击获取