
简介针对备忘录管理信息化需求开发的毕业设计文档面向计算机相关专业学生及需要快速理解SSM框架MySQL开发流程的开发者。内容涵盖系统用户管理、备忘录管理、日志管理、登录与退出等核心模块的设计思路并完整呈现绪论、技术介绍、系统分析、测试等章节可作为课程设计或毕业设计的参考模板。资源包仅含1个docx文档大小690KB已有78人学习。文档基于Java语言采用Spring、SpringMVC、MyBatis三层架构结合Eclipse工具与MySQL数据库详细说明了从需求分析到系统测试的全过程对比传统管理模式突出了信息化管理在效率与经济成本上的优势有助于读者快速掌握SSM框架在小型管理系统中的实际应用。1. 为什么“课程设计级”的备忘录系统反而值得认真拆解很多人看到“基于Java的备忘录管理系统”这种题目第一反应是“这不就是个增删改查吗有什么好写的”。这个反应我太熟悉了因为我自己带过不少新人也评审过大量类似的课设、毕设项目。但我想说的是正因为这类系统看起来简单它才是一个绝佳的试金石——它能检验你对Java基础、面向对象设计、数据持久化、异常处理、用户交互这一整条链路是否真的吃透了。备忘录管理系统的核心价值不在于“记事”本身而在于它几乎覆盖了Java后台开发的全部基本功JavaBean的封装、集合框架的使用、文件或数据库的读写、分层架构的拆解、时间日期的处理、界面与逻辑的分离。这些能力是通用的今天你把它用在备忘录上明天换成图书管理、日程管理、个人财务架构思路完全一致。我在实战中见过两种极端一种是纯面向过程的写法所有逻辑堆在一个类里界面代码和业务代码纠缠在一起改一个按钮就要翻几百行代码另一种是过度设计为了一个500行的系统引入了Spring Boot、MyBatis、Maven多模块结果环境配置花了两天还没写一行业务代码。这两种都很可惜。这篇博文我会按一个“刚好合适”的粒度来拆解——既保留分层设计带来的可维护性又不至于用大炮打蚊子。适合正在做课程设计、毕业设计或者想用一个小项目把Java基础串起来的朋友参考。2. 系统的功能定位与核心需求分析2.1 备忘录系统“应该”做成什么样在动手写代码之前先想清楚系统边界。我见过太多人一上来就写代码写到一半发现需求含糊又开始返工。备忘录系统的核心功能其实非常聚焦就三类记事新增、编辑、删除备忘录条目记录标题、正文、创建时间、最后修改时间。分类与检索按类别或关键词筛选备忘录否则记事越来越多之后根本无法使用。提醒与状态管理给备忘录设置提醒时间或优先级把“已完成”“待办”状态区分开。从用户角度来说一个能用的备忘录系统不需要花哨但一定要逻辑闭环。新增的记录能查到改过的内容能反映删除之前有确认搜索时能命中。这些流程打通之后系统才是“可用”的——很多课程设计恰恰就败在这上面功能按钮摆了一排但数据流是断的新增完去列表页看不到改完状态刷新还是旧值。2.2 典型用户画像与使用场景我建议在需求分析阶段做一次用户场景的推演。比如用户在早上创建一个“下午三点给客户回电”的备忘设置优先级为高类别为工作下午处理完以后把状态改为已完成晚上查看时能过滤“只看未完成事项”。这整个流程就是系统设计的验收标准。这样的场景推导看起来很基础但它的作用非常大它会决定你的类怎么切、方法怎么命名、异常怎么处理。例如你会发现需要一条“查询所有未完成备忘”的方法那代码结构里就必须有findByStatus这类接口而不是每次查出来再在内存里循环过滤——后者在小数据量下能跑但做法是错的而且一旦数据量上来就暴露出性能问题。2.3 技术选型Swing还是JavaFX文件还是数据库技术选型是每个Java桌面项目绕不开的问题而很多人都选错了。界面方面Java Swing和JavaFX是目前主流的两条路线。对于课程设计我更推荐 Swing理由很实际学习资料多、教材覆盖率极高、JDK自带无需额外配置、代码示例随便一搜就有。JavaFX 虽然界面更现代但它从JDK 11开始被剥离出标准JDK需要单独引入依赖对于只想专注业务逻辑的初学者反而多了一道坎。数据存储方面我建议分阶段先做文件存储如TXT或CSV把序列化和IO读写练熟再进阶到SQLite或H2嵌入式数据库。不要一上来就装MySQL桌面级备忘录工具用独立的数据库服务器太重了而且部署和移植都麻烦。SQLite是单文件数据库Java里通过JDBC驱动就能操作体验和文件存储几乎一样简单但已经能把SQL语句练上手了。架构方面不管数据层用什么分层是最重要的界面层只负责展示和收集输入业务层处理规则和流转数据层管持久化。这样分层之后你把文件存储换成数据库界面和业务代码几乎不用动。这个收益在写代码时感受不明显等需求变化、出bug排查时你就知道分层的价值了。3. 核心模块设计与关键代码实现3.1 实体层设计备忘录对象该怎么建模实体类是整个系统的地基地基没打好后面的代码处处别扭。我的建议是使用一个Memo类来承载核心数据字段设计如下public class Memo implements Serializable { private Integer id; // 唯一标识自增 private String title; // 标题必填 private String content; // 正文内容 private String category; // 分类工作/生活/学习等 private Integer priority; // 优先级1高 2中 3低 private Integer status; // 状态0待办 1已完成 private LocalDateTime createdAt; // 创建时间 private LocalDateTime remindAt; // 提醒时间可为空 private LocalDateTime updatedAt; // 最后修改时间 }有几个设计细节值得展开说。第一id字段建议用包装类型Integer而不是基本类型int因为后面从数据源读取时可能为空自动拆箱容易抛NullPointerException。第二时间字段统一使用LocalDateTime而不是DateLocalDateTime是Java 8引入的新时间API线程安全且操作方法丰富格式化解析也更直观。第三priority和status我建议用int而不是直接存字符串这样查询排序和过滤时比较方便显示层再根据数值映射成文字。实体类要重写toString()方法方便开发阶段打印日志排查问题同时实现Serializable接口如果第一版用文件存对象这个接口是必须的。这是很多资料里不会强调但实际开发中非常实用的细节。3.2 数据访问层从文件存储到JDBC的完整演进数据访问层我建议按两条路线来写不是二选一而是循序渐进。第一版文件存储练序列化和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(); } } }这段代码看着简单但这套思路很关键try-with-resources语法保证流自动关闭避免内存泄漏先判断文件是否存在再读取避免首次运行抛异常用ArrayList作为默认返回值避免上层拿到null再去判空。这些都是实战中非常常见的坑。第二版SQLite数据库练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(); } } }这里有一个特别容易踩的坑SQLite没有专门的日期时间类型所以时间字段用TEXT字符串存储。那么在写入和读取时就必须统一格式。我建议在DAO层做转换用DateTimeFormatter.ISO_LOCAL_DATE_TIME格式化后再存入数据库取出时再解析回LocalDateTime。如果有人在写入时不格式化直接拼接对象toString取出时就解析不回来。再强调一个点JDBC操作里PreparedStatement是必须的千万不要用字符串拼接SQL来传参。既防SQL注入又不用手动处理引号转义代码还更清晰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(); } }注意两个细节setInt和setString之后如果字段为null要用setNull或者像上面这样提前判空否则LocalDateTime.format会直接空指针执行增删改用executeUpdate()执行查询用executeQuery()这两个方法绝对不能混用。3.3 业务层设计把“规则”从界面中剥离出来业务层是很多课设忽略的但恰恰是拉开水平差距的地方。比如“删除备忘录时需要二次确认”“已完成状态不能修改内容只允许删除”“统计不同分类的数量”——这些都是业务规则处理它们的代码应该统一放在MemoService而不是散落在各个窗口的监听器里。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(标题不能为空); } 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); } }业务层的好处体现在两个场景。第一是参数校验的统一收敛标题为空这种错误不管用户是在哪个窗口按下保存都触发同一套校验逻辑不会出现“主窗口能存空标题、编辑窗口就不行”这种诡异现象。第二是扩展的便利性以后想加一个“到期备忘录自动标记为超期”的规则只需在Service里增加一个方法界面层完全不用改。3.4 界面层设计Java Swing布局要点与事件绑定Swing界面层很容易写成一坨乱麻核心原因是布局管理器没有选对。我的建议是主界面用BorderLayout北边放搜索和筛选工具栏中间放JTable列表南边放操作按钮。编辑界面用GridBagLayout或GroupLayout虽然这两个布局写起来繁琐但效果和伸缩性远超null布局。我这里要严厉警告一点绝对不要用setLayout(null)绝对坐标定位。这样做的后果是窗口一旦拉伸控件全部挤在一起在不同分辨率的屏幕上效果还会不同后期加一个按钮所有坐标全部要重新算。我看到过太多课设代码就是这么写的虽然能截图交差但实际一运行全是问题。表格的构建是界面层的核心建议使用DefaultTableModel包装数据DefaultTableModel model new DefaultTableModel( new Object[]{ID, 标题, 分类, 优先级, 状态, 提醒时间}, 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)) }); } }表格刷新有个常见问题直接model.setRowCount(0)再重新addRow会闪烁得很厉害。数据量小的时候看不出来一旦超过几百条体验就很糟糕。印象中比较高效的做法是使用TableModel的事件机制或者直接重新setModel一个新的DefaultTableModel这两者刷新效率都更好。事件绑定方面每个按钮的ActionListener里只做三件事收集界面输入、调用Service方法、刷新表格。不要在监听器里写任何业务逻辑比如“判断标题是否为空”这种逻辑必须放到Service层。这样监听器基本就是三行代码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. 关键功能实现提醒机制与事务边界4.1 提醒功能的实时性与轮询方案取舍备忘录要“有点用”提醒功能是加分项。Swing是单线程模型所有界面操作都在**EDT事件分发线程**上执行所以在EDT里写死循环等待就会卡死界面。最朴素的方案是用一个定时器轮询数据库看看有没有到点的未提醒备忘。我的做法是使用javax.swing.Timer而不是java.util.Timer原因在于Swing版本的回调方法会在EDT线程上执行可以直接安全地更新界面而无须手动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构造函数第一个参数是毫秒间隔30秒检查一次即可满足大多数场景。不要设成1秒一次纯属浪费CPU而且弹窗频率过密会严重干扰使用。这里有个设计细节service.findDueAndUnnotified()在数据库层对应的SQL是WHERE remind_at ? AND notified 0所以实体类中还需要加一个notified字段或者复用status字段来标记“已提醒”。我的经验是单独加notified字段更清晰免得状态语义混淆。4.2 时间比较的精度问题从毫秒到分钟时间提醒有一个典型陷阱——LocalDateTime存的是纳秒精度而界面展示和用户认知通常精确到分钟。你在数据库里存了2025-06-08T15:30:00用户看到的是15:30但在程序内部比较时会带上纳秒部分导致两个“看起来一样”的时间equals返回false。所以在解析用户输入时我建议统一做一次truncatedTo(ChronoUnit.MINUTES)处理把秒和纳秒全部截断。这个动作既能让时间的相等比较符合直觉还能避免数据库里存出一堆莫名其妙的秒级时间数据LocalDateTime remindAt LocalDateTime.parse(text, formatter) .truncatedTo(ChronoUnit.MINUTES);另外提醒时间是过去的时间该怎么办用户在新增备忘时已经把提醒时间设在昨天这种数据存进去没有任何意义还可能导致启动程序时瞬间弹出无数个历史提醒窗口。所以Service的addMemo方法里必须有这个校验remindAt.isBefore(LocalDateTime.now())就抛IllegalArgumentException(提醒时间不能早于当前时间)。4.3 删除操作的二次确认与事务边界删除备忘录时弹一个确认框是对用户的基本尊重。但真正容易忽略的是删除关联数据的事务边界。如果你的系统有“备忘录和提醒记录是两张表”的关系删除备忘录时必须同时删除它对应的提醒记录这两个操作必须在一个事务里完成。JDBC事务的写法要牢记先conn.setAutoCommit(false)然后执行多个SQL全部成功再commit()任何一步失败都要rollback()最后在finally里恢复autoCommit并关闭连接。很多新人只会写单个SQL不知道Connection默认是自动提交的结果总以为两条SQL之间天然就是原子的一旦第二条执行失败数据就出现了脏数据。不过话又说回来对于备忘录管理系统这种体量的项目单表就能解决问题不一定要设计成多表关联。表拆分得越多代码复杂度上升越明显即使有事务兜底也要考虑值不值。我的建议是把提醒状态作为备忘录表的一个字段放在同一张表里这样根本不需要事务一条UPDATE就能解决简单可靠。5. 踩过的坑从运行环境到代码细节的实战排查5.1 JDK环境变量配置的经典问题界面、业务、数据层都写完以后最尴尬的事情往往发生在运行环境上。很多同学的代码在IDE里点“运行”完全正常但双击Jar包就报错或者命令行java -jar启动不了。排查下来十有八九是环境变量的问题。我在热词里看到“java环境变量配置详细教程”“java安装教程详细”是高频搜索说明这确实是拦路虎。环境变量配置的核心只有两件事JAVA_HOME指向JDK安装目录Path里加上%JAVA_HOME%\bin。配置完以后在命令行输入java -version和javac -version验证两个都能正常输出版本号才算成功。如果安装了多个JDK版本一定要把高版本的Path条目移到前面否则命令行用的还是旧版本。还有一个容易忽略的坑JDK和JRE混用。现在JDK自带了JRE但如果你的机器上单独装了JRE并且Path里JRE的bin排在JDK前面那么java命令用的是JRE。而JRE没有javac所以就会出现“能运行不能编译”的怪象。这属于排查了半天都找不出原因、最后发现就是路径顺序问题的典型case。5.2 编译期“软件包不存在”与Lombok冲突问题有很多人在项目里用了LombokData注解写得很爽结果换一台电脑或者用命令行编译时就报“程序包lombok不存在”。这个问题我在热词里也看到了“you arent using a compiler supported by lombok, so lombok will not work”。这说明编译环境和Lombok的适配出了问题。对于备忘录管理系统这个体量我的建议是放弃Lombok手写getter/setter。虽然多打个十几行字但它省掉的麻烦是实打实的不用装IDE插件、不用配注解处理器、不用担心编译环境之间的兼容性、生成的class文件也绝对不会有问题。一个课程设计项目把精力花在环境适配和奇怪报错上太不值得了。如果非要用Lombok至少确认两件事IDEA里安装Lombok插件并开启Enable annotation processing项目的编译方式用Maven或Gradle管理依赖而不是手动引入jar包。否则在别人电脑上拉下来代码编译报错分分钟让人崩溃。还有一个坑和编码相关Windows环境下命令行编译Java源文件时如果代码里有中文而源文件是UTF-8编码直接javac编译很可能报“未结束的字符串文字”或乱码。解决方案是编译时显式指定javac -encoding UTF-8 *.java。这个参数在IDE里通常默认配好了但命令行编译时非常容易踩到尤其是从Windows的cmd窗口直接操作时。备忘录系统里到处都是中文字符串编码问题可以说是必修课。5.3 JVM内存不足与数组越界的高发场景“java: outofmemoryerror: insufficient memory”这个报错我在查看JVM类项目时没少见过。备忘录系统虽然是轻量级应用但如果你一次性把整个文件读进来处理或者表格里的数据不翻页全量加载数据量到达一定级别时同样能撑爆堆内存。我的经验是做桌面工具时养成两个习惯一是在主函数入口给JVM设合理的初始堆大小java -Xms64m -Xmx256m -jar memo.jar预留比实际需求稍高的空间但也不要无脑给几个G二是数据加载尽量分批比如查询列表时默认只加载前500条用户搜索时再加条件减少单次内存压力。数组越界异常也高发在界面层。比如表格选择行时没有判断getSelectedRow()的返回值是不是-1用户没选中任何行就直接点删除代码就会崩。这个异常在明明“不可能”的情况下就发生了因为在快速点击时表格的选中状态可能已经变化了。我的防御性写法是所有从界面控件取值的地方都先判空或判断下标范围int selectedRow table.getSelectedRow(); if (selectedRow 0) { JOptionPane.showMessageDialog(this, 请先选择一条记录, 提示, JOptionPane.WARNING_MESSAGE); return; }这种处理看起来啰嗦但正是这些防御性判断让你的程序从“能跑”变成“不会莫名其妙崩”。一个无人维护的课设项目最怕的就是用户在没有任何提示的情况下看到程序异常退出这种体验非常差。6. 让系统更“值钱”的三个进阶方向6.1 从文件存储升级到数据库的完整迁移如果第一版用文件存储升级到SQLite其实非常顺滑因为DAO接口的两个实现对外暴露的方法是一致的。操作步骤无非是新写一个满足相同方法签名的MemoDao实现类把内部逻辑换成JDBC再做一个数据迁移方法读取原文件数据批量写入数据库。界面层和业务层的代码完全不用动。这就是分层设计带来的核心优势——数据源替换对上层透明。迁移时要注意主键的连续性。文件存储时你可能自己用一个AtomicInteger生成id但SQLite的自增主键是由数据库管理的。迁移时要么显式插入旧id要么重新分配id。如果备忘之间有关联关系比如提醒记录引用备忘id就一定要保留旧id否则关联就断了。备忘录系统通常没有这种关系但培养这种意识很重要。6.2 导出功能与Java反射的初步结合给系统加一个“导出为CSV”功能是性价比很高的一个加分项。用户可以把备忘录列表导出成表格文件用Excel打开。CSV本身就是一个纯文本格式每一行是按逗号分隔的字段。生成的要点有两个字段中含有逗号或换行时要加双引号包裹文件写入时用UTF-8 BOM否则Windows下的Excel打开中文会乱码。代码非常简单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,标题,分类,优先级,状态,提醒时间\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这个辅助方法的逻辑是如果字段包含逗号、双引号或换行就用双引号包起来并把字段内部的双引号替换成两个双引号。这个规则是CSV格式的标准不处理就会出现导出文件错位。这个小功能可以引导你了解一种轻量级的数据交换格式也为以后对接别的系统打下基础。6.3 面向对象思想的进阶场代码重构备忘录系统写完之后一个很好的自我训练方式是“重构”。重构不是改bug而是不改变外部功能的前提下让代码结构更合理。比如你发现Memo类的方法越写越多可以按职责拆出MemoValidator类统一管理字段校验比如你发现MemoService里同时处理了业务逻辑和提醒时间解析可以把事件解析工具类抽出来。这些重构动作本质上都是在练习“识别坏味道、找到更优设计”的肌肉记忆。以后你面对真实的后台系统时面对一堆屎山代码才不至于手足无措——因为你在备忘录这种小项目上已经积累过“化繁为简”的经验了。根据我个人经验来说完成一个备忘录系统最好的方式不是闷头一天写完而是先花半小时把类和接口的关系画在纸上再动手代码写完核心流程以后运行一次观察哪里交互不顺手再迭代一版。这个“设计-实现-反思-重构”的循环才是Java基础真正长在身上的过程。系统本身做得好不好是其次你在整个过程中建立起来的工程直觉才是未来能带走的财富。本文还有配套的精品资源点击获取