简介:这是一份面向Java初学者与J2EE入门开发者的日志管理系统实践项目,聚焦企业级日志采集、存储、查询与可视化等核心需求,助力理解多层架构设计与日志运维逻辑。资源包共241个文件,含63个Java源码(如_manage__log.class、_readLog.class等业务逻辑类)、22个JSP页面(Web层交互)、63个Class字节码、25张JPG界面截图及8个XML配置文件(含web.xml、applicationContext.xml等),辅以数据库表结构(8个table)、JPA映射文件(19个jpa)和Jar依赖库,完整覆盖J2EE三层架构实现;压缩包大小为11.6MB。已有171人学习下载。读者可直接部署运行,深入剖析Servlet+JSP+Spring/EJB+JDBC的协同机制,掌握日志模块的DAO封装、会话管理(SessionAllBean)、权限控制(logWebTier.cdi/logBusTier.cdi)及典型CRUD流程,是理解企业级日志系统落地的高价值参考范例。
1. 这不是日志查看器,而是一套可拆解的J2EE三层日志治理骨架
你拿到myLog3.rar解压后看到一堆.class文件和两个.cdi文件,第一反应可能是“这怎么跑?”——它既不带web.xml显式声明 Servlet,也没有pom.xml或build.gradle,甚至没提供数据库脚本。但恰恰是这种“残缺感”,暴露了它的真实价值:一个剥离了框架包装、直击 J2EE 分层契约本质的日志管理最小可行骨架。它不依赖 Spring Boot 自动装配,不靠 Logback 做格式化,所有逻辑都压在logWebTier.cdi(Web 层契约接口)、logBusTier.cdi(业务层契约接口)和一串_manage__xxx.class实现类上。这意味着:你能清晰看到 HTTP 请求如何穿透SessionAllBean.class落入_readLog.class,再经_manage__log.class调用 DAO 层;也能直接修改_manage__logMessage.class中的 SQL 拼接逻辑,观察 WHERE 条件如何影响查询性能。适合两类人:一是正在啃《J2EE 核心规范》却苦于没有真实分层代码对照的中级开发者;二是需要快速搭建审计级日志模块、但拒绝引入整套 ELK 的运维开发混合角色。它解决的不是“怎么查日志”,而是“当 Web 容器、事务边界、会话隔离全部由你手工控制时,日志流转的每一步该落在哪一层、谁负责序列化、谁承担异常兜底”。
2. 从.cdi契约到.class实现:解析 J2EE 分层契约与类加载路径
2.1.cdi文件不是配置,而是编译期生成的契约描述符
logWebTier.cdi和logBusTier.cdi并非 XML 或 JSON 配置文件,而是 J2EE 1.4 时代常见的Component Definition Interface编译产物。它们由早期 EJB 工具链(如 WebLogic Builder 或 JBoss IDE)在编译 EJB 接口时生成,用于在部署阶段向容器声明组件类型、方法签名及事务属性。虽然现代 IDE 不再生成此类文件,但myLog3.rar保留它们,恰恰说明项目原始目标容器是 WebLogic 8.x 或 JBoss 4.x 这类严格遵循 J2EE 1.4 规范的服务器。验证方式很简单:用javap -verbose logWebTier.cdi查看其常量池,会发现Signature属性指向LlogWebTier;—— 这正是 Web 层接口的二进制签名。关键点在于:.cdi文件本身不执行任何逻辑,它只是告诉容器“这个目录下的 class 文件必须实现logWebTier接口,且其doLogQuery()方法需声明RequiresNew事务属性”。因此,部署前必须确保logWebTier.class(接口类)与logWebTier.cdi同目录,否则 Tomcat 会报ClassNotFoundException,而 WebLogic 则直接拒绝部署。
提示:若你使用 Tomcat 7+ 部署此项目,需手动补全
logWebTier.class和logBusTier.class接口定义。根据_manage__log.class反编译结果,logWebTier至少包含public String getLogByTimeRange(String startTime, String endTime)方法,返回 JSON 字符串;logBusTier则定义public List<LogEntry> queryLogs(Date start, Date end),参数类型为java.util.Date而非字符串——这是 Web 层与业务层解耦的关键证据。
2.2 类名命名规则暴露调用链路与职责边界
观察解压出的 class 文件名:_update__selfinfor.class、_manage__album.class、_manage__logMessage.class。下划线双划线(__)是典型的手工分层标识符,而非 Java 命名规范。通过javap -cp . _manage__log.class反编译(需 JDK 8),可确认其继承自javax.servlet.http.HttpServlet,且doPost()方法中存在如下关键调用:
// _manage__log.class 片段 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action = req.getParameter("action"); if ("query".equals(action)) { // 1. 从 Session 获取用户权限上下文 SessionAllBean sessionBean = (SessionAllBean) req.getSession().getAttribute("sessionBean"); // 2. 调用业务层执行查询 logBusTier busTier = new logBusTierImpl(); // 注意:此处硬编码实例化,无 DI List<LogEntry> logs = busTier.queryLogs( parseDate(req.getParameter("start")), parseDate(req.getParameter("end")) ); // 3. 将结果写入响应 resp.setContentType("application/json"); PrintWriter out = resp.getWriter(); out.print(new Gson().toJson(logs)); } }这段代码揭示了三个硬性约束:
- Web 层只做协议转换:
doPost()不处理日期解析、SQL 拼接、权限校验,仅提取参数、调用业务层、序列化返回; - 业务层承担核心逻辑:
logBusTierImpl必须实现logBusTier接口,且其queryLogs()方法内部包含 JDBC 连接获取、PreparedStatement 设置、结果集映射; - Session 状态被显式传递:
SessionAllBean.class是会话状态载体,存储userId、roleLevel、lastAccessTime,_manage__log.class通过req.getSession().getAttribute()获取,而非依赖容器注入。
2.3 部署路径与类加载顺序决定功能是否生效
myLog3.rar未提供标准 WAR 结构,需手动构建部署包。正确路径结构如下:
myLog3/ ├── WEB-INF/ │ ├── web.xml # 必须手动创建,声明 Servlet 映射 │ ├── lib/ # 放入 gson-2.8.9.jar、mysql-connector-java-5.1.49.jar │ └── classes/ # 所有 .class 文件放在此目录 │ ├── _manage__log.class │ ├── logWebTier.cdi │ └── SessionAllBean.class └── index.jsp # 前端入口,调用 /logServlet?action=queryweb.xml关键配置必须包含:
<servlet> <servlet-name>LogServlet</servlet-name> <servlet-class>_manage__log</servlet-class> <!-- 注意:此处写类名,非包名 --> </servlet> <servlet-mapping> <servlet-name>LogServlet</servlet-name> <url-pattern>/logServlet</url-pattern> </servlet-mapping>注意:
<servlet-class>标签值必须与.class文件名完全一致(不含.class后缀),且不能带包路径。因为myLog3所有类均位于默认包(no package),Tomcat 的WebappClassLoader会直接从WEB-INF/classes/下查找同名类。若错误写成com.mylog._manage__log,将触发ClassNotFoundException,且错误日志中不会提示“找不到包”,只会显示“_manage__lognot found”。
3. 数据库适配与查询优化:从硬编码 SQL 到可维护参数化查询
3.1 逆向工程logBusTierImpl的 JDBC 实现细节
_manage__logMessage.class反编译后显示其saveLog()方法包含如下 SQL:
String sql = "INSERT INTO log_table (level,module,message,timestamp) VALUES ('" + level + "','" + module + "','" + message + "'," + System.currentTimeMillis() + ")"; Statement stmt = conn.createStatement(); stmt.executeUpdate(sql);这是典型的SQL 注入高危写法,也是myLog3作为教学案例的刻意设计——它迫使你动手重构。真正的优化路径分三步:
- 识别表结构:通过
grep -r "log_table" *.class发现_manage__log.class中存在CREATE TABLE log_table语句,字段为id BIGINT PRIMARY KEY AUTO_INCREMENT, level VARCHAR(10), module VARCHAR(50), message TEXT, timestamp BIGINT; - 定位 DAO 入口:
_manage__log.class的doPost()调用logBusTier.queryLogs(),而logBusTierImpl.class(需反编译)中该方法实际调用private List<LogEntry> executeQuery(String sql); - 替换为 PreparedStatement:在
logBusTierImpl.class的executeQuery方法中,将硬编码 SQL 替换为参数化模板:
// 替换前(危险) String sql = "SELECT * FROM log_table WHERE timestamp BETWEEN " + start.getTime() + " AND " + end.getTime(); // 替换后(安全) String sql = "SELECT id, level, module, message, timestamp FROM log_table WHERE timestamp BETWEEN ? AND ?"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setLong(1, start.getTime()); pstmt.setLong(2, end.getTime()); ResultSet rs = pstmt.executeQuery();3.2 时间范围查询的性能陷阱与索引策略
myLog3默认按timestamp字段查询,但原始 SQL 未建索引。在 MySQL 中执行:
-- 创建复合索引,覆盖查询高频字段 ALTER TABLE log_table ADD INDEX idx_timestamp_level (timestamp, level); -- 若需按 module 查询,追加索引 ALTER TABLE log_table ADD INDEX idx_module_timestamp (module, timestamp);验证索引效果需对比执行计划:
EXPLAIN SELECT * FROM log_table WHERE timestamp BETWEEN 1609459200000 AND 1612137600000;若key列显示idx_timestamp_level,且rows值显著低于全表扫描行数(如从 10000 降至 200),则索引生效。注意:timestamp字段为BIGINT存储毫秒时间戳,而非DATETIME类型,因此不能使用DATE()函数进行范围查询,否则索引失效。所有时间条件必须传入毫秒值,前端 JavaScript 需调用new Date().getTime()而非toISOString()。
3.3 日志级别枚举化与查询条件标准化
myLog3中日志级别(level)以字符串形式存入数据库(如"ERROR"、"INFO"),但_manage__logMessage.class的saveLog()方法未做校验。为提升查询可靠性,需在业务层增加枚举约束:
// 新增 LogLevel.java 枚举 public enum LogLevel { DEBUG(1), INFO(2), WARN(3), ERROR(4), FATAL(5); private final int value; LogLevel(int value) { this.value = value; } public int getValue() { return value; } } // 在 logBusTierImpl.queryLogs() 中添加参数校验 public List<LogEntry> queryLogs(Date start, Date end, String level) { if (level != null && !Arrays.asList("DEBUG","INFO","WARN","ERROR","FATAL").contains(level)) { throw new IllegalArgumentException("Invalid log level: " + level); } // ... 后续 SQL 构建 }对应 SQL 查询需改为:
SELECT * FROM log_table WHERE timestamp BETWEEN ? AND ? AND level = ? -- 此处 level 为字符串,非数值提示:若后续需按级别做统计(如
COUNT(*) GROUP BY level),建议将level字段改为TINYINT,存储LogLevel.getValue(),并建立INDEX idx_level_timestamp (level, timestamp)。这样既能避免字符串比较开销,又支持按级别快速聚合。
4. 安全加固与生产就绪改造:Session 管理、输入校验与错误隔离
4.1SessionAllBean的会话劫持风险与修复方案
SessionAllBean.class存储用户身份信息,但_manage__log.class直接通过req.getSession().getAttribute("sessionBean")获取,未校验 Session 是否过期或被篡改。攻击者可通过 Cookie 植入伪造JSESSIONID访问他人日志。修复需两步:
- 增加 Session 有效性检查:在
_manage__log.class的doPost()开头插入:
HttpSession session = req.getSession(false); // false 表示不创建新 Session if (session == null || session.getAttribute("sessionBean") == null) { resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Session expired or invalid"); return; }- 强制 HTTPS 传输:在
web.xml中添加安全约束:
<security-constraint> <web-resource-collection> <web-resource-name>Secure Logs</web-resource-name> <url-pattern>/logServlet</url-pattern> </web-resource-collection> <user-data-constraint> <transport-guarantee>CONFIDENTIAL</transport-guarantee> </user-data-constraint> </security-constraint>4.2 输入参数的深度校验与 XSS 防御
myLog3的index.jsp直接将request.getParameter("message")输出到页面,构成 XSS 漏洞。修复需在_manage__logMessage.class的saveLog()方法中对message参数做净化:
// 使用 OWASP Java Encoder 库(需引入 encoder-1.2.3.jar) import org.owasp.encoder.Encode; ... String safeMessage = Encode.forHtmlContent(message); // 转义 HTML 特殊字符 String sql = "INSERT INTO log_table (...) VALUES (...,'" + safeMessage + "',...)";同时,在index.jsp的日志展示区域,必须对输出内容再次编码:
<!-- 错误写法 --> <td><%= log.getMessage() %></td> <!-- 正确写法 --> <td><%= org.owasp.encoder.Encode.forHtmlContent(log.getMessage()) %></td>4.3 异常堆栈的分级捕获与日志脱敏
_manage__log.class的doPost()方法用try-catch包裹全部逻辑,但catch块仅打印e.printStackTrace(),导致敏感信息(如数据库连接 URL、密码)泄露到 Tomcat 控制台。生产环境必须分级处理:
} catch (SQLException e) { // 仅记录错误码和简要信息,不暴露 SQL logger.error("Database query failed for user {}, error code: {}", sessionBean.getUserId(), e.getSQLState()); resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Query failed"); } catch (Exception e) { // 通用异常记录堆栈,但过滤敏感字段 String stackTrace = e.getStackTrace().toString(); if (stackTrace.contains("password") || stackTrace.contains("jdbc:mysql://")) { logger.error("Unexpected error occurred, stack trace redacted for security"); } else { logger.error("Unexpected error", e); } resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Service unavailable"); }5. 日志可视化增强:基于原生 JSP 的轻量级时间线渲染与导出
5.1 使用 Chart.js 在 JSP 中渲染日志时间线
myLog3原始版本无图表,但index.jsp已预留<div id="logChart"></div>。添加 Chart.js 支持只需三步:
- 在
index.jsp<head>中引入 CDN:
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>- 在页面底部添加初始化脚本,从 Servlet 获取 JSON 数据:
<script> fetch('/logServlet?action=query&start=1609459200000&end=1612137600000') .then(r => r.json()) .then(data => { const ctx = document.getElementById('logChart').getContext('2d'); new Chart(ctx, { type: 'line', data: { labels: data.map(d => new Date(d.timestamp).toLocaleString()), datasets: [{ label: 'Log Count per Hour', data: groupByHour(data), // 需实现分组函数 borderColor: '#36A2EB' }] } }); }); </script>- 实现
groupByHour()辅助函数(JavaScript):
function groupByHour(logs) { const map = new Map(); logs.forEach(log => { const hourKey = new Date(log.timestamp).toDateString(); // 简化为日期粒度 map.set(hourKey, (map.get(hourKey) || 0) + 1); }); return Array.from(map.values()); }5.2 CSV 导出功能的零依赖实现
为避免引入 Apache POI 等大依赖,直接用 Servlet 输出 CSV:
// 在 _manage__log.class 中新增 exportCsv() 方法 private void exportCsv(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setContentType("text/csv"); resp.setHeader("Content-Disposition", "attachment; filename=logs_" + System.currentTimeMillis() + ".csv"); PrintWriter out = resp.getWriter(); out.println("Level,Module,Message,Timestamp"); // CSV 头 List<LogEntry> logs = logBusTier.queryLogs( parseDate(req.getParameter("start")), parseDate(req.getParameter("end")) ); for (LogEntry log : logs) { // 对 message 字段做 CSV 转义:双引号内含逗号需用双引号包裹,双引号本身转义为两个双引号 String escapedMsg = log.getMessage().replace("\"", "\"\""); out.printf("\"%s\",\"%s\",\"%s\",%d%n", log.getLevel(), log.getModule(), escapedMsg, log.getTimestamp()); } }调用方式:前端链接<a href="/logServlet?action=export&start=...&end=...">Export CSV</a>,Servlet 的doPost()根据action=export分支调用此方法。
5.3 日志级别颜色标记的 CSS 实现
在index.jsp的表格中,为不同级别添加背景色提升可读性:
<td style="background-color: <% switch(log.getLevel()) { case "ERROR": out.print("#ffebee"); break; case "WARN": out.print("#fff3cd"); break; case "INFO": out.print("#d4edda"); break; default: out.print("#e2e3e5"); } %>"> <%= log.getLevel() %> </td>此方案无需额外 JS,纯服务端渲染,兼容所有浏览器,且颜色值可直接映射到 Material Design 调色板(#ffebee为 Error 的浅红,#d4edda为 Success 的浅绿),符合现代 UI 规范。
提示:若需支持深色模式,可在
<body>添加>:root { --level-error: #ffcdd2; --level-warn: #ffecb3; } [data-theme="dark"] { --level-error: #b71c1c; --level-warn: #ff8f00; } .level-cell { background-color: var(--level-error); }
本文还有配套的精品资源,点击获取