news 2026/10/15 11:39:41

Java SE项目开发实战:工程骨架、三层结构与SQLite打包完全指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java SE项目开发实战:工程骨架、三层结构与SQLite打包完全指南

简介:面向Java程序设计学习者的综合项目开发资料包,适合有Java基础并希望提升实战能力的学生或开发者。包含12个典型练习案例:俄罗斯方块、双人俄罗斯方块、五子棋与人机对战五子棋、多媒体展示系统、野人与传教士过河、蚁群算法模拟、SchoolEbay、Ajax网上教学平台、简易编译器、RMI分布计算与Agent分布式计算,覆盖游戏开发、算法模拟、Web应用和分布式编程。资源共817个文件,压缩包为rar格式、约14.92MB,以class编译文件和java源码为主,另有jar依赖库、jpg图片、jsp页面、tld标签定义及配置文件,便于对照源码与运行结果学习。当前已有221人学习下载,可作课程设计、毕业设计或自学参考。通过这组案例,读者能获得完整项目源码、界面素材和模块划分,理解从需求分析到算法实现、分布式部署的完整过程,有助于巩固Java SE核心知识并积累项目经验。

1. Java SE项目开发不是写语法:先看一个差点翻车的单机业务工具

Java SE项目开发这个标题,一大批人是在学完基础语法之后才真正理解的。语法看得懂,但要把一个能录入数据、能查询、能统计的小工具完整做出来,你面对的根本不是「还有哪个类不会用」,而是「一堆类怎么组织才不会烂掉」。我见过一个还算熟练的开发者在做一个本地工时登记工具时,把数据库连接、界面刷新、统计逻辑全写在按钮事件里,结果数据一多,窗口直接白屏,二十分钟才缓过来。这类翻车的根源不是代码不会写,而是项目结构、线程边界、数据访问方式从一开始就是错的。

这篇内容就围绕一个完整落地路径展开:先定项目形态和存储选型,再搭工程骨架,然后写三层结构的核心实现,最后把项目打成可执行文件交给别人。适合三类读者——刚上完 Java SE 想做个练手项目的新手,准备课程设计但不知道从哪下手的同学,以及在公司里要快速交付一个小型本地工具的开发者。字多,但每一步都能直接照着抄。

2. 动手前先定三件事:单机还是C/S、界面技术、数据存储

很多人做 Java SE 项目一上来就写代码,写到第五天发现界面和业务缠在一起改不动。这种事我见得多了。与其返工,不如在动手前花一小时把三件事定下来,后面所有代码都围绕这三个决定展开。

2.1 单机程序还是C/S架构:没有明确需求就别上Socket

Java SE 这个词本身只代表「标准版」,不限定程序形态。你可以做一个本地单机工具,也可以做一个 Socket 客户端去连一个 Java 写的服务端。但以我的经验,课设、练手、内部小工具,百分之九十以上选单机就对了。C/S 意味着你要同时维护服务端的连接管理、消息协议、并发访问,这已经超出「SE项目开发」的核心练习目标了。

什么时候才需要 C/S?比如多个工位的电脑要录入同一份数据,且数据要实时汇总到主管的屏幕上,单机文件方案满足不了这种共享需求。但我一般会劝退一上来就写 Socket 的人——Java SE 项目里最常见的 C/S 需求,其实用「单机程序 + 共享数据库文件」就能覆盖,SQLite 本身就支持多进程并发读。真要上 Socket,也先确认是不是能用 RMI 或 HTTP 接口解决,别一上来就手写心跳和粘包处理。

提示:判断标准很简单——数据要不要跨机器同步?要,再考虑 C/S;不要,老老实实做单机。

2.2 界面层三选一:Swing、JavaFX、纯命令行

界面技术是 Java SE 项目第一个绕不开的选择。常规选项有三种,我直接给结论:

方案优点缺点适合场景
SwingJDK 自带,不需要额外依赖,JTable/JTree 成熟界面代码量大,默认外观老旧传统课设、内部工具、快速交付
JavaFX界面现代,支持 CSS 和 FXML 分离需要单独引入模块,打包体积大,老教程偏少对界面美观有要求、愿意学新东西
纯命令行代码最少,好调试交互差,没法直观展示表格数据批处理工具、数据迁移脚本

我会默认选 Swing。原因很现实:资料多,坑都被人踩平了。JTable 做表格录入、JFileChooser 做文件选择、GridBagLayout 做表单布局,每一块都有大量可查的解决方案。JavaFX 虽好,但你在课程设计答辩现场调整样式时,远没有 Swing 那么省心。

2.3 存储选型:别把数据当玩具,也别一上来就上重型数据库

Java SE 项目的存储方案通常三个:SQLite 嵌入式数据库、文件序列化、纯内存集合。初学者最爱用第三个,因为写起来最快;但项目开发这个题目,你至少要选第一个或者第二个,否则数据一关程序就丢,连「开发」的门都没摸着。

方案持久化能力查询与统计接入成本风险
SQLite单文件,可靠SQL 支持完整,适合统计加一个 JDBC 驱动即可多进程写入有锁冲突
文件序列化有,但不耐操只能全量读出来算几乎为零文件损坏整库报废
纯内存 Map/List无只能手动遍历零程序退出数据即失

我的决策习惯是:只要有查询、统计、按日期汇总这类需求,就直接上 SQLite 加 JDBC。这个方案把数据访问变成标准 SQL,统计逻辑不用自己写循环累加,而且 SQLite 就是一个文件,备份和分发都方便。文件序列化作为次要方案,只在记录条数少、几乎没有查询需求时用。

3. 工程骨架与开发环境:让空项目先能编译能跑

形态定好之后,下一步是把工程骨架搭起来。这一步经常被新手跳过——他们在一个默认包里塞了几十个类,最后连同名类都分不清谁是谁。项目开发的第一步,不是写业务,而是把「编译、运行、包结构」这三件事理顺。

3.1 JDK、IDE与构建工具:能用一条命令编译,就已经赢了一半

环境方面我的建议很简单:用较新的长期支持版 JDK,别追每个新版本。开发周期长的项目,版本太新会遇到第三方库不兼容;版本太老又没法用新语法。至于 IDE,IntelliJ IDEA 和 Eclipse 都行,选你熟悉的,这个不影响项目本身。

真正决定工程化程度的是构建工具。现在的 Java SE 项目即便没有 Spring,也应该用 Maven 或 Gradle 管理依赖和打包。理由有三个:第一,SQLite 驱动这类第三方 jar 不用手动下载拷进 lib 目录,Maven 自动拉取;第二,打包成可运行 jar 只需要一条命令;第三,别人接手你的代码时,不用到处找依赖包。纯手写 javac 命令编译只能算练习,不能算开发。

3.2 分包规范:从 com 开头,别用默认包

Java SE 项目再小,也应该按模块分包。我常用的结构是五层:

com.example.timesheet ├── Main.java # 入口类 ├── model/ # 实体类:WorkRecord、Employee ├── dao/ # 数据访问:SQLite操作封装 ├── service/ # 业务逻辑:工时统计、校验 ├── ui/ # 界面层:主窗口、表单、表格 └── util/ # 工具类:日期转换、资源读取

分包的核心原则是依赖方向:ui 依赖 service,service 依赖 dao,model 谁都依赖但谁都不依赖它。这条规则能保证你改数据库实现的时候,界面代码一个字都不用动。

3.3 最小可运行入口:Main类先跑起来

不管业务多复杂,入口类都应该长成下面这样。这段代码解决了 Swing 程序最常见的启动问题——界面创建必须放在事件分发线程里。

package com.example.timesheet; import com.example.timesheet.ui.MainFrame; import javax.swing.SwingUtilities; public class Main { public static void main(String[] args) { // 所有Swing组件创建必须放在事件分发线程(EDT)中 SwingUtilities.invokeLater(() -> { MainFrame frame = new MainFrame(); frame.setVisible(true); }); } }

这段代码里SwingUtilities.invokeLater是唯一的重点。它把窗口创建任务丢给 Swing 的事件分发线程去执行,避免在 main 线程直接弹窗口引发偶发的界面初始化问题。至于 MainFrame 内部做什么,那是第 4 章的内容,但现在这个骨架已经能跑起来空窗口了。到这里,项目的第一步就算完成了——能编译、能运行、包结构清晰,后面写代码都是在往骨架上填肉。

4. 把录入、查询、统计写进三层结构:一套能抄的核心代码

工程骨架就位后,核心业务代码是重头戏。这一章以一个「工时登记工具」为例,展示 model、dao、service、ui 四层的标准写法。这套代码不炫技,每一行都在解决一个具体问题。

4.1 实体类与DAO:数据长什么样,怎么存取

先定义实体类。工时记录的字段包括员工、日期、小时数、备注。注意小时数这里我建议用BigDecimal而不是double,因为涉及汇总统计时浮点数会积累误差,这是做过工时系统的开发者都懂的坑。

package com.example.timesheet.model; import java.math.BigDecimal; import java.time.LocalDate; public class WorkRecord { private int id; private String staffId; private LocalDate workDate; private BigDecimal hours; private String remark; public WorkRecord(String staffId, LocalDate workDate, BigDecimal hours, String remark) { this.staffId = staffId; this.workDate = workDate; this.hours = hours; this.remark = remark; } // getter和setter省略,实际开发中可用Lombok或手写 }

然后是 DAO 层。DAO 的唯一职责是跟 SQLite 打交道,不包含任何业务判断。插入和按日期区间查询是最基本的两个方法。

package com.example.timesheet.dao; import com.example.timesheet.model.WorkRecord; import java.sql.*; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; public class WorkRecordDao { private Connection conn; public WorkRecordDao(Connection conn) { this.conn = conn; // 连接由上层统一管理,DAO不负责开关连接 } public void insert(WorkRecord r) throws SQLException { String sql = "INSERT INTO work_record(staff_id, work_date, hours, remark) VALUES(?,?,?,?)"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, r.getStaffId()); ps.setObject(2, r.getWorkDate()); // SQLite JDBC支持LocalDate,避免手动转字符串 ps.setBigDecimal(3, r.getHours()); ps.setString(4, r.getRemark()); ps.executeUpdate(); } } public List<WorkRecord> findByDateRange(LocalDate start, LocalDate end) throws SQLException { String sql = "SELECT * FROM work_record WHERE work_date BETWEEN ? AND ? ORDER BY work_date"; List<WorkRecord> list = new ArrayList<>(); try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setObject(1, start); ps.setObject(2, end); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { list.add(new WorkRecord( rs.getString("staff_id"), rs.getObject("work_date", LocalDate.class), rs.getBigDecimal("hours"), rs.getString("remark") )); } } } return list; } }

这里有两个值得注意的参数细节。第一,INSERT语句用PreparedStatement而不是直接拼字符串,一是防 SQL 注入,二是避免日期格式拼接出错;第二,rs.getObject("work_date", LocalDate.class)返回LocalDate,比传统的getDate()再转换干净得多。try-with-resources自动关闭PreparedStatement和ResultSet,这条在第 5 章还会展开讲。

4.2 界面与业务解耦:按钮事件里别写数据库代码

新手最常见的写法是把查询写进按钮的ActionListener里,点一下就执行 SQL 并直接往 JTable 塞数据。这个写法有两个问题:查询耗时界面会卡死;数据访问逻辑没法单独测试。正确做法是让界面层只做两件事——收集输入、刷新展示,中间调用 service。

package com.example.timesheet.ui; import com.example.timesheet.dao.WorkRecordDao; import com.example.timesheet.model.WorkRecord; import com.example.timesheet.service.StatsService; import javax.swing.*; import java.math.BigDecimal; import java.time.LocalDate; import java.util.List; public class MainFrame extends JFrame { private WorkRecordDao dao; private StatsService statsService; private JTable table; private JTextField staffField; private JTextField hoursField; public MainFrame() { // 构造方法中初始化组件,省略布局代码 JButton saveBtn = new JButton("保存"); saveBtn.addActionListener(e -> saveRecord()); JButton statBtn = new JButton("统计本月"); statBtn.addActionListener(e -> showMonthStats()); } private void saveRecord() { String staffId = staffField.getText().trim(); if (staffId.isEmpty()) { JOptionPane.showMessageDialog(this, "员工编号不能为空"); return; } // 耗时操作放到独立线程,防止界面阻塞 new Thread(() -> { try { WorkRecord record = new WorkRecord(staffId, LocalDate.now(), new BigDecimal(hoursField.getText()), ""); dao.insert(record); SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(this, "保存成功")); } catch (Exception ex) { SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(this, "保存失败: " + ex.getMessage())); } }).start(); } }

这段代码展示了一个关键原则:耗时操作放独立线程,界面更新放回事件分发线程。new Thread(...).start()把数据库插入丢到后台,执行完毕后再用SwingUtilities.invokeLater回到 EDT 弹提示框。你可能会问为什么不直接在主线程调用——因为 SQLite 写入涉及磁盘 IO,在数据量稍大时延迟明显,而 EDT 被占用时整个窗口会冻结,用户会以为程序死了。

4.3 统计逻辑放在service层:可测试才是硬道理

统计是这类工具的核心价值。把统计逻辑抽到 service 层,意味着你可以不打开界面、不连数据库,直接写单元测试跑这个类。

package com.example.timesheet.service; import com.example.timesheet.model.WorkRecord; import java.math.BigDecimal; import java.util.List; public class StatsService { // 计算指定记录列表的总工时 public BigDecimal totalHours(List<WorkRecord> records) { return records.stream() .map(r -> r.getHours()) .reduce(BigDecimal.ZERO, BigDecimal::add); } // 按员工分组统计 public java.util.Map<String, BigDecimal> groupByStaff(List<WorkRecord> records) { java.util.Map<String, BigDecimal> map = new java.util.HashMap<>(); for (WorkRecord r : records) { map.merge(r.getStaffId(), r.getHours(), BigDecimal::add); } return map; } }

BigDecimal::add作为累加器,比double加法精确,这在月度汇总时不会出现 39.599999 这种尴尬结果。Map.merge方法接收一个初始值和累加函数,三行搞定按人分组。这个 service 类不依赖任何 Swing 和 JDBC 类型,输入输出都是纯 Java 对象,测试起来毫无障碍——这就是把它单独放一层的原因。

注意:统计逻辑里不要出现日期字符串拼接、数据库连接等东西。如果一段逻辑能放在不引入java.sql包的类里,就放在那里。

5. 避坑:Java SE项目踩过的五个经典问题

这一章是整篇的精华所在。下面五个问题是我做 Java SE 项目时真实遇到过的,也是论坛里出现频率最高的类型。每一条按「现象 → 原因 → 解决」展开,你照着排查就行。

5.1 界面卡死:窗口白屏,点哪都没反应

现象:点击「查询」按钮后,窗口标题栏显示「未响应」,鼠标变成转圈,几分钟后恢复或直接崩溃。

原因:数据库查询、文件读写这类耗时操作直接写在按钮事件里,霸占了事件分发线程(EDT)。Swing 的界面刷新、鼠标事件处理全靠这一个线程,它被占用,整个窗口就瘫痪了。

解决:所有耗时操作一律放到新线程或SwingWorker里执行。参考第 4 章的写法,后台线程跑完再用SwingUtilities.invokeLater回 EDT 更新界面。检查你项目里所有addActionListener方法体,只要有 JDBC 调用或文件读写,就按这个模式拆。

5.2 日期显示和查询结果对不上

现象:界面上显示的是 2026-03-05,但数据库里查出来变成了 2026-03-04;或者按日期区间查询时,边界日期始终查不到。

原因:Java 8 以前的java.util.Date和java.sql.Date转换时丢失了时区信息,SQLite 驱动处理日期字符串时还可能受默认时区影响;另一个常见成因是查询参数用了String拼接日期,边界值00:00:00和23:59:59没处理好,导致当天记录漏掉。

解决:实体类全部用LocalDate/LocalDateTime,DAO 层用ps.setObject传参、rs.getObject(..., LocalDate.class)读回,不要碰java.sql.Date.valueOf这类手工转换。区间查询的结束日期要加一天并使用work_date < ? AND work_date >= ?这种半开区间写法,而不是BETWEEN,避免边界漏数据。

5.3 运行一段时间后数据库报 too many connections

现象:程序早上还好好的,下午开始报SQLiteException: database is locked或连接数超限,重启程序又能跑一阵。

原因:DAO 方法的try-catch只关了ResultSet,PreparedStatement或Connection没有关。单机程序里连接池未必存在,但每漏一次就占用一个文件句柄,SQLite 对文件锁比较敏感,最终把连接耗尽或锁死。

解决:所有 JDBC 资源都用 try-with-resources 嵌套关闭,顺序是 ResultSet -> Statement -> Connection。如果Connection是外部传入的,由打开的调用方负责关闭,DAO 内层不关。最常见的干净写法是try (Connection conn = DriverManager.getConnection(...); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()),三个资源一行全关。

5.4 资源文件读取不到:properties 和图标偶尔加载失败

现象:在 IDE 里运行正常,打包成 jar 后运行,config.properties读不到,图标显示不出来。或者配置文件里的中文变成乱码。

原因:new File("config.properties")用的是相对于「当前工作目录」的路径,IDE 里工作目录是项目根目录,命令行运行时却是 jar 所在目录,一变就找不到。Properties 文件默认编码是 ISO-8859-1,写入中文自然乱码。

解决:类路径下的配置文件用getClass().getResourceAsStream("/config.properties")读取,它走 classpath,跟工作目录无关。配置文件编码统一用 UTF-8,读取时new InputStreamReader(stream, StandardCharsets.UTF_8)再传给 Properties。放 jar 包同目录的配置文件则用Paths.get(System.getProperty("user.dir"), "config.properties")拼接,别写死绝对路径。

5.5 打包后 ClassNotFoundException:SQLite驱动不见了

现象:在 IDE 里运行一切正常,用java -jar app.jar运行时直接报ClassNotFoundException: org.sqlite.JDBC。

原因:IDE 运行时自动把依赖 jar 加进了 classpath,但java -jar只加载你打出来的这个 jar 内的类。默认的 jar 打包方式不包含依赖,SQLite 驱动自然找不到。

解决:改用 Maven 的maven-assembly-plugin或maven-shade-plugin打一个 fat jar(把所有依赖的 class 合并进同一个 jar),设置Main-Class后一条命令交付。第 6 章会给出具体配置。排查时先执行jar tf app.jar | grep sqlite,看看驱动类到底在不在里,在就是清单文件问题,不在就是插件没配。

6. 收尾:把项目完整交付出去的打包与自查习惯

开发和测试跑通只是第一步,Java SE 项目交到别人手里还能正常跑,才是真正结束。这一章讲打包方式和三个自查习惯,直接决定项目能不能给别人用。

先说打包。用 Maven 打 fat jar 的配置如下,放在项目根目录的pom.xml里:

<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <configuration> <archive> <manifest> <mainClass>com.example.timesheet.Main</mainClass> </manifest> </archive> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> </plugins> </build>

配置里的mainClass指向你项目入口类的完整类名,jar-with-dependencies表示生成包含所有依赖的独立 jar。执行mvn clean package后,target 目录会出现一个timesheet-1.0-jar-with-dependencies.jar,拷到任何装了 JDK 的机器上都能启动。

然后是启动脚本。Windows 下创建一个start.bat比让用户手敲命令更友好,里面通常会加上 JVM 参数:

@echo off java -Xmx512m -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai -jar timesheet-1.0-jar-with-dependencies.jar

-Xmx512m限制最大堆内存,防止内存泄漏拖垮整机;-Dfile.encoding=UTF-8解决 Windows 默认编码导致的乱码;-Duser.timezone固定时区避免日期偏移。这几项参数看似小,但跨机器运行时就是「别人能跑我不能跑」的常见原因。

最后说一个我坚持了很久的自查习惯。每次发布之前,我会做三件事:第一,找一台干净机器(没有 IDE、没配过环境变量的那种),用命令行跑一遍启动脚本,确认没有半点依赖 IDE 的东西;第二,故意输入非法数据——空白字符串、超长文本、负数工时——看程序的校验提示是否正常;第三,检查数据文件生成位置,确认没有把 SQLite 文件写到只读目录。这三关过完,心里才有底。这个习惯帮我挡掉过不少返工,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 11:39:32

Django博客系统开发:MVT架构与核心功能实现

1. 项目概述&#xff1a;DjangoBlog的核心定位DjangoBlog是一个基于Django框架开发的博客系统&#xff0c;它完美体现了Django的MVT&#xff08;Model-View-Template&#xff09;架构思想。作为一个全栈开发者的练手项目&#xff0c;它涵盖了用户认证、文章管理、评论系统等博客…

作者头像 李华
网站建设 2026/10/15 11:39:29

WordPress主题GraceTheme深度解析与优化指南

1. 主题定位与核心特色解析 GraceTheme是一款面向高端内容创作者和企业官网设计的WordPress主题。我在实际建站项目中多次使用这款主题&#xff0c;发现它特别适合需要展现专业形象但又不想过度设计的用户群体。主题采用响应式布局&#xff0c;默认适配从手机到4K显示器的各种屏…

作者头像 李华
网站建设 2026/10/15 11:38:56

企业微信文档预览技术方案与实现

1. 企业办公场景下的文件预览痛点解析在PC端企业微信和小程序环境中处理日常办公文档时&#xff0c;我们经常遇到这样的场景&#xff1a;同事发来一份合同PDF需要快速确认&#xff0c;财务共享的Excel报表需要查看数据&#xff0c;或是领导传阅的PPT方案需要审阅。传统处理方式…

作者头像 李华
网站建设 2026/10/15 11:38:50

偏光显微镜在材料分析中的核心应用与操作指南

1. 为什么偏光显微镜在材料实验室里不可替代先说结论&#xff1a;偏光显微镜不是给样品拍个漂亮照片那么简单的工具&#xff0c;它是目前观察透明材料内部微观结构最直接、最便宜、也最不可替代的手段之一。很多材料——矿物、陶瓷、玻璃、高分子聚合物、纤维、液晶材料——用普…

作者头像 李华
网站建设 2026/10/15 11:38:47

同步发电机短路暂态过程Simulink仿真建模与实践

1. 同步发电机短路暂态过程概述同步发电机作为电力系统的核心设备&#xff0c;其暂态特性直接关系到电网的稳定运行。三相短路作为最严重的故障类型&#xff0c;会在毫秒级时间内引发机组转速突变、励磁电流激增等复杂电磁-机械耦合现象。传统实验受限于安全性和成本&#xff0…

作者头像 李华
网站建设 2026/10/15 11:38:21

韶音OpenFit Pro:开放式滤噪新物种的技术与体验解析

最近几年我逛各类消费电子展&#xff0c;雷打不动的一件事是去耳机区转一圈。今年在AWE2026现场&#xff0c;开放式耳机的热度明显又上了一个台阶&#xff0c;好几个品牌的展台都围了不少人&#xff0c;但排队最长的还是韶音这里。这次他们带来的主角是OpenFit Pro&#xff0c;…

作者头像 李华