简介这是一套面向Java初学者与课程设计实践者的个人日记本系统完整开发资料聚焦Web应用开发全流程训练解决从需求分析到部署上线的典型学习断层问题。资源包含4个核心文件2个MP4辅导视频覆盖项目演示与部署实操、1个SQL数据库脚本含用户表、日记表等规范化结构及初始化数据、1个ZIP源代码包含ServletJSP后端逻辑、登录/增删改查等完整功能模块总大小53MB结构清晰、开箱即用。已有434人学习下载适合高校Java Web课程实训、毕业设计参考或自学进阶。读者可直接导入IDE运行系统结合视频理解MVC分层实现、MySQL连接配置、密码加密存储等关键细节并通过源码掌握异常处理、会话管理及基础前端交互逻辑是少有的集可运行代码、可执行数据库与过程化教学视频于一体的闭环学习资源。1. 为什么一个“个人日记本”值得用 Java 重做一遍不是写个 CRUD 就叫系统设计你手头可能早就有几个 Markdown 文件夹、几款 App、甚至某云笔记的几十篇草稿——但它们都不是「你的日记本系统」。真正的个人日记本系统核心不在记而在「可控的私有化沉淀」文字能按年月日关键词双向检索图片不依赖第三方图床密码保护不是摆设导出格式要能离线阅读删库前还得弹三次确认。这不是功能堆砌而是对数据主权的最小闭环实践。这个标题里的“基于 Java 的个人日记本系统”本质是一套可本地部署、可审计源码、可随硬件升级平滑演进的桌面级数据管理范式。它适合三类人刚学完 JDBC 和 Swing 的新手想串起知识链需要交付课程设计但拒绝网上抄模板的学生还有那些厌倦了 SaaS 日记平台悄悄改协议、删接口、加会员墙的务实型记录者。它不追求高并发但要求零外部依赖、启动即用、异常不崩、日志可查——这才是“设计与实现”四个字的分量。2. 从零搭起骨架用 Maven H2 Swing 构建最小可运行系统2.1 为什么选 H2 而不是 MySQL 或 SQLite很多初学者一上来就配 MySQL结果卡在环境变量、服务启动、端口冲突上三天没跑出第一行SELECT。而 SQLite 又缺乏完整的 JDBC 事务支持在多线程写入日记时容易触发database is locked。H2 是唯一满足全部硬性条件的嵌入式数据库单 jar 启动、内存/文件双模式、ACID 完整、自带 Web 控制台调试时直接浏览器看表、JDBC URL 简洁到一行搞定。它不是“凑合用”而是为这类单机工具型应用量身定制的存储引擎。提示H2 的jdbc:h2:~/diarydb表示数据库文件存放在当前用户主目录下的diarydb.mv.db无需手动创建目录首次连接自动初始化。2.2 Maven 依赖配置精简到 4 个核心包!-- pom.xml -- dependencies !-- 嵌入式数据库 -- dependency groupIdcom.h2database/groupId artifactIdh2/artifactId version2.2.224/version /dependency !-- Java 桌面 GUI -- dependency groupIdjavax.swing/groupId artifactIdswing/artifactId version0.0.1/version scopesystem/scope systemPath${java.home}/lib/jfxrt.jar/systemPath /dependency !-- 日志避免 System.out.println 满天飞 -- dependency groupIdorg.slf4j/groupId artifactIdslf4j-simple/artifactId version2.0.9/version /dependency !-- 日期时间处理Java 8 时间 API 更安全 -- dependency groupIdorg.threeten/groupId artifactIdthreetenbp/artifactId version1.6.8/version /dependency /dependencies说明swing依赖标记为system是因为 JDK 11 已移除 JavaFX 模块但 Swing 本身仍在rt.jar中无需额外引入若用 JDK 8可直接删掉此依赖。slf4j-simple是最轻量的日志门面实现输出到控制台且无配置文件适合调试阶段快速定位 SQL 执行路径。threetenbp是 Java 8java.time的 backport确保 JDK 7 兼容性课程设计常有版本限制。2.3 初始化数据库表结构用 DDL 脚本而非 ORM 自动生成不要用 Hibernate/JPA 自动生成表——它会把简单问题复杂化且无法精确控制字段长度、索引和默认值。直接手写schema.sql放在src/main/resources/下-- src/main/resources/schema.sql CREATE TABLE IF NOT EXISTS diary_entry ( id BIGINT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200) NOT NULL, content CLOB NOT NULL, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, tags VARCHAR(100), -- 逗号分隔如 工作,会议,反思 is_pinned BOOLEAN DEFAULT FALSE, password_hash CHAR(64) NULL -- SHA-256 加密后的密码空表示未加密 ); CREATE INDEX idx_create_time ON diary_entry(create_time DESC); CREATE INDEX idx_tags ON diary_entry(tags);逻辑说明CLOB类型用于存储长文本H2 支持最大 2GB远超单篇日记需求update_time使用ON UPDATE CURRENT_TIMESTAMP是 H2 特性MySQL 需用触发器模拟tags字段用VARCHAR(100)而非单独建tag表因个人日记标签数量有限通常 20反规范化反而提升查询速度password_hash为CHAR(64)对应 SHA-256 固定长度避免VARCHAR存储开销。2.4 主窗口类Swing 不是过时技术而是可控性的代名词// src/main/java/com/example/diary/DiaryFrame.java public class DiaryFrame extends JFrame { private final JTable table; private final DefaultTableModel tableModel; private final DiaryService diaryService; public DiaryFrame() { this.diaryService new DiaryService(); // 业务逻辑层实例 this.tableModel new DefaultTableModel( new String[]{ID, 标题, 日期, 标签, 置顶}, 0 ); this.table new JTable(tableModel); initUI(); loadAllEntries(); // 启动时加载全部日记 } private void initUI() { setTitle(Java 个人日记本 v1.0); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLayout(new BorderLayout()); // 表格区域 JScrollPane scrollPane new JScrollPane(table); add(scrollPane, BorderLayout.CENTER); // 按钮面板 JPanel buttonPanel new JPanel(new FlowLayout()); JButton addButton new JButton(新增); addButton.addActionListener(e - showAddDialog()); buttonPanel.add(addButton); JButton deleteButton new JButton(删除); deleteButton.addActionListener(e - deleteSelectedEntry()); buttonPanel.add(deleteButton); add(buttonPanel, BorderLayout.SOUTH); pack(); setLocationRelativeTo(null); // 居中显示 setVisible(true); } private void loadAllEntries() { ListDiaryEntry entries diaryService.findAll(); tableModel.setRowCount(0); // 清空旧数据 for (DiaryEntry entry : entries) { tableModel.addRow(new Object[]{ entry.getId(), entry.getTitle().length() 20 ? entry.getTitle().substring(0, 17) ... : entry.getTitle(), entry.getCreateTime().toLocalDate(), entry.getTags(), entry.isPinned() ? ✓ : }); } } }参数说明DefaultTableModel直接操作二维数组比AbstractTableModel更适合小数据量场景避免重复造轮子entry.getTitle().length() 20是真实 UI 优化表格列宽有限强制截断防撑开界面符合桌面应用直觉setLocationRelativeTo(null)比setLocation(100, 100)更可靠——它依赖系统 DPI 自动居中适配高分屏pack()在setVisible(true)前调用确保窗口尺寸自适应内容避免空白边距。3. 核心业务落地日记增删改查与加密保护的 Java 实现3.1 DiaryEntry 实体类用 Lombok 减少样板代码但必须理解其原理// src/main/java/com/example/diary/DiaryEntry.java import lombok.Data; import lombok.Builder; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import java.time.LocalDateTime; Data Builder NoArgsConstructor AllArgsConstructor public class DiaryEntry { private Long id; private String title; private String content; private LocalDateTime createTime; private LocalDateTime updateTime; private String tags; private Boolean isPinned; private String passwordHash; // 注意不存明文密码 }逻辑说明Data自动生成 getter/setter/toString/equals/hashCode但必须清楚它不生成构造函数——所以补NoArgsConstructor和AllArgsConstructorpasswordHash字段名强调“哈希值”杜绝password这种危险命名LocalDateTime替代Date避免时区陷阱所有时间操作在本地时区完成不涉及网络同步。3.2 DiaryService封装 JDBC 操作隔离 SQL 细节// src/main/java/com/example/diary/DiaryService.java public class DiaryService { private static final String DB_URL jdbc:h2:~/diarydb;DB_CLOSE_ON_EXITFALSE; public ListDiaryEntry findAll() { String sql SELECT id, title, content, create_time, update_time, tags, is_pinned, password_hash FROM diary_entry ORDER BY create_time DESC; ListDiaryEntry list new ArrayList(); try (Connection conn DriverManager.getConnection(DB_URL); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { list.add(DiaryEntry.builder() .id(rs.getLong(id)) .title(rs.getString(title)) .content(rs.getString(content)) .createTime(rs.getTimestamp(create_time).toLocalDateTime()) .updateTime(rs.getTimestamp(update_time).toLocalDateTime()) .tags(rs.getString(tags)) .isPinned(rs.getBoolean(is_pinned)) .passwordHash(rs.getString(password_hash)) .build()); } } catch (SQLException e) { // 记录错误但不抛出避免 UI 崩溃 LoggerFactory.getLogger(DiaryService.class).error(查询日记失败, e); } return list; } public void save(DiaryEntry entry) { String sql; if (entry.getId() null) { sql INSERT INTO diary_entry (title, content, tags, is_pinned, password_hash) VALUES (?, ?, ?, ?, ?); } else { sql UPDATE diary_entry SET title?, content?, tags?, is_pinned?, password_hash?, update_timeCURRENT_TIMESTAMP WHERE id?; } try (Connection conn DriverManager.getConnection(DB_URL); PreparedStatement ps conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, entry.getTitle()); ps.setString(2, entry.getContent()); ps.setString(3, entry.getTags()); ps.setBoolean(4, entry.isPinned()); ps.setString(5, entry.getPasswordHash()); if (entry.getId() ! null) { ps.setLong(6, entry.getId()); } int affected ps.executeUpdate(); if (affected 0 entry.getId() null) { // 新增时获取自增 ID try (ResultSet keys ps.getGeneratedKeys()) { if (keys.next()) { entry.setId(keys.getLong(1)); } } } } catch (SQLException e) { LoggerFactory.getLogger(DiaryService.class).error(保存日记失败, e); } } }关键点解析DB_CLOSE_ON_EXITFALSE是 H2 关键参数防止 JVM 退出时自动关闭数据库保证多窗口操作下数据不丢失Statement.RETURN_GENERATED_KEYS用于获取AUTO_INCREMENT的 ID比SELECT LAST_INSERT_ID()更标准ps.setString(5, entry.getPasswordHash())传入的是哈希值绝不是明文密码——加密逻辑在上层 Dialog 中完成catch块中只打日志不 throw因 Swing 是单线程异常未捕获会导致整个 UI 冻结这是血泪经验。3.3 密码加密模块SHA-256 Salt 是个人系统的安全底线// src/main/java/com/example/diary/SecurityUtil.java public class SecurityUtil { private static final String SALT DiaryApp2024SecureSalt; // 硬编码 salt 仅限单机场景 public static String hashPassword(String rawPassword) { if (rawPassword null || rawPassword.trim().isEmpty()) { return null; } try { MessageDigest digest MessageDigest.getInstance(SHA-256); byte[] hash digest.digest((rawPassword SALT).getBytes(StandardCharsets.UTF_8)); return bytesToHex(hash); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(SHA-256 不可用, e); } } private static String bytesToHex(byte[] hash) { StringBuilder hexString new StringBuilder(); for (byte b : hash) { String hex Integer.toHexString(0xff b); if (hex.length() 1) hexString.append(0); hexString.append(hex); } return hexString.toString(); } }参数说明SALT硬编码在类中是可接受的——因为这是单机应用没有服务端密钥分发问题且 salt 不需保密只需防彩虹表0xff b是 Java 字节转十六进制的标准写法避免负数问题此处不使用 BCrypt/PBKDF2因它们需要随机 salt 和迭代次数而 H2 表结构已固定为CHAR(64)强行改会破坏兼容性SHA-256 足够抵御暴力破解假设密码长度 ≥ 8 位。3.4 新增日记对话框Swing 输入验证的真实写法// src/main/java/com/example/diary/AddDiaryDialog.java public class AddDiaryDialog extends JDialog { private final JTextField titleField; private final JTextArea contentArea; private final JTextField tagsField; private final JCheckBox pinnedBox; private final JPasswordField passwordField; private final DiaryService service; public AddDiaryDialog(Frame owner, DiaryService service) { super(owner, 新增日记, true); this.service service; this.titleField new JTextField(30); this.contentArea new JTextArea(10, 40); this.tagsField new JTextField(20); this.pinnedBox new JCheckBox(置顶); this.passwordField new JPasswordField(20); initUI(); } private void initUI() { setLayout(new BorderLayout()); JPanel formPanel new JPanel(new GridBagLayout()); GridBagConstraints gbc new GridBagConstraints(); gbc.insets new Insets(5, 5, 5, 5); gbc.anchor GridBagConstraints.WEST; // 标题行 gbc.gridx 0; gbc.gridy 0; formPanel.add(new JLabel(标题), gbc); gbc.gridx 1; formPanel.add(titleField, gbc); // 内容行 gbc.gridx 0; gbc.gridy 1; formPanel.add(new JLabel(内容), gbc); gbc.gridx 1; gbc.fill GridBagConstraints.BOTH; gbc.weightx 1.0; gbc.weighty 1.0; formPanel.add(new JScrollPane(contentArea), gbc); // 标签行 gbc.fill GridBagConstraints.HORIZONTAL; gbc.weighty 0.0; gbc.gridx 0; gbc.gridy 2; formPanel.add(new JLabel(标签逗号分隔), gbc); gbc.gridx 1; formPanel.add(tagsField, gbc); // 置顶 密码 gbc.gridx 0; gbc.gridy 3; formPanel.add(pinnedBox, gbc); gbc.gridx 1; formPanel.add(new JLabel(密码保护留空则不加密), gbc); gbc.gridx 2; formPanel.add(passwordField, gbc); add(formPanel, BorderLayout.CENTER); // 按钮行 JPanel buttonPanel new JPanel(new FlowLayout()); JButton okButton new JButton(确定); okButton.addActionListener(e - onSave()); buttonPanel.add(okButton); JButton cancelButton new JButton(取消); cancelButton.addActionListener(e - dispose()); buttonPanel.add(cancelButton); add(buttonPanel, BorderLayout.SOUTH); pack(); setLocationRelativeTo(getOwner()); } private void onSave() { String title titleField.getText().trim(); String content contentArea.getText().trim(); if (title.isEmpty() || content.isEmpty()) { JOptionPane.showMessageDialog(this, 标题和内容不能为空, 输入错误, JOptionPane.ERROR_MESSAGE); return; } DiaryEntry entry DiaryEntry.builder() .title(title) .content(content) .tags(tagsField.getText().trim()) .isPinned(pinnedBox.isSelected()) .build(); String rawPassword new String(passwordField.getPassword()).trim(); if (!rawPassword.isEmpty()) { entry.setPasswordHash(SecurityUtil.hashPassword(rawPassword)); } service.save(entry); JOptionPane.showMessageDialog(this, 保存成功); dispose(); } }逻辑说明GridBagLayout是 Swing 中唯一能精准控制组件对齐和拉伸的布局管理器比BorderLayoutFlowLayout嵌套更可靠JTextArea必须包裹在JScrollPane中否则超出区域不可见这是新手高频翻车点new String(passwordField.getPassword())是获取密码字符串的唯一正确方式getText()方法已被废弃且不安全JOptionPane.showMessageDialog弹窗阻塞当前对话框线程避免用户连续点击“确定”导致重复提交。4. 避坑指南这 4 个问题让 80% 的初学者卡在编译后第一运行4.1 现象程序启动后窗口空白控制台无报错JTable 不显示数据原因tableModel.setRowCount(0)后未调用loadAllEntries()或loadAllEntries()中 SQL 查询返回空集合但未打印日志。解决在loadAllEntries()开头加日志log.info(开始加载日记当前数据库URL: {}, DB_URL)检查schema.sql是否被正确执行启动时查看 H2 控制台http://localhost:8082用jdbc:h2:~/diarydb连接手动执行SELECT COUNT(*) FROM diary_entry。4.2 现象输入中文标题后保存再打开显示乱码如æä¸ªæ¥è®°原因H2 数据库 URL 缺少字符集参数H2 默认用平台编码Windows 是 GBKLinux 是 UTF-8导致跨平台不一致。解决将数据库 URL 改为jdbc:h2:~/diarydb;DB_CLOSE_ON_EXITFALSE;DEFAULT_CHARSETutf8并确保pom.xml中maven-compiler-plugin的encoding设为UTF-8。4.3 现象点击“删除”按钮后界面上日记条目消失但重启程序又出现原因deleteSelectedEntry()方法中只删除了tableModel的行但未调用diaryService.delete(id)删除数据库记录。解决在deleteSelectedEntry()中添加数据库删除逻辑private void deleteSelectedEntry() { int selectedRow table.getSelectedRow(); if (selectedRow -1) return; Long id (Long) tableModel.getValueAt(selectedRow, 0); diaryService.delete(id); // 新增此行 tableModel.removeRow(selectedRow); }并在DiaryService中补充delete(Long id)方法SQL 为DELETE FROM diary_entry WHERE id?。4.4 现象打包成 jar 后双击无反应命令行运行提示NoClassDefFoundError: javax/swing/JFrame原因JDK 11 移除了 JavaFX 模块但部分 Swing 类仍依赖java.desktop模块而某些精简版 JRE 未包含。解决使用jlink构建最小运行时或直接要求用户安装完整 JDK。更稳妥的做法是在pom.xml中添加maven-shade-plugin将所有依赖打包进 fat jarplugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.4.1/version executions execution phasepackage/phase goals goalshade/goal /goals configuration transformers transformer implementationorg.apache.maven.plugins.shade.resource.ManifestResourceTransformer mainClasscom.example.diary.DiaryApp/mainClass /transformer /transformers /configuration /execution /executions /plugin5. 进阶技巧让日记本真正“属于你”的 3 个落地动作5.1 导出为 PDF用 iText 7 生成带样式的离线文档很多人以为导出就是FileWriter写.txt但真正的“属于你”意味着格式可读、排版可控、长期存档不丢样式。iText 7 是目前最轻量单 jar 3MB、API 最清晰的 PDF 生成库且完全开源AGPL个人使用无风险。!-- pom.xml 添加 -- dependency groupIdcom.itextpdf/groupId artifactIditext7-core/artifactId version7.2.5/version typepom/type /dependency导出核心逻辑简化版public void exportToPdf(ListDiaryEntry entries, File outputFile) { try (PdfWriter writer new PdfWriter(outputFile); PdfDocument pdf new PdfDocument(writer); Document document new Document(pdf)) { document.setMargins(36, 36, 36, 36); // 0.5英寸页边距 for (DiaryEntry entry : entries) { // 标题样式 Paragraph title new Paragraph(entry.getTitle()) .setFontColor(ColorConstants.BLACK) .setFontSize(16) .setBold(); document.add(title); // 日期标签 Paragraph date new Paragraph(日期 entry.getCreateTime().format(DateTimeFormatter.ofPattern(yyyy年MM月dd日 HH:mm))) .setFontColor(ColorConstants.GRAY) .setFontSize(10); document.add(date); // 正文自动换行、保留缩进 Paragraph content new Paragraph(entry.getContent()) .setFontSize(12) .setFirstLineIndent(20); // 首行缩进20pt document.add(content); // 分页符 document.add(new AreaBreak()); } } catch (Exception e) { log.error(PDF 导出失败, e); } }关键点AreaBreak()是 iText 7 的分页符比document.newPage()更语义化setFirstLineIndent(20)模拟中文排版习惯避免每段顶格所有字体默认用 HelveticaiText 内置无需额外加载中文字体——因为 PDF 查看器会自动 fallback 到系统字体确保中文正常显示。5.2 全局搜索用 Lucene 实现毫秒级关键词检索H2 的LIKE %keyword%在千条日记时响应明显卡顿。Lucene 是为搜索而生的库它把全文索引存在本地文件与数据库解耦且 Java 原生支持。步骤创建索引目录new File(data/lucene-index)初始化IndexWriter配置中文分词器推荐smartcnH2 自带无需额外依赖每次保存日记时向索引添加文档Document doc new Document(); doc.add(new StringField(id, String.valueOf(entry.getId()), Store.YES)); doc.add(new TextField(title, entry.getTitle(), Store.YES)); doc.add(new TextField(content, entry.getContent(), Store.YES)); writer.addDocument(doc);搜索时用QueryParser解析用户输入IndexSearcher执行查询。注意Lucene 索引需定期writer.commit()且IndexWriter应单例复用避免频繁打开/关闭文件句柄。5.3 备份与迁移用 H2 的SCRIPT TO backup.sql实现一键迁移H2 提供原生命令导出为标准 SQL 脚本这是跨平台迁移的终极方案public void backupDatabase(File backupFile) { try (Connection conn DriverManager.getConnection(jdbc:h2:~/diarydb)) { Statement stmt conn.createStatement(); stmt.execute(SCRIPT TO backupFile.getAbsolutePath() ); } catch (SQLException e) { log.error(备份失败, e); } }生成的backup.sql可直接在 H2 控制台执行恢复也可导入 MySQL/PostgreSQL需少量语法调整。它比复制.mv.db文件更安全——后者在写入时可能损坏而SCRIPT命令会自动加锁。我坚持在每个新项目里先写backupDatabase()再写第一个业务方法。因为数据无价而备份逻辑往往被排到最后最后总被 deadline 吃掉。这个习惯救过我三次——一次是误删测试数据一次是硬盘故障一次是学生交作业前手抖格式化了 U 盘。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?