简介:用Java开发的轻量级图书馆管理系统,基于JDBC直连数据库,不依赖Tomcat等容器,下载解压就能跑。包含图书录入、借阅/归还操作、读者信息维护、库存实时查询等核心功能。资源里有编译好的dbms-1.0.0.jar,双击即可启动图形界面;配套jdbc.properties文件方便改数据库地址和账号;SQL目录下提供建表语句和初始测试数据,支持MySQL及其它兼容JDBC的数据库;Maven结构清晰(dbms-master),代码全注释,表设计符合第三范式,涵盖图书、读者、借阅记录三张主表及关联逻辑;附带介绍.txt说明运行步骤,log目录自动记录操作日志,jar目录存好所有依赖包,适合学生做数据库或Java课程设计交作业、课堂演示或自己练手二次开发。
1. 项目概述:一个真正“能跑起来”的教学级图书馆系统
你有没有遇到过这样的情况:数据库课刚讲完ER图、范式、SQL语法,老师布置课程设计——“做一个图书馆管理系统”,结果翻遍CSDN、GitHub,下载十几个“Java图书馆系统”压缩包,解压后不是缺jar包就是报ClassNotFoundException,改了配置又卡在SQLException: Access denied,折腾半天连登录界面都出不来?更别说还要自己建库、写SQL、配驱动、调Maven依赖……最后交作业全靠截图+文字描述,心里清楚:这系统根本没跑起来过。
这个项目,就是为解决这个问题而生的。它不是一个“理论上能运行”的Demo,而是一个从课堂到桌面无缝衔接的教学实践闭环体:你下载、解压、双击dbms-1.0.0.jar,3秒内弹出图形界面;填上本地MySQL账号密码(默认root/root),点“初始化数据库”,5秒内自动建好4张表、插入20条测试数据;接着就能真实录入新书、给学生办读者证、模拟借还流程、实时查看库存余量——所有操作背后,是标准JDBC代码一行行执行,是事务控制确保“借书成功但扣库存失败”这种异常不会发生,是日志文件里清清楚楚记着“2024-06-12 14:22:03 [INFO] 借阅记录ID=107,图书《算法导论》已成功借出,读者ID=2023001”。
它不炫技,不用Spring Boot自动装配,不套MyBatis动态SQL,甚至没用连接池(HikariCP)——因为它的核心目标很明确:让大二学生在48小时内,亲手把“数据库原理”和“Java编程”这两门课的知识点,焊死在一个可触摸、可调试、可提交的.exe式体验里。 所有代码都在dbms-master/src/main/java下,按controller、service、dao、entity分层,每个类开头都有中文注释说明职责,每个SQL语句后面都跟着// 查询读者姓名+学号+专业,用于借阅界面下拉选择这样的白话解释。sql/目录下的init.sql不是一段黑盒脚本,而是逐行带注释的建表语句:-- 图书表:主键book_id自增,isbn唯一约束防重复录入,status字段枚举值'IN_STOCK','BORROWED','LOST'。这不是一个要你“研究”的系统,而是一个让你“上手就干”的工具箱。
关键词里的“Java课程设计”不是虚词——它对应着课程设计报告里必须体现的“需求分析→概念设计(ER图)→逻辑设计(第三范式分解)→物理实现(SQL建表)→编码实现(JDBC事务处理)→测试验证(边界值:同一本书被借三次?读者证号重复?)”全流程。“JDBC实战”意味着你打开BookDao.java,能看到PreparedStatement如何预编译防止SQL注入,Connection.setAutoCommit(false)如何开启事务,try-with-resources如何确保ResultSet和Statement必然关闭。“SQL建表脚本”不只是CREATE TABLE,还包括ALTER TABLE borrow_record ADD CONSTRAINT fk_reader_id FOREIGN KEY (reader_id) REFERENCES reader(reader_id) ON DELETE CASCADE这样的外键级联删除,让你直观理解“为什么第三范式要求消除传递依赖”——因为一旦读者表里删掉一个学生,他所有借阅记录必须自动清理,否则数据库就“脏”了。这个系统,是你交作业时敢贴上真实运行截图的底气,是你面试时能指着某段代码说“这里我用了事务回滚,因为……”的资本。
2. 整体架构与设计思路拆解:为什么是JDBC而不是Spring Boot?
2.1 轻量即正义:剥离框架依赖的底层逻辑
很多同学一上来就想用Spring Boot,觉得“自动配置多方便”。但课程设计的本质不是比谁用的框架新,而是考察你对数据流动本质的理解。我们刻意选择纯JDBC,原因非常实在:
第一,可见性。当你在BookService.java里看到String sql = "UPDATE book SET stock = stock - 1 WHERE book_id = ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setInt(1, bookId); ps.executeUpdate();,你就100%清楚:此刻程序正在执行一条UPDATE语句,它会直接修改数据库里某一行的stock字段。而如果换成bookRepository.save(book),你得先跳进Repository接口,再看Impl实现,再查BaseJpaRepository源码,最后才定位到那条SQL——这对初学者是认知黑洞。JDBC把“Java对象 ↔ SQL语句 ↔ 数据库记录”这条链路完全摊开在你眼皮底下。
第二,可控性。课程设计常要求实现“借书时检查库存是否为0”,这是一个典型的业务规则嵌入。用JDBC,你可以在BorrowService.borrowBook()方法里,先SELECT stock FROM book WHERE book_id = ?,判断结果后再决定是否执行INSERT INTO borrow_record和UPDATE book。整个事务由你手动控制:conn.setAutoCommit(false) → checkStock() → insertRecord() → updateStock() → conn.commit(),任何一步失败都conn.rollback()。这种“把事务开关握在自己手里”的感觉,是框架封装后永远无法获得的肌肉记忆。
第三,部署零成本。Spring Boot打成的jar包动辄30MB以上,里面塞满了Tomcat、Spring MVC、Jackson等你课程设计根本用不到的组件。而本系统的dbms-1.0.0.jar只有2.1MB,因为它只打包了src/main/java下的业务代码 + mysql-connector-java-8.0.33.jar这个唯一依赖。双击运行,启动的是Swing GUI线程,不是Web容器;访问的是本地窗口,不是http://localhost:8080。这意味着你不需要装JDK以外的任何东西,不需要配环境变量,甚至不需要懂什么是Servlet——只要电脑能运行Java,就能跑起来。这才是“开箱即用”的本意:降低启动门槛,把精力聚焦在业务逻辑本身。
提示:有同学问“能不能改成Web版”?当然可以,但请先确保你已完全吃透当前Swing版的JDBC事务流程。把
BorrowController里的借书逻辑原样搬到Spring MVC的@PostMapping("/borrow")里,只是把JOptionPane.showMessageDialog换成return "success",而数据库操作部分几乎不用改——这才是框架演进的正确姿势:先掌握地基,再盖高楼。
2.2 数据库设计:第三范式的教科书级落地
系统共4张核心表:book(图书)、reader(读者)、borrow_record(借阅记录)、admin(管理员)。它们不是随意堆砌,而是严格遵循第三范式(3NF)推导而来。我们以“图书”为例,还原设计过程:
原始需求:“一本书有书名、作者、ISBN、出版社、出版年份、价格、库存数量、所属分类”。如果全塞进一张表,会出现问题:
- 数据冗余:同一出版社(如“机械工业出版社”)可能出现在上百本书里,修改出版社地址时要更新上百行;
- 更新异常:只改了某本书的出版社,忘了其他同社书籍,数据就不一致;
- 插入异常:想新增一个分类“人工智能”,但还没录入该分类的书,就无法插入分类信息;
- 删除异常:删掉最后一本“计算机网络”分类的书,整个分类信息就丢失了。
解决方案是范式分解:
1. 第一范式(1NF):确保每列原子性。author字段不能存“王珊,萨师煊”,要拆成author1, author2或新建book_author关联表(本系统采用后者,见下文);
2. 第二范式(2NF):消除非主属性对码的部分函数依赖。book表主键是book_id,isbn是候选键,但category_name(分类名称)只依赖于category_id,不直接依赖book_id,所以要把分类独立成category表;
3. 第三范式(3NF):消除传递依赖。book表里如果存publisher_address(出版社地址),它依赖于publisher_name,而publisher_name又依赖于book_id,形成book_id → publisher_name → publisher_address的传递依赖。必须拆出publisher表。
最终表结构如下(摘自sql/init.sql):
-- 分类表:存储图书分类,如'计算机科学','文学'
CREATE TABLE category (
category_id INT PRIMARY KEY AUTO_INCREMENT,
category_name VARCHAR(50) NOT NULL UNIQUE,
description VARCHAR(200)
);
-- 出版社表:存储出版社信息,避免地址冗余
CREATE TABLE publisher (
publisher_id INT PRIMARY KEY AUTO_INCREMENT,
publisher_name VARCHAR(100) NOT NULL,
address VARCHAR(200),
contact_phone VARCHAR(20)
);
-- 图书主表:只存与book_id直接相关的属性
CREATE TABLE book (
book_id INT PRIMARY KEY AUTO_INCREMENT,
isbn VARCHAR(20) NOT NULL UNIQUE,
title VARCHAR(200) NOT NULL,
price DECIMAL(8,2) NOT NULL CHECK(price > 0),
stock INT NOT NULL DEFAULT 0 CHECK(stock >= 0),
publish_date DATE,
status ENUM('IN_STOCK','BORROWED','LOST') DEFAULT 'IN_STOCK',
category_id INT NOT NULL,
publisher_id INT NOT NULL,
FOREIGN KEY (category_id) REFERENCES category(category_id),
FOREIGN KEY (publisher_id) REFERENCES publisher(publisher_id)
);
-- 多对多关系表:一本图书可有多个作者,一个作者可写多本书
CREATE TABLE author (
author_id INT PRIMARY KEY AUTO_INCREMENT,
author_name VARCHAR(100) NOT NULL,
birth_year YEAR
);
CREATE TABLE book_author (
book_id INT NOT NULL,
author_id INT NOT NULL,
PRIMARY KEY (book_id, author_id),
FOREIGN KEY (book_id) REFERENCES book(book_id) ON DELETE CASCADE,
FOREIGN KEY (author_id) REFERENCES author(author_id) ON DELETE CASCADE
);
看到这里,你应该明白为什么book表里没有publisher_address字段了——它被抽离到publisher表,通过publisher_id外键关联。这样,当机械工业出版社搬家时,只需更新publisher表里publisher_id=5的address字段,所有引用它的图书记录自动生效。这就是第三范式解决“更新异常”的实际价值。sql/init.sql里每张表的COMMENT都写着设计意图,比如-- 读者表:学号为主键,确保一人一证;email加UNIQUE索引,防重复注册,读SQL就是在读数据库设计说明书。
2.3 工程结构:Maven目录即学习路线图
打开dbms-master/目录,你会看到标准Maven结构:
dbms-master/
├── pom.xml # 依赖管理:只声明mysql-connector-java和slf4j-simple
├── src/
│ ├── main/
│ │ ├── java/ # 核心代码
│ │ │ └── com/example/dbms/
│ │ │ ├── controller/ # Swing界面事件处理器,如LoginFrame、MainForm
│ │ │ ├── service/ # 业务逻辑门面,如BookService、BorrowService
│ │ │ ├── dao/ # 数据访问对象,直连JDBC,如BookDao、ReaderDao
│ │ │ ├── entity/ # Java Bean,与数据库表一一映射
│ │ │ └── util/ # 工具类:JDBC连接工厂、日期格式化、日志工具
│ │ └── resources/ # 配置文件:jdbc.properties、logback.xml
│ └── test/ # JUnit测试用例(虽未提供,但目录已预留)
├── sql/ # SQL脚本:建表、初始化数据、常用查询
├── log/ # 运行时日志输出目录(程序自动创建)
└── target/ # Maven编译输出(含最终jar包)
这个结构本身就是一门微型架构课。controller层只做一件事:响应按钮点击,调用service层方法,并把返回结果展示在界面上。它不碰SQL,不处理事务,纯粹是UI和业务的粘合剂。service层是真正的业务中枢,比如BorrowService.borrowBook(int bookId, int readerId)方法里,会依次调用bookDao.checkStock(bookId)、readerDao.findById(readerId)、borrowDao.insertRecord(...)、bookDao.updateStock(bookId, -1),并在catch块里统一回滚事务。dao层则专注和数据库对话:BookDao里全是public List<Book> findAll()、public void updateStock(int bookId, int delta)这样的方法,每个方法内部就是Connection→PreparedStatement→executeUpdate的标准三步曲。entity包下的Book.java,字段名和book表列名完全一致,private Integer bookId; private String isbn;,连getter/setter都用IDEA一键生成,毫无花哨。这种“分层清晰、职责单一”的结构,让你在调试时能快速定位问题:界面卡死?看controller;借书失败?进service查事务;查不到数据?直奔dao看SQL是否写错。它不教你“高并发怎么优化”,但教会你“代码该怎么组织才不混乱”。
3. 核心功能模块详解与实操要点
3.1 数据库连接与初始化:从jdbc.properties到自动建库
系统启动的第一步,永远是连上数据库。所有连接参数都集中在根目录的jdbc.properties文件里:
# 数据库驱动类名(MySQL 8.0+必须用com.mysql.cj.jdbc.Driver)
driverClassName=com.mysql.cj.jdbc.Driver
# JDBC URL:注意useSSL=false和serverTimezone=Asia/Shanghai,否则中文乱码或时区错误
url=jdbc:mysql://localhost:3306/library_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
# 数据库用户名和密码
username=root
password=root
# 连接池最大活跃连接数(本系统未用连接池,此参数仅作占位)
maxActive=20
关键细节与避坑指南:
- url中的library_db是数据库名,不是表名。首次运行前,你需要手动在MySQL里创建这个库:CREATE DATABASE library_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;。为什么用utf8mb4?因为utf8在MySQL里实际是utf8mb3,不支持emoji和部分生僻汉字,而utf8mb4才是真正的UTF-8。
- serverTimezone=Asia/Shanghai必不可少。MySQL默认时区是SYSTEM,而JVM时区可能是GMT+0,导致NOW()函数返回的时间比Java里new Date()慢8小时,借阅记录的时间戳就全乱了。加上这个参数,强制双方使用东八区时间。
- characterEncoding=utf8保证中文不乱码。但光有这个不够,你还得确保MySQL服务端也配置了UTF-8:在my.cnf里添加[mysqld] default-character-set = utf8mb4和[client] default-character-set = utf8mb4,然后重启MySQL服务。
初始化数据库的操作藏在主界面的“系统设置”菜单里。点击“初始化数据库”,程序会做三件事:
1. 检测库是否存在:执行SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = 'library_db';
2. 若不存在则创建:执行CREATE DATABASE ...语句;
3. 执行建表脚本:逐行读取sql/init.sql,跳过注释行(以--开头),对每条CREATE TABLE或INSERT语句调用Statement.execute()。
sql/init.sql的前几行就体现了教学设计的用心:
-- 初始化脚本:创建library_db数据库及所有表结构,并插入测试数据
-- 注意:请先确保MySQL服务已启动,且root用户有CREATE权限
-- 第一步:切换到目标数据库
USE library_db;
-- 第二步:创建分类表
CREATE TABLE category ( ... );
-- 第三步:创建出版社表
CREATE TABLE publisher ( ... );
-- 第四步:创建图书表(外键依赖category和publisher)
CREATE TABLE book ( ... );
-- 第五步:插入3条测试分类数据
INSERT INTO category (category_name, description) VALUES
('计算机科学', '涵盖编程语言、算法、操作系统等'),
('文学', '中外小说、散文、诗歌'),
('经济管理', '市场营销、财务管理、人力资源');
-- 第六步:插入5条测试出版社数据
INSERT INTO publisher (publisher_name, address, contact_phone) VALUES
('清华大学出版社', '北京市海淀区成府路', '010-62782989'),
('机械工业出版社', '北京市西城区百万庄大街', '010-88379833');
看到“第五步”、“第六步”这样的注释,你就知道这个脚本是按执行顺序精心编排的。因为book表的外键category_id和publisher_id必须指向已存在的记录,所以必须先插category和publisher,再插book。如果你手动执行SQL时顺序错了,就会报Cannot add or update a child row: a foreign key constraint fails。这个错误,正是第三范式外键约束最直观的体现——它在逼你思考数据之间的依赖关系。
3.2 图书管理模块:从录入到状态追踪的完整生命周期
图书管理是系统的核心,覆盖了CRUD(增删改查)全部操作,但重点在于“状态管理”。book表的status字段是ENUM('IN_STOCK','BORROWED','LOST'),它不是简单的标记,而是驱动业务规则的引擎。
录入新书(Create):
在“图书管理”→“新增图书”界面,你需要填写:
- ISBN(必填,唯一校验):输入978-7-302-53572-7,程序会实时查询数据库,若已存在则弹窗提示“该ISBN图书已存在”;
- 书名、作者(通过book_author关联表实现多作者):点击“添加作者”按钮,弹出子窗口录入作者姓名和出生年份,保存后自动关联;
- 分类和出版社(下拉选择):数据来自category和publisher表,确保引用完整性;
- 库存数量(stock):初始值必须≥0,否则保存失败。
关键代码逻辑(BookService.addBook()):
public boolean addBook(Book book, List<Author> authors) {
Connection conn = null;
try {
conn = JdbcUtil.getConnection(); // 从jdbc.properties读取配置获取连接
conn.setAutoCommit(false); // 开启事务
// 1. 插入图书主记录
int bookId = bookDao.insert(book);
// 2. 为每个作者建立关联
for (Author author : authors) {
// 先尝试根据姓名查找作者,避免重复插入
Author existing = authorDao.findByName(author.getAuthorName());
int authorId = (existing != null) ? existing.getAuthorId() : authorDao.insert(author);
// 再插入关联记录
bookAuthorDao.insert(bookId, authorId);
}
conn.commit();
return true;
} catch (SQLException e) {
if (conn != null) {
try { conn.rollback(); } catch (SQLException ex) {}
}
logger.error("新增图书失败", e);
return false;
} finally {
JdbcUtil.close(conn);
}
}
这段代码展示了JDBC事务的典型用法:conn.setAutoCommit(false)关闭自动提交 → 执行多个SQL → conn.commit()统一提交,或conn.rollback()整体回滚。特别注意authorDao.findByName()这一步——它体现了“避免冗余”的设计思想。如果直接authorDao.insert(),同一作者(如“王珊”)被不同图书多次录入,就会在author表里产生多条重复记录。通过先查后插,确保作者信息全局唯一。
库存查询与状态联动(Read & Update):
在“库存查询”界面,你可以按书名、ISBN、分类模糊搜索。搜索结果表格里,“状态”列会根据stock和借阅记录动态计算:
- 若stock > 0且无未归还记录 → IN_STOCK
- 若stock == 0且存在未归还记录 → BORROWED
- 若stock == 0且所有记录已归还,但status字段被手动设为LOST → LOST
这个逻辑不在SQL里硬编码,而是在BookDao.findBooksByCondition()返回List<Book>后,在BookService层用Java代码计算:
// 伪代码:计算图书实时状态
for (Book book : books) {
int borrowedCount = borrowDao.countUnreturnedByBookId(book.getBookId());
if (book.getStock() > 0 && borrowedCount == 0) {
book.setStatus("IN_STOCK");
} else if (book.getStock() == 0 && borrowedCount > 0) {
book.setStatus("BORROWED");
} else if (book.getStock() == 0 && borrowedCount == 0 && "LOST".equals(book.getStatus())) {
book.setStatus("LOST");
}
}
为什么这么做?因为stock字段只记录“可借数量”,而BORROWED状态需要结合借阅记录表才能确定。如果把这部分逻辑写进SQL的CASE WHEN,虽然可行,但会让SQL变得臃肿,且难以调试。Java层处理更灵活,也符合“业务逻辑在Service层”的分层原则。
3.3 借阅与归还模块:事务安全的双操作原子性
借阅和归还是系统最易出错的环节,也是教学重点。一次借书操作,必须同时完成两件事:
1. 在borrow_record表插入一条新记录(book_id, reader_id, borrow_date);
2. 在book表将对应图书的stock字段减1。
这两步必须“全成功或全失败”,否则会出现“书被借走了,但库存没扣”(超借)或“库存扣了,但借阅记录没生成”(数据丢失)的严重问题。这就是事务(Transaction)存在的意义。
借阅流程(BorrowService.borrowBook()):
public boolean borrowBook(int bookId, int readerId) {
Connection conn = null;
try {
conn = JdbcUtil.getConnection();
conn.setAutoCommit(false);
// 步骤1:检查图书库存
Book book = bookDao.findById(bookId);
if (book == null || book.getStock() <= 0) {
throw new BusinessException("图书库存不足,无法借阅");
}
// 步骤2:检查读者是否已借满(最多借5本)
int borrowedCount = borrowDao.countUnreturnedByReaderId(readerId);
if (borrowedCount >= 5) {
throw new BusinessException("读者已借阅5本书,达到上限");
}
// 步骤3:插入借阅记录
BorrowRecord record = new BorrowRecord();
record.setBookId(bookId);
record.setReaderId(readerId);
record.setBorrowDate(new Date());
borrowDao.insert(record);
// 步骤4:扣减库存
bookDao.updateStock(bookId, -1);
conn.commit();
return true;
} catch (BusinessException e) {
// 业务异常,不回滚(如库存不足),直接抛出给界面提示
throw e;
} catch (SQLException e) {
// 数据库异常,必须回滚
if (conn != null) {
try { conn.rollback(); } catch (SQLException ex) {}
}
logger.error("借阅失败", e);
throw new RuntimeException("借阅操作异常,请重试", e);
} finally {
JdbcUtil.close(conn);
}
}
这里有两个精妙的设计:
- 业务异常(BusinessException)不回滚:比如库存不足、借阅超限,这是预期内的业务规则拦截,应该直接提示用户“哪里错了”,而不是静默回滚让用户困惑“我点了借书,怎么没反应?”。只有SQLException这类意外错误才触发回滚。
- countUnreturnedByReaderId()的实现:它执行的SQL是SELECT COUNT(*) FROM borrow_record WHERE reader_id = ? AND return_date IS NULL。注意return_date IS NULL,这是判断“未归还”的关键。return_date字段默认为NULL,归还时才更新为具体时间。这种设计比加一个is_returned布尔字段更灵活,因为未来可以扩展“逾期未还”统计(return_date IS NULL AND borrow_date < DATE_SUB(NOW(), INTERVAL 30 DAY))。
归还流程(ReturnService.returnBook()):
归还操作同样需要事务,但逻辑稍复杂:
1. 更新borrow_record表的return_date字段为当前时间;
2. 将book表对应图书的stock加1;
3. (可选)检查该读者是否有逾期记录,如有则更新其信用分。
ReturnService.returnBook()的代码结构与borrowBook()高度相似,只是SQL语句从INSERT和UPDATE stock=-1变成了UPDATE borrow_record SET return_date=?和UPDATE book SET stock=stock+1。这种对称性,正是良好设计的标志——借和还,本就是一对互逆操作。
3.4 日志系统:用SLF4J记录每一次操作的来龙去脉
系统在src/main/resources/logback.xml中配置了日志框架(SLF4J + Logback),所有关键操作都会写入log/目录下的文件。打开log/app.log,你会看到类似这样的记录:
2024-06-12 14:22:03 [INFO] 借阅记录ID=107,图书《算法导论》已成功借出,读者ID=2023001
2024-06-12 14:25:18 [WARN] 管理员admin尝试删除读者ID=2023005,但该读者仍有未归还图书,操作被拒绝
2024-06-12 14:30:02 [ERROR] 数据库连接失败,URL=jdbc:mysql://localhost:3306/library_db,错误:Communications link failure
这些日志不是随便写的。[INFO]级别记录正常业务流,[WARN]记录业务规则拦截(如删除有借阅记录的读者),[ERROR]记录系统级故障(如数据库宕机)。日志内容包含上下文信息:借阅记录ID、图书名、读者ID,而不是笼统的“借书成功”。这极大方便了问题排查——如果老师说“张三借的《深入理解Java虚拟机》没显示在借阅列表里”,你直接搜深入理解Java虚拟机,就能定位到那条日志,再根据ID查数据库,立刻知道是插入失败还是查询条件写错了。
日志配置的关键点在logback.xml:
<!-- 定义日志输出格式 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>log/app.log</file>
<!-- 每天生成一个新日志文件,保留30天 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>log/app.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%level] %msg%n</pattern>
</encoder>
</appender>
<maxFileSize>10MB</maxFileSize>和<maxHistory>30</maxHistory>确保日志文件不会无限膨胀,占用磁盘空间。这对于长期运行的系统至关重要。而%d{yyyy-MM-dd HH:mm:ss}精确到秒的时间戳,则是分析操作时序的基础——比如你想确认“借书”和“扣库存”是否真的在同一事务里,就看两条日志的时间是否完全一致(毫秒级)。
4. 实操全过程:从零开始运行、调试与二次开发
4.1 首次运行:5分钟搞定环境搭建
前置条件检查:
1. JDK版本:必须JDK 8或JDK 11(本系统编译目标为1.8)。在命令行输入java -version,确认输出类似java version "1.8.0_361"。如果显示Command not found,请先安装JDK并配置JAVA_HOME环境变量。
2. MySQL服务:确保MySQL已安装并正在运行。Windows用户可在任务管理器“服务”里找MySQL80,Mac用户执行brew services list | grep mysql,Linux用户执行sudo systemctl status mysql。如果未运行,请启动它。
3. MySQL root密码:默认是root。如果修改过,请记住你的密码,后续要填在jdbc.properties里。
步骤详解:
1. 解压资源包:将下载的ZIP文件解压到任意目录,例如D:\dbms-master。确保目录结构完整,特别是jdbc.properties、sql/、log/这些文件夹都在根目录下。
2. 配置数据库连接:用记事本打开jdbc.properties,修改username和password为你MySQL的实际账号密码。如果MySQL不是装在本机,还需修改url里的localhost为服务器IP地址。
3. 创建数据库(可选,初始化脚本会自动创建):虽然脚本会自动创建,但建议手动执行一次,确保权限没问题。打开MySQL命令行客户端(或Navicat、DBeaver等GUI工具),执行:
sql CREATE DATABASE library_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
4. 双击运行jar包:找到dbms-1.0.0.jar,直接双击。如果弹出“无法打开”的提示,说明系统没有关联Java,此时右键jar包→“打开方式”→“选择其他应用”→勾选“始终使用此应用打开”→浏览到C:\Program Files\Java\jdk-xx\jre\bin\javaw.exe(Windows路径,Mac/Linux是/Library/Java/JavaVirtualMachines/jdk-xx.jdk/Contents/Home/bin/java)。
5. 初始化数据库:程序启动后,进入主界面,点击顶部菜单栏“系统设置”→“初始化数据库”。等待几秒钟,状态栏显示“初始化成功!共创建4张表,插入25条测试数据”。此时打开MySQL客户端,执行USE library_db; SHOW TABLES;,你应该能看到author、book、borrow_record等表名。
6. 登录体验:使用默认账号密码登录。管理员账号:admin/123456;读者账号:2023001/123456(学号即账号)。登录后,你就可以在各个功能模块里真实操作了。
注意:如果初始化失败,第一步看
log/app.log末尾的[ERROR]日志。最常见的原因是MySQL服务没启动,或jdbc.properties里密码错了。不要反复点击“初始化”,先解决根本问题。
4.2 代码调试:在IDEA里读懂每一行JDBC
想真正掌握,必须动手调试。以下是在IntelliJ IDEA中导入并调试项目的完整流程:
- 导入Maven工程:打开IDEA → “Open” → 选择解压后的
dbms-master文件夹 → 选择“Open as Project” → 等待Maven自动下载依赖(主要是mysql-connector-java)。 - 配置运行参数:点击右上角“Add Configuration” → “+” → “Application” → 名称填
DbmsMain→ Main class填com.example.dbms.controller.MainForm(这是程序入口) → Working directory填项目根目录(如D:\dbms-master) → 点击“OK”。 - 打断点调试:在
BorrowService.borrowBook()方法的第一行打个断点(红色圆点)。然后点击绿色三角形“Debug”启动。程序会在登录后停在断点处。 - 观察变量:当执行到
Book book = bookDao.findById(bookId);时,鼠标悬停在book变量上,IDEA会显示它的所有字段值:bookId=101,title="Java编程思想",stock=5。接着F8单步执行,看borrowDao.insert(record)后,数据库里是否真的多了一条记录(可同时打开MySQL客户端刷新borrow_record表)。 - 模拟异常:为了理解事务回滚,可以临时修改代码,在
bookDao.updateStock(bookId, -1);之前加一行int i = 1/0;(制造除零异常)。再次Debug,你会发现借阅记录插入了,但库存没扣——因为SQLException触发了rollback(),两条SQL都被撤销了。
这种“边走边看”的调试方式,比读10篇博客都管用。你亲眼看到PreparedStatement如何把?替换成真实值,看到ResultSet如何一行行遍历查询结果,看到Connection对象在finally块里被close()。这才是JDBC的真面目。
4.3 二次开发:添加“图书续借”功能的完整实践
假设课程设计要求增加“续借”功能(读者可延长借阅期限),这是绝佳的二次开发练习。我们来走一遍从需求分析到代码落地的全过程:
需求分析:
- 续借只能对“已借出但未归还”的图书进行;
- 每本书最多续借2次;
- 续借后,borrow_record表的due_date字段(需新增)更新为当前日期+30天;
- 需要在借阅记录列表里增加“续借”按钮。
步骤分解:
1. 修改数据库:在sql/init.sql末尾添加:
sql -- 为借阅记录表添加应还日期字段 ALTER TABLE borrow_record ADD COLUMN due_date DATE AFTER borrow_date; -- 为已有记录设置默认应还日期(借阅日+30天) UPDATE borrow_record SET due_date = DATE_ADD(borrow_date, INTERVAL 30 DAY) WHERE return_date IS NULL;
然后重新运行“初始化数据库”,或手动执行这条ALTER TABLE。
-
更新Entity:打开
src/main/java/com/example/dbms/entity/BorrowRecord.java,添加字段:
java private Date dueDate; // 应还日期 // getter/setter... -
更新DAO:在
BorrowDao.java里,修改insert()方法,插入时设置due_date = DATE_ADD(NOW(), INTERVAL 30 DAY);新增updateDueDate(int recordId, Date newDueDate)方法。 -
编写Service:在
BorrowService.java里新增方法:
```java
public boolean renewBook(int recordId) {
Connection conn = null;
try {
conn = JdbcUtil.getConnection();
conn.setAutoCommit(false);// 检查记录是否存在且未归还 BorrowRecord record = borrowDao.findById(recordId); if (record == null || record.getReturnDate() != null) { throw new BusinessException("该借阅记录不存在或已归还,无法续借"); } // 检查续借次数(需在borrow_record表加renew_count字段,此处略) // 更新due_date为当前日期+30天 Date newDueDate = DateUtil.addDays(new Date(), 30); borrowDao.updateDueDate(recordId, newDueDate); conn.commit(); return true;} catch (SQLException e) {
if (conn != null) try { conn.rollback(); } catch (SQLException ex) {}
throw new RuntimeException(e);
} finally {
JdbcUtil.close(conn);
}
}
``` -
修改Controller:在借阅记录列表的
JTable右键菜单里,添加“续借”选项,点击时调用BorrowService.renewBook(selectedRecordId)。
这个过程,涵盖了数据库变更、实体类同步、DAO层SQL编写、Service层事务控制、Controller层事件绑定——正是企业开发的标准流程。你做的每一个动作,都在强化“数据-代码-界面”的映射关系。
5. 常见问题与排查技巧实录
5.1 启动失败:jar包双击无反应或闪退
这是新手最常遇到的问题,90%的原因与Java环境有关。
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 双击jar包,桌面一闪而过,什么都没出现 | 1. 系统未安装JRE/JDK 2. jar包关联了错误的程序(如WinRAR) | 在命令行执行java -jar dbms-1.0.0.jar。如果提示'java' is not recognized,说明没装Java;如果提示no main manifest attribute,说明jar包损坏或关联错误。解决方案:重新下载jar包;右键jar包→“属性”→“更改默认应用”→选择Java平台。 |
命令行运行java -jar ...,报错Exception in thread "main" java.lang.UnsupportedClassVersionError: ... has been compiled by a more recent version of the Java Runtime | JDK版本不匹配:jar包用JDK 11编译,但你用JDK 8运行 | 查看java -version,确保运行时JDK版本≥编译时版本。本系统编译目标为1.8,所以JDK 8、11、17均可运行。 |
| 程序启动,登录界面弹出,但点击“登录”后卡住,状态栏显示“连接数据库中…” | 数据库服务未启动,或jdbc.properties配置错误 | 检查MySQL服务状态;用MySQL客户端尝试连接localhost:3306,确认账号密码正确;检查jdbc.properties里url的端口号是否正确(默认3306,不是3307)。 |
实操心得:永远先用命令行启动,而不是双击。命令行会把所有错误堆栈打印出来,这是诊断问题的黄金线索。比如看到
java.sql.SQLException: Access denied for user 'root'@'localhost',你就立刻知道是密码错了;看到java.net.ConnectException: Connection refused,就知道是MySQL没启动或端口不对。
5.2 功能异常:借书失败、查询无结果、中文乱码
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
新增图书时,中文书名显示为?????? | MySQL服务端、数据库、表、字段的字符集未统一为utf8mb4 | 执行SHOW VARIABLES LIKE 'character_set%';,确保character_set_server、collation_server都是utf8mb4;对library_db库执行ALTER DATABASE library_db CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;;对每张表执行ALTER TABLE table_name CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;。 |
| “库存查询”搜索书名,明明数据库里有《Java核心技术》,却查不到结果 | SQL查询语句用了LIKE '%Java%',但数据库字段类型是VARCHAR且有索引,而%在开头会导致索引失效,大数据量时极慢 | 本系统数据量小,不影响。但要知道:生产环境应避免LIKE '%xxx',可改用全文索引或Elasticsearch。 |
借书成功,但“库存查询”里这本书的stock没变少 | bookDao.updateStock()方法里的SQL写错了,比如UPDATE book SET stock = stock - 1 WHERE id = ?,但表字段名是book_id不是id | 打开BookDao.java,检查updateStock()方法的SQL字符串。用MySQL客户端手动执行一遍,看是否成功。 |
5.3 日志与调试:如何从海量日志中快速定位问题
log/app.log文件可能很大,学会高效检索是必备技能。
- 按时间筛选:用文本编辑器(如Notepad++)的“查找”功能,搜索
2024-06-12 14:,快速定位当天下午的操作。 - 按级别筛选:搜索
[ERROR]或[WARN],优先处理这些高亮问题。 - 按关键词筛选:比如搜索
borrowBook,就能看到所有借书操作的日志,对比成功的和失败的,找出差异点。 - 关联ID追踪:日志里提到的
借阅记录ID=107,直接在MySQL里执行SELECT * FROM borrow_record WHERE record_id = 107;,查看这条记录的完整状态。
个人经验:我在调试一个“归还后库存没加1”的bug时,日志里只有一条
[INFO] 图书《代码大全》已归还,毫无异常。后来我意识到,问题可能出在updateStock()的SQL里。于是我在BookDao.updateStock()方法里加了一句logger.debug("执行SQL: {}, 参数: {}", sql, delta);,重新运行,日志里就出现了执行SQL: UPDATE book SET stock = stock + ? WHERE book_id = ?, 参数: 1, 101。我复制这条SQL到MySQL客户端执行,发现报错Unknown column 'book_id' in 'where clause'——原来表字段名是id!这就是日志加调试语句的价值:把隐式逻辑显性化。
6. 总结与延伸:从课程设计到工程能力的跃迁
这个图书馆系统,表面看只是一个Java+JDBC的小项目,但它承载的,是软件工程最基础也最重要的几块基石:需求转化能力、数据库设计思维、分层架构意识、事务安全实践、以及调试排错的耐心。 当你第一次看着自己修改的SQL成功建出4张表,第一次在debug模式下单步执行看到stock字段真的从5变成4,第一次在日志里找到那个导致闪退的NullPointerException并修复它——那一刻,你获得的不是一份课程设计分数,而是一种确信:代码不是魔法,它是可理解、可控制、可预测的工具。
它后续的延伸方向非常清晰:
- Web化:把Swing界面换成Thymeleaf模板,用Spring Boot暴露REST API,前端用Vue写一个漂亮的网页版。这时你会发现,BorrowService.borrowBook()方法几乎不用改,只是调用方从JButton的ActionListener变成了@PostMapping的Controller。
- 权限细化:当前只有管理员和读者两级。可以增加“图书管理员”角色,只能管理图书,不能删除读者;增加“系统审计员”,只能查看日志,不能修改数据。这会迫使你学习RBAC(基于角色的访问控制)模型。
- 数据可视化:用JFreeChart或ECharts,在管理后台增加“月度借阅排行榜”、“各分类借阅量饼图”。这会带你接触数据聚合SQL(GROUP BY、COUNT)和图表渲染。
但所有这些延伸,都应该建立在你已经彻底吃透当前Swing版的基础上。就像学游泳,必须先在浅水区扑腾够了,才能挑战深水区。这个系统,就是为你准备的那个最合适的浅水区——水深刚好没过膝盖,水流平缓,四周有扶手,而教练(也就是这套文档)就站在池边,随时告诉你下一步该怎么做。
最后分享一个小技巧:每次成功运行后,别急着关程序。打开log/目录,用记事本打开最新的app.log,从头到尾读一遍。那些[INFO]日志,就是你亲手指挥Java和MySQL协同工作的战报。读着读着,你会突然发现,那些曾经陌生的术语——连接池、事务隔离、外键约束、字符集——不再是课本上的铅字,而是你指尖下流淌的真实数据流。这种“知行合一”的感觉,才是编程最迷人的地方。
简介:用Java开发的轻量级图书馆管理系统,基于JDBC直连数据库,不依赖Tomcat等容器,下载解压就能跑。包含图书录入、借阅/归还操作、读者信息维护、库存实时查询等核心功能。资源里有编译好的dbms-1.0.0.jar,双击即可启动图形界面;配套jdbc.properties文件方便改数据库地址和账号;SQL目录下提供建表语句和初始测试数据,支持MySQL及其它兼容JDBC的数据库;Maven结构清晰(dbms-master),代码全注释,表设计符合第三范式,涵盖图书、读者、借阅记录三张主表及关联逻辑;附带介绍.txt说明运行步骤,log目录自动记录操作日志,jar目录存好所有依赖包,适合学生做数据库或Java课程设计交作业、课堂演示或自己练手二次开发。

869

被折叠的 条评论
为什么被折叠?



