简介:这是一套面向Java初学者与课程设计实践者的个人日记本系统完整开发资料,聚焦Web应用开发全流程训练,解决从需求分析到部署上线的典型学习断层问题。资源包含4个核心文件:2个MP4辅导视频(覆盖项目演示与部署实操)、1个SQL数据库脚本(含用户表、日记表等规范化结构及初始化数据)、1个ZIP源代码包(含Servlet+JSP后端逻辑、登录/增删改查等完整功能模块),总大小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> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>2.2.224</version> </dependency> <!-- Java 桌面 GUI --> <dependency> <groupId>javax.swing</groupId> <artifactId>swing</artifactId> <version>0.0.1</version> <scope>system</scope> <systemPath>${java.home}/lib/jfxrt.jar</systemPath> </dependency> <!-- 日志(避免 System.out.println 满天飞) --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>2.0.9</version> </dependency> <!-- 日期时间处理(Java 8+ 时间 API 更安全) --> <dependency> <groupId>org.threeten</groupId> <artifactId>threetenbp</artifactId> <version>1.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() { List<DiaryEntry> 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和@AllArgsConstructor;passwordHash字段名强调“哈希值”,杜绝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_EXIT=FALSE"; public List<DiaryEntry> findAll() { String sql = "SELECT id, title, content, create_time, update_time, tags, is_pinned, password_hash FROM diary_entry ORDER BY create_time DESC"; List<DiaryEntry> 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_time=CURRENT_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_EXIT=FALSE是 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 中唯一能精准控制组件对齐和拉伸的布局管理器,比BorderLayout+FlowLayout嵌套更可靠;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 是 GBK,Linux 是 UTF-8),导致跨平台不一致。
解决:将数据库 URL 改为jdbc:h2:~/diarydb;DB_CLOSE_ON_EXIT=FALSE;DEFAULT_CHARSET=utf8,并确保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 jar:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.4.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.example.diary.DiaryApp</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin>5. 进阶技巧:让日记本真正“属于你”的 3 个落地动作
5.1 导出为 PDF:用 iText 7 生成带样式的离线文档
很多人以为导出就是FileWriter写.txt,但真正的“属于你”意味着格式可读、排版可控、长期存档不丢样式。iText 7 是目前最轻量(单 jar < 3MB)、API 最清晰的 PDF 生成库,且完全开源(AGPL,个人使用无风险)。
<!-- pom.xml 添加 --> <dependency> <groupId>com.itextpdf</groupId> <artifactId>itext7-core</artifactId> <version>7.2.5</version> <type>pom</type> </dependency>导出核心逻辑(简化版):
public void exportToPdf(List<DiaryEntry> 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)模拟中文排版习惯,避免每段顶格;- 所有字体默认用 Helvetica(iText 内置),无需额外加载中文字体——因为 PDF 查看器会自动 fallback 到系统字体,确保中文正常显示。
5.2 全局搜索:用 Lucene 实现毫秒级关键词检索
H2 的LIKE '%keyword%'在千条日记时响应明显卡顿。Lucene 是为搜索而生的库,它把全文索引存在本地文件,与数据库解耦,且 Java 原生支持。
步骤:
- 创建索引目录:
new File("data/lucene-index"); - 初始化
IndexWriter,配置中文分词器(推荐smartcn,H2 自带,无需额外依赖); - 每次保存日记时,向索引添加文档:
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 盘。希望帮到你。
本文还有配套的精品资源,点击获取