源碼:JSP+Servlet+MySQL實(shí)戰(zhàn)解析)
簡(jiǎn)介面向中小型團(tuán)隊(duì)與高校實(shí)訓(xùn)場(chǎng)景的 Java 考勤管理系統(tǒng)完整源碼包適合具備 Java Web 基礎(chǔ)、希望掌握 Spring 風(fēng)格業(yè)務(wù)開發(fā)或畢業(yè)設(shè)計(jì)復(fù)用的學(xué)習(xí)者。包內(nèi)按業(yè)務(wù)模塊劃分覆蓋員工管理、考勤打卡、請(qǐng)假/會(huì)議申請(qǐng)、固定資產(chǎn)與任務(wù)調(diào)度等常見功能壓縮包共 2000 個(gè)文件約 55MB以 1336 個(gè) Markdown 文檔、500 個(gè) JavaScript 文件、67 個(gè) Java 文件及 JSON/XML 配置為主文檔與配置占比高便于對(duì)照閱讀、按需檢索和二次開發(fā)。已有 170 人學(xué)習(xí)下載。其中包含控制器、服務(wù)、實(shí)體與工具類等完整分層主線功能覆蓋考勤申請(qǐng)、會(huì)議與員工管理。借助該源碼可快速理解考勤業(yè)務(wù)表設(shè)計(jì)、接口分層、審批流轉(zhuǎn)與前后端交互方式尤其適合課程設(shè)計(jì)、項(xiàng)目實(shí)訓(xùn)或企業(yè)內(nèi)部門禁考勤需求的原型搭建Java、XML、SQL 等關(guān)鍵文件可直接導(dǎo)入 IDE 運(yùn)行調(diào)試Markdown 文檔輔助梳理實(shí)現(xiàn)思路。1. 考勤管理系統(tǒng)源碼學(xué)生項(xiàng)目里最容易被低估的 Java 練手資源做 Java 課程設(shè)計(jì)或者畢業(yè)設(shè)計(jì)的人十有八九會(huì)撞上考勤系統(tǒng)這個(gè)題目。這份源碼就是一個(gè)典型的 Java Web 考勤管理系統(tǒng)基于 JSP Servlet MySQL 實(shí)現(xiàn)包含員工管理、部門管理、上下班打卡、考勤統(tǒng)計(jì)和月度報(bào)表導(dǎo)出幾個(gè)模塊。它不像商城項(xiàng)目那么繞但足夠把 Java Web 開發(fā)里最核心的東西串一遍數(shù)據(jù)庫(kù)設(shè)計(jì)、會(huì)話管理、請(qǐng)求轉(zhuǎn)發(fā)、SQL 聚合統(tǒng)計(jì)。適合兩類人一類是急著交課程設(shè)計(jì)的學(xué)生拿回去改改就能跑另一類是剛學(xué)完 Java 基礎(chǔ)、想找一個(gè)完整項(xiàng)目練手的人拆開源碼看一遍比看十篇教程都管用。2. 技術(shù)棧與模塊拆解JSP Servlet MySQL 這條老鏈路為什么還在用2.1 項(xiàng)目形態(tài)與選型理由先說(shuō)項(xiàng)目形態(tài)。這份源碼是典型的 Java Web 三層結(jié)構(gòu)JSP 負(fù)責(zé)頁(yè)面展示Servlet 處理請(qǐng)求轉(zhuǎn)發(fā)JDBC 訪問(wèn) MySQL 數(shù)據(jù)庫(kù)。沒(méi)有引入 Spring Boot也沒(méi)有用 MyBatis整條鏈路全是原生 API依賴只有 Tomcat 和 MySQL 驅(qū)動(dòng)兩個(gè) jar 包。很多讀者拿到手第一反應(yīng)是這太老了。我一般會(huì)勸一句老鏈路恰恰是學(xué)習(xí)成本最低的鏈路。Spring Boot 幫你自動(dòng)配置了太多東西出了問(wèn)題反而不容易定位而 JSP Servlet 的每一行代碼都是顯式的請(qǐng)求怎么進(jìn)來(lái)、怎么查庫(kù)、怎么把結(jié)果塞進(jìn) request 再轉(zhuǎn)發(fā)到頁(yè)面全程沒(méi)有黑匣子。選型理由就一條這份資源的定位是能跑、能改、能看懂不是生產(chǎn)級(jí)高并發(fā)。用原生 Servlet 的好處是依賴少只要 Tomcat 起來(lái)了就能跑不用跟 Maven 倉(cāng)庫(kù)里的版本沖突較勁。常見做法是登錄校驗(yàn)寫在 Filter 里業(yè)務(wù)邏輯寫在 Servlet 的 doGet/doPost 里數(shù)據(jù)庫(kù)訪問(wèn)單獨(dú)抽出 DBUtil 工具類。后面章節(jié)的代碼走讀會(huì)按這個(gè)結(jié)構(gòu)展開。2.2 數(shù)據(jù)庫(kù)設(shè)計(jì)三張核心表的字段與關(guān)聯(lián)考勤系統(tǒng)的數(shù)據(jù)庫(kù)設(shè)計(jì)一般圍繞三張表展開員工表、部門表、考勤記錄表。員工表掛在部門表下面考勤記錄表引用員工表的主鍵。這個(gè)資源里常見的建表 SQL 大概是這樣的CREATE DATABASE attendance DEFAULT CHARACTER SET utf8mb4; USE attendance; CREATE TABLE dept ( id INT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(50) NOT NULL ); CREATE TABLE employee ( id INT PRIMARY KEY AUTO_INCREMENT, dept_id INT NOT NULL, emp_no VARCHAR(20) NOT NULL UNIQUE, emp_name VARCHAR(30) NOT NULL, password VARCHAR(32) NOT NULL, role TINYINT DEFAULT 1 COMMENT 1-普通員工 0-管理員, FOREIGN KEY (dept_id) REFERENCES dept(id) ); CREATE TABLE attendance_record ( id INT PRIMARY KEY AUTO_INCREMENT, emp_id INT NOT NULL, work_date DATE NOT NULL, check_in TIME, check_out TIME, status VARCHAR(10) DEFAULT 正常, UNIQUE KEY uk_emp_date (emp_id, work_date), FOREIGN KEY (emp_id) REFERENCES employee(id) );這三張表就是整個(gè)系統(tǒng)的地基。注意兩個(gè)設(shè)計(jì)細(xì)節(jié)一是 attendance_record 里加了唯一索引 (emp_id, work_date)目的是防止同一個(gè)人同一天插入兩條打卡記錄二是 status 字段做了冗余存儲(chǔ)后續(xù)統(tǒng)計(jì)直接按狀態(tài)分組不用每次用上班和下班時(shí)間現(xiàn)算。如果把這三張表畫成關(guān)系圖就是典型的部門 1 對(duì)多 員工、員工 1 對(duì)多 考勤記錄。部門表是獨(dú)立的員工表通過(guò) dept_id 關(guān)聯(lián)部門考勤記錄表通過(guò) emp_id 關(guān)聯(lián)員工。查詢員工考勤的時(shí)候只需要一次 JOIN 就能把員工姓名和部門名稱帶上這也是后面統(tǒng)計(jì) SQL 能寫短的前提。初始化數(shù)據(jù)也是常見的一個(gè)坑。很多版本自帶的初始化腳本只建表不插數(shù)據(jù)登錄頁(yè)沒(méi)有默認(rèn)賬號(hào)。建議手動(dòng)插入一條管理員數(shù)據(jù)工號(hào) admin、密碼 admin、role 設(shè)為 0否則第一次登錄會(huì)卡在賬號(hào)不存在。2.3 角色權(quán)限管理員與普通員工的兩套菜單邏輯權(quán)限這塊原項(xiàng)目沒(méi)有引入權(quán)限框架就是用最簡(jiǎn)單的方式employee 表里 role 字段區(qū)分管理員和普通員工登錄成功之后把角色值存進(jìn) Session。管理員能訪問(wèn)員工管理、部門管理、考勤統(tǒng)計(jì)這些路徑普通員工只能看到自己的打卡入口和個(gè)人考勤記錄。實(shí)現(xiàn)上常見做法是寫一個(gè)權(quán)限攔截的 Filter在請(qǐng)求到達(dá) Servlet 之前先檢查 Session 里的角色// AuthFilter.java 權(quán)限攔截核心邏輯 public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpSession session request.getSession(false); // 未登錄一律踢回登錄頁(yè) if (session null || session.getAttribute(user) null) { resp.sendRedirect(request.getContextPath() /login.jsp); return; } // 已登錄但角色不匹配提示無(wú)權(quán)訪問(wèn) String role (String) session.getAttribute(role); String uri request.getRequestURI(); if (uri.contains(/admin/) !0.equals(role)) { request.setAttribute(msg, 沒(méi)有管理員權(quán)限); request.getRequestDispatcher(/error.jsp).forward(req, resp); return; } chain.doFilter(req, resp); }這段 Filter 的邏輯說(shuō)明第一層判斷只解決有沒(méi)有登錄第二層判斷解決登錄了但能不能進(jìn)管理頁(yè)面。如果把這兩層合并成一個(gè) if就會(huì)出現(xiàn)普通員工直接輸入管理頁(yè)面地址也能進(jìn)去的漏洞。參數(shù)說(shuō)明Session 里存的是 user 對(duì)象和 role 字符串role 用 String 而不是 int是為了和頁(yè)面 JSTL 標(biāo)簽里的比較寫法對(duì)齊。改成 int 也行但所有比較的地方都要同步改很容易漏。另一個(gè)容易踩的點(diǎn)是 request.getSession(false) 里面的 false它表示拿不到 Session 時(shí)返回 null 而不是新建一個(gè)配合后面的 null 判斷正好覆蓋未登錄場(chǎng)景如果誤寫成 getSession(true)匿名用戶也會(huì)被強(qiáng)制創(chuàng)建 Session攔截邏輯就廢了。3. 本地跑起來(lái)JDK、MySQL、Tomcat 三件套的配置順序3.1 環(huán)境版本怎么選先把環(huán)境定下來(lái)再談導(dǎo)入。這類老項(xiàng)目對(duì)版本敏感版本不匹配的翻車現(xiàn)場(chǎng)我見過(guò)太多給你一個(gè)不會(huì)踩雷的組合JDK 8不要拿 JDK 11 或更高跑老項(xiàng)目JSP 編譯和某些 JDBC 驅(qū)動(dòng)在 JDK 9 之后行為有變化MySQL 5.7 或 8.08.0 需要改驅(qū)動(dòng)類名和時(shí)區(qū)參數(shù)細(xì)節(jié)見 3.3Tomcat 8.5兼容性最好Tomcat 9 對(duì) JSP 的 EL 表達(dá)式支持有輕微差異IDE 用 Eclipse 或 IntelliJ IDEA 都行關(guān)鍵是確認(rèn)項(xiàng)目是什么工程結(jié)構(gòu)順序上先裝 JDK再裝 MySQL最后解壓 Tomcat。裝完 Tomcat 先別急著往里面塞項(xiàng)目先啟動(dòng)一次確認(rèn) 8080 端口能正常訪問(wèn)。這里有一個(gè)新手常犯的錯(cuò)誤以為 Tomcat 一定要單獨(dú)裝。其實(shí)用 IDE 內(nèi)置的 Tomcat 插件跑更方便控制臺(tái)日志和斷點(diǎn)調(diào)試都更順手。版本選型的原則就一句話和項(xiàng)目里 lib 目錄下 jar 包的編譯版本對(duì)齊。你可以在項(xiàng)目里找到 mysql-connector-java 的 jar看它支持的最低 JDK 版本再?zèng)Q定裝哪個(gè) JDK。JDK 和 Tomcat 都是用前先查版本很多詭異報(bào)錯(cuò)都是版本不匹配引發(fā)。3.2 導(dǎo)入工程與初始化數(shù)據(jù)庫(kù)拿到 zip 解壓之后常見做法是把整個(gè)文件夾拷到 Tomcat 的 webapps 目錄下直接運(yùn)行但我不推薦這么干。更好的方式是導(dǎo)入 IDE用 IDE 內(nèi)置的 Tomcat 插件跑能看到控制臺(tái)日志和斷點(diǎn)。初始化數(shù)據(jù)庫(kù)的步驟固定三步# 第一步登錄 MySQL mysql -uroot -p # 第二步執(zhí)行建庫(kù)建表腳本腳本文件一般在 db 或 sql 目錄下 source /your/path/attendance.sql; # 第三步驗(yàn)證表是否創(chuàng)建成功 USE attendance; SHOW TABLES;執(zhí)行完這三步數(shù)據(jù)庫(kù)側(cè)就緒。注意 source 命令后面要寫文件的絕對(duì)路徑別用相對(duì)路徑否則會(huì)報(bào) file not found。導(dǎo)入 IDE 的時(shí)候有一個(gè)常見的坑項(xiàng)目如果是 Eclipse 的 Dynamic Web Project 結(jié)構(gòu)直接 File - Open 打開目錄即可如果拿到手是一個(gè)普通文件夾沒(méi)有 .project 文件要先轉(zhuǎn)成 Maven 工程或者手動(dòng)勾選 Web Facet否則 IDE 不認(rèn)它是 Web 項(xiàng)目運(yùn)行按鈕是灰的。我一般會(huì)先看一眼工程根目錄下有沒(méi)有 pom.xml。有 pom.xml 就用 Maven 方式導(dǎo)入沒(méi)有就用純 Web 工程方式導(dǎo)入。兩種方式對(duì)應(yīng)的啟動(dòng)按鈕不一樣混淆了會(huì)在后面部署階段報(bào)各種奇怪錯(cuò)誤。3.3 數(shù)據(jù)源配置jdbc.properties 逐項(xiàng)說(shuō)明老項(xiàng)目的數(shù)據(jù)源配置通常寫在 src 目錄下的 jdbc.properties 或 db.properties 里。打開之后你會(huì)看到四五行配置每一行都別亂改jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/attendance?useUnicodetruecharacterEncodingutf8 jdbc.usernameroot jdbc.password123456逐項(xiàng)說(shuō)明jdbc.driverMySQL 5.7 用 com.mysql.jdbc.DriverMySQL 8.0 必須改成 com.mysql.cj.jdbc.Driverjdbc.urllocalhost 換成遠(yuǎn)程數(shù)據(jù)庫(kù) IP 就是連遠(yuǎn)程庫(kù)characterEncodingutf8 是保中文不亂碼的關(guān)鍵參數(shù)jdbc.username / jdbc.password按本地 MySQL 的實(shí)際賬號(hào)改8.0 如果因?yàn)槊艽a加密方式導(dǎo)致連接失敗要在 url 后面追加 serverTimezoneAsia/Shanghai改完配置之后重啟 Tomcat 讓配置重新加載。驗(yàn)證配置是否生效的方式很簡(jiǎn)單隨便打開一個(gè)需要查庫(kù)的頁(yè)面如果頁(yè)面能正常顯示數(shù)據(jù)列表說(shuō)明數(shù)據(jù)源通了如果報(bào) SQLException 或 Connection refused看 5.1 的排查思路。還有一個(gè)細(xì)節(jié)jdbc.password 明文寫在 properties 文件里是課程設(shè)計(jì)項(xiàng)目的常態(tài)別因?yàn)檫@個(gè)覺(jué)得項(xiàng)目有問(wèn)題。生產(chǎn)環(huán)境用加密或環(huán)境變量那是另一個(gè)話題。但要注意 properties 文件保存時(shí)編碼必須是 ASCII 或 UTF-8 without BOM否則中文注釋會(huì)變成亂碼甚至導(dǎo)致解析失敗。3.4 第一個(gè)冒煙測(cè)試從登錄頁(yè)到主界面數(shù)據(jù)庫(kù)初始化完、Tomcat 啟動(dòng)成功之后先做一個(gè)冒煙測(cè)試打開瀏覽器訪問(wèn) http://localhost:8080/項(xiàng)目名/login.jsp用默認(rèn)賬號(hào)登錄看能不能正常跳轉(zhuǎn)到主界面。如果項(xiàng)目名帶了中文或者空格URL 里的路徑會(huì)出問(wèn)題建議把 webapps 下的項(xiàng)目文件夾重命名成純英文比如 attendance。冒煙測(cè)試通過(guò)之后再逐個(gè)點(diǎn)一遍菜單員工管理、部門管理、打卡、統(tǒng)計(jì)。這一步主要是確認(rèn)每個(gè) Servlet 的路由映射有沒(méi)有配全。很多老項(xiàng)目的 web.xml 里配置了一堆 servlet-mapping少配一條就會(huì)在點(diǎn)某個(gè)菜單時(shí) 404。我用一個(gè)笨辦法快速確認(rèn)路由把 web.xml 打開把每個(gè) url-pattern 和對(duì)應(yīng) Servlet 類名抄在一張紙上然后逐個(gè)訪問(wèn)。抄一遍下來(lái)整個(gè)項(xiàng)目的請(qǐng)求流轉(zhuǎn)關(guān)系就清楚了比反復(fù)點(diǎn)頁(yè)面快得多。提示如果冒煙測(cè)試階段就報(bào) 404先別懷疑代碼優(yōu)先檢查訪問(wèn)路徑的大小寫。JSP 文件名和 Servlet 映射都是區(qū)分大小寫的login.jsp 寫成 Login.jsp 直接 404。4. 核心代碼走讀登錄、打卡與統(tǒng)計(jì) SQL 的三個(gè)落點(diǎn)4.1 登錄邏輯Session 與密碼校驗(yàn)登錄是第一個(gè)值得逐行看的模塊。常見寫法是 LoginServlet 接收頁(yè)面提交的 emp_no 和 password拿著這兩個(gè)參數(shù)去查 employee 表查到就把用戶對(duì)象放進(jìn) Session。// LoginServlet.java 核心校驗(yàn)邏輯 String empNo request.getParameter(empNo); String pwd request.getParameter(password); // 查詢用戶注意這里要用 PreparedStatement 而不是拼接 SQL String sql SELECT * FROM employee WHERE emp_no? AND password?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, empNo); ps.setString(2, pwd); ResultSet rs ps.executeQuery(); if (rs.next()) { // 登錄成功user 對(duì)象和 role 同時(shí)入 Session request.getSession().setAttribute(user, empNo); request.getSession().setAttribute(role, rs.getString(role)); resp.sendRedirect(request.getContextPath() /index.jsp); } else { // 登錄失敗回到登錄頁(yè)并攜帶錯(cuò)誤提示 request.setAttribute(msg, 工號(hào)或密碼錯(cuò)誤); request.getRequestDispatcher(/login.jsp).forward(req, resp); }邏輯說(shuō)明整個(gè)登錄成敗就取決于 rs.next() 是否為 true。這里有兩個(gè)容易忽略的細(xì)節(jié)。第一個(gè)細(xì)節(jié)是請(qǐng)求轉(zhuǎn)發(fā)和重定向的區(qū)別。登錄失敗用 forward 回到 login.jsprequest 里的 msg 屬性還能帶上登錄頁(yè)可以顯示工號(hào)或密碼錯(cuò)誤登錄成功用 sendRedirect 跳到 index.jsp讓瀏覽器地址欄變成 index.jsp這樣刷新頁(yè)面不會(huì)重復(fù)提交表單。如果把兩者搞反會(huì)出現(xiàn)刷新一次就重復(fù)登錄一次的怪現(xiàn)象。第二個(gè)細(xì)節(jié)是密碼校驗(yàn)。PreparedStatement 的 ? 占位符天然防 SQL 注入老項(xiàng)目里如果看到字符串拼接的查詢語(yǔ)句建議順手改掉。employee 表里 password 字段存的是明文課程設(shè)計(jì)階段不用糾結(jié)生產(chǎn)環(huán)境必須做加鹽哈希處理。參數(shù)說(shuō)明empNo 和 password 的取值來(lái)自 request.getParameter這兩個(gè)參數(shù)名要和 login.jsp 表單里的 name 屬性完全一致差一個(gè)字母就取到 null。老項(xiàng)目里最常見的登錄失敗原因不是密碼錯(cuò)而是參數(shù)名拼寫不一致。4.2 上下班打卡時(shí)間判定與去重打卡模塊是考勤系統(tǒng)的靈魂。常見做法是同一個(gè) PunchServlet 處理上班和下班兩個(gè)動(dòng)作通過(guò)頁(yè)面按鈕區(qū)分。時(shí)間判定邏輯是這樣的// PunchServlet.java 打卡時(shí)間判定 Timestamp now new Timestamp(System.currentTimeMillis()); java.sql.Date today new java.sql.Date(now.getTime()); // 先查今天有沒(méi)有打卡記錄 String querySql SELECT check_in, check_out FROM attendance_record WHERE emp_id? AND work_date?; PreparedStatement ps conn.prepareStatement(querySql); ps.setInt(1, empId); ps.setDate(2, today); ResultSet rs ps.executeQuery(); if (!rs.next()) { // 今天第一次打卡記為上班時(shí)間 insertSql INSERT INTO attendance_record(emp_id, work_date, check_in) VALUES(?,?,?); } else if (rs.getTime(check_in) ! null rs.getTime(check_out) null) { // 已有上班時(shí)間但沒(méi)有下班時(shí)間補(bǔ)下班時(shí)間 updateSql UPDATE attendance_record SET check_out? WHERE emp_id? AND work_date?; } else { // 上下班都打過(guò)了提示重復(fù)打卡 }邏輯說(shuō)明這個(gè)分支結(jié)構(gòu)把一天內(nèi)的打卡行為拆成三種狀態(tài)——沒(méi)打過(guò)、打了上班沒(méi)打下班、都打過(guò)了。注意 check_out 為 NULL 的判斷用 rs.getTime(check_out) null而不是 rs.getString(...).equals()因?yàn)閿?shù)據(jù)庫(kù) TIME 字段為空時(shí)是 NULL 而不是空字符串。這是新人最容易寫錯(cuò)的地方寫錯(cuò)了會(huì)導(dǎo)致已打過(guò)的下班卡被重復(fù)覆蓋。打卡去重靠的是 2.2 里那張表的唯一索引 uk_emp_date。即使代碼邏輯漏了判斷數(shù)據(jù)庫(kù)層也會(huì)攔住重復(fù)插入并拋 DuplicateKeyException。這個(gè)設(shè)計(jì)就是典型的前后端雙保險(xiǎn)代碼判斷先攔一次數(shù)據(jù)庫(kù)唯一索引兜底。如果資源里的代碼沒(méi)有這個(gè)唯一索引建議自己加上。參數(shù)說(shuō)明System.currentTimeMillis() 取的是服務(wù)器時(shí)間。如果你的應(yīng)用部署在云服務(wù)器上而用戶在北京時(shí)區(qū)不一致會(huì)導(dǎo)致打卡時(shí)間差 8 小時(shí)解決辦法見 5.3。另外 empId 這個(gè)變量來(lái)自登錄 Session 里的用戶信息不要從前端傳過(guò)來(lái)否則任何用戶都能偽造別人的打卡記錄。4.3 月度統(tǒng)計(jì)一條 SQL 出報(bào)表考勤統(tǒng)計(jì)頁(yè)面是管理員最常用的模塊。常見做法是選一個(gè)月度然后按員工分組統(tǒng)計(jì)遲到、早退、正常天數(shù)SELECT e.emp_no AS 工號(hào), e.emp_name AS 姓名, COUNT(*) AS 出勤天數(shù), SUM(CASE WHEN r.check_in 09:00:00 THEN 1 ELSE 0 END) AS 遲到天數(shù), SUM(CASE WHEN r.check_out 18:00:00 THEN 1 ELSE 0 END) AS 早退天數(shù) FROM attendance_record r JOIN employee e ON r.emp_id e.id WHERE DATE_FORMAT(r.work_date, %Y-%m) ? GROUP BY e.id, e.emp_no, e.emp_name;這個(gè) SQL 的兩個(gè)要點(diǎn)一是 DATE_FORMAT 把日期字段格式化成 YYYY-MM 和傳入?yún)?shù)做匹配注意如果 work_date 列有索引DATE_FORMAT 會(huì)導(dǎo)致索引失效數(shù)據(jù)量大了之后建議改成范圍查詢 BETWEEN 2024-01-01 AND 2024-01-31二是 SUM(CASE WHEN ...) 這種寫法把遲到和早退天數(shù)一次算完避免多次掃表。參數(shù)說(shuō)明遲到閾值 09:00:00 和早退閾值 18:00:00 是寫死的不同公司上下班時(shí)間不同改這兩個(gè)字符串即可但格式必須是 HH:mm:ss少一位冒號(hào)都會(huì)導(dǎo)致比較失效。分組這里有個(gè)細(xì)節(jié)GROUP BY 后面寫的是 e.id, e.emp_no, e.emp_name 三個(gè)字段。理論上按 e.id 分組就夠了但因?yàn)?SELECT 里查了 emp_no 和 emp_nameMySQL 在 ONLY_FULL_GROUP_BY 模式下要求這些字段都出現(xiàn)在 GROUP BY 里否則直接報(bào)錯(cuò)。很多新手把 GROUP BY 縮寫成只按 e.id 分組然后在別人的機(jī)器上能跑、自己機(jī)器上報(bào)錯(cuò)就是 MySQL 的 sql_mode 配置不同導(dǎo)致的。統(tǒng)計(jì)結(jié)果在頁(yè)面上的展示常見做法是循環(huán) ResultSet 拼 HTML 表格或者用 JSTL 標(biāo)簽遍歷 List。如果資源里用的是后者你會(huì)發(fā)現(xiàn)代碼量明顯更少這也是 JSP 頁(yè)面里比 scriptlet 更推薦的寫法。5. 避坑排查數(shù)據(jù)庫(kù)連不上、亂碼、時(shí)間錯(cuò)位的五個(gè)現(xiàn)場(chǎng)老項(xiàng)目跑不起來(lái)九成問(wèn)題出在環(huán)境配置而不是代碼本身。我把這類考勤系統(tǒng)最常見的五個(gè)報(bào)錯(cuò)現(xiàn)場(chǎng)整理出來(lái)每個(gè)都按現(xiàn)象、原因、解決給出處理步驟照著排查比瞎試快得多。5.1 現(xiàn)象一啟動(dòng) Tomcat 報(bào) ClassNotFoundException現(xiàn)象第一次啟動(dòng) Tomcat 或訪問(wèn)任意頁(yè)面時(shí)控制臺(tái)報(bào) java.lang.ClassNotFoundException: com.mysql.jdbc.Driver緊接著是一大串 Caused by最后頁(yè)面顯示 500。報(bào)錯(cuò)位置通常定位在 DBUtil 工具類的靜態(tài)代碼塊里 Class.forName(driver) 那一行。原因MySQL 驅(qū)動(dòng) jar 沒(méi)有放進(jìn) WebContent/WEB-INF/lib 目錄。IDE 的 build path 里加了 jar編譯能通過(guò)但 Tomcat 運(yùn)行時(shí)加載的是 WEB-INF/lib 下的 jar兩邊不一致就會(huì)出現(xiàn)編譯過(guò)、運(yùn)行炸的迷惑現(xiàn)象。解決分三步走。第一步確認(rèn) lib 目錄下有沒(méi)有 mysql-connector-java 的 jar沒(méi)有就下載對(duì)應(yīng)版本放進(jìn)去第二步右鍵項(xiàng)目 Clean讓 IDE 重新打包部署第三步檢查 Project Structure 里的 Artifacts 配置確認(rèn) lib 目錄被標(biāo)記為 jar 目錄。多數(shù)情況做完第一步就好后兩步是防止 IDE 緩存搗亂。5.2 現(xiàn)象二頁(yè)面中文全部變成問(wèn)號(hào)現(xiàn)象登錄頁(yè)面正常但員工姓名、部門名稱等中文數(shù)據(jù)顯示成 ????或者向數(shù)據(jù)庫(kù)插入中文后查出來(lái)是亂碼。有些是頁(yè)面顯示亂碼但庫(kù)里正常有些是頁(yè)面正常但庫(kù)里是亂碼兩種情況原因不同。原因三個(gè)環(huán)節(jié)至少有一個(gè)字符集沒(méi)對(duì)齊——數(shù)據(jù)庫(kù)表字符集、JDBC 連接串、JSP 頁(yè)面編碼。頁(yè)面亂碼而庫(kù)正常問(wèn)題出在 JSP 編碼或 response 編碼庫(kù)亂碼則基本是 JDBC 連接串缺了 characterEncoding。解決按順序檢查先把 jdbc.url 加上 characterEncodingutf8再確認(rèn)建表時(shí)用了 utf8mb4最后在 JSP 頁(yè)面頂部寫 % page contentTypetext/html; charsetUTF-8 %。三個(gè)地方統(tǒng)一成 UTF-8 之后重啟亂碼問(wèn)題基本消失。還有一個(gè)冷門原因properties 文件保存時(shí)被 IDE 轉(zhuǎn)成了 GBK 編碼讀出來(lái)的連接串本身就帶亂碼這種情況把文件重新保存為 UTF-8 即可。5.3 現(xiàn)象三打卡時(shí)間比本地時(shí)間慢 8 小時(shí)現(xiàn)象用戶上午 9 點(diǎn)打卡數(shù)據(jù)庫(kù)里 check_in 顯示 01:00:00整個(gè)系統(tǒng)的打卡時(shí)間全部偏移 8 小時(shí)。原因服務(wù)器或 MySQL 的時(shí)區(qū)設(shè)置是 UTC而用戶在東八區(qū)。MySQL 8.0 的驅(qū)動(dòng)默認(rèn)讀取 serverTimezone 參數(shù)不指定就按 JVM 默認(rèn)時(shí)區(qū)算兩邊不一致就出現(xiàn) 8 小時(shí)偏移。解決在 jdbc.url 后面追加 serverTimezoneAsia/Shanghai同時(shí)檢查操作系統(tǒng)時(shí)區(qū)。如果項(xiàng)目里取時(shí)間用的是 new Date()取到的是 JVM 時(shí)區(qū)的時(shí)間還要看 Tomcat 啟動(dòng)的 JVM 參數(shù)有沒(méi)有 -Duser.timezoneAsia/Shanghai。改完配置必須重啟 Tomcat光改配置文件不重啟等于白改。5.4 現(xiàn)象四導(dǎo)出報(bào)表 Excel 打開是亂碼現(xiàn)象管理員導(dǎo)出月度考勤報(bào)表用 Excel 打開之后姓名列全是亂碼或者干脆打不開提示格式錯(cuò)誤。原因很多老項(xiàng)目導(dǎo)出 Excel 用的是往 response 里直接寫 HTML 表格的偽 Excel 方案文件后綴是 .xls 但內(nèi)容其實(shí)是 HTML。如果 response 沒(méi)設(shè)置正確編碼瀏覽器按默認(rèn)編碼解析中文就亂碼。解決在導(dǎo)出 Servlet 里顯式設(shè)置 response.setCharacterEncoding(UTF-8)并且給 Content-Type 加 charset。更穩(wěn)妥的方案是把偽 Excel 換成真正的 Excel 導(dǎo)出組件但課程設(shè)計(jì)階段先調(diào)編碼就夠了。注意導(dǎo)出的文件名如果是中文要對(duì)文件名做 URL 編碼處理否則瀏覽器端顯示文件名亂碼。5.5 現(xiàn)象五8080 端口被占用導(dǎo)致啟動(dòng)失敗現(xiàn)象啟動(dòng) Tomcat 時(shí)控制臺(tái)報(bào) Port 8080 required by Tomcat at localhost is already in useIDE 彈窗提示端口沖突項(xiàng)目根本起不來(lái)。原因要么之前啟動(dòng)的 Tomcat 實(shí)例沒(méi)關(guān)閉干凈要么其他程序占用了 8080 端口。常見的是前一次異常退出IDE 里的 Tomcat 進(jìn)程還掛在后臺(tái)。解決先關(guān)掉 IDE 里所有 Tomcat 實(shí)例再在命令行執(zhí)行 netstat -ano | findstr 8080 查占用進(jìn)程的 PID然后 taskkill /PID 對(duì)應(yīng)PID /F。如果端口是被別的服務(wù)固定占用也可以直接改 Tomcat 的 server.xml 里的 Connector port把 8080 改成 8081但要記得訪問(wèn) URL 里的端口號(hào)同步改掉。提示排查順序建議固定為端口、驅(qū)動(dòng)、字符集、時(shí)區(qū)。這個(gè)順序覆蓋了考勤系統(tǒng)九成以上的啟動(dòng)和運(yùn)行報(bào)錯(cuò)每次照著走一遍比東查一下西查一下省時(shí)間。6. 二次開發(fā)方向把考勤系統(tǒng)改造成 Spring Boot 的遷移路線6.1 從 Servlet 到 Controller先跑通再改造如果你不想停留在能跑的階段最值得做的二次開發(fā)是把它遷移到 Spring Boot。遷移的思路不是重寫而是逐層替換。先建一個(gè) Spring Boot 工程引入 web 和 mybatis 兩個(gè) starter然后把 LoginServlet 里的邏輯原樣搬進(jìn) ControllerPostMapping(/login) public String login(String empNo, String password, HttpSession session) { Employee emp employeeMapper.findByEmpNoAndPassword(empNo, password); if (emp ! null) { session.setAttribute(user, emp.getEmpNo()); session.setAttribute(role, emp.getRole()); return redirect:/index; } return redirect:/login?msgerror; }這里的 employeeMapper 是 MyBatis 的 Mapper 接口SQL 用注解直接寫在方法上。遷移時(shí)注意兩點(diǎn)一是 HttpSession 的用法不變Spring MVC 可以直接注入 HttpSession二是原來(lái) Filter 里的權(quán)限判斷改造成 Spring 的 HandlerInterceptor邏輯幾乎一模一樣。6.2 用接口清單驗(yàn)證遷移結(jié)果遷移完別急著收工。我的習(xí)慣是列一份接口清單登錄、打卡、統(tǒng)計(jì)、導(dǎo)出逐個(gè)對(duì)比遷移前后的輸出。從那次以后我每次拿到這類老項(xiàng)目都先花十分鐘把數(shù)據(jù)庫(kù)腳本和配置參數(shù)過(guò)一遍再啟動(dòng)項(xiàng)目。先跑通、再讀碼、后改造這個(gè)順序幫我省了大量查錯(cuò)時(shí)間。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取