跳转到正文
← 返回学习板块
Java 实习自学 · 设备运维工单平台

第 1 至 6 周 Java 与数据库

对象与封装、集合与文件、Maven、SQL、事务和JDBC。

完整学习目录 · 14 篇
本篇目录

这六周把你已经接触过的对象、异常和文件读写,变成自己能够修改的工单程序,再把数据放入 MySQL。每周以一个能演示的成果为目标。表中的六个任务各约 3 小时,包含阅读、编码、排错和复盘,总计约 18 小时;可以自行选日期,考试周拆成两周。提前完成就复写和查漏,不必把时间填满。

先独立做任务,再按输入和输出验收。一个问题卡住 30 分钟时,先读报错第一行和自己代码对应行,缩成小例子;仍不明白再看本阶段参考答案。答案提供局部示例,不要求依赖聊天才能继续。每周三道自测先口述或写下自己的答案。

创建或打开 ops-ticket 项目,在其中写代码。第 1 周只需 src/Ticket.java 和 src/TicketDemo.java;第 3 周再调整 Maven 目录。各周练习逐步扩大范围,不要提前写完整六张表或全部接口。项目字段总约定见数据与接口约定。

第 1 周 创建一个始终合法的工单对象 #

日期窗口:2026 年 10 月 12 日至 18 日。前置:会写 main、变量、if 和方法调用;能说明 private 与成绩范围检查的区别。成果:运行 TicketDemo,创建两张工单、读取标题、修改标题,并证明非法输入不会破坏原对象。

任务约用时做到什么程度
A 写对象3 小时写 private long id、private String title、构造器和 getter;在 Demo 中创建编号 1、2 的对象,逐个打印
B 加校验3 小时编号必须大于 0;标题先判断 null,再 trim(),处理后长度为 1 至 50。先校验再赋值
C 修改标题3 小时加 rename(String newTitle);构造与修改共用一个私有校验方法;不提供 setId
D 观察执行过程3 小时在构造器、校验方法和 rename 各设断点;观察 this、形参、字段;尝试让两个变量指向同一对象
E 基础与保存3 小时用循环找整数数组最大值,验证空数组如何处理;在项目目录学习 git status、git diff、一次明确文件的提交
F 独立验收3 小时合上答案重写标题校验;运行下面用例,记录一个错误及修复;回答三道自测

对象是某张工单,类是描述它有哪些数据和操作的代码。this.title 是当前对象的标题,参数 title 是调用者刚传进来的值。两者同名不代表它们自动关联。private 把直接修改入口收拢到类内部;入口中的校验才负责阻止非法数据。异常会中断当前正常执行路径;捕获后才能继续执行后面的演示用例。

标题长度本阶段按 Java String.length() 计算,验收先用普通汉字与英文字母;这不是“所有 Unicode 字符都占一个位置”的承诺。trim() 的规则也不要自行换成其他空白处理方式。先保持项目规则统一。

输入或操作预期结果
new Ticket(1L, " 投影仪无法开机 ")编号 1,标题为 投影仪无法开机,两端空格已去掉
标题为 null、""、" " 或 51 个 A抛 IllegalArgumentException,该次创建不成功
标题为 1 个或 50 个 A创建成功
编号为 0 或 -1抛 IllegalArgumentException
合法工单调用 rename(" ")抛异常,原来的标题仍在
创建两张工单后只修改第一张第二张标题不变

自测:①为什么构造器也要校验,不能只在 rename 校验?②Ticket b = a; b.rename("新标题"); 后 a 的标题是什么,为什么?③为什么不能把 id、title 写成 static?

卡住提示:构造器与类同名,没有返回类型;读取方法需要 return。如果 title = title 没有效果,比较参数与字段的含义。先实现普通输入,再一次加一条边界。阅读只需 Dev.java 的面向对象与异常入口,不必看完全部 Java 教程。

第 2 周 管理多张工单并保存到文件 #

日期窗口:10 月 19 日至 25 日。前置:第 1 周用例全部通过;会追踪一次异常。成果:控制台可以列出工单、按状态筛选;退出后重新启动,编号、标题、状态和处理人仍能恢复。

任务约用时做到什么程度
A 集合操作3 小时用 List<Ticket> 保存 3 张工单;循环按编号查找,查不到给明确提示;用 Map<Long, Ticket> 再做一次并比较
B 状态与派单3 小时定义四种 TicketStatus;新增工单为 OPEN;加可为空的 Long assigneeId,仅实现 assign(long engineerId):正数编号、只允许 OPEN,成功转 IN_PROGRESS
C 筛选与去重3 小时用普通循环筛选状态并生成新列表;用 Set<Long> 检查重复编号;写一个统计数组数字出现次数的小练习
D 写文件3 小时用 Properties 把字段变成字符串,再用 UTF-8 Writer 保存;明确文件的绝对位置;保存前先保留上一份练习文件
E 读文件3 小时读回字符串、解析编号与枚举、恢复对象;缺文件视为首次启动,坏数据报清字段且停止加载,不能默默当空数据覆盖旧文件
F 重启验收3 小时保存后结束进程,再启动;新增编号使用现有最大编号加 1;验证重复编号、坏状态、中文与特殊字符,独立说明转换过程

List 适合按顺序遍历;Map 用编号直接定位工单;Set 记录“某编号是否出现过”。集合里存的是对象引用,并不会自动把数据写入硬盘。枚举将可选状态限制为 OPEN、IN_PROGRESS、RESOLVED、CLOSED;本周只实现前两种之间的派单,处理与验收留到第 9 周。

文件使用现成的 Properties 格式,避免自己设计逗号和换行的转义。约定键为 count、ticket.0.id、ticket.0.title、ticket.0.status、ticket.0.assigneeId,后续记录按下标递增。编号等数字用十进制字符串;未派单的处理人保存为空字符串。这个下标只是文件位置,不是工单编号。库会处理标题里的 =、反斜杠和换行;不要自己按 = 切割文件。Java Properties 文档

读取旧数据需要单独的 restore 工厂方法或仅供存储层使用的构造器;仍校验编号、标题、状态,以及 OPEN 没有处理人、其他状态有正数处理人。不要为了恢复文件开放任意 setStatus。try-with-resources 在正常或异常退出时关闭 Reader/Writer;“关闭”与“捕获异常后假装成功”是两件事。

验收:1、2、3 三张中把 2 派给工程师 20,筛选 OPEN 得到 1、3;重启后仍是同样结果,新工单编号为 4。把文件中的一个状态改成 DONE,启动应指出对应记录不合法并保留原文件;修回去后可加载。标题 A=B\\C 保存读取后完全一致。重复派单抛异常,处理人不改变。

自测:①Map.put 遇到已有编号会怎样,创建工单该怎么处理?②为什么重新启动后不能让编号从 1 开始?③程序打印“保存成功”但重启找不到文件,先查哪两件事?

卡住提示:先只保存、读取一张工单的标题,再加编号、状态和循环。先让内存程序跑通再接文件;本周不实现文件并发写入或自定义二进制序列化。

第 3 周 拆分职责并用 Maven 构建 #

日期窗口:10 月 26 日至 11 月 1 日。前置:能独立解释第 2 周的对象到文件再回到对象。成果:控制台 v0.1 有固定启动步骤;切换内存与文件存储时,业务校验不需要复制两份。

任务约用时做到什么程度
A 统一环境3 小时比较 IDEA Project SDK、java -version、javac -version、mvn -v;统一为 Java 21,记录实际路径;首次依赖下载保留网络时间
B 整理 Maven3 小时学 pom.xml;把类移到 src/main/java/com/example/opsticket,加对应 package;用 Maven 编译和打包
C 提取接口3 小时定义 TicketRepository 的 add、findById、findAll、update;先写内存实现,定义不存在与重复编号的行为
D 拆分业务与交互3 小时ConsoleApp 负责输入输出;TicketService 负责用例;构造器接收 Repository;让文件实现遵守同一接口
E 独立小改3 小时增加“标题包含关键词”的筛选;用普通循环完成后再读一个 Lambda 示例;练两数之和,先用双循环再用 Map
F 交付与复盘3 小时运行同一组用例验证两种存储;记录启动命令、数据文件位置、已知限制;提交 v0.1 代码与说明

接口是调用方与实现方的约定。例如业务只需要“按编号找工单”,无需知道它来自 Map 还是文件。构造器传入依赖,就是创建 Service 时把它需要的 Repository 交给它。这会让第 8 周更换数据库实现时保留业务规则,不是为了多建几个空类。

接口可以采用下面的签名,Optional.empty() 表示不存在。add 遇到同编号应报错;update 找不到原编号也应报错。返回新 List 防止调用方直接增删仓库的集合;其中的 Ticket 仍可能是同一对象,修改后必须显式调用 update,不要依赖内存引用恰好同步的效果。

public interface TicketRepository {
    void add(Ticket ticket);
    Optional<Ticket> findById(long id);
    List<Ticket> findAll();
    void update(Ticket ticket);
}

验收:把 new InMemoryTicketRepository() 换成 new FileTicketRepository(path),创建、查询、改标题和派单结果一致;文件版每次成功修改后保存,重启数据保留。保存失败必须报告失败,不能打印成功。输入不存在的编号 999 显示“工单不存在”;关键词“投影”只返回包含该文字的工单;用 Maven 构建出现 BUILD SUCCESS,打包后从命令行运行同一入口。

Maven 负责按配置取依赖、编译、测试与打包;它不会自动替你写业务。首次联网准备好 Maven、插件和依赖,之后无新增依赖时可尝试离线构建。maven.compiler.release 指定目标 Java 版本;不要只改 IDEA 的字体旁语言级别就认为终端同步了。参考 Maven 入门与目录结构及编译目标版本。

自测:①为什么业务类不应在自己的方法里反复 new FileTicketRepository(...)?②将存储实现换掉后,哪些类应变化,哪些规则应保留?③IDEA 能跑但 Maven 报 release version 21 not supported,你要检查什么?

卡住提示:先保留 TicketDemo 直接调用 Service,不必做复杂菜单。环境不通时先写接口和内存实现;正式进入 Web 前必须解决构建问题。

第 4 周 建立用户 设备与工单三张表 #

日期窗口:11 月 2 日至 8 日。前置:会区分内存与持久化,能读懂基本方法调用。成果:拥有自己的 SQL 建表文件与样本数据文件,能手写增删改查并观察数据库约束。

任务约用时做到什么程度
A 连接与隔离3 小时确认 MySQL 服务、账号、端口;查询 SELECT VERSION(), DATABASE();;建立练习库 ops_ticket_dev,使用 utf8mb4
B 表和关系3 小时画出“一名用户报多单、一台设备有多单、一单最多一名处理人”;按统一字段建立三张 InnoDB 表
C 数据与约束3 小时插入下方样本;验证主键、唯一用户名、唯一设备编码、外键、非空约束;理解 NULL 与 0 的区别
D 基础查询3 小时手写查所有、按状态查、按报修人查、按标题关键词查;每个查询先预测结果,再运行核对
E 修改与删除3 小时用 WHERE id = ... 改设备位置;练习设备停用;只对自己新增且未被引用的练习行做删除,先 SELECT 确认目标
F 独立验收3 小时不看答案写一条 INSERT、两条 SELECT、一条 UPDATE;比较 Java 校验和数据库约束职责;解释所有外键

表是一类数据,行是一条记录,列是字段。主键 id 用于稳定识别记录;名字可以变化,所以不能靠“投影仪”唯一识别设备。外键要求工单引用的用户和设备存在,但它不自动判断用户是不是工程师,也不判断设备是否启用,这些要在业务层检查。数据库约束是额外保障,不替代 Java 的清晰错误提示。

字段固定为 app_user(id, username, password_hash, role, enabled),device(id, code, name, location, enabled),ticket(id, title, description, device_id, reporter_id, assignee_id, status, version, created_at, updated_at)。assignee_id 初始为 SQL NULL,版本号初始 0;创建时间、更新时间用 DATETIME(6),本地学习统一按 Asia/Shanghai 解释,部署时再统一应用与数据库时间设置。完整类型和关键 DDL 在参考答案中。

样本明确内容
用户1 reporter_a 为 REPORTER;2 engineer_a 为 ENGINEER;3 admin_a 为 ADMIN;4 reporter_b 为 REPORTER,均启用
设备1 PJ-001 投影仪;2 AC-001 空调;3 PR-001 打印机,均启用
工单 101投影仪无法开机,设备 1,报修人 1,OPEN,无处理人
工单 102空调不制冷,设备 2,报修人 1,IN_PROGRESS,处理人 2
工单 103投影画面闪烁,设备 1,报修人 4,OPEN,无处理人

密码列只放 NOT_A_LOGIN_HASH 这一显式占位值;本阶段没有登录功能。第 12 周必须通过 PasswordEncoder 生成真正的密码哈希,不能把占位值或明文当作可登录数据。

验收:查 OPEN 返回 101、103;查报修人 1 返回 101、102;查未派单用 IS NULL 返回 101、103。插入重复 PJ-001 失败;引用设备 999 的工单失败;把设备 1 停用后,其历史工单仍可查询。保存自己写的 SQL,不依赖图形工具自动生成全部语句。

自测:①assignee_id = NULL 为什么不是正确查询?②设备名称修改后,为什么不应逐条修改工单里的设备编号?③外键存在,能否保证派给用户 1 的工单合理?

卡住提示:先只建 device 表练 INSERT/SELECT,再建其余表。连不上时区分服务未启动、端口错和账号错;不要重装所有软件。参阅 MySQL 建表,已有 8.0 环境可先练本阶段通用 SQL。

第 5 周 关联 统计 事务与索引 #

日期窗口:11 月 9 日至 15 日。前置:第 4 周三张表与样本可重复使用。成果:一份工单列表查询、一份设备工单统计,以及提交与回滚的可解释记录。

任务约用时做到什么程度
A 关联列表3 小时JOIN 设备和报修人;对可能为空的处理人使用 LEFT JOIN;给两个用户关联起不同别名
B 分组统计3 小时按状态计数;按设备统计包含零工单的设备;区分 COUNT(*) 和 COUNT(t.id)
C 稳定分页3 小时按 created_at DESC, id DESC 排序;每页 2 条,写第一页和第二页;用纸上样本解释同一时间需要 id 兜底
D 事务实验3 小时在同一连接执行 START TRANSACTION、UPDATE、ROLLBACK;再实验 COMMIT;记录提交前后另一个连接的可见结果
E 索引实验3 小时对“报修人筛选并按时间排序”读 EXPLAIN;建立相应联合索引后比较计划,不用三条数据宣称性能提升
F 复写与验收3 小时独立写 LEFT JOIN 统计;补一个字符串或哈希表算法小题;写事务失败时该怎么做,回答自测

JOIN 根据编号把行拼成结果;LEFT JOIN 会保留左侧没有匹配项的行。因此未派单的工单也应出现在列表中。GROUP BY 将同一组放在一起计算。对“所有设备及其工单数”,没有工单的设备仍占结果的一行,应该统计右表非空工单编号,而不是简单数结果行。MySQL JOIN 语法

事务把几个修改当作一个工作单元。只有提交后才确认这些修改;主动回滚会撤销尚未提交的修改。本周用设备位置更新做小实验,下阶段才把工单状态与操作日志一起提交。SQL 报错后不要假设整个事务自动回滚;练习时明确执行 ROLLBACK。建表、改表等 DDL 不拿来演示普通事务回滚。MySQL 提交与回滚

索引像按某种顺序整理的查找入口,会占空间并增加写入维护成本。联合索引 (reporter_id, created_at, id) 对本周查询有合理的使用场景,但优化器仍按成本选计划。三条样本走全表扫描并不等于索引错误。重点是解释 possible_keys、key、rows、Extra,不要编造百分比。

验收:关联列表仍有 3 行,101 和 103 的处理人显示为空或“未派单”;状态统计为 OPEN 2、IN_PROGRESS 1;设备统计为投影仪 2、空调 1、打印机 0。样本时间依次为 09:00、10:00、11:00,第一页编号 103、102,第二页 101。设备 1 位置改为“临时位置”后回滚,恢复原值;改为“新教室”并提交,再连接仍是新值。

自测:①处理人使用 INNER JOIN 为什么可能丢工单?②统计打印机工单数为什么不能直接用 COUNT(*)?③事务中第一条更新成功、第二条失败,为什么仍要显式回滚?

卡住提示:先只关联 ticket 与 device;结果正确再加两个用户别名。事务每次只做一个实验,记录原值,结束时确保 COMMIT 或 ROLLBACK,避免连接一直持有未提交修改。

第 6 周 用 JDBC 连接 Java 与数据库 #

日期窗口:11 月 16 日至 22 日。前置:能手写插入、关联查询,知道事务提交与回滚。成果:Java 程序创建工单后退出,重新运行仍能查到;能画出浏览器发 HTTP 请求到服务返回 JSON 的过程。

任务约用时做到什么程度
A 接入驱动3 小时Maven 添加 Connector/J;从环境变量读 URL、用户、密码;用 JDBC 执行 SELECT 1,打印结果 1
B 创建数据3 小时写一个创建方法,复用标题校验;固定 SQL 用 ? 占位并绑定参数;数据库生成 id,读取生成编号;不再从最大编号加 1
C 查询数据3 小时按 id 查询,循环或判断 ResultSet;把每列映射成 Java 字段,正确处理 SQL NULL 的处理人;查不到返回明确结果
D 资源与失败3 小时Connection、PreparedStatement、ResultSet 用 try-with-resources 关闭;记录 SQLState 和原因;验证不存在设备插入失败且没有半成品
E HTTP 过渡3 小时读一个 GET 请求和 JSON 响应,区分方法、路径、状态码、请求体;在纸上写出创建与查询接口,不急着搭 Spring
F 重启与独立修改3 小时重启后查新编号;独立增加按 OPEN 查询的方法;复习数组/哈希题,回答自测并整理 JDBC 到 MyBatis 的过渡笔记

JDBC 是 Java 操作关系数据库的一套接口,MySQL 驱动负责具体连接和通信。Connection 是这次数据库会话;PreparedStatement 保存 SQL 结构和绑定参数;ResultSet 保存查询结果,游标先位于第一行之前,要先 next() 才读取。? 只绑定数据值,不用来绑定表名、列名或排序方向。

本周短练习可以单独写 JdbcTicketDemo,只做创建和查询。创建方法接受标题、描述、设备编号、报修人编号,返回数据库生成的 long id;进入数据库后编号由数据库生成。不要为了兼容原来带 id 的构造器,让新增工单乱填一个假编号;可以先完成插入,得到 id 后再构造对象。业务时间与状态使用前述约定,SQL 列表写全,不依赖列的物理顺序。

HTTP 把请求与响应带过网络,JSON 是其中一种数据格式。GET /api/tickets/101 读取工单;POST /api/tickets 提交新工单。下一阶段正常创建返回 201,查询成功 200,参数错误 400,不存在 404。最终报修人来自登录身份,本周控制台传编号只是没有登录时的学习接口。HTTP 概览

验收:新增 " O'Reilly投影仪异常 ",数据库保存去掉两端空格的原文字,单引号不破坏 SQL;退出程序后按新 id 查得到。创建时设备为 999,外键拒绝且记录数不增加。查询 999999 无结果;SQL NULL 映射成 Long null,不是编号 0。查 OPEN 在初始样本上返回 101、103,新增 OPEN 工单后也能返回它。

自测:①为什么不把标题直接拼进 SQL 字符串?②为什么 Java 程序结束后数据还在,而第 2 周的纯 Map 不在?③收到 200 和 JSON 是否代表一定查到了想要的工单,还应核对什么?

卡住提示:连接不通时先用同一主机、端口和账号在数据库客户端连接;能连接后再查驱动与依赖。No suitable driver 与 Access denied 原因不同。这里的 JDBC 是理解数据访问的桥梁,不继续扩成另一套正式持久层。参考驱动接入与DriverManager 连接说明。

进入第 7 周的门槛 #

你能脱离答案完成:一个带校验的对象、一个集合筛选、一次文件往返、一条关联统计 SQL、一次 JDBC 插入与查询,并解释构造器、接口、事务、参数绑定分别解决什么问题。未通过的项最多选两项补练,先完成它们再开始 Web;不用为了赶日期把不会的内容全部积压到 Spring 阶段。

← 浏览全部课程回到顶部 ↑