開(kāi)發(fā)實(shí)戰(zhàn):從Swing界面到JDBC持久化)
簡(jiǎn)介針對(duì)備忘錄管理信息化需求開(kāi)發(fā)的畢業(yè)設(shè)計(jì)文檔面向計(jì)算機(jī)相關(guān)專業(yè)學(xué)生及需要快速理解SSM框架MySQL開(kāi)發(fā)流程的開(kāi)發(fā)者。內(nèi)容涵蓋系統(tǒng)用戶管理、備忘錄管理、日志管理、登錄與退出等核心模塊的設(shè)計(jì)思路并完整呈現(xiàn)緒論、技術(shù)介紹、系統(tǒng)分析、測(cè)試等章節(jié)可作為課程設(shè)計(jì)或畢業(yè)設(shè)計(jì)的參考模板。資源包僅含1個(gè)docx文檔大小690KB已有78人學(xué)習(xí)。文檔基于Java語(yǔ)言采用Spring、SpringMVC、MyBatis三層架構(gòu)結(jié)合Eclipse工具與MySQL數(shù)據(jù)庫(kù)詳細(xì)說(shuō)明了從需求分析到系統(tǒng)測(cè)試的全過(guò)程對(duì)比傳統(tǒng)管理模式突出了信息化管理在效率與經(jīng)濟(jì)成本上的優(yōu)勢(shì)有助于讀者快速掌握SSM框架在小型管理系統(tǒng)中的實(shí)際應(yīng)用。1. 為什么“課程設(shè)計(jì)級(jí)”的備忘錄系統(tǒng)反而值得認(rèn)真拆解很多人看到“基于Java的備忘錄管理系統(tǒng)”這種題目第一反應(yīng)是“這不就是個(gè)增刪改查嗎有什么好寫的”。這個(gè)反應(yīng)我太熟悉了因?yàn)槲易约簬н^(guò)不少新人也評(píng)審過(guò)大量類似的課設(shè)、畢設(shè)項(xiàng)目。但我想說(shuō)的是正因?yàn)檫@類系統(tǒng)看起來(lái)簡(jiǎn)單它才是一個(gè)絕佳的試金石——它能檢驗(yàn)?zāi)銓?duì)Java基礎(chǔ)、面向?qū)ο笤O(shè)計(jì)、數(shù)據(jù)持久化、異常處理、用戶交互這一整條鏈路是否真的吃透了。備忘錄管理系統(tǒng)的核心價(jià)值不在于“記事”本身而在于它幾乎覆蓋了Java后臺(tái)開(kāi)發(fā)的全部基本功JavaBean的封裝、集合框架的使用、文件或數(shù)據(jù)庫(kù)的讀寫、分層架構(gòu)的拆解、時(shí)間日期的處理、界面與邏輯的分離。這些能力是通用的今天你把它用在備忘錄上明天換成圖書管理、日程管理、個(gè)人財(cái)務(wù)架構(gòu)思路完全一致。我在實(shí)戰(zhàn)中見(jiàn)過(guò)兩種極端一種是純面向過(guò)程的寫法所有邏輯堆在一個(gè)類里界面代碼和業(yè)務(wù)代碼糾纏在一起改一個(gè)按鈕就要翻幾百行代碼另一種是過(guò)度設(shè)計(jì)為了一個(gè)500行的系統(tǒng)引入了Spring Boot、MyBatis、Maven多模塊結(jié)果環(huán)境配置花了兩天還沒(méi)寫一行業(yè)務(wù)代碼。這兩種都很可惜。這篇博文我會(huì)按一個(gè)“剛好合適”的粒度來(lái)拆解——既保留分層設(shè)計(jì)帶來(lái)的可維護(hù)性又不至于用大炮打蚊子。適合正在做課程設(shè)計(jì)、畢業(yè)設(shè)計(jì)或者想用一個(gè)小項(xiàng)目把Java基礎(chǔ)串起來(lái)的朋友參考。2. 系統(tǒng)的功能定位與核心需求分析2.1 備忘錄系統(tǒng)“應(yīng)該”做成什么樣在動(dòng)手寫代碼之前先想清楚系統(tǒng)邊界。我見(jiàn)過(guò)太多人一上來(lái)就寫代碼寫到一半發(fā)現(xiàn)需求含糊又開(kāi)始返工。備忘錄系統(tǒng)的核心功能其實(shí)非常聚焦就三類記事新增、編輯、刪除備忘錄條目記錄標(biāo)題、正文、創(chuàng)建時(shí)間、最后修改時(shí)間。分類與檢索按類別或關(guān)鍵詞篩選備忘錄否則記事越來(lái)越多之后根本無(wú)法使用。提醒與狀態(tài)管理給備忘錄設(shè)置提醒時(shí)間或優(yōu)先級(jí)把“已完成”“待辦”狀態(tài)區(qū)分開(kāi)。從用戶角度來(lái)說(shuō)一個(gè)能用的備忘錄系統(tǒng)不需要花哨但一定要邏輯閉環(huán)。新增的記錄能查到改過(guò)的內(nèi)容能反映刪除之前有確認(rèn)搜索時(shí)能命中。這些流程打通之后系統(tǒng)才是“可用”的——很多課程設(shè)計(jì)恰恰就敗在這上面功能按鈕擺了一排但數(shù)據(jù)流是斷的新增完去列表頁(yè)看不到改完?duì)顟B(tài)刷新還是舊值。2.2 典型用戶畫像與使用場(chǎng)景我建議在需求分析階段做一次用戶場(chǎng)景的推演。比如用戶在早上創(chuàng)建一個(gè)“下午三點(diǎn)給客戶回電”的備忘設(shè)置優(yōu)先級(jí)為高類別為工作下午處理完以后把狀態(tài)改為已完成晚上查看時(shí)能過(guò)濾“只看未完成事項(xiàng)”。這整個(gè)流程就是系統(tǒng)設(shè)計(jì)的驗(yàn)收標(biāo)準(zhǔn)。這樣的場(chǎng)景推導(dǎo)看起來(lái)很基礎(chǔ)但它的作用非常大它會(huì)決定你的類怎么切、方法怎么命名、異常怎么處理。例如你會(huì)發(fā)現(xiàn)需要一條“查詢所有未完成備忘”的方法那代碼結(jié)構(gòu)里就必須有findByStatus這類接口而不是每次查出來(lái)再在內(nèi)存里循環(huán)過(guò)濾——后者在小數(shù)據(jù)量下能跑但做法是錯(cuò)的而且一旦數(shù)據(jù)量上來(lái)就暴露出性能問(wèn)題。2.3 技術(shù)選型Swing還是JavaFX文件還是數(shù)據(jù)庫(kù)技術(shù)選型是每個(gè)Java桌面項(xiàng)目繞不開(kāi)的問(wèn)題而很多人都選錯(cuò)了。界面方面Java Swing和JavaFX是目前主流的兩條路線。對(duì)于課程設(shè)計(jì)我更推薦 Swing理由很實(shí)際學(xué)習(xí)資料多、教材覆蓋率極高、JDK自帶無(wú)需額外配置、代碼示例隨便一搜就有。JavaFX 雖然界面更現(xiàn)代但它從JDK 11開(kāi)始被剝離出標(biāo)準(zhǔn)JDK需要單獨(dú)引入依賴對(duì)于只想專注業(yè)務(wù)邏輯的初學(xué)者反而多了一道坎。數(shù)據(jù)存儲(chǔ)方面我建議分階段先做文件存儲(chǔ)如TXT或CSV把序列化和IO讀寫練熟再進(jìn)階到SQLite或H2嵌入式數(shù)據(jù)庫(kù)。不要一上來(lái)就裝MySQL桌面級(jí)備忘錄工具用獨(dú)立的數(shù)據(jù)庫(kù)服務(wù)器太重了而且部署和移植都麻煩。SQLite是單文件數(shù)據(jù)庫(kù)Java里通過(guò)JDBC驅(qū)動(dòng)就能操作體驗(yàn)和文件存儲(chǔ)幾乎一樣簡(jiǎn)單但已經(jīng)能把SQL語(yǔ)句練上手了。架構(gòu)方面不管數(shù)據(jù)層用什么分層是最重要的界面層只負(fù)責(zé)展示和收集輸入業(yè)務(wù)層處理規(guī)則和流轉(zhuǎn)數(shù)據(jù)層管持久化。這樣分層之后你把文件存儲(chǔ)換成數(shù)據(jù)庫(kù)界面和業(yè)務(wù)代碼幾乎不用動(dòng)。這個(gè)收益在寫代碼時(shí)感受不明顯等需求變化、出bug排查時(shí)你就知道分層的價(jià)值了。3. 核心模塊設(shè)計(jì)與關(guān)鍵代碼實(shí)現(xiàn)3.1 實(shí)體層設(shè)計(jì)備忘錄對(duì)象該怎么建模實(shí)體類是整個(gè)系統(tǒng)的地基地基沒(méi)打好后面的代碼處處別扭。我的建議是使用一個(gè)Memo類來(lái)承載核心數(shù)據(jù)字段設(shè)計(jì)如下public class Memo implements Serializable { private Integer id; // 唯一標(biāo)識(shí)自增 private String title; // 標(biāo)題必填 private String content; // 正文內(nèi)容 private String category; // 分類工作/生活/學(xué)習(xí)等 private Integer priority; // 優(yōu)先級(jí)1高 2中 3低 private Integer status; // 狀態(tài)0待辦 1已完成 private LocalDateTime createdAt; // 創(chuàng)建時(shí)間 private LocalDateTime remindAt; // 提醒時(shí)間可為空 private LocalDateTime updatedAt; // 最后修改時(shí)間 }有幾個(gè)設(shè)計(jì)細(xì)節(jié)值得展開(kāi)說(shuō)。第一id字段建議用包裝類型Integer而不是基本類型int因?yàn)楹竺鎻臄?shù)據(jù)源讀取時(shí)可能為空自動(dòng)拆箱容易拋NullPointerException。第二時(shí)間字段統(tǒng)一使用LocalDateTime而不是DateLocalDateTime是Java 8引入的新時(shí)間API線程安全且操作方法豐富格式化解析也更直觀。第三priority和status我建議用int而不是直接存字符串這樣查詢排序和過(guò)濾時(shí)比較方便顯示層再根據(jù)數(shù)值映射成文字。實(shí)體類要重寫toString()方法方便開(kāi)發(fā)階段打印日志排查問(wèn)題同時(shí)實(shí)現(xiàn)Serializable接口如果第一版用文件存對(duì)象這個(gè)接口是必須的。這是很多資料里不會(huì)強(qiáng)調(diào)但實(shí)際開(kāi)發(fā)中非常實(shí)用的細(xì)節(jié)。3.2 數(shù)據(jù)訪問(wèn)層從文件存儲(chǔ)到JDBC的完整演進(jìn)數(shù)據(jù)訪問(wèn)層我建議按兩條路線來(lái)寫不是二選一而是循序漸進(jìn)。第一版文件存儲(chǔ)練序列化和IO。public class MemoFileDao { private static final String FILE_PATH memos.dat; public void save(ListMemo memos) throws IOException { try (ObjectOutputStream oos new ObjectOutputStream( new FileOutputStream(FILE_PATH))) { oos.writeObject(memos); } } SuppressWarnings(unchecked) public ListMemo load() throws IOException, ClassNotFoundException { File file new File(FILE_PATH); if (!file.exists()) { return new ArrayList(); } try (ObjectInputStream ois new ObjectInputStream( new FileInputStream(file))) { return (ListMemo) ois.readObject(); } } }這段代碼看著簡(jiǎn)單但這套思路很關(guān)鍵try-with-resources語(yǔ)法保證流自動(dòng)關(guān)閉避免內(nèi)存泄漏先判斷文件是否存在再讀取避免首次運(yùn)行拋異常用ArrayList作為默認(rèn)返回值避免上層拿到null再去判空。這些都是實(shí)戰(zhàn)中非常常見(jiàn)的坑。第二版SQLite數(shù)據(jù)庫(kù)練JDBC和SQL。public class MemoDao { private Connection getConnection() throws SQLException { return DriverManager.getConnection(jdbc:sqlite:memo.db); } public void createTable() { String sql CREATE TABLE IF NOT EXISTS memo ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, category TEXT, priority INTEGER DEFAULT 2, status INTEGER DEFAULT 0, created_at TEXT, remind_at TEXT, updated_at TEXT); try (Connection conn getConnection(); Statement stmt conn.createStatement()) { stmt.execute(sql); } catch (SQLException e) { e.printStackTrace(); } } }這里有一個(gè)特別容易踩的坑SQLite沒(méi)有專門的日期時(shí)間類型所以時(shí)間字段用TEXT字符串存儲(chǔ)。那么在寫入和讀取時(shí)就必須統(tǒng)一格式。我建議在DAO層做轉(zhuǎn)換用DateTimeFormatter.ISO_LOCAL_DATE_TIME格式化后再存入數(shù)據(jù)庫(kù)取出時(shí)再解析回LocalDateTime。如果有人在寫入時(shí)不格式化直接拼接對(duì)象toString取出時(shí)就解析不回來(lái)。再?gòu)?qiáng)調(diào)一個(gè)點(diǎn)JDBC操作里PreparedStatement是必須的千萬(wàn)不要用字符串拼接SQL來(lái)傳參。既防SQL注入又不用手動(dòng)處理引號(hào)轉(zhuǎn)義代碼還更清晰public void insert(Memo memo) { String sql INSERT INTO memo(title, content, category, priority, status, created_at, remind_at, updated_at) VALUES(?, ?, ?, ?, ?, ?, ?, ?); try (Connection conn getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, memo.getTitle()); ps.setString(2, memo.getContent()); ps.setString(3, memo.getCategory()); ps.setInt(4, memo.getPriority()); ps.setInt(5, memo.getStatus()); ps.setString(6, memo.getCreatedAt() null ? null : memo.getCreatedAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); ps.setString(7, memo.getRemindAt() null ? null : memo.getRemindAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); ps.setString(8, memo.getUpdatedAt() null ? null : memo.getUpdatedAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } }注意兩個(gè)細(xì)節(jié)setInt和setString之后如果字段為null要用setNull或者像上面這樣提前判空否則LocalDateTime.format會(huì)直接空指針執(zhí)行增刪改用executeUpdate()執(zhí)行查詢用executeQuery()這兩個(gè)方法絕對(duì)不能混用。3.3 業(yè)務(wù)層設(shè)計(jì)把“規(guī)則”從界面中剝離出來(lái)業(yè)務(wù)層是很多課設(shè)忽略的但恰恰是拉開(kāi)水平差距的地方。比如“刪除備忘錄時(shí)需要二次確認(rèn)”“已完成狀態(tài)不能修改內(nèi)容只允許刪除”“統(tǒng)計(jì)不同分類的數(shù)量”——這些都是業(yè)務(wù)規(guī)則處理它們的代碼應(yīng)該統(tǒng)一放在MemoService而不是散落在各個(gè)窗口的監(jiān)聽(tīng)器里。public class MemoService { private MemoDao memoDao; public MemoService() { this.memoDao new MemoDao(); } public boolean addMemo(String title, String content, String category, int priority, LocalDateTime remindAt) { if (title null || title.trim().isEmpty()) { throw new IllegalArgumentException(標(biāo)題不能為空); } Memo memo new Memo(); memo.setTitle(title.trim()); // 其余字段set... memo.setStatus(0); memo.setCreatedAt(LocalDateTime.now()); memo.setUpdatedAt(LocalDateTime.now()); memoDao.insert(memo); return true; } public ListMemo findUnfinished() { return memoDao.findByStatus(0); } }業(yè)務(wù)層的好處體現(xiàn)在兩個(gè)場(chǎng)景。第一是參數(shù)校驗(yàn)的統(tǒng)一收斂標(biāo)題為空這種錯(cuò)誤不管用戶是在哪個(gè)窗口按下保存都觸發(fā)同一套校驗(yàn)邏輯不會(huì)出現(xiàn)“主窗口能存空標(biāo)題、編輯窗口就不行”這種詭異現(xiàn)象。第二是擴(kuò)展的便利性以后想加一個(gè)“到期備忘錄自動(dòng)標(biāo)記為超期”的規(guī)則只需在Service里增加一個(gè)方法界面層完全不用改。3.4 界面層設(shè)計(jì)Java Swing布局要點(diǎn)與事件綁定Swing界面層很容易寫成一坨亂麻核心原因是布局管理器沒(méi)有選對(duì)。我的建議是主界面用BorderLayout北邊放搜索和篩選工具欄中間放JTable列表南邊放操作按鈕。編輯界面用GridBagLayout或GroupLayout雖然這兩個(gè)布局寫起來(lái)繁瑣但效果和伸縮性遠(yuǎn)超null布局。我這里要嚴(yán)厲警告一點(diǎn)絕對(duì)不要用setLayout(null)絕對(duì)坐標(biāo)定位。這樣做的后果是窗口一旦拉伸控件全部擠在一起在不同分辨率的屏幕上效果還會(huì)不同后期加一個(gè)按鈕所有坐標(biāo)全部要重新算。我看到過(guò)太多課設(shè)代碼就是這么寫的雖然能截圖交差但實(shí)際一運(yùn)行全是問(wèn)題。表格的構(gòu)建是界面層的核心建議使用DefaultTableModel包裝數(shù)據(jù)DefaultTableModel model new DefaultTableModel( new Object[]{ID, 標(biāo)題, 分類, 優(yōu)先級(jí), 狀態(tài), 提醒時(shí)間}, 0); public void refreshTable(ListMemo memos) { model.setRowCount(0); for (Memo m : memos) { model.addRow(new Object[]{ m.getId(), m.getTitle(), m.getCategory(), priorityText(m.getPriority()), statusText(m.getStatus()), m.getRemindAt() null ? — : m.getRemindAt().format(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm)) }); } }表格刷新有個(gè)常見(jiàn)問(wèn)題直接model.setRowCount(0)再重新addRow會(huì)閃爍得很厲害。數(shù)據(jù)量小的時(shí)候看不出來(lái)一旦超過(guò)幾百條體驗(yàn)就很糟糕。印象中比較高效的做法是使用TableModel的事件機(jī)制或者直接重新setModel一個(gè)新的DefaultTableModel這兩者刷新效率都更好。事件綁定方面每個(gè)按鈕的ActionListener里只做三件事收集界面輸入、調(diào)用Service方法、刷新表格。不要在監(jiān)聽(tīng)器里寫任何業(yè)務(wù)邏輯比如“判斷標(biāo)題是否為空”這種邏輯必須放到Service層。這樣監(jiān)聽(tīng)器基本就是三行代碼saveBtn.addActionListener(e - { try { service.addMemo(titleField.getText(), contentArea.getText(), categoryBox.getSelectedItem().toString(), priorityBox.getSelectedIndex() 1, remindTimeField.getText().isEmpty() ? null : LocalDateTime.parse(remindTimeField.getText(), formatter)); refreshTable(service.findAll()); clearInputs(); } catch (IllegalArgumentException ex) { JOptionPane.showMessageDialog(this, ex.getMessage(), 提示, JOptionPane.WARNING_MESSAGE); } });4. 關(guān)鍵功能實(shí)現(xiàn)提醒機(jī)制與事務(wù)邊界4.1 提醒功能的實(shí)時(shí)性與輪詢方案取舍備忘錄要“有點(diǎn)用”提醒功能是加分項(xiàng)。Swing是單線程模型所有界面操作都在**EDT事件分發(fā)線程**上執(zhí)行所以在EDT里寫死循環(huán)等待就會(huì)卡死界面。最樸素的方案是用一個(gè)定時(shí)器輪詢數(shù)據(jù)庫(kù)看看有沒(méi)有到點(diǎn)的未提醒備忘。我的做法是使用javax.swing.Timer而不是java.util.Timer原因在于Swing版本的回調(diào)方法會(huì)在EDT線程上執(zhí)行可以直接安全地更新界面而無(wú)須手動(dòng)SwingUtilities.invokeLater。示例代碼如下Timer reminderTimer new Timer(1000 * 30, e - checkReminders()); reminderTimer.start(); private void checkReminders() { ListMemo dueMemos service.findDueAndUnnotified(); for (Memo memo : dueMemos) { JOptionPane.showMessageDialog(mainFrame, 提醒 memo.getTitle(), 備忘錄提醒, JOptionPane.INFORMATION_MESSAGE); service.markNotified(memo.getId()); } }這里的Timer構(gòu)造函數(shù)第一個(gè)參數(shù)是毫秒間隔30秒檢查一次即可滿足大多數(shù)場(chǎng)景。不要設(shè)成1秒一次純屬浪費(fèi)CPU而且彈窗頻率過(guò)密會(huì)嚴(yán)重干擾使用。這里有個(gè)設(shè)計(jì)細(xì)節(jié)service.findDueAndUnnotified()在數(shù)據(jù)庫(kù)層對(duì)應(yīng)的SQL是WHERE remind_at ? AND notified 0所以實(shí)體類中還需要加一個(gè)notified字段或者復(fù)用status字段來(lái)標(biāo)記“已提醒”。我的經(jīng)驗(yàn)是單獨(dú)加notified字段更清晰免得狀態(tài)語(yǔ)義混淆。4.2 時(shí)間比較的精度問(wèn)題從毫秒到分鐘時(shí)間提醒有一個(gè)典型陷阱——LocalDateTime存的是納秒精度而界面展示和用戶認(rèn)知通常精確到分鐘。你在數(shù)據(jù)庫(kù)里存了2025-06-08T15:30:00用戶看到的是15:30但在程序內(nèi)部比較時(shí)會(huì)帶上納秒部分導(dǎo)致兩個(gè)“看起來(lái)一樣”的時(shí)間equals返回false。所以在解析用戶輸入時(shí)我建議統(tǒng)一做一次truncatedTo(ChronoUnit.MINUTES)處理把秒和納秒全部截?cái)?。這個(gè)動(dòng)作既能讓時(shí)間的相等比較符合直覺(jué)還能避免數(shù)據(jù)庫(kù)里存出一堆莫名其妙的秒級(jí)時(shí)間數(shù)據(jù)LocalDateTime remindAt LocalDateTime.parse(text, formatter) .truncatedTo(ChronoUnit.MINUTES);另外提醒時(shí)間是過(guò)去的時(shí)間該怎么辦用戶在新增備忘時(shí)已經(jīng)把提醒時(shí)間設(shè)在昨天這種數(shù)據(jù)存進(jìn)去沒(méi)有任何意義還可能導(dǎo)致啟動(dòng)程序時(shí)瞬間彈出無(wú)數(shù)個(gè)歷史提醒窗口。所以Service的addMemo方法里必須有這個(gè)校驗(yàn)remindAt.isBefore(LocalDateTime.now())就拋IllegalArgumentException(提醒時(shí)間不能早于當(dāng)前時(shí)間)。4.3 刪除操作的二次確認(rèn)與事務(wù)邊界刪除備忘錄時(shí)彈一個(gè)確認(rèn)框是對(duì)用戶的基本尊重。但真正容易忽略的是刪除關(guān)聯(lián)數(shù)據(jù)的事務(wù)邊界。如果你的系統(tǒng)有“備忘錄和提醒記錄是兩張表”的關(guān)系刪除備忘錄時(shí)必須同時(shí)刪除它對(duì)應(yīng)的提醒記錄這兩個(gè)操作必須在一個(gè)事務(wù)里完成。JDBC事務(wù)的寫法要牢記先conn.setAutoCommit(false)然后執(zhí)行多個(gè)SQL全部成功再commit()任何一步失敗都要rollback()最后在finally里恢復(fù)autoCommit并關(guān)閉連接。很多新人只會(huì)寫單個(gè)SQL不知道Connection默認(rèn)是自動(dòng)提交的結(jié)果總以為兩條SQL之間天然就是原子的一旦第二條執(zhí)行失敗數(shù)據(jù)就出現(xiàn)了臟數(shù)據(jù)。不過(guò)話又說(shuō)回來(lái)對(duì)于備忘錄管理系統(tǒng)這種體量的項(xiàng)目單表就能解決問(wèn)題不一定要設(shè)計(jì)成多表關(guān)聯(lián)。表拆分得越多代碼復(fù)雜度上升越明顯即使有事務(wù)兜底也要考慮值不值。我的建議是把提醒狀態(tài)作為備忘錄表的一個(gè)字段放在同一張表里這樣根本不需要事務(wù)一條UPDATE就能解決簡(jiǎn)單可靠。5. 踩過(guò)的坑從運(yùn)行環(huán)境到代碼細(xì)節(jié)的實(shí)戰(zhàn)排查5.1 JDK環(huán)境變量配置的經(jīng)典問(wèn)題界面、業(yè)務(wù)、數(shù)據(jù)層都寫完以后最尷尬的事情往往發(fā)生在運(yùn)行環(huán)境上。很多同學(xué)的代碼在IDE里點(diǎn)“運(yùn)行”完全正常但雙擊Jar包就報(bào)錯(cuò)或者命令行java -jar啟動(dòng)不了。排查下來(lái)十有八九是環(huán)境變量的問(wèn)題。我在熱詞里看到“java環(huán)境變量配置詳細(xì)教程”“java安裝教程詳細(xì)”是高頻搜索說(shuō)明這確實(shí)是攔路虎。環(huán)境變量配置的核心只有兩件事JAVA_HOME指向JDK安裝目錄Path里加上%JAVA_HOME%\bin。配置完以后在命令行輸入java -version和javac -version驗(yàn)證兩個(gè)都能正常輸出版本號(hào)才算成功。如果安裝了多個(gè)JDK版本一定要把高版本的Path條目移到前面否則命令行用的還是舊版本。還有一個(gè)容易忽略的坑JDK和JRE混用?,F(xiàn)在JDK自帶了JRE但如果你的機(jī)器上單獨(dú)裝了JRE并且Path里JRE的bin排在JDK前面那么java命令用的是JRE。而JRE沒(méi)有javac所以就會(huì)出現(xiàn)“能運(yùn)行不能編譯”的怪象。這屬于排查了半天都找不出原因、最后發(fā)現(xiàn)就是路徑順序問(wèn)題的典型case。5.2 編譯期“軟件包不存在”與Lombok沖突問(wèn)題有很多人在項(xiàng)目里用了LombokData注解寫得很爽結(jié)果換一臺(tái)電腦或者用命令行編譯時(shí)就報(bào)“程序包lombok不存在”。這個(gè)問(wèn)題我在熱詞里也看到了“you arent using a compiler supported by lombok, so lombok will not work”。這說(shuō)明編譯環(huán)境和Lombok的適配出了問(wèn)題。對(duì)于備忘錄管理系統(tǒng)這個(gè)體量我的建議是放棄Lombok手寫getter/setter。雖然多打個(gè)十幾行字但它省掉的麻煩是實(shí)打?qū)嵉牟挥醚bIDE插件、不用配注解處理器、不用擔(dān)心編譯環(huán)境之間的兼容性、生成的class文件也絕對(duì)不會(huì)有問(wèn)題。一個(gè)課程設(shè)計(jì)項(xiàng)目把精力花在環(huán)境適配和奇怪報(bào)錯(cuò)上太不值得了。如果非要用Lombok至少確認(rèn)兩件事IDEA里安裝Lombok插件并開(kāi)啟Enable annotation processing項(xiàng)目的編譯方式用Maven或Gradle管理依賴而不是手動(dòng)引入jar包。否則在別人電腦上拉下來(lái)代碼編譯報(bào)錯(cuò)分分鐘讓人崩潰。還有一個(gè)坑和編碼相關(guān)Windows環(huán)境下命令行編譯Java源文件時(shí)如果代碼里有中文而源文件是UTF-8編碼直接javac編譯很可能報(bào)“未結(jié)束的字符串文字”或亂碼。解決方案是編譯時(shí)顯式指定javac -encoding UTF-8 *.java。這個(gè)參數(shù)在IDE里通常默認(rèn)配好了但命令行編譯時(shí)非常容易踩到尤其是從Windows的cmd窗口直接操作時(shí)。備忘錄系統(tǒng)里到處都是中文字符串編碼問(wèn)題可以說(shuō)是必修課。5.3 JVM內(nèi)存不足與數(shù)組越界的高發(fā)場(chǎng)景“java: outofmemoryerror: insufficient memory”這個(gè)報(bào)錯(cuò)我在查看JVM類項(xiàng)目時(shí)沒(méi)少見(jiàn)過(guò)。備忘錄系統(tǒng)雖然是輕量級(jí)應(yīng)用但如果你一次性把整個(gè)文件讀進(jìn)來(lái)處理或者表格里的數(shù)據(jù)不翻頁(yè)全量加載數(shù)據(jù)量到達(dá)一定級(jí)別時(shí)同樣能撐爆堆內(nèi)存。我的經(jīng)驗(yàn)是做桌面工具時(shí)養(yǎng)成兩個(gè)習(xí)慣一是在主函數(shù)入口給JVM設(shè)合理的初始堆大小java -Xms64m -Xmx256m -jar memo.jar預(yù)留比實(shí)際需求稍高的空間但也不要無(wú)腦給幾個(gè)G二是數(shù)據(jù)加載盡量分批比如查詢列表時(shí)默認(rèn)只加載前500條用戶搜索時(shí)再加條件減少單次內(nèi)存壓力。數(shù)組越界異常也高發(fā)在界面層。比如表格選擇行時(shí)沒(méi)有判斷getSelectedRow()的返回值是不是-1用戶沒(méi)選中任何行就直接點(diǎn)刪除代碼就會(huì)崩。這個(gè)異常在明明“不可能”的情況下就發(fā)生了因?yàn)樵诳焖冱c(diǎn)擊時(shí)表格的選中狀態(tài)可能已經(jīng)變化了。我的防御性寫法是所有從界面控件取值的地方都先判空或判斷下標(biāo)范圍int selectedRow table.getSelectedRow(); if (selectedRow 0) { JOptionPane.showMessageDialog(this, 請(qǐng)先選擇一條記錄, 提示, JOptionPane.WARNING_MESSAGE); return; }這種處理看起來(lái)啰嗦但正是這些防御性判斷讓你的程序從“能跑”變成“不會(huì)莫名其妙崩”。一個(gè)無(wú)人維護(hù)的課設(shè)項(xiàng)目最怕的就是用戶在沒(méi)有任何提示的情況下看到程序異常退出這種體驗(yàn)非常差。6. 讓系統(tǒng)更“值錢”的三個(gè)進(jìn)階方向6.1 從文件存儲(chǔ)升級(jí)到數(shù)據(jù)庫(kù)的完整遷移如果第一版用文件存儲(chǔ)升級(jí)到SQLite其實(shí)非常順滑因?yàn)镈AO接口的兩個(gè)實(shí)現(xiàn)對(duì)外暴露的方法是一致的。操作步驟無(wú)非是新寫一個(gè)滿足相同方法簽名的MemoDao實(shí)現(xiàn)類把內(nèi)部邏輯換成JDBC再做一個(gè)數(shù)據(jù)遷移方法讀取原文件數(shù)據(jù)批量寫入數(shù)據(jù)庫(kù)。界面層和業(yè)務(wù)層的代碼完全不用動(dòng)。這就是分層設(shè)計(jì)帶來(lái)的核心優(yōu)勢(shì)——數(shù)據(jù)源替換對(duì)上層透明。遷移時(shí)要注意主鍵的連續(xù)性。文件存儲(chǔ)時(shí)你可能自己用一個(gè)AtomicInteger生成id但SQLite的自增主鍵是由數(shù)據(jù)庫(kù)管理的。遷移時(shí)要么顯式插入舊id要么重新分配id。如果備忘之間有關(guān)聯(lián)關(guān)系比如提醒記錄引用備忘id就一定要保留舊id否則關(guān)聯(lián)就斷了。備忘錄系統(tǒng)通常沒(méi)有這種關(guān)系但培養(yǎng)這種意識(shí)很重要。6.2 導(dǎo)出功能與Java反射的初步結(jié)合給系統(tǒng)加一個(gè)“導(dǎo)出為CSV”功能是性價(jià)比很高的一個(gè)加分項(xiàng)。用戶可以把備忘錄列表導(dǎo)出成表格文件用Excel打開(kāi)。CSV本身就是一個(gè)純文本格式每一行是按逗號(hào)分隔的字段。生成的要點(diǎn)有兩個(gè)字段中含有逗號(hào)或換行時(shí)要加雙引號(hào)包裹文件寫入時(shí)用UTF-8 BOM否則Windows下的Excel打開(kāi)中文會(huì)亂碼。代碼非常簡(jiǎn)單public void exportCsv(ListMemo memos, Path target) throws IOException { try (BufferedWriter writer Files.newBufferedWriter(target, StandardCharsets.UTF_8)) { writer.write(\uFEFF); // UTF-8 BOM防止Excel中文亂碼 writer.write(ID,標(biāo)題,分類,優(yōu)先級(jí),狀態(tài),提醒時(shí)間\n); for (Memo m : memos) { writer.write(String.join(,, String.valueOf(m.getId()), escapeCsv(m.getTitle()), m.getCategory(), String.valueOf(m.getPriority()), String.valueOf(m.getStatus()), m.getRemindAt() null ? : m.getRemindAt().toString() )); writer.newLine(); } } }escapeCsv這個(gè)輔助方法的邏輯是如果字段包含逗號(hào)、雙引號(hào)或換行就用雙引號(hào)包起來(lái)并把字段內(nèi)部的雙引號(hào)替換成兩個(gè)雙引號(hào)。這個(gè)規(guī)則是CSV格式的標(biāo)準(zhǔn)不處理就會(huì)出現(xiàn)導(dǎo)出文件錯(cuò)位。這個(gè)小功能可以引導(dǎo)你了解一種輕量級(jí)的數(shù)據(jù)交換格式也為以后對(duì)接別的系統(tǒng)打下基礎(chǔ)。6.3 面向?qū)ο笏枷氲倪M(jìn)階場(chǎng)代碼重構(gòu)備忘錄系統(tǒng)寫完之后一個(gè)很好的自我訓(xùn)練方式是“重構(gòu)”。重構(gòu)不是改bug而是不改變外部功能的前提下讓代碼結(jié)構(gòu)更合理。比如你發(fā)現(xiàn)Memo類的方法越寫越多可以按職責(zé)拆出MemoValidator類統(tǒng)一管理字段校驗(yàn)比如你發(fā)現(xiàn)MemoService里同時(shí)處理了業(yè)務(wù)邏輯和提醒時(shí)間解析可以把事件解析工具類抽出來(lái)。這些重構(gòu)動(dòng)作本質(zhì)上都是在練習(xí)“識(shí)別壞味道、找到更優(yōu)設(shè)計(jì)”的肌肉記憶。以后你面對(duì)真實(shí)的后臺(tái)系統(tǒng)時(shí)面對(duì)一堆屎山代碼才不至于手足無(wú)措——因?yàn)槟阍趥渫涍@種小項(xiàng)目上已經(jīng)積累過(guò)“化繁為簡(jiǎn)”的經(jīng)驗(yàn)了。根據(jù)我個(gè)人經(jīng)驗(yàn)來(lái)說(shuō)完成一個(gè)備忘錄系統(tǒng)最好的方式不是悶頭一天寫完而是先花半小時(shí)把類和接口的關(guān)系畫在紙上再動(dòng)手代碼寫完核心流程以后運(yùn)行一次觀察哪里交互不順手再迭代一版。這個(gè)“設(shè)計(jì)-實(shí)現(xiàn)-反思-重構(gòu)”的循環(huán)才是Java基礎(chǔ)真正長(zhǎng)在身上的過(guò)程。系統(tǒng)本身做得好不好是其次你在整個(gè)過(guò)程中建立起來(lái)的工程直覺(jué)才是未來(lái)能帶走的財(cái)富。本文還有配套的精品資源點(diǎn)擊獲取