第三阶段:项目实战①小型项目开发(中篇)

第三阶段:项目实战①小型项目开发(中篇)


📋 中篇概述

欢迎来到项目实战的中篇!在上篇中,我们完成了学生管理系统的控制台版本,掌握了Spring Boot + JPA的基础开发技能。

在中篇中,我们将开发一个更加复杂和实用的项目——图书管理系统(Swing GUI版)。这个项目将带您进入桌面应用程序开发的世界,学习如何创建用户友好的图形界面。

🎯 中篇学习目标

⏱️ 预计学习时间:8-12小时

  • 📖 理论学习:3-4小时
  • 💻 代码实践:4-6小时
  • 🧪 测试调试:1-2小时

完成中篇学习后,您将能够:

  • GUI编程技能:掌握Java Swing组件的使用和布局管理
  • 事件驱动编程:理解GUI应用的事件处理机制
  • MVC架构:在GUI应用中实现Model-View-Controller模式
  • 用户体验设计:创建直观、易用的用户界面
  • 数据绑定:实现GUI组件与数据模型的双向绑定
  • 异常处理:在GUI环境中进行优雅的错误处理

📈 学习路径图

第一阶段:环境准备 (30分钟)
    ↓
第二阶段:GUI理论基础 (3-4小时)
├── GUI编程概念
├── Swing框架特点
├── MVC架构理解
├── 组件使用方法
├── 布局管理器
├── 事件处理机制
└── 最佳实践
    ↓
第三阶段:项目实战 (4-6小时)
├── 实体类设计
├── 数据访问层
├── 业务逻辑层
├── GUI界面实现
├── 事件处理
└── 对话框设计
    ↓
第四阶段:测试验证 (1-2小时)
├── 单元测试
├── 集成测试
├── 功能验证
└── 性能优化
    ↓
第五阶段:总结提升
├── 学习检查
├── 进阶建议
└── 项目扩展

🚀 项目二:图书管理系统(Swing GUI版)

项目概述

开发一个基于Swing GUI的图书管理系统,提供完整的图书信息管理功能。相比控制台版本,GUI版本提供了更加直观和用户友好的操作界面。

📸 效果预览

以下是图书管理系统GUI版的运行效果图:

1. 主界面 - 现代化的Swing GUI

在这里插入图片描述

现代化的Swing界面,包含菜单栏、工具栏、数据表格和状态栏,采用FlatLaf主题提供优雅的视觉效果

2. 编辑图书对话框 - 用户友好的数据录入

在这里插入图片描述

模态对话框设计,提供完整的图书信息编辑功能,包含数据验证和用户提示

3. 统计信息功能 - 数据可视化展示

在这里插入图片描述

详细的统计信息展示,包括图书总数、库存统计、价格分析等,帮助用户了解数据概况

技术要求

  • 核心框架:Spring Boot 3.2+(禁用Web功能)
  • GUI框架:Java Swing
  • 数据持久化:Spring Data JPA、Hibernate 6.x
  • 数据库:MySQL 8.0+
  • Java版本:JDK 17+
  • 工具包:Hutool 5.8+(字符串处理、日期操作)
  • 构建工具:Maven
  • 设计模式:MVC模式、观察者模式

功能需求

核心功能
  1. 图书信息管理

    • 添加图书信息(ISBN、书名、作者、出版社、价格、库存等)
    • 查看图书列表(表格显示,支持排序)
    • 修改图书信息(双击编辑或编辑按钮)
    • 删除图书信息(确认对话框)
    • 搜索图书(按书名、作者、ISBN等条件)
  2. 用户界面特性

    • 主窗口:菜单栏、工具栏、表格、状态栏
    • 编辑对话框:模态对话框进行图书信息编辑
    • 搜索功能:实时搜索或按钮搜索
    • 统计功能:分类统计、库存预警等
  3. 数据持久化

    • 使用Spring Data JPA进行数据持久化
    • 支持复杂查询和统计功能
    • 事务管理和数据一致性

环境准备

Maven依赖配置

pom.xml中添加以下配置:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>
    
    <groupId>org.chapter09</groupId>
    <artifactId>chapter09_02</artifactId>
    <version>1.0.0</version>
    <name>Chapter09_02 - Book Management GUI</name>
    <description>图书管理系统(GUI版)</description>
    
    <properties>
        <java.version>17</java.version>
    </properties>
    
    <dependencies>
        <!-- Spring Boot Data JPA Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        
        <!-- MySQL驱动 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!-- Spring Boot Test Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- Spring Boot Validation Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!-- H2数据库用于开发和测试 -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- Hutool工具包 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.25</version>
        </dependency>

        <!-- FlatLaf - 现代化的Swing外观 -->
        <dependency>
            <groupId>com.formdev</groupId>
            <artifactId>flatlaf</artifactId>
            <version>3.2.5</version>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>

            <!-- JaCoCo测试覆盖率插件 -->
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.8.8</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>prepare-agent</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>report</id>
                        <phase>test</phase>
                        <goals>
                            <goal>report</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
应用配置

创建src/main/resources/application.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:${MYSQL_PORT}/book_management?useSSL=false&serverTimezone=UTC&characterEncoding=utf8
    username: root
    password: ${MYSQL_PASSWORD}   # 请修改为您的实际密码
    driver-class-name: com.mysql.cj.jdbc.Driver

  jpa:
    hibernate:
      ddl-auto: update  # 自动创建/更新表结构
    show-sql: false     # 生产环境关闭SQL显示
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQLDialect
        format_sql: false

  # 应用配置
  application:
    name: book-management-gui

  # 禁用Web功能,这是一个桌面应用
  main:
    web-application-type: none

# 日志配置
logging:
  level:
    org.chapter09: INFO
    org.hibernate.SQL: WARN

数据库准备

-- 创建数据库
CREATE DATABASE book_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

📖 Java Swing GUI编程基础

在开始实现图书管理系统之前,让我们先系统地学习Java Swing GUI编程的基础知识。这将帮助初学者更好地理解后续的代码实现。

🎯 GUI编程学习目标

通过本章节的学习,您将能够:

  1. 理解GUI编程的基本概念和原理
  2. 掌握Swing组件的使用方法
  3. 学会设计用户友好的界面
  4. 能够独立开发简单的GUI应用程序

🔍 什么是GUI编程?

GUI(Graphical User Interface)编程是创建图形用户界面的编程方式,与命令行界面不同,它提供了:

  • 可视化界面:用户通过图形元素(按钮、文本框、菜单等)与程序交互
  • 事件驱动:程序响应用户的操作(点击、输入、选择等)
  • 直观操作:用户无需记忆命令,通过鼠标和键盘直接操作

💡 效果预览:您可以参考本文开头的效果预览图,了解我们将要创建的现代化GUI应用程序的最终效果。从主界面的专业布局到编辑对话框的用户友好设计,都体现了优秀GUI应用的特点。

🏗️ Swing框架特点

Swing是Java提供的图形用户界面(GUI)工具包,用于创建桌面应用程序。它具有以下特点:

1. 轻量级组件
// 🔍 学习要点:Swing组件完全用Java编写,不依赖本地系统组件
JButton button = new JButton("点击我");  // 轻量级按钮
JLabel label = new JLabel("标签文本");   // 轻量级标签
JTextField field = new JTextField();    // 轻量级文本框
2. 可插拔外观感觉(Look and Feel)
// 🔍 学习要点:可以动态改变应用程序的外观
try {
    // 设置系统默认外观
    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeel());
    // 或者设置特定的外观主题
    // UIManager.setLookAndFeel("com.formdev.flatlaf.FlatLightLaf");

    // 更新所有组件的外观
    SwingUtilities.updateComponentTreeUI(frame);
} catch (Exception e) {
    e.printStackTrace();
}
3. MVC架构支持
// 🔍 学习要点:Swing组件采用MVC架构设计
// 以JTable为例:
JTable table = new JTable();                    // View(视图)
DefaultTableModel model = new DefaultTableModel(); // Model(模型)
// Controller由Swing内部的UI代理处理
table.setModel(model); // 将模型绑定到视图

🧩 Swing组件层次结构

理解Swing的组件层次结构对于GUI编程至关重要:

Container(容器)
├── Window
│   ├── Frame
│   │   └── JFrame ⭐ 主窗口
│   └── Dialog
│       └── JDialog ⭐ 对话框
├── Panel
│   └── JPanel ⭐ 面板容器
└── Applet
    └── JApplet

Component(组件)
├── JButton ⭐ 按钮
├── JLabel ⭐ 标签
├── JTextField ⭐ 文本框
├── JTable ⭐ 表格
├── JList ⭐ 列表
└── JTree ⭐ 树形组件

🔄 事件驱动编程模型

GUI应用程序基于事件驱动模型工作:

// 🔍 学习要点:事件处理流程
// 事件源 → 事件对象 → 事件监听器 → 事件处理方法

JButton button = new JButton("点击");

// 传统方式:匿名内部类
button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("按钮被点击了!");
    }
});

// 现代方式:Lambda表达式
button.addActionListener(e -> System.out.println("按钮被点击了!"));

// 方法引用方式
button.addActionListener(this::handleButtonClick);

📊 MVC架构在GUI应用中的体现

在图书管理系统中,MVC模式的具体应用:

1. Model层(模型)
// 🔍 实体类 - 数据模型
@Entity
public class Book {
    private String isbn;
    private String title;
    private String author;
    // ... 其他属性和方法
}

// 🔍 业务逻辑 - 服务层
@Service
public class BookService {
    public List<Book> findAll() { /* 业务逻辑 */ }
    public void save(Book book) { /* 业务逻辑 */ }
    public void delete(String isbn) { /* 业务逻辑 */ }
}

// 🔍 数据访问 - 仓储层
@Repository
public interface BookRepository extends JpaRepository<Book, String> {
    List<Book> findByTitleContaining(String title);
}
2. View层(视图)
// 🔍 GUI组件 - 视图层
@Component
public class MainFrame extends JFrame {
    private JTable bookTable;           // 数据展示组件
    private DefaultTableModel tableModel; // 表格数据模型

    // 🔍 显示数据的方法
    private void displayBooks(List<Book> books) {
        tableModel.setRowCount(0); // 清空现有数据
        for (Book book : books) {
            Object[] row = {
                book.getIsbn(), book.getTitle(), book.getAuthor()
            };
            tableModel.addRow(row); // 添加新行数据
        }
    }
}
3. Controller层(控制器)
// 🔍 事件处理 - 控制器逻辑
public class MainFrame extends JFrame {
    @Autowired
    private BookService bookService; // 注入Model层服务

    // 🔍 控制器方法 - 处理用户操作
    private void addBook() {
        // 1. 获取用户输入(View层)
        String title = titleField.getText();
        String author = authorField.getText();

        // 2. 创建数据模型(Model层)
        Book book = new Book();
        book.setTitle(title);
        book.setAuthor(author);

        // 3. 调用业务逻辑(Model层)
        try {
            bookService.save(book);
            // 4. 更新视图(View层)
            loadBookData();
            showMessage("图书添加成功!");
        } catch (Exception e) {
            showError("添加失败:" + e.getMessage());
        }
    }
}

🎨 核心组件学习路径

第一阶段:容器组件
1. JFrame - 主窗口
// 🔍 学习要点:JFrame是顶级容器,所有其他组件的根容器
public class MyFrame extends JFrame {
    public MyFrame() {
        // 基本设置
        setTitle("我的应用程序");              // 设置窗口标题
        setSize(800, 600);                   // 设置窗口大小
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 设置关闭操作
        setLocationRelativeTo(null);         // 窗口居中显示

        // 设置最小尺寸
        setMinimumSize(new Dimension(600, 400));

        // 设置窗口图标(可选)
        // setIconImage(Toolkit.getDefaultToolkit().getImage("icon.png"));
    }
}
2. JPanel - 面板容器
// 🔍 学习要点:JPanel是中间容器,用于组织其他组件
JPanel mainPanel = new JPanel(new BorderLayout());
mainPanel.setBorder(BorderFactory.createTitledBorder("主面板"));

// 创建子面板
JPanel buttonPanel = new JPanel(new FlowLayout());
buttonPanel.add(new JButton("确定"));
buttonPanel.add(new JButton("取消"));

// 将子面板添加到主面板
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
第二阶段:基础组件
1. JLabel - 标签
// 🔍 学习要点:显示只读文本或图标
JLabel titleLabel = new JLabel("图书标题:");
titleLabel.setFont(new Font("微软雅黑", Font.PLAIN, 12));
titleLabel.setHorizontalAlignment(SwingConstants.CENTER);

// HTML格式标签
JLabel htmlLabel = new JLabel("<html><b>粗体</b> <i>斜体</i> <font color='red'>红色</font></html>");

// 带图标的标签
JLabel iconLabel = new JLabel("状态", icon, SwingConstants.LEFT);
2. JTextField - 文本输入框
// 🔍 学习要点:单行文本输入组件
JTextField nameField = new JTextField(20); // 20列宽度
nameField.setToolTipText("请输入图书名称");

// 添加文档监听器(实时监听文本变化)
nameField.getDocument().addDocumentListener(new DocumentListener() {
    @Override
    public void insertUpdate(DocumentEvent e) { validateInput(); }
    @Override
    public void removeUpdate(DocumentEvent e) { validateInput(); }
    @Override
    public void changedUpdate(DocumentEvent e) { validateInput(); }
});

// 添加动作监听器(回车键)
nameField.addActionListener(e -> searchBooks());
3. JButton - 按钮
// 🔍 学习要点:用户操作触发器
JButton saveButton = new JButton("保存");

// 设置图标
saveButton.setIcon(new ImageIcon("save.png"));

// 设置快捷键
saveButton.setMnemonic('S'); // Alt+S

// 设置工具提示
saveButton.setToolTipText("保存当前图书信息");

// 添加事件监听器
saveButton.addActionListener(e -> saveBook());

// 设置按钮样式
saveButton.setPreferredSize(new Dimension(100, 30));
saveButton.setFont(new Font("微软雅黑", Font.PLAIN, 12));
第三阶段:高级组件
1. JTable - 表格
// 🔍 学习要点:表格用于显示结构化数据
String[] columnNames = {"ID", "书名", "作者", "价格"};

// 创建只读表格模型
DefaultTableModel model = new DefaultTableModel(columnNames, 0) {
    @Override
    public boolean isCellEditable(int row, int column) {
        return false; // 设置为只读
    }
};

// 创建表格
JTable table = new JTable(model);

// 设置表格属性
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // 单选
table.setRowHeight(25); // 行高
table.getTableHeader().setReorderingAllowed(false); // 禁止列重排

// 设置列宽
table.getColumnModel().getColumn(0).setPreferredWidth(50);  // ID列
table.getColumnModel().getColumn(1).setPreferredWidth(200); // 书名列

// 添加选择监听器
table.getSelectionModel().addListSelectionListener(e -> {
    if (!e.getValueIsAdjusting()) {
        int selectedRow = table.getSelectedRow();
        if (selectedRow >= 0) {
            // 处理行选择事件
            handleRowSelection(selectedRow);
        }
    }
});

🎨 布局管理器选择指南

布局管理器负责自动安排容器中组件的位置和大小,使界面能够适应不同的窗口尺寸。

1. BorderLayout - 边界布局

🔍 适用场景:主窗口布局,需要明确的区域划分

// 🔍 学习要点:BorderLayout将容器分为5个区域
JFrame frame = new JFrame();
frame.setLayout(new BorderLayout());

// 五个区域的使用
frame.add(menuBar, BorderLayout.NORTH);      // 菜单栏(顶部)
frame.add(toolBar, BorderLayout.NORTH);      // 工具栏(顶部)
frame.add(mainPanel, BorderLayout.CENTER);   // 主内容区(中央)
frame.add(statusBar, BorderLayout.SOUTH);    // 状态栏(底部)
frame.add(sidePanel, BorderLayout.WEST);     // 侧边栏(左侧)
// frame.add(rightPanel, BorderLayout.EAST); // 右侧面板(右侧)

特点

  • 五个区域:NORTH、SOUTH、EAST、WEST、CENTER
  • CENTER区域会填充剩余空间
  • 边缘区域根据组件首选大小调整
2. FlowLayout - 流式布局

🔍 适用场景:按钮组、工具栏、简单的水平排列

// 🔍 学习要点:FlowLayout从左到右排列组件
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 5));
// 参数:对齐方式、水平间距、垂直间距

buttonPanel.add(new JButton("确定"));
buttonPanel.add(new JButton("取消"));
buttonPanel.add(new JButton("帮助"));

特点

  • 从左到右排列组件
  • 空间不足时自动换行
  • 支持LEFT、CENTER、RIGHT对齐
3. GridLayout - 网格布局

🔍 适用场景:计算器界面、规整的表单布局

// 🔍 学习要点:GridLayout创建规整的网格
JPanel gridPanel = new JPanel(new GridLayout(3, 2, 5, 5));
// 参数:行数、列数、水平间距、垂直间距

gridPanel.add(new JLabel("姓名:"));
gridPanel.add(new JTextField());
gridPanel.add(new JLabel("年龄:"));
gridPanel.add(new JTextField());
gridPanel.add(new JLabel("邮箱:"));
gridPanel.add(new JTextField());

特点

  • 所有组件大小相同
  • 严格的网格排列
  • 适合规整的界面
4. GridBagLayout - 网格包布局

🔍 适用场景:复杂的表单布局,需要精确控制

// 🔍 学习要点:GridBagLayout提供最灵活的布局控制
JPanel panel = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();

// 设置约束条件
gbc.insets = new Insets(5, 5, 5, 5); // 边距
gbc.anchor = GridBagConstraints.WEST; // 对齐方式

// 添加标签
gbc.gridx = 0; gbc.gridy = 0;
panel.add(new JLabel("ISBN:"), gbc);

// 添加文本框
gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weightx = 1.0;
panel.add(new JTextField(20), gbc);

⚡ 事件处理详细说明

1. 事件处理流程
用户操作 → 事件对象 → 事件监听器 → 事件处理方法 → 业务逻辑
2. 常用事件类型
ActionListener - 动作事件
// 🔍 学习要点:处理按钮点击、菜单选择、回车键等
JButton button = new JButton("保存");

// 方式1:Lambda表达式(推荐)
button.addActionListener(e -> {
    System.out.println("按钮被点击了!");
    // 获取事件源
    JButton source = (JButton) e.getSource();
    System.out.println("事件源:" + source.getText());
});

// 方式2:方法引用
button.addActionListener(this::handleSaveAction);

// 方式3:传统匿名内部类
button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        handleSaveAction(e);
    }
});
MouseListener - 鼠标事件
// 🔍 学习要点:处理鼠标点击、进入、离开等事件
component.addMouseListener(new MouseAdapter() {
    @Override
    public void mouseClicked(MouseEvent e) {
        if (e.getClickCount() == 2) {
            System.out.println("双击事件");
            editSelectedItem();
        }
    }

    @Override
    public void mouseEntered(MouseEvent e) {
        // 鼠标进入时改变样式
        component.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
    }

    @Override
    public void mouseExited(MouseEvent e) {
        // 鼠标离开时恢复默认样式
        component.setCursor(Cursor.getDefaultCursor());
    }
});
DocumentListener - 文档变化事件
// 🔍 学习要点:实时监听文本框内容变化
textField.getDocument().addDocumentListener(new DocumentListener() {
    @Override
    public void insertUpdate(DocumentEvent e) {
        textChanged(); // 文本插入时触发
    }

    @Override
    public void removeUpdate(DocumentEvent e) {
        textChanged(); // 文本删除时触发
    }

    @Override
    public void changedUpdate(DocumentEvent e) {
        textChanged(); // 文本属性变化时触发
    }

    private void textChanged() {
        String text = textField.getText();
        // 实时验证输入
        validateInput(text);
        // 实时搜索
        performSearch(text);
    }
});

🎯 GUI设计最佳实践

1. 代码组织结构
// 🔍 学习要点:清晰的代码组织结构
public class MainFrame extends JFrame {

    // 1. 组件声明
    private JTable bookTable;
    private JTextField searchField;
    private JLabel statusLabel;

    // 2. 构造方法
    public MainFrame() {
        initializeComponents();
    }

    // 3. 初始化方法
    private void initializeComponents() {
        setupWindow();      // 窗口设置
        createComponents(); // 创建组件
        setupLayout();      // 布局设置
        setupEventHandlers(); // 事件处理
        loadInitialData();  // 加载数据
    }

    // 4. 组件创建方法
    private void createComponents() {
        bookTable = new JTable();
        searchField = new JTextField(20);
        statusLabel = new JLabel("就绪");
    }

    // 5. 布局设置方法
    private void setupLayout() {
        setLayout(new BorderLayout());
        add(createToolBar(), BorderLayout.NORTH);
        add(createMainPanel(), BorderLayout.CENTER);
        add(createStatusBar(), BorderLayout.SOUTH);
    }

    // 6. 事件处理方法
    private void setupEventHandlers() {
        // 集中设置所有事件处理器
    }
}
2. 线程安全编程
// 🔍 学习要点:Swing的单线程模型
// ✅ 正确:在EDT线程中操作GUI
SwingUtilities.invokeLater(() -> {
    updateTableData();
    statusLabel.setText("数据更新完成");
});

// ❌ 错误:在后台线程中直接操作GUI
new Thread(() -> {
    // 这可能导致线程安全问题
    table.setModel(newModel);
}).start();

// ✅ 正确:使用SwingWorker处理耗时操作
SwingWorker<List<Book>, Void> worker = new SwingWorker<List<Book>, Void>() {
    @Override
    protected List<Book> doInBackground() throws Exception {
        // 在后台线程中执行耗时操作
        return bookService.getAllBooks();
    }

    @Override
    protected void done() {
        try {
            // 在EDT线程中更新GUI
            List<Book> books = get();
            updateTable(books);
        } catch (Exception e) {
            showErrorMessage("加载数据失败:" + e.getMessage());
        }
    }
};
worker.execute();
3. 异常处理和用户反馈
// 🔍 学习要点:优雅的错误处理
private void saveBook() {
    try {
        // 输入验证
        if (!validateInput()) {
            return;
        }

        // 执行保存操作
        Book book = createBookFromInput();
        bookService.save(book);

        // 成功反馈
        showSuccessMessage("图书保存成功!");
        refreshTable();
        clearForm();

    } catch (Exception e) {
        // 错误处理
        showErrorMessage("保存失败:" + e.getMessage());
        logger.error("保存图书失败", e);
    }
}

private boolean validateInput() {
    if (titleField.getText().trim().isEmpty()) {
        showWarningMessage("请输入图书标题");
        titleField.requestFocus();
        return false;
    }

    if (authorField.getText().trim().isEmpty()) {
        showWarningMessage("请输入作者姓名");
        authorField.requestFocus();
        return false;
    }

    return true;
}

private void showSuccessMessage(String message) {
    JOptionPane.showMessageDialog(this, message, "成功", JOptionPane.INFORMATION_MESSAGE);
}

private void showErrorMessage(String message) {
    JOptionPane.showMessageDialog(this, message, "错误", JOptionPane.ERROR_MESSAGE);
}

private void showWarningMessage(String message) {
    JOptionPane.showMessageDialog(this, message, "警告", JOptionPane.WARNING_MESSAGE);
}

🔧 调试GUI应用的技巧

1. 组件状态检查
// 🔍 学习要点:调试GUI组件状态
private void debugComponentState() {
    System.out.println("=== 组件状态调试 ===");
    System.out.println("表格行数:" + bookTable.getRowCount());
    System.out.println("选中行:" + bookTable.getSelectedRow());
    System.out.println("搜索框内容:'" + searchField.getText() + "'");
    System.out.println("窗口大小:" + getSize());
    System.out.println("窗口可见:" + isVisible());
}
2. 事件调试
// 🔍 学习要点:调试事件处理
button.addActionListener(e -> {
    System.out.println("=== 按钮点击事件调试 ===");
    System.out.println("事件源:" + e.getSource());
    System.out.println("动作命令:" + e.getActionCommand());
    System.out.println("修饰键:" + e.getModifiers());
    System.out.println("时间戳:" + e.getWhen());

    // 执行实际逻辑
    handleButtonClick();
});

实现步骤

第一步:主启动类
package org.chapter09.bookmanagement;

import com.formdev.flatlaf.FlatLightLaf;
import org.chapter09.bookmanagement.gui.MainFrame;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import javax.swing.*;

/**
 * 图书管理系统主启动类(GUI版)
 * <p>
 * 基于Spring Boot 3.x的GUI桌面应用程序,实现完整的图书信息管理功能。
 * 该系统采用现代化的技术栈,结合Swing GUI和Spring Boot框架。
 *
 * <h3>🔍 学习要点:</h3>
 * <ul>
 *   <li><strong>Spring Boot GUI应用</strong> - 理解桌面应用的启动流程</li>
 *   <li><strong>FlatLaf外观</strong> - 掌握现代化Swing外观的使用</li>
 *   <li><strong>异常处理</strong> - 学习GUI应用的错误处理</li>
 *   <li><strong>线程安全</strong> - 理解EDT线程的重要性</li>
 * </ul>
 */
@SpringBootApplication
public class BookManagementGuiApplication {

    /**
     * 应用程序主入口方法
     * <p>
     * 启动Spring Boot应用程序并初始化Swing GUI界面。
     *
     * <h3>🔍 启动流程:</h3>
     * <ol>
     *   <li><strong>设置外观主题</strong> - 使用FlatLaf提升界面美观度</li>
     *   <li><strong>启动Spring容器</strong> - 初始化Spring应用上下文</li>
     *   <li><strong>创建GUI界面</strong> - 在EDT线程中创建主窗口</li>
     *   <li><strong>异常处理</strong> - 优雅处理启动过程中的异常</li>
     * </ol>
     */
    public static void main(String[] args) {
        try {
            // 🎨 设置现代化的外观和感觉
            setupLookAndFeel();

            // 🚀 启动Spring Boot应用
            System.out.println("正在启动图书管理系统...");
            ConfigurableApplicationContext context = SpringApplication.run(BookManagementGuiApplication.class, args);

            // 🖥️ 获取主窗口组件并显示
            try {
                // 从Spring容器中获取主窗口组件
                MainFrame mainFrame = context.getBean(MainFrame.class);

                // 在EDT线程中显示GUI
                SwingUtilities.invokeLater(() -> {
                    mainFrame.setVisible(true);
                    System.out.println("✅ 图书管理系统启动成功!");
                });

            } catch (Exception e) {
                System.err.println("❌ GUI界面创建失败:" + e.getMessage());
                e.printStackTrace();
                // 关闭Spring容器
                context.close();
                System.exit(1);
            }

        } catch (Exception e) {
            System.err.println("❌ 应用程序启动失败:" + (e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()));
            e.printStackTrace();
            System.exit(1);
        }
    }

    /**
     * 设置应用程序的外观和感觉
     * <p>
     * 使用FlatLaf库提供现代化的Swing外观,提升用户体验。
     *
     * <h3>🔍 FlatLaf特点:</h3>
     * <ul>
     *   <li><strong>现代化设计</strong>:扁平化设计风格</li>
     *   <li><strong>高DPI支持</strong>:适配高分辨率显示器</li>
     *   <li><strong>主题支持</strong>:支持亮色和暗色主题</li>
     *   <li><strong>更好的字体</strong>:优化的字体渲染</li>
     * </ul>
     */
    private static void setupLookAndFeel() {
        try {
            // 设置FlatLaf亮色主题
            UIManager.setLookAndFeel(new FlatLightLaf());

            // 设置一些UI属性以优化显示效果
            UIManager.put("Button.arc", 8);  // 按钮圆角
            UIManager.put("Component.arc", 8);  // 组件圆角
            UIManager.put("TextComponent.arc", 8);  // 文本组件圆角

            System.out.println("✅ FlatLaf外观主题设置成功");

        } catch (Exception e) {
            System.err.println("⚠️ FlatLaf外观设置失败,使用默认外观:" + e.getMessage());
            // 继续使用Java默认外观
        }
    }
}
第二步:图书实体类
package org.chapter09.bookmanagement.entity;

import cn.hutool.core.date.DateUtil;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;

/**
 * 图书信息实体类
 */
@Entity
@Table(name = "t_books")
public class Book {

    @Id
    @Column(name = "isbn_", length = 20)
    private String isbn;  // ISBN号

    @Column(name = "title_", nullable = false, length = 200)
    private String title;  // 书名

    @Column(name = "author_", nullable = false, length = 100)
    private String author;  // 作者

    @Column(name = "publisher_", length = 100)
    private String publisher;  // 出版社

    @Column(name = "price_", precision = 10, scale = 2)
    private BigDecimal price;  // 价格

    @Column(name = "quantity_", nullable = false)
    private Integer quantity;  // 库存数量

    @Column(name = "category_", length = 50)
    private String category;  // 分类

    @Column(name = "publish_date_")
    private LocalDate publishDate;  // 出版日期

    @Column(name = "description_", columnDefinition = "TEXT")
    private String description;  // 描述

    @CreationTimestamp
    @Column(name = "created_time_", updatable = false)
    private LocalDateTime createdTime;  // 创建时间

    @UpdateTimestamp
    @Column(name = "updated_time_")
    private LocalDateTime updatedTime;  // 更新时间

    // 默认构造方法
    public Book() {}

    // 带参构造方法
    public Book(String isbn, String title, String author, String publisher,
                BigDecimal price, Integer quantity, String category) {
        this.isbn = isbn;
        this.title = title;
        this.author = author;
        this.publisher = publisher;
        this.price = price;
        this.quantity = quantity;
        this.category = category;
    }

    // getter和setter方法
    public String getIsbn() { return isbn; }
    public void setIsbn(String isbn) { this.isbn = isbn; }

    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }

    public String getAuthor() { return author; }
    public void setAuthor(String author) { this.author = author; }

    public String getPublisher() { return publisher; }
    public void setPublisher(String publisher) { this.publisher = publisher; }

    public BigDecimal getPrice() { return price; }
    public void setPrice(BigDecimal price) { this.price = price; }

    public Integer getQuantity() { return quantity; }
    public void setQuantity(Integer quantity) { this.quantity = quantity; }

    public String getCategory() { return category; }
    public void setCategory(String category) { this.category = category; }

    public LocalDate getPublishDate() { return publishDate; }
    public void setPublishDate(LocalDate publishDate) { this.publishDate = publishDate; }

    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }

    public LocalDateTime getCreatedTime() { return createdTime; }
    public void setCreatedTime(LocalDateTime createdTime) { this.createdTime = createdTime; }

    public LocalDateTime getUpdatedTime() { return updatedTime; }
    public void setUpdatedTime(LocalDateTime updatedTime) { this.updatedTime = updatedTime; }

    @Override
    public String toString() {
        return String.format("ISBN: %s, 书名: %s, 作者: %s, 出版社: %s, 价格: %.2f, 库存: %d",
                isbn, title, author, publisher, price, quantity);
    }
}
第三步:数据访问层(Repository)
package org.chapter09.bookmanagement.repository;

import org.chapter09.bookmanagement.entity.Book;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.math.BigDecimal;
import java.util.List;

/**
 * 图书数据访问接口
 */
@Repository
public interface BookRepository extends JpaRepository<Book, String> {

    /**
     * 根据书名查找图书
     */
    List<Book> findByTitleContaining(String title);

    /**
     * 根据作者查找图书
     */
    List<Book> findByAuthorContaining(String author);

    /**
     * 根据出版社查找图书
     */
    List<Book> findByPublisherContaining(String publisher);

    /**
     * 根据分类查找图书
     */
    List<Book> findByCategory(String category);

    /**
     * 根据价格范围查找图书
     */
    List<Book> findByPriceBetween(BigDecimal minPrice, BigDecimal maxPrice);

    /**
     * 查找库存不足的图书
     */
    List<Book> findByQuantityLessThan(Integer quantity);

    /**
     * 自定义查询:根据关键词搜索图书(书名、作者、出版社)
     */
    @Query("SELECT b FROM Book b WHERE b.title LIKE %:keyword% OR b.author LIKE %:keyword% OR b.publisher LIKE %:keyword%")
    List<Book> searchByKeyword(@Param("keyword") String keyword);

    /**
     * 自定义查询:根据分类统计图书数量
     */
    @Query("SELECT b.category, COUNT(b) FROM Book b GROUP BY b.category")
    List<Object[]> countBooksByCategory();

    /**
     * 自定义查询:根据作者统计图书数量
     */
    @Query("SELECT b.author, COUNT(b) FROM Book b GROUP BY b.author ORDER BY COUNT(b) DESC")
    List<Object[]> countBooksByAuthor();

    /**
     * 自定义查询:查找热门图书(可根据销量等指标,这里简单按库存倒序)
     */
    @Query("SELECT b FROM Book b ORDER BY b.quantity DESC")
    List<Book> findPopularBooks();
}
第四步:业务逻辑层(Service)
package org.chapter09.bookmanagement.service;

import cn.hutool.core.util.StrUtil;
import org.chapter09.bookmanagement.entity.Book;
import org.chapter09.bookmanagement.repository.BookRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;

/**
 * 图书管理业务逻辑类
 */
@Service
@Transactional
public class BookService {

    @Autowired
    private BookRepository bookRepository;

    /**
     * 添加图书
     */
    public Book addBook(Book book) {
        // 检查ISBN是否已存在
        if (bookRepository.existsById(book.getIsbn())) {
            throw new RuntimeException("ISBN已存在:" + book.getIsbn());
        }

        // 数据验证
        validateBook(book);

        return bookRepository.save(book);
    }

    /**
     * 根据ISBN查找图书
     */
    public Book findBookByIsbn(String isbn) {
        Optional<Book> book = bookRepository.findById(isbn);
        return book.orElse(null);
    }

    /**
     * 获取所有图书
     */
    public List<Book> getAllBooks() {
        return bookRepository.findAll();
    }

    /**
     * 更新图书信息
     */
    public Book updateBook(Book book) {
        // 检查图书是否存在
        if (!bookRepository.existsById(book.getIsbn())) {
            throw new RuntimeException("图书不存在:" + book.getIsbn());
        }

        // 数据验证
        validateBook(book);

        return bookRepository.save(book);
    }

    /**
     * 删除图书
     */
    public void deleteBook(String isbn) {
        if (!bookRepository.existsById(isbn)) {
            throw new RuntimeException("图书不存在:" + isbn);
        }
        bookRepository.deleteById(isbn);
    }

    /**
     * 搜索图书
     */
    public List<Book> searchBooks(String keyword) {
        return bookRepository.searchByKeyword(keyword);
    }

    /**
     * 更新图书库存
     */
    public Book updateBookQuantity(String isbn, Integer quantity) {
        Book book = findBookByIsbn(isbn);
        if (book == null) {
            throw new RuntimeException("图书不存在:" + isbn);
        }

        book.setQuantity(quantity);
        return bookRepository.save(book);
    }

    /**
     * 查找库存不足的图书
     */
    public List<Book> findLowStockBooks(Integer threshold) {
        return bookRepository.findByQuantityLessThan(threshold);
    }

    /**
     * 统计各分类图书数量
     */
    public List<Object[]> countBooksByCategory() {
        return bookRepository.countBooksByCategory();
    }

    /**
     * 统计各作者图书数量
     */
    public List<Object[]> countBooksByAuthor() {
        return bookRepository.countBooksByAuthor();
    }

    /**
     * 验证图书信息
     */
    private void validateBook(Book book) {
        if (StrUtil.isBlank(book.getIsbn())) {
            throw new RuntimeException("ISBN不能为空");
        }

        if (StrUtil.isBlank(book.getTitle())) {
            throw new RuntimeException("书名不能为空");
        }

        if (StrUtil.isBlank(book.getAuthor())) {
            throw new RuntimeException("作者不能为空");
        }

        if (book.getPrice() != null && book.getPrice().compareTo(BigDecimal.ZERO) < 0) {
            throw new RuntimeException("价格不能为负数");
        }

        if (book.getQuantity() != null && book.getQuantity() < 0) {
            throw new RuntimeException("库存数量不能为负数");
        }
    }
}
第五步:GUI主界面
package org.chapter09.bookmanagement.gui;

import org.chapter09.bookmanagement.entity.Book;
import org.chapter09.bookmanagement.service.BookService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.swing.*;
import javax.swing.table.DefaultTableModel;
import java.awt.*;
import java.math.BigDecimal;
import java.util.List;

/**
 * 图书管理系统主窗口
 * <p>
 * 基于Swing的图形用户界面主窗口,展示了现代Java桌面应用程序的开发模式。
 * 该类实现了完整的图书管理功能,包括增删改查、搜索、统计等操作。
 *
 * <h3>🔍 学习要点:</h3>
 * <ul>
 *   <li><strong>Swing GUI开发</strong> - 学会使用Swing组件构建用户界面</li>
 *   <li><strong>MVC模式</strong> - 在GUI应用中实现Model-View-Controller</li>
 *   <li><strong>事件处理</strong> - 掌握GUI事件驱动编程</li>
 *   <li><strong>表格操作</strong> - 学会使用JTable显示和操作数据</li>
 * </ul>
 */
@Component
public class MainFrame extends JFrame implements InitializingBean {

    /**
     * 图书业务服务
     * <p>
     * 通过Spring依赖注入获取BookService实例,
     * 用于执行所有与图书相关的业务操作。
     *
     * <h3>🔍 依赖注入学习要点:</h3>
     * <ul>
     *   <li><strong>@Autowired注解</strong> - Spring自动装配,无需手动创建对象</li>
     *   <li><strong>松耦合设计</strong> - GUI层不直接依赖具体实现,便于测试和维护</li>
     *   <li><strong>生命周期管理</strong> - Spring容器负责对象的创建、初始化和销毁</li>
     * </ul>
     */
    @Autowired
    private BookService bookService;

    // ==================== GUI组件声明 ====================

    /**
     * 主表格,用于显示图书列表
     */
    private JTable bookTable;

    /**
     * 表格数据模型
     */
    private DefaultTableModel tableModel;

    /**
     * 搜索文本框
     */
    private JTextField searchField;

    /**
     * 状态栏标签
     */
    private JLabel statusLabel;

    // ==================== 表格列定义 ====================

    /**
     * 表格列名数组
     * <p>
     * 定义了图书信息表格的列标题,这些列对应Book实体的主要属性。
     */
    private final String[] COLUMN_NAMES = {
        "ISBN", "书名", "作者", "出版社", "价格", "库存", "分类"
    };

    /**
     * 默认构造方法
     * <p>
     * 构造方法中不进行任何初始化,等待Spring依赖注入完成后
     * 通过InitializingBean接口的afterPropertiesSet方法进行初始化。
     */
    public MainFrame() {
        // 构造方法中不进行任何初始化,等待@PostConstruct方法
    }

    /**
     * Spring依赖注入完成后的初始化方法
     * <p>
     * 该方法在Spring容器完成依赖注入后自动调用,确保所有依赖都已准备就绪。
     *
     * <h3>🔍 Spring生命周期学习要点:</h3>
     * <ul>
     *   <li><strong>InitializingBean接口</strong> - Spring提供的初始化回调接口</li>
     *   <li><strong>afterPropertiesSet方法</strong> - 在所有属性设置完成后调用</li>
     *   <li><strong>依赖注入时机</strong> - 确保@Autowired的依赖已经注入完成</li>
     *   <li><strong>初始化顺序</strong> - 构造方法 → 依赖注入 → afterPropertiesSet</li>
     * </ul>
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        // 🔍 关键概念:SwingUtilities.invokeLater
        // 这个方法将GUI初始化任务提交到EDT(Event Dispatch Thread)线程中执行
        // 确保所有Swing组件的创建和操作都在正确的线程中进行
        // 这是Swing线程安全编程的基本要求
        SwingUtilities.invokeLater(this::initializeComponents);
    }

    /**
     * 初始化所有GUI组件
     * <p>
     * 这个方法在Spring依赖注入完成后调用,确保所有依赖都已准备就绪。
     *
     * <h3>🔍 GUI构建学习要点:</h3>
     * <ul>
     *   <li><strong>构建顺序</strong> - 从容器到组件,从外层到内层</li>
     *   <li><strong>职责分离</strong> - 每个方法负责一个特定的界面部分</li>
     *   <li><strong>模块化设计</strong> - 便于维护和修改</li>
     *   <li><strong>初始化流程</strong> - 窗口→菜单→工具栏→内容→状态栏→数据→显示</li>
     * </ul>
     */
    private void initializeComponents() {
        // 🏠 窗口基本设置
        // 🔍 学习要点:JFrame的基本属性设置,这些是每个桌面应用的必需步骤
        setupWindow();

        // 📋 创建菜单栏
        // 🔍 学习要点:菜单栏提供应用程序的主要功能入口,通常包含文件、编辑、查看、帮助等菜单
        createMenuBar();

        // 🔧 创建工具栏
        // 🔍 学习要点:工具栏提供常用功能的快速访问,通常放置在窗口顶部
        createToolBar();

        // 📊 创建主面板
        // 🔍 学习要点:主内容区域是用户主要交互的地方,包含数据展示和操作控件
        createMainPanel();

        // 📍 创建状态栏
        // 🔍 学习要点:状态栏显示应用程序的状态信息,通常放置在窗口底部
        createStatusBar();

        // 📚 加载初始数据
        // 🔍 学习要点:界面创建完成后加载数据,避免空白界面,提供更好的用户体验
        loadBookData();

        // 🎯 设置窗口居中显示
        // 🔍 学习要点:setLocationRelativeTo(null)让窗口在屏幕中央显示
        setLocationRelativeTo(null);
    }

    /**
     * 设置窗口基本属性
     * <p>
     * 配置主窗口的基本属性,包括标题、大小、关闭操作等。
     *
     * <h3>🔍 JFrame基础属性学习要点:</h3>
     * <ul>
     *   <li><strong>setTitle()</strong> - 设置窗口标题栏显示的文本</li>
     *   <li><strong>setDefaultCloseOperation()</strong> - 设置点击关闭按钮时的行为</li>
     *   <li><strong>setSize()</strong> - 设置窗口的初始大小(宽度,高度)</li>
     *   <li><strong>setMinimumSize()</strong> - 设置窗口可以缩小到的最小尺寸</li>
     * </ul>
     */
    private void setupWindow() {
        // 🔍 设置窗口标题
        setTitle("图书管理系统 v1.0.0");

        // 🔍 设置关闭操作
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        // 🔍 设置窗口初始大小
        setSize(1200, 800);

        // 🔍 设置最小尺寸
        setMinimumSize(new Dimension(800, 600));
    }

    private void createMenuBar() {
        JMenuBar menuBar = new JMenuBar();

        // 文件菜单
        JMenu fileMenu = new JMenu("文件");
        JMenuItem exitItem = new JMenuItem("退出");
        exitItem.addActionListener(e -> System.exit(0));
        fileMenu.add(exitItem);

        // 图书管理菜单
        JMenu bookMenu = new JMenu("图书管理");
        JMenuItem addItem = new JMenuItem("添加图书");
        JMenuItem editItem = new JMenuItem("编辑图书");
        JMenuItem deleteItem = new JMenuItem("删除图书");
        JMenuItem refreshItem = new JMenuItem("刷新列表");

        addItem.addActionListener(e -> showAddBookDialog());
        editItem.addActionListener(e -> showEditBookDialog());
        deleteItem.addActionListener(e -> deleteSelectedBook());
        refreshItem.addActionListener(e -> loadBookData());

        bookMenu.add(addItem);
        bookMenu.add(editItem);
        bookMenu.add(deleteItem);
        bookMenu.addSeparator();
        bookMenu.add(refreshItem);

        // 统计菜单
        JMenu statsMenu = new JMenu("统计分析");
        JMenuItem categoryStatsItem = new JMenuItem("分类统计");
        JMenuItem authorStatsItem = new JMenuItem("作者统计");
        JMenuItem lowStockItem = new JMenuItem("库存预警");

        categoryStatsItem.addActionListener(e -> showCategoryStatistics());
        authorStatsItem.addActionListener(e -> showAuthorStatistics());
        lowStockItem.addActionListener(e -> showLowStockBooks());

        statsMenu.add(categoryStatsItem);
        statsMenu.add(authorStatsItem);
        statsMenu.add(lowStockItem);

        menuBar.add(fileMenu);
        menuBar.add(bookMenu);
        menuBar.add(statsMenu);

        setJMenuBar(menuBar);
    }

    private void createToolBar() {
        JToolBar toolBar = new JToolBar();
        toolBar.setFloatable(false);

        // 添加按钮
        JButton addButton = new JButton("添加");
        addButton.addActionListener(e -> showAddBookDialog());

        JButton editButton = new JButton("编辑");
        editButton.addActionListener(e -> showEditBookDialog());

        JButton deleteButton = new JButton("删除");
        deleteButton.addActionListener(e -> deleteSelectedBook());

        JButton refreshButton = new JButton("刷新");
        refreshButton.addActionListener(e -> loadBookData());

        // 搜索组件
        JLabel searchLabel = new JLabel("搜索:");
        searchField = new JTextField(20);
        JButton searchButton = new JButton("搜索");
        searchButton.addActionListener(e -> searchBooks());

        // 添加组件到工具栏
        toolBar.add(addButton);
        toolBar.add(editButton);
        toolBar.add(deleteButton);
        toolBar.addSeparator();
        toolBar.add(refreshButton);
        toolBar.addSeparator();
        toolBar.add(searchLabel);
        toolBar.add(searchField);
        toolBar.add(searchButton);

        add(toolBar, BorderLayout.NORTH);
    }

    private void createMainPanel() {
        // 创建表格模型
        tableModel = new DefaultTableModel(columnNames, 0) {
            @Override
            public boolean isCellEditable(int row, int column) {
                return false; // 表格不可直接编辑
            }
        };

        // 创建表格
        bookTable = new JTable(tableModel);
        bookTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        bookTable.setRowHeight(25);

        // 双击编辑
        bookTable.addMouseListener(new java.awt.event.MouseAdapter() {
            @Override
            public void mouseClicked(java.awt.event.MouseEvent e) {
                if (e.getClickCount() == 2) {
                    showEditBookDialog();
                }
            }
        });

        // 创建滚动面板
        JScrollPane scrollPane = new JScrollPane(bookTable);
        add(scrollPane, BorderLayout.CENTER);
    }

    private void createStatusBar() {
        JPanel statusPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
        statusLabel = new JLabel("就绪");
        statusPanel.add(statusLabel);
        add(statusPanel, BorderLayout.SOUTH);
    }

    // 加载图书数据到表格
    public void loadBookData() {
        try {
            List<Book> books = bookService.getAllBooks();
            updateTable(books);
            updateStatus("共加载 " + books.size() + " 本图书 - " + DateUtil.now());
        } catch (Exception e) {
            showErrorMessage("加载数据失败:" + e.getMessage());
        }
    }

    // 更新表格数据
    private void updateTable(List<Book> books) {
        tableModel.setRowCount(0); // 清空表格

        for (Book book : books) {
            Object[] row = {
                book.getIsbn(),
                book.getTitle(),
                book.getAuthor(),
                book.getPublisher(),
                book.getPrice(),
                book.getQuantity(),
                book.getCategory(),
                book.getPublishDate()
            };
            tableModel.addRow(row);
        }
    }

    // 其他方法实现...
    // (为节省篇幅,完整代码请参考原文档)
}
第六步:图书编辑对话框
package org.chapter09.bookmanagement.gui;

import org.chapter09.bookmanagement.entity.Book;

import javax.swing.*;
import java.awt.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 图书信息编辑对话框
 * <p>
 * 用于添加和编辑图书信息的模态对话框。
 * 该对话框展示了Swing对话框的标准设计模式和数据验证技术。
 *
 * <h3>🔍 学习要点:</h3>
 * <ul>
 *   <li><strong>模态对话框</strong> - 阻塞父窗口,强制用户完成操作</li>
 *   <li><strong>表单设计</strong> - 使用GridBagLayout创建复杂表单</li>
 *   <li><strong>数据验证</strong> - 客户端输入验证和错误提示</li>
 *   <li><strong>对话框模式</strong> - 确定/取消的标准交互模式</li>
 * </ul>
 */
public class BookEditDialog extends JDialog {

    private Book book;
    private boolean confirmed = false;

    // 输入组件
    private JTextField isbnField;
    private JTextField titleField;
    private JTextField authorField;
    private JTextField publisherField;
    private JTextField priceField;
    private JTextField quantityField;
    private JTextField categoryField;
    private JTextField publishDateField;
    private JTextArea descriptionArea;

    public BookEditDialog(Frame parent, String title, Book book) {
        super(parent, title, true);
        this.book = book != null ? book : new Book();
        initializeDialog();
        if (book != null) {
            populateFields();
        }
    }

    private void initializeDialog() {
        setSize(500, 600);
        setLocationRelativeTo(getParent());
        setDefaultCloseOperation(DISPOSE_ON_CLOSE);

        // 创建主面板
        JPanel mainPanel = new JPanel(new BorderLayout());

        // 创建表单面板
        JPanel formPanel = createFormPanel();
        mainPanel.add(formPanel, BorderLayout.CENTER);

        // 创建按钮面板
        JPanel buttonPanel = createButtonPanel();
        mainPanel.add(buttonPanel, BorderLayout.SOUTH);

        add(mainPanel);
    }

    private JPanel createFormPanel() {
        JPanel panel = new JPanel(new GridBagLayout());
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.insets = new Insets(5, 5, 5, 5);

        // ISBN
        gbc.gridx = 0; gbc.gridy = 0; gbc.anchor = GridBagConstraints.WEST;
        panel.add(new JLabel("ISBN:"), gbc);
        gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weightx = 1.0;
        isbnField = new JTextField(20);
        panel.add(isbnField, gbc);

        // 书名
        gbc.gridx = 0; gbc.gridy = 1; gbc.fill = GridBagConstraints.NONE; gbc.weightx = 0;
        panel.add(new JLabel("书名:"), gbc);
        gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weightx = 1.0;
        titleField = new JTextField(20);
        panel.add(titleField, gbc);

        // 作者
        gbc.gridx = 0; gbc.gridy = 2; gbc.fill = GridBagConstraints.NONE; gbc.weightx = 0;
        panel.add(new JLabel("作者:"), gbc);
        gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weightx = 1.0;
        authorField = new JTextField(20);
        panel.add(authorField, gbc);

        // 出版社
        gbc.gridx = 0; gbc.gridy = 3; gbc.fill = GridBagConstraints.NONE; gbc.weightx = 0;
        panel.add(new JLabel("出版社:"), gbc);
        gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weightx = 1.0;
        publisherField = new JTextField(20);
        panel.add(publisherField, gbc);

        // 价格
        gbc.gridx = 0; gbc.gridy = 4; gbc.fill = GridBagConstraints.NONE; gbc.weightx = 0;
        panel.add(new JLabel("价格:"), gbc);
        gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weightx = 1.0;
        priceField = new JTextField(20);
        panel.add(priceField, gbc);

        // 库存
        gbc.gridx = 0; gbc.gridy = 5; gbc.fill = GridBagConstraints.NONE; gbc.weightx = 0;
        panel.add(new JLabel("库存:"), gbc);
        gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weightx = 1.0;
        quantityField = new JTextField(20);
        panel.add(quantityField, gbc);

        // 分类
        gbc.gridx = 0; gbc.gridy = 6; gbc.fill = GridBagConstraints.NONE; gbc.weightx = 0;
        panel.add(new JLabel("分类:"), gbc);
        gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weightx = 1.0;
        categoryField = new JTextField(20);
        panel.add(categoryField, gbc);

        // 出版日期
        gbc.gridx = 0; gbc.gridy = 7; gbc.fill = GridBagConstraints.NONE; gbc.weightx = 0;
        panel.add(new JLabel("出版日期:"), gbc);
        gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weightx = 1.0;
        publishDateField = new JTextField(20);
        publishDateField.setToolTipText("格式: yyyy-MM-dd");
        panel.add(publishDateField, gbc);

        // 描述
        gbc.gridx = 0; gbc.gridy = 8; gbc.fill = GridBagConstraints.NONE; gbc.weightx = 0;
        gbc.anchor = GridBagConstraints.NORTHWEST;
        panel.add(new JLabel("描述:"), gbc);
        gbc.gridx = 1; gbc.fill = GridBagConstraints.BOTH; gbc.weightx = 1.0; gbc.weighty = 1.0;
        descriptionArea = new JTextArea(5, 20);
        descriptionArea.setLineWrap(true);
        descriptionArea.setWrapStyleWord(true);
        JScrollPane scrollPane = new JScrollPane(descriptionArea);
        panel.add(scrollPane, gbc);

        return panel;
    }

    private JPanel createButtonPanel() {
        JPanel panel = new JPanel(new FlowLayout());

        JButton confirmButton = new JButton("确定");
        confirmButton.addActionListener(e -> confirmAction());

        JButton cancelButton = new JButton("取消");
        cancelButton.addActionListener(e -> dispose());

        panel.add(confirmButton);
        panel.add(cancelButton);

        return panel;
    }

    private void populateFields() {
        if (book != null) {
            isbnField.setText(book.getIsbn());
            titleField.setText(book.getTitle());
            authorField.setText(book.getAuthor());
            publisherField.setText(book.getPublisher());
            if (book.getPrice() != null) {
                priceField.setText(book.getPrice().toString());
            }
            if (book.getQuantity() != null) {
                quantityField.setText(book.getQuantity().toString());
            }
            categoryField.setText(book.getCategory());
            if (book.getPublishDate() != null) {
                publishDateField.setText(book.getPublishDate().toString());
            }
            descriptionArea.setText(book.getDescription());
        }
    }

    private void confirmAction() {
        try {
            // 验证并设置数据
            if (StrUtil.isBlank(isbnField.getText())) {
                showError("ISBN不能为空");
                return;
            }
            book.setIsbn(isbnField.getText().trim());

            if (StrUtil.isBlank(titleField.getText())) {
                showError("书名不能为空");
                return;
            }
            book.setTitle(titleField.getText().trim());

            if (StrUtil.isBlank(authorField.getText())) {
                showError("作者不能为空");
                return;
            }
            book.setAuthor(authorField.getText().trim());

            book.setPublisher(publisherField.getText().trim());

            // 价格
            if (StrUtil.isNotBlank(priceField.getText())) {
                try {
                    book.setPrice(new BigDecimal(priceField.getText().trim()));
                } catch (NumberFormatException e) {
                    showError("价格格式错误");
                    return;
                }
            }

            // 库存
            if (StrUtil.isNotBlank(quantityField.getText())) {
                try {
                    book.setQuantity(Integer.parseInt(quantityField.getText().trim()));
                } catch (NumberFormatException e) {
                    showError("库存格式错误");
                    return;
                }
            }

            book.setCategory(categoryField.getText().trim());

            // 出版日期
            if (StrUtil.isNotBlank(publishDateField.getText())) {
                try {
                    book.setPublishDate(LocalDate.parse(publishDateField.getText().trim(),
                        DateTimeFormatter.ofPattern("yyyy-MM-dd")));
                } catch (DateTimeParseException e) {
                    showError("出版日期格式错误,请使用 yyyy-MM-dd 格式");
                    return;
                }
            }

            book.setDescription(descriptionArea.getText().trim());

            confirmed = true;
            dispose();

        } catch (Exception e) {
            showError("数据验证失败:" + e.getMessage());
        }
    }

    private void showError(String message) {
        JOptionPane.showMessageDialog(this, message, "错误", JOptionPane.ERROR_MESSAGE);
    }

    public boolean isConfirmed() {
        return confirmed;
    }

    public Book getBook() {
        return book;
    }
}

测试与运行实践

GUI组件测试

package org.chapter09.bookmanagement.gui;

import org.chapter09.bookmanagement.entity.Book;
import org.chapter09.bookmanagement.service.BookService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

/**
 * GUI组件单元测试
 */
@ExtendWith(MockitoExtension.class)
class MainFrameTest {

    @Mock
    private BookService bookService;

    private MainFrame mainFrame;
    private List<Book> testBooks;

    @BeforeEach
    void setUp() {
        // 创建测试数据
        Book book1 = new Book();
        book1.setIsbn("978-7-111-54742-6");
        book1.setTitle("Java核心技术");
        book1.setAuthor("Cay S. Horstmann");
        book1.setPublisher("机械工业出版社");
        book1.setPrice(new BigDecimal("119.00"));
        book1.setQuantity(50);
        book1.setCategory("编程");

        testBooks = Arrays.asList(book1);

        // 初始化GUI(在测试环境中)
        System.setProperty("java.awt.headless", "true");
        mainFrame = new MainFrame();
        // 手动注入mock service
        // 注意:实际测试中应该使用@InjectMocks或Spring测试框架
    }

    @Test
    void testLoadBookData() {
        // Given
        when(bookService.getAllBooks()).thenReturn(testBooks);

        // When
        SwingUtilities.invokeLater(() -> {
            mainFrame.loadBookData();
        });

        // Then
        verify(bookService).getAllBooks();
    }
}

运行脚本

创建scripts/run-gui.sh

#!/bin/bash

echo "=== 图书管理系统(GUI版) ==="
echo "请确保MySQL数据库已启动并创建了book_management数据库"

JAR_FILE="target/chapter09_02-1.0.0.jar"

if [ ! -f "$JAR_FILE" ]; then
    echo "JAR文件不存在,开始编译..."
    mvn clean package -DskipTests

    if [ $? -ne 0 ]; then
        echo "编译失败,请检查代码"
        exit 1
    fi
fi

echo "启动图书管理系统GUI版..."
echo "注意:这是一个桌面GUI应用,将打开图形界面"

# 设置JVM参数
JAVA_OPTS="-Xmx512m -Xms256m"
JAVA_OPTS="$JAVA_OPTS -Dfile.encoding=UTF-8"
JAVA_OPTS="$JAVA_OPTS -Djava.awt.headless=false"

java $JAVA_OPTS -jar $JAR_FILE

🎯 运行效果说明

当您成功运行GUI应用程序后,将看到如下效果:

1. 应用程序启动阶段
  • Spring Boot在后台完成初始化
  • FlatLaf主题自动应用,提供现代化外观
  • 数据库连接建立,表结构自动创建
  • 主窗口在屏幕中央显示
2. 主界面功能展示
  • 菜单栏:文件、编辑、查看、帮助等标准菜单
  • 工具栏:新增、编辑、删除、刷新、统计等快捷按钮
  • 数据表格
    • 清晰的列标题(ISBN、书名、作者、出版社、价格、库存、分类)
    • 支持行选择和双击编辑
    • 自动调整列宽,优化显示效果
  • 搜索功能:实时搜索,支持模糊匹配
  • 状态栏:显示当前操作状态和记录数量
3. 编辑对话框特性
  • 模态设计:阻塞主窗口,确保数据完整性
  • 表单布局:使用GridBagLayout实现专业的表单设计
  • 数据验证
    • 必填字段检查(书名、作者不能为空)
    • 数据格式验证(价格必须为正数)
    • 重复性检查(ISBN唯一性)
  • 用户反馈:清晰的错误提示和成功消息
4. 统计功能展示
  • 数据概览:图书总数、总库存、平均价格
  • 分类统计:各类别图书数量分布
  • 价格分析:价格区间分布统计
  • 库存预警:低库存图书提醒
5. 用户体验亮点
  • 现代化外观:FlatLaf主题提供扁平化设计风格
  • 响应式交互:鼠标悬停效果、按钮状态变化
  • 键盘支持:Tab键导航、快捷键操作
  • 国际化友好:完整的中文界面支持

💡 运行提示

  • 确保MySQL服务已启动
  • 首次运行会自动创建数据库表
  • 建议添加一些测试数据以获得最佳演示效果
  • 如果界面显示异常,请检查Java版本是否为JDK 17+

项目特点

  1. 现代化GUI界面:如效果图所示,采用FlatLaf主题的现代化Swing界面,提供专业的桌面应用体验
  2. 完整的MVC架构:清晰的分层设计,GUI层、业务层、数据层职责分明
  3. Spring Boot集成:利用Spring的依赖注入和自动配置,简化开发复杂度
  4. 用户友好交互
    • 直观的菜单栏和工具栏设计
    • 模态对话框确保数据完整性
    • 实时搜索和数据验证
    • 详细的统计信息展示
  5. 数据管理功能
    • 完整的CRUD操作支持
    • 数据持久化存储
    • 事务管理和数据一致性
  6. 技术栈现代化
    • Java 17+ 新特性支持
    • Spring Boot 3.x 最新版本
    • JPA/Hibernate 数据访问
    • Maven 项目管理


🎯 GUI编程学习进度检查表

完成图书管理系统的学习后,请检查您是否掌握了以下技能:

基础概念理解 □

  • 理解GUI编程与控制台编程的区别
  • 掌握Swing框架的特点和优势
  • 理解MVC架构在GUI应用中的应用
  • 了解事件驱动编程的基本概念

组件使用技能 □

  • 熟练使用JFrame创建主窗口
  • 会使用JPanel组织界面布局
  • 掌握基础组件(JLabel、JTextField、JButton)的使用
  • 理解JTable的数据绑定和显示机制
  • 会创建和使用JMenuBar、JToolBar

布局管理能力 □

  • 掌握BorderLayout在主窗口中的应用
  • 理解FlowLayout适用的场景
  • 会使用GridLayout创建规整表单
  • 了解GridBagLayout的灵活布局能力
  • 能够嵌套使用多种布局管理器

事件处理技能 □

  • 理解事件处理的基本流程
  • 会使用ActionListener处理按钮和菜单事件
  • 掌握MouseListener处理鼠标交互
  • 能够使用DocumentListener实现实时输入监听
  • 会处理表格选择和双击事件

最佳实践应用 □

  • 代码结构清晰,职责分离明确
  • 正确处理Swing的线程安全问题
  • 添加适当的异常处理和用户反馈
  • 提供良好的用户体验和界面响应
  • 会使用调试技巧排查GUI问题

项目实战能力 □

  • 能够独立创建完整的GUI应用程序
  • 会集成Spring Boot与Swing框架
  • 掌握数据绑定和界面更新技术
  • 能够设计用户友好的交互界面
  • 具备GUI应用的测试和调试能力

🚀 进阶学习建议

1. 深入学习方向

  • 自定义组件开发:学习如何创建自定义的Swing组件
  • 高级布局技术:掌握复杂界面的布局设计
  • 外观主题定制:学习Look and Feel的深度定制
  • 国际化支持:实现多语言界面切换

2. 实践项目推荐

  • 计算器应用:练习基础组件和事件处理
  • 文本编辑器:练习菜单、工具栏和文件操作
  • 图片查看器:学习图形绘制和文件处理
  • 数据分析工具:练习图表组件和数据可视化

3. 现代化技术探索

  • JavaFX:学习更现代的Java GUI框架
  • Web前端技术:了解基于Web的用户界面开发
  • 跨平台解决方案:探索Electron等跨平台桌面应用技术

❓ 常见问题解答

Q1: 为什么选择Swing而不是JavaFX?

A: Swing是Java标准库的一部分,无需额外依赖,学习成本较低。虽然JavaFX更现代,但Swing仍然广泛应用于企业级桌面应用开发,是学习GUI编程的良好起点。

Q2: GUI应用如何处理数据库连接异常?

A: 在GUI应用中,应该:

  • 使用try-catch捕获数据库异常
  • 通过JOptionPane向用户显示友好的错误信息
  • 记录详细的错误日志供开发者调试
  • 提供重试机制或降级方案

Q3: 如何让Swing应用看起来更现代?

A: 可以通过以下方式改善:

  • 使用FlatLaf等现代化Look and Feel
  • 合理使用颜色和字体
  • 添加图标和视觉元素
  • 优化布局和间距
  • 提供良好的用户反馈

Q4: GUI应用的性能如何优化?

A: 主要优化策略:

  • 避免在EDT线程中执行耗时操作
  • 使用SwingWorker处理后台任务
  • 合理使用缓存减少数据库查询
  • 优化表格数据的加载和显示
  • 及时释放不需要的资源

Q5: 如何调试GUI应用的问题?

A: 调试技巧包括:

  • 使用IDE的调试器设置断点
  • 添加日志输出跟踪程序执行
  • 使用组件检查器查看组件状态
  • 检查事件处理的执行流程
  • 验证数据绑定是否正确

Q6: 为什么我的GUI界面与效果图不一致?

A: 可能的原因和解决方案:

  • Look and Feel问题:确保正确设置了FlatLaf主题,检查UIManager.setLookAndFeel()调用
  • 字体显示异常:确保系统支持中文字体,可以显式设置字体:new Font("微软雅黑", Font.PLAIN, 12)
  • 布局错乱:检查布局管理器设置,确保组件的约束条件正确
  • 组件大小异常:调用pack()方法自动调整窗口大小,或使用setPreferredSize()设置组件大小
  • 颜色主题不一致:确保FlatLaf依赖正确添加到项目中

Q7: GUI应用启动后没有显示窗口怎么办?

A: 检查以下几点:

  • EDT线程问题:确保GUI创建在事件分发线程中:SwingUtilities.invokeLater()
  • 窗口可见性:确保调用了setVisible(true)
  • 窗口位置:可能窗口显示在屏幕外,使用setLocationRelativeTo(null)居中显示
  • 异常阻塞:检查控制台是否有异常信息,可能初始化过程中出现错误
  • 系统兼容性:确保Java版本兼容,建议使用JDK 17+

Q8: 如何获得与效果图相同的数据展示效果?

A: 建议添加以下测试数据:

// 在应用启动时添加测试数据
Book book1 = new Book("978-7-111-54742-6", "Java核心技术", "Cay S. Horstmann",
                     "机械工业出版社", new BigDecimal("119.00"), 50, "编程");
Book book2 = new Book("978-7-115-48831-2", "Spring Boot实战", "Craig Walls",
                     "人民邮电出版社", new BigDecimal("89.00"), 30, "编程");
// 添加更多测试数据...

这样可以获得与效果图相同的丰富数据展示效果。


📖 中篇小结

在中篇中,我们完成了:

GUI编程基础:系统学习了Swing框架的核心概念和组件使用
MVC架构应用:深入理解了MVC模式在GUI应用中的具体实现
布局管理掌握:学会了各种布局管理器的选择和使用技巧
事件处理机制:掌握了GUI应用的事件驱动编程模式
最佳实践指导:学习了GUI开发的代码组织和调试技巧
GUI框架搭建:Spring Boot + Swing的完整集成
实体设计:图书实体类和复杂的业务逻辑
数据访问:Repository接口和统计查询方法
GUI界面:主窗口、菜单栏、工具栏、表格展示,如效果图所示的专业界面设计
对话框设计:模态对话框和数据验证,提供用户友好的交互体验
事件处理:用户交互和数据绑定,实现响应式的GUI应用
视觉效果:现代化的FlatLaf主题,提供与效果图一致的优雅界面

🔗 相关文章


🎉 中篇完成!

你已经掌握了Swing GUI应用开发的核心技能!
接下来让我们进入下篇,学习项目运行和最佳实践。

📖 继续阅读:下篇 - 项目运行与最佳实践

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值