
简介这是一套面向Java初学者与数据库入门实践者的简易数据库系统源码聚焦基础CRUD操作与前后端协同实现适合课程设计、小型项目原型开发或数据库原理教学辅助。资源共29个文件压缩包仅181KB包含13个Java核心类如Interpreter、Buffer_Manager、Record_Manager等支撑数据库解析、缓存管理、记录操作等关键逻辑11个XML配置文件用于定义数据库连接、IDEA项目结构及UI组件参数另有HTML前端界面、SQL建库脚本、LICENSE授权文件、README说明文档及IntelliJ IDEA项目配置文件形成完整可运行闭环。已有304人学习下载代码结构清晰、模块职责分明配套SQL初始化脚本与详细配置说明便于快速导入IDE运行调试是理解嵌入式数据库底层机制与Web交互逻辑的优质实践材料。1. 为什么一个“简易数据库系统”要用 Java HTML 而不是直接上 MySQL 或 SQLite这不是在造轮子而是在拆解数据库的“黑匣子”——当你用 Java 写出一个能建表、插数据、查记录、删字段的控制台网页交互系统你才真正看清 CREATE TABLE 背后是内存结构体还是文件偏移量SELECT WHERE 不是 SQL 解析器 magic而是你亲手写的字符串匹配循环过滤。这个“基于 Java 与 HTML 的简易数据库系统设计源码”本质是一套可调试、可打断点、可单步追踪的数据库教学沙盒Java 负责核心逻辑数据存储、CRUD、事务模拟、HTML 提供轻量前端界面避免 Swing/AWT 的过时感和复杂度二者通过嵌入式 HTTP 服务如 Jetty 或自研 Socket Server桥接。它不替代生产级数据库但能让你在面试被问到“事务隔离级别怎么实现”时不是背八股文而是指着自己写的lockMap.put(table, new ReentrantLock())说“我试过加锁粒度从行级降到页级性能掉 40%但幻读真没了”。适合 Java 初学者巩固集合/IO/多线程也适合数据库原理课学生把 ER 图、范式、B树索引这些抽象概念落到HashMapString, ListRow和FileChannel.write()的真实字节里。2. 核心架构三层分离不是口号是必须动手画清的数据流这个简易系统绝不能写成“一个 Java 类塞满所有逻辑 一个 HTML 表单硬编码提交”。必须强制分层否则后续加索引、改存储引擎、换前端框架会直接翻车。我一般会按以下三块组织源码结构每层职责清晰、接口明确2.1 数据层Data Layer用纯 Java 实现“表”的最小可行模型核心是Table类它不依赖任何第三方库只用 JDK 原生类public class Table { private final String name; private final ListString columns; // 字段名列表顺序即列序 private final ListMapString, Object rows; // 每行是 Map字段名, 值 private final MapString, Integer columnIndices; // 字段名 → 列索引加速 get() public Table(String name, ListString columns) { this.name name; this.columns new ArrayList(columns); this.rows new ArrayList(); this.columnIndices new HashMap(); for (int i 0; i columns.size(); i) { columnIndices.put(columns.get(i), i); } } public void insert(MapString, Object row) { // 验证字段数一致、类型暂不校验简易版 if (row.size() ! columns.size()) { throw new IllegalArgumentException(Column count mismatch); } rows.add(new HashMap(row)); // 深拷贝防外部修改 } public ListMapString, Object select(MapString, Object conditions) { ListMapString, Object result new ArrayList(); for (MapString, Object row : rows) { boolean match true; for (Map.EntryString, Object cond : conditions.entrySet()) { String col cond.getKey(); Object val cond.getValue(); if (!Objects.equals(row.get(col), val)) { match false; break; } } if (match) result.add(new HashMap(row)); } return result; } }关键设计说明columns用ListString而非Set保证列序稳定影响 INSERT 语句解析和显示顺序rows存MapString, Object而非Object[]牺牲一点内存换字段名可读性方便 HTML 表格渲染columnIndices是性能关键避免每次select都遍历columns找索引O(1) 定位insert中new HashMap(row)是血泪经验——不深拷贝前端传来的row被后续修改会污染存储。2.2 服务层Service Layer暴露 REST-like 接口屏蔽底层细节用 Java 自带HttpServerJDK 18或嵌入 Jetty兼容老 JDK定义/api/table/{name}等路径。重点不是做完整 REST而是让 HTML 表单能fetch(/api/insert, {method:POST, body: JSON.stringify(...)})直接调用// 示例处理 POST /api/insert 请求 private void handleInsert(HttpExchange exchange) throws IOException { String requestBody new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); JsonObject json JsonParser.parseString(requestBody).getAsJsonObject(); String tableName json.get(table).getAsString(); JsonObject data json.getAsJsonObject(data); Table table database.getTable(tableName); // database 是全局 TableRegistry MapString, Object row new HashMap(); for (Map.EntryString, JsonElement entry : data.entrySet()) { // 简单类型转换字符串、数字、布尔值 JsonElement value entry.getValue(); if (value.isJsonNull()) { row.put(entry.getKey(), null); } else if (value.isJsonPrimitive()) { JsonPrimitive p value.getAsJsonPrimitive(); if (p.isBoolean()) row.put(entry.getKey(), p.getAsBoolean()); else if (p.isNumber()) row.put(entry.getKey(), p.getAsNumber()); else row.put(entry.getKey(), p.getAsString()); } } table.insert(row); exchange.sendResponseHeaders(200, 0); OutputStream os exchange.getResponseBody(); os.write(OK.getBytes(StandardCharsets.UTF_8)); os.close(); }参数说明requestBody必须用StandardCharsets.UTF_8显式指定编码否则中文字段名/值会乱码JsonParser来自com.google.gson需引入依赖比原生JSONObject更健壮table.insert(row)是唯一调用数据层的地方服务层绝不碰rows列表——这是分层铁律。2.3 表示层Presentation LayerHTML 原生 JS拒绝框架绑架前端不用 Vue/React就用最朴素的formfetch。关键在于表单动态生成用户输入CREATE TABLE user (id INT, name VARCHAR(20))后JS 解析字段名和类型生成对应 input!-- 动态生成的插入表单 -- form idinsertForm onsubmitreturn handleSubmit(event) input typehidden nametable valueuser div classfield-group labelid:/label input typenumber nameid required /div div classfield-group labelname:/label input typetext namename maxlength20 required /div button typesubmit插入记录/button /form script function handleSubmit(e) { e.preventDefault(); const form e.target; const data new FormData(form); const obj {}; for (let [key, value] of data.entries()) { if (key ! table) obj[key] value; // 过滤隐藏字段 } fetch(/api/insert, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({ table: form.querySelector([nametable]).value, data: obj }) }) .then(r r.text()) .then(text { if (text OK) { alert(插入成功); form.reset(); } else { alert(插入失败 text); } }); return false; } /script为什么坚持原生 JS面试官看你源码时一眼能看懂逻辑不被v-model或useState干扰调试时console.log(obj)直接看到提交数据没有框架中间层遮挡FormData自动处理空值/类型比手写JSON.stringify({id: document.getElementById(id).value})更鲁棒。3. 存储持久化别让数据重启就消失文件存取的 3 种落地方式内存存储ListTable只是起点真正的“数据库”必须落盘。这里提供三种渐进式方案按可靠性排序强烈建议从方案一做起再逐步升级3.1 方案一CSV 文件存储新手友好调试直观每个表存为一个 CSV 文件如user.csv首行为字段名后续为数据行。Java 用Files.write()写Files.readAllLines()读public void saveToCsv(Table table, Path dir) throws IOException { Path file dir.resolve(table.getName() .csv); ListString lines new ArrayList(); // 第一行字段名用逗号分隔 lines.add(String.join(,, table.getColumns())); // 后续行每行数据字符串转义简单处理双引号包裹含逗号的值 for (MapString, Object row : table.getRows()) { ListString values new ArrayList(); for (String col : table.getColumns()) { Object val row.get(col); String str (val null) ? : val.toString(); // 简单转义含逗号、换行、双引号的值用双引号包裹内部双引号变两个 if (str.contains(,) || str.contains(\n) || str.contains(\)) { str \ str.replace(\, \\) \; } values.add(str); } lines.add(String.join(,, values)); } Files.write(file, lines, StandardCharsets.UTF_8); }优势与边界✅ 用 Excel 或记事本直接打开查看调试零门槛✅Files.write()原子写入JDK 7避免写一半崩溃❌ 不支持并发写多线程同时saveToCsv会覆盖❌ 无事务INSERT成功但saveToCsv失败数据丢失适用场景单用户本地工具、课程实验、原型验证。3.2 方案二JSON 文件存储支持嵌套结构更清晰用 Gson 库将整个Table对象序列化为 JSON每个表一个.json文件public void saveToJson(Table table, Path dir) throws IOException { Path file dir.resolve(table.getName() .json); JsonObject json new JsonObject(); json.addProperty(name, table.getName()); json.add(columns, new Gson().toJsonTree(table.getColumns())); JsonArray rowsArray new JsonArray(); for (MapString, Object row : table.getRows()) { JsonObject rowObj new JsonObject(); for (Map.EntryString, Object entry : row.entrySet()) { Object val entry.getValue(); if (val null) { rowObj.add(entry.getKey(), JsonNull.INSTANCE); } else if (val instanceof Number) { rowObj.add(entry.getKey(), new JsonPrimitive((Number) val)); } else if (val instanceof Boolean) { rowObj.add(entry.getKey(), new JsonPrimitive((Boolean) val)); } else { rowObj.add(entry.getKey(), new JsonPrimitive(val.toString())); } } rowsArray.add(rowObj); } json.add(rows, rowsArray); Files.write(file, new Gson().toJson(json).getBytes(StandardCharsets.UTF_8)); }参数说明JsonNull.INSTANCE正确表示null避免 Gson 默认忽略 null 字段new Gson().toJson(json)生成紧凑 JSON若要可读格式用new GsonBuilder().setPrettyPrinting().create()注意JSON 不支持Date、BigDecimal等类型简易版只处理基础类型符合教学定位。3.3 方案三SQLite 嵌入式存储生产可用无缝升级当 CSV/JSON 无法满足查询性能或并发需求时替换数据层为 SQLite JDBC// 替换 Table 类为 SQLiteTable public class SQLiteTable { private final String tableName; private final Connection conn; // 共享 Connection由 DatabaseManager 管理 public SQLiteTable(String tableName, Connection conn) { this.tableName tableName; this.conn conn; initTable(); // CREATE TABLE IF NOT EXISTS ... } public void insert(MapString, Object row) throws SQLException { String placeholders String.join(,, Collections.nCopies(row.size(), ?)); String sql INSERT INTO tableName VALUES ( placeholders ); try (PreparedStatement ps conn.prepareStatement(sql)) { int i 1; for (Object val : row.values()) { if (val null) ps.setNull(i, Types.NULL); else if (val instanceof Number) ps.setObject(i, val); else ps.setString(i, val.toString()); i; } ps.executeUpdate(); } } }迁移要点只需重写Table实现类服务层和 HTML 层完全不动——这就是分层的价值SQLite JDBC 驱动 jar 包如sqlite-jdbc-3.45.1.0.jar加入 classpath 即可Connection必须池化用 HikariCP或至少复用避免频繁创建连接开销。4. 避坑指南这 5 个错误让我重写了 3 次核心模块写这个简易数据库时我踩过的坑比代码行数还多。以下是高频、隐蔽、且修复成本高的 5 个问题按“现象 → 原因 → 解决”列出全是真实翻车现场4.1 现象HTML 表单提交中文字段名后端收到乱码如??原因HTTP 请求默认编码是 ISO-8859-1而 HTML 页面声明了 UTF-8但HttpExchange.getRequestBody()未指定 charset。解决在读取请求体时强制指定 UTF-8String requestBody new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);注意exchange.getRequestHeaders().getFirst(Content-Type)可能为空不能依赖 header 判断编码。4.2 现象多次点击“插入”按钮同一条数据被插了 3 次原因前端fetch成功后没禁用按钮用户连续点击触发多次请求后端无幂等校验。解决前端提交后立即button.disabled true成功/失败后恢复后端对INSERT请求加简单去重如检查row的主键字段是否已存在或用synchronized块包裹table.insert()简易版够用。4.3 现象SELECT * FROM user WHERE name张三返回空但数据明明存在原因select方法中Objects.equals(row.get(col), val)比较的是String和JsonElementGson 解析后类型不匹配永远为false。解决统一转换为String比较或在conditions构建时就转好类型// 服务层解析条件时统一转 String if (value.isJsonPrimitive()) { row.put(entry.getKey(), value.getAsString()); // 强制转字符串 }4.4 现象CSV 文件保存后Excel 打开显示所有内容挤在第一列原因Windows Excel 默认用逗号分隔但若系统区域设置为“中文中国”Excel 可能用分号分隔或 CSV 缺少 BOM 头导致编码识别错误。解决文件开头写入 UTF-8 BOM\uFEFFFiles.write(file, (\uFEFF content).getBytes(StandardCharsets.UTF_8))或改用制表符\t分隔更兼容并保存为.tsv。4.5 现象Table对象被多个线程同时读写偶尔抛ConcurrentModificationException原因ArrayList和HashMap非线程安全rows.add(...)和for (Map row : rows)迭代同时发生必然崩溃。解决读多写少场景用CopyOnWriteArrayList存rows迭代安全写操作稍慢但可接受或更彻底用ReentrantReadWriteLock读操作 lock.readLock()写操作 lock.writeLock()绝对不要用Collections.synchronizedList()—— 它只同步单个方法size()get(i)组合仍不安全。5. 查询优化实战从 O(n) 全表扫描到 O(log n) 索引加速简易系统默认select是全表扫描1000 行耗时可忽略但 10 万行就会卡顿。加索引不是炫技而是理解 B 树本质的第一步。这里用最简方式实现单字段哈希索引不引入外部库50 行代码搞定5.1 设计索引结构为user.id字段建唯一索引在Table类中增加索引映射// Table.java 新增字段 private final MapString, MapObject, ListInteger indexes; // 字段名 → (值 → 行号列表) public Table(String name, ListString columns) { // ...原有初始化 this.indexes new HashMap(); } public void createIndex(String columnName) { if (!columns.contains(columnName)) return; MapObject, ListInteger indexMap new HashMap(); for (int i 0; i rows.size(); i) { MapString, Object row rows.get(i); Object val row.get(columnName); indexMap.computeIfAbsent(val, k - new ArrayList()).add(i); } indexes.put(columnName, indexMap); }5.2 改写select方法自动命中索引public ListMapString, Object select(MapString, Object conditions) { // 优先检查单条件且有索引的字段 if (conditions.size() 1) { String col conditions.keySet().iterator().next(); Object val conditions.get(col); if (indexes.containsKey(col)) { ListInteger rowIndices indexes.get(col).get(val); if (rowIndices ! null) { ListMapString, Object result new ArrayList(); for (Integer idx : rowIndices) { result.add(new HashMap(rows.get(idx))); } return result; } } } // 无索引或复合条件退化为全表扫描 // ...原有逻辑 }5.3 在 HTML 界面暴露索引管理功能加一个简单的索引管理表单让用户手动建索引教学意义大于实用h3创建索引/h3 form onsubmitcreateIndex(event) select nametable required option value选择表/option !-- 后端 API 返回表名列表 -- /select input typetext namecolumn placeholder字段名 required button typesubmit创建索引/button /form script function createIndex(e) { e.preventDefault(); const form e.target; const data new FormData(form); fetch(/api/index, { method: POST, body: JSON.stringify({ table: data.get(table), column: data.get(column) }) }); } /script效果对比10 万行测试查询方式条件耗时全表扫描WHERE nameAlice~1200ms哈希索引WHERE id12345~3ms为什么只做哈希索引B 树索引需维护树结构、分裂合并代码量爆炸偏离“简易”定位哈希索引完美支持查询覆盖 80% 的教学场景LIKE %abc或范围查询BETWEEN自然走全表让学生明白索引局限性。6. 面试与教学价值如何把这套源码变成你的技术谈资这套“基于 Java 与 HTML 的简易数据库系统”最大的价值不在功能多强而在于它是一块可触摸、可调试、可辩论的技术琥珀。我带实习生时让他们每人 fork 一份在上面加一个功能有人加事务日志WAL有人加 MVCC 版本控制有人把 HTML 换成 Vue 并对比性能。过程中暴露的问题比十道八股文深刻得多。比如当他试图给INSERT加事务发现saveToCsv()失败后rows已修改必须实现rollback()—— 这时才懂 ACID 的 “A”原子性不是一句口号当他尝试用ConcurrentHashMap替换HashMap存rows结果select依然报ConcurrentModificationException—— 这时才明白“线程安全容器”不等于“线程安全操作”当他把indexMap从HashMap换成ConcurrentHashMap发现computeIfAbsent在高并发下仍可能重复计算 —— 这时才去查 Javadoc发现它不是原子操作。所以别把它当作业交完就删。我的习惯是保留 commit 记录git log --oneline能看到你从 CSV 存储 → JSON → SQLite 的演进写 README.md 时用表格对比每种存储的优劣如“CSV✅ 人眼可读 ❌ 无并发”在src/test/java下写 JUnit 测试哪怕只有 3 个用例testInsertAndSelect,testIndexSpeed,testChineseCharset面试时主动展示不等面试官问“做过什么项目”直接说“我写过一个简易数据库这里有个 bug 我花了两天才定位到……” —— 技术深度藏在 debug 过程里不在功能列表里。希望帮到你。本文还有配套的精品资源点击获取