Java写的图书馆管理小系统:带数据库脚本、可直接运行的JAR包和完整工程

该文章已生成可运行项目,

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:用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下,按controllerservicedaoentity分层,每个类开头都有中文注释说明职责,每个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如何确保ResultSetStatement必然关闭。“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_recordUPDATE 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_idisbn是候选键,但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=5address字段,所有引用它的图书记录自动生效。这就是第三范式解决“更新异常”的实际价值。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 TABLEINSERT语句调用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_idpublisher_id必须指向已存在的记录,所以必须先插categorypublisher,再插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关联表实现多作者):点击“添加作者”按钮,弹出子窗口录入作者姓名和出生年份,保存后自动关联;
- 分类和出版社(下拉选择):数据来自categorypublisher表,确保引用完整性;
- 库存数量(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字段被手动设为LOSTLOST

这个逻辑不在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语句从INSERTUPDATE 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.propertiessql/log/这些文件夹都在根目录下。
2. 配置数据库连接:用记事本打开jdbc.properties,修改usernamepassword为你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;,你应该能看到authorbookborrow_record等表名。
6. 登录体验:使用默认账号密码登录。管理员账号:admin/123456;读者账号:2023001/123456(学号即账号)。登录后,你就可以在各个功能模块里真实操作了。

注意:如果初始化失败,第一步看log/app.log末尾的[ERROR]日志。最常见的原因是MySQL服务没启动,或jdbc.properties里密码错了。不要反复点击“初始化”,先解决根本问题。

4.2 代码调试:在IDEA里读懂每一行JDBC

想真正掌握,必须动手调试。以下是在IntelliJ IDEA中导入并调试项目的完整流程:

  1. 导入Maven工程:打开IDEA → “Open” → 选择解压后的dbms-master文件夹 → 选择“Open as Project” → 等待Maven自动下载依赖(主要是mysql-connector-java)。
  2. 配置运行参数:点击右上角“Add Configuration” → “+” → “Application” → 名称填DbmsMain → Main class填com.example.dbms.controller.MainForm(这是程序入口) → Working directory填项目根目录(如D:\dbms-master) → 点击“OK”。
  3. 打断点调试:在BorrowService.borrowBook()方法的第一行打个断点(红色圆点)。然后点击绿色三角形“Debug”启动。程序会在登录后停在断点处。
  4. 观察变量:当执行到Book book = bookDao.findById(bookId);时,鼠标悬停在book变量上,IDEA会显示它的所有字段值:bookId=101, title="Java编程思想", stock=5。接着F8单步执行,看borrowDao.insert(record)后,数据库里是否真的多了一条记录(可同时打开MySQL客户端刷新borrow_record表)。
  5. 模拟异常:为了理解事务回滚,可以临时修改代码,在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

  1. 更新Entity:打开src/main/java/com/example/dbms/entity/BorrowRecord.java,添加字段:
    java private Date dueDate; // 应还日期 // getter/setter...

  2. 更新DAO:在BorrowDao.java里,修改insert()方法,插入时设置due_date = DATE_ADD(NOW(), INTERVAL 30 DAY);新增updateDueDate(int recordId, Date newDueDate)方法。

  3. 编写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);
    }
    }
    ```

  4. 修改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 RuntimeJDK版本不匹配:jar包用JDK 11编译,但你用JDK 8运行查看java -version,确保运行时JDK版本≥编译时版本。本系统编译目标为1.8,所以JDK 8、11、17均可运行。
程序启动,登录界面弹出,但点击“登录”后卡住,状态栏显示“连接数据库中…”数据库服务未启动,或jdbc.properties配置错误检查MySQL服务状态;用MySQL客户端尝试连接localhost:3306,确认账号密码正确;检查jdbc.propertiesurl的端口号是否正确(默认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_servercollation_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()方法几乎不用改,只是调用方从JButtonActionListener变成了@PostMapping的Controller。
- 权限细化:当前只有管理员和读者两级。可以增加“图书管理员”角色,只能管理图书,不能删除读者;增加“系统审计员”,只能查看日志,不能修改数据。这会迫使你学习RBAC(基于角色的访问控制)模型。
- 数据可视化:用JFreeChart或ECharts,在管理后台增加“月度借阅排行榜”、“各分类借阅量饼图”。这会带你接触数据聚合SQL(GROUP BYCOUNT)和图表渲染。

但所有这些延伸,都应该建立在你已经彻底吃透当前Swing版的基础上。就像学游泳,必须先在浅水区扑腾够了,才能挑战深水区。这个系统,就是为你准备的那个最合适的浅水区——水深刚好没过膝盖,水流平缓,四周有扶手,而教练(也就是这套文档)就站在池边,随时告诉你下一步该怎么做。

最后分享一个小技巧:每次成功运行后,别急着关程序。打开log/目录,用记事本打开最新的app.log,从头到尾读一遍。那些[INFO]日志,就是你亲手指挥Java和MySQL协同工作的战报。读着读着,你会突然发现,那些曾经陌生的术语——连接池、事务隔离、外键约束、字符集——不再是课本上的铅字,而是你指尖下流淌的真实数据流。这种“知行合一”的感觉,才是编程最迷人的地方。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:用Java开发的轻量级图书馆管理系统,基于JDBC直连数据库,不依赖Tomcat等容器,下载解压就能跑。包含图书录入、借阅/归还操作、读者信息维护、库存实时查询等核心功能。资源里有编译好的dbms-1.0.0.jar,双击即可启动图形界面;配套jdbc.properties文件方便改数据库地址和账号;SQL目录下提供建表语句和初始测试数据,支持MySQL及其它兼容JDBC的数据库;Maven结构清晰(dbms-master),代码全注释,表设计符合第三范式,涵盖图书、读者、借阅记录三张主表及关联逻辑;附带介绍.txt说明运行步骤,log目录自动记录操作日志,jar目录存好所有依赖包,适合学生做数据库或Java课程设计交作业、课堂演示或自己练手二次开发。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

本文章已经生成可运行项目
内容概要:本文围绕可变桨叶四旋翼无人机的规范控制与点对点运动模拟展开,重点研究优化推力分配策略在翻转动作中的应用与性能比较。通过Matlab代码实现,构了四旋翼动力学模型,并设计了多种控制算法以实现精确的姿态调整与轨迹跟踪。研究对比了不同推力分配方案在执行高机动性翻转动作时的稳定性、能耗效率与响应速度,旨在提升无人机在复杂飞行任务中的动态性能与控制精度。该仿真研究为无人机飞控系统的设计与优化提供了理论依据技术支持。; 适合人群:具备一定自动控制理论基础Matlab编程能力,从事无人机控制、飞行器动力学或机器人系统研究的科研人员及研究生。; 使用场景及目标:① 实现四旋翼无人机在三维空间中的精确点对点运动控制;② 对比分析不同推力分配策略在执行翻转等高难度动作时的控制效果与能耗现,优化飞行性能;③ 为无人机自主飞行、特技飞行及复杂环境下的机动控制提供算法验证平台。; 阅读议:此资源以Matlab仿真为核心,议读者结合相关控制理论知识,深入理解代码实现细节,重点关注动力学模、控制律设计与推力分配模块。在学习过程中,应动手调试参数,复现文中翻转动作的仿真结果,并尝试拓展至其他复杂飞行任务,以加深对无人机控制机理的理解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值