1. 项目概述与开发环境搭建
作为一名有10年Java开发经验的工程师,我认为图书管理系统是Java初学者最好的练手项目之一。这个项目涵盖了Java核心语法、面向对象设计、数据库操作和GUI开发等关键知识点,能够全面锻炼初学者的编程能力。下面我将从零开始,详细讲解如何开发一个功能完善的图书管理系统。
1.1 开发工具准备
工欲善其事,必先利其器。在开始编码前,我们需要准备好开发环境:
JDK选择:推荐使用JDK 17 LTS版本,这是目前企业开发的主流选择。与JDK 8相比,它提供了更多现代语言特性,同时保持了长期支持。
bash复制# 检查JDK版本
java -version
IDE配置:IntelliJ IDEA是最适合Java开发的IDE。安装后需要配置:
- 设置合适的JVM参数(Help -> Edit Custom VM Options)
- 安装Lombok插件减少样板代码
- 配置代码风格和代码模板
数据库选择:MySQL 8.0是最佳选择,它提供了完善的社区支持和新特性:
sql复制-- 创建数据库
CREATE DATABASE library_management
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
构建工具:使用Maven管理项目依赖。pom.xml中需要包含以下关键依赖:
xml复制<dependencies>
<!-- JDBC驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- Swing组件增强 -->
<dependency>
<groupId>org.swinglabs</groupId>
<artifactId>swingx</artifactId>
<version>1.6.1</version>
</dependency>
<!-- 日志组件 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.7</version>
</dependency>
</dependencies>
1.2 项目结构设计
良好的项目结构能显著提高代码可维护性。推荐采用分层架构:
code复制src/main/java
├── com.library
│ ├── config # 配置类
│ ├── controller # 控制器
│ ├── dao # 数据访问层
│ ├── dto # 数据传输对象
│ ├── entity # 实体类
│ ├── exception # 自定义异常
│ ├── service # 业务逻辑层
│ ├── util # 工具类
│ └── view # 视图层
src/main/resources
├── db # 数据库脚本
├── i18n # 国际化资源
└── logback.xml # 日志配置
提示:使用Maven的standard目录结构,可以方便地与CI/CD工具集成。resources目录下的文件会被自动复制到classpath中。
2. 核心业务逻辑实现
2.1 实体类设计
实体类是系统的核心数据结构。采用领域驱动设计(DDD)思想,我们先设计几个关键实体:
java复制// 图书实体
@Entity
@Table(name = "books")
@Getter
@Setter
@NoArgsConstructor
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String title;
@Column(nullable = false, length = 50)
private String author;
@Column(unique = true, length = 20)
private String isbn;
@Column(name = "publish_date")
private LocalDate publishDate;
@Enumerated(EnumType.STRING)
private BookStatus status = BookStatus.AVAILABLE;
@ManyToOne
@JoinColumn(name = "category_id")
private Category category;
}
// 读者实体
@Entity
@Table(name = "readers")
public class Reader {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50)
private String name;
@Column(unique = true, length = 18)
private String idCard;
@OneToMany(mappedBy = "reader")
private List<BorrowRecord> borrowRecords = new ArrayList<>();
}
// 借阅记录实体
@Entity
@Table(name = "borrow_records")
public class BorrowRecord {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "book_id", nullable = false)
private Book book;
@ManyToOne
@JoinColumn(name = "reader_id", nullable = false)
private Reader reader;
@Column(name = "borrow_date", nullable = false)
private LocalDate borrowDate;
@Column(name = "due_date", nullable = false)
private LocalDate dueDate;
@Column(name = "return_date")
private LocalDate returnDate;
@Column(precision = 10, scale = 2)
private BigDecimal fine;
}
注意事项:实体类中使用JPA注解实现ORM映射。@ManyToOne和@OneToMany注解会自动处理外键关系,但要注意双向关联时的循环引用问题。
2.2 数据库访问层
使用Spring Data JPA简化数据库操作:
java复制public interface BookRepository extends JpaRepository<Book, Long> {
// 根据书名模糊查询
@Query("SELECT b FROM Book b WHERE b.title LIKE %:keyword%")
List<Book> findByTitleContaining(@Param("keyword") String keyword);
// 根据作者查询
List<Book> findByAuthor(String author);
// 根据状态查询
List<Book> findByStatus(BookStatus status);
// 复杂查询:查找某类别下可借阅的图书
@Query("SELECT b FROM Book b WHERE b.category.id = :categoryId AND b.status = 'AVAILABLE'")
List<Book> findAvailableByCategory(@Param("categoryId") Long categoryId);
}
@Service
@RequiredArgsConstructor
public class BookService {
private final BookRepository bookRepository;
public Book addBook(Book book) {
// 验证ISBN唯一性
if (bookRepository.existsByIsbn(book.getIsbn())) {
throw new BusinessException("ISBN已存在");
}
return bookRepository.save(book);
}
public Page<Book> searchBooks(String keyword, Pageable pageable) {
return bookRepository.findByTitleContainingOrAuthorContainingOrIsbnContaining(
keyword, keyword, keyword, pageable);
}
@Transactional
public void borrowBook(Long bookId, Long readerId) {
Book book = bookRepository.findById(bookId)
.orElseThrow(() -> new EntityNotFoundException("图书不存在"));
if (book.getStatus() != BookStatus.AVAILABLE) {
throw new BusinessException("图书不可借阅");
}
book.setStatus(BookStatus.BORROWED);
bookRepository.save(book);
// 创建借阅记录
BorrowRecord record = new BorrowRecord();
record.setBook(book);
record.setReader(readerRepository.getById(readerId));
record.setBorrowDate(LocalDate.now());
record.setDueDate(LocalDate.now().plusDays(30));
borrowRecordRepository.save(record);
}
}
2.3 业务逻辑实现
图书管理系统的核心业务包括借阅、归还和罚款计算:
java复制public class BorrowServiceImpl implements BorrowService {
private static final BigDecimal DAILY_FINE = new BigDecimal("0.50");
private static final int MAX_BORROW_DAYS = 30;
@Override
@Transactional
public BorrowResult borrowBook(Long bookId, Long readerId) {
// 检查读者借阅数量是否超限
long borrowedCount = borrowRecordRepository.countByReaderIdAndReturnDateIsNull(readerId);
if (borrowedCount >= 5) {
throw new BusinessException("借阅数量已达上限");
}
Book book = bookRepository.findByIdWithLock(bookId)
.orElseThrow(() -> new EntityNotFoundException("图书不存在"));
if (book.getStatus() != BookStatus.AVAILABLE) {
throw new BusinessException("图书已被借出");
}
// 更新图书状态
book.setStatus(BookStatus.BORROWED);
bookRepository.save(book);
// 创建借阅记录
BorrowRecord record = new BorrowRecord();
record.setBook(book);
record.setReader(readerRepository.getReferenceById(readerId));
record.setBorrowDate(LocalDate.now());
record.setDueDate(LocalDate.now().plusDays(MAX_BORROW_DAYS));
borrowRecordRepository.save(record);
return new BorrowResult(record.getId(), record.getDueDate());
}
@Override
@Transactional
public ReturnResult returnBook(Long recordId) {
BorrowRecord record = borrowRecordRepository.findById(recordId)
.orElseThrow(() -> new EntityNotFoundException("借阅记录不存在"));
if (record.getReturnDate() != null) {
throw new BusinessException("图书已归还");
}
// 更新图书状态
Book book = record.getBook();
book.setStatus(BookStatus.AVAILABLE);
bookRepository.save(book);
// 计算罚款
LocalDate returnDate = LocalDate.now();
record.setReturnDate(returnDate);
BigDecimal fine = BigDecimal.ZERO;
if (returnDate.isAfter(record.getDueDate())) {
long overdueDays = ChronoUnit.DAYS.between(record.getDueDate(), returnDate);
fine = DAILY_FINE.multiply(BigDecimal.valueOf(overdueDays));
record.setFine(fine);
}
borrowRecordRepository.save(record);
return new ReturnResult(fine, returnDate);
}
}
经验分享:在事务方法中使用@Transactional注解时,要注意:
- 默认只对RuntimeException回滚,检查异常不会触发回滚
- 避免在事务方法中进行远程调用,会导致事务时间过长
- 只读查询可以添加@Transactional(readOnly = true)提升性能
3. 用户界面开发
3.1 Swing界面设计
虽然现在流行Web开发,但Swing仍然是学习GUI编程的好选择。我们使用MigLayout作为布局管理器:
java复制public class MainFrame extends JFrame {
private final BookService bookService;
private final DefaultTableModel bookTableModel;
public MainFrame() {
super("图书管理系统");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(1000, 600);
setLocationRelativeTo(null);
this.bookService = ServiceFactory.getBookService();
this.bookTableModel = createBookTableModel();
initUI();
loadBookData();
}
private void initUI() {
// 主面板使用MigLayout布局
JPanel mainPanel = new JPanel(new MigLayout("fill", "[grow]", "[][grow][]"));
// 搜索面板
JPanel searchPanel = new JPanel(new MigLayout("", "[][grow][]", ""));
JTextField searchField = new JTextField(20);
JButton searchButton = new JButton("搜索");
searchButton.addActionListener(e -> searchBooks(searchField.getText()));
searchPanel.add(new JLabel("搜索:"), "gapright 5");
searchPanel.add(searchField, "growx");
searchPanel.add(searchButton, "gapleft 5");
mainPanel.add(searchPanel, "growx, wrap");
// 图书表格
JTable bookTable = new JTable(bookTableModel);
bookTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
bookTable.getSelectionModel().addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) {
updateButtonStates();
}
});
JScrollPane scrollPane = new JScrollPane(bookTable);
mainPanel.add(scrollPane, "grow, wrap");
// 按钮面板
JPanel buttonPanel = new JPanel(new MigLayout("", "[][][]", ""));
JButton addButton = new JButton("添加图书");
addButton.addActionListener(e -> showAddBookDialog());
JButton editButton = new JButton("编辑图书");
editButton.addActionListener(e -> showEditBookDialog());
JButton deleteButton = new JButton("删除图书");
deleteButton.addActionListener(e -> deleteSelectedBook());
buttonPanel.add(addButton);
buttonPanel.add(editButton);
buttonPanel.add(deleteButton);
mainPanel.add(buttonPanel, "growx");
add(mainPanel);
}
private DefaultTableModel createBookTableModel() {
return new DefaultTableModel(
new Object[]{"ID", "书名", "作者", "ISBN", "状态"}, 0) {
@Override
public boolean isCellEditable(int row, int column) {
return false;
}
};
}
private void loadBookData() {
List<Book> books = bookService.getAllBooks();
bookTableModel.setRowCount(0);
for (Book book : books) {
bookTableModel.addRow(new Object[]{
book.getId(),
book.getTitle(),
book.getAuthor(),
book.getIsbn(),
book.getStatus().getDescription()
});
}
}
private void searchBooks(String keyword) {
List<Book> books = bookService.searchBooks(keyword);
bookTableModel.setRowCount(0);
for (Book book : books) {
bookTableModel.addRow(new Object[]{
book.getId(),
book.getTitle(),
book.getAuthor(),
book.getIsbn(),
book.getStatus().getDescription()
});
}
}
}
3.2 对话框设计
使用JDialog创建各种功能对话框:
java复制public class BookDialog extends JDialog {
private final BookService bookService;
private final DefaultTableModel tableModel;
private Book currentBook;
public BookDialog(Frame owner, String title, Book book, DefaultTableModel tableModel) {
super(owner, title, true);
this.bookService = ServiceFactory.getBookService();
this.currentBook = book;
this.tableModel = tableModel;
setSize(400, 300);
setLocationRelativeTo(owner);
initUI();
}
private void initUI() {
JPanel panel = new JPanel(new MigLayout("wrap 2", "[right][grow]", "[]10[]"));
// 表单字段
JTextField titleField = new JTextField(20);
JTextField authorField = new JTextField(20);
JTextField isbnField = new JTextField(20);
JComboBox<Category> categoryCombo = new JComboBox<>();
// 如果是编辑模式,填充现有数据
if (currentBook != null) {
titleField.setText(currentBook.getTitle());
authorField.setText(currentBook.getAuthor());
isbnField.setText(currentBook.getIsbn());
}
// 添加组件到面板
panel.add(new JLabel("书名:"));
panel.add(titleField, "growx");
panel.add(new JLabel("作者:"));
panel.add(authorField, "growx");
panel.add(new JLabel("ISBN:"));
panel.add(isbnField, "growx");
panel.add(new JLabel("分类:"));
panel.add(categoryCombo, "growx");
// 按钮面板
JPanel buttonPanel = new JPanel(new MigLayout("", "[grow][]", ""));
JButton saveButton = new JButton("保存");
saveButton.addActionListener(e -> {
try {
saveBook(
titleField.getText(),
authorField.getText(),
isbnField.getText(),
(Category) categoryCombo.getSelectedItem()
);
dispose();
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
}
});
JButton cancelButton = new JButton("取消");
cancelButton.addActionListener(e -> dispose());
buttonPanel.add(saveButton);
buttonPanel.add(cancelButton, "gapleft 10");
panel.add(buttonPanel, "span 2, growx");
add(panel);
}
private void saveBook(String title, String author, String isbn, Category category) {
if (title.isEmpty() || author.isEmpty() || isbn.isEmpty()) {
throw new IllegalArgumentException("所有字段都必须填写");
}
if (currentBook == null) {
// 新增图书
Book book = new Book();
book.setTitle(title);
book.setAuthor(author);
book.setIsbn(isbn);
book.setCategory(category);
bookService.addBook(book);
} else {
// 更新图书
currentBook.setTitle(title);
currentBook.setAuthor(author);
currentBook.setIsbn(isbn);
currentBook.setCategory(category);
bookService.updateBook(currentBook);
}
// 刷新表格数据
((MainFrame) getOwner()).refreshBookData();
}
}
界面设计技巧:
- 使用MigLayout可以创建灵活的布局,比原生布局管理器更强大
- 对话框设置为模态(modal),可以阻塞父窗口操作
- 表格模型与数据分离,便于数据更新和刷新
4. 系统测试与部署
4.1 单元测试
使用JUnit 5和Mockito编写单元测试:
java复制@ExtendWith(MockitoExtension.class)
class BookServiceTest {
@Mock
private BookRepository bookRepository;
@InjectMocks
private BookService bookService;
@Test
void shouldAddBookSuccessfully() {
// 准备测试数据
Book book = new Book();
book.setTitle("Effective Java");
book.setAuthor("Joshua Bloch");
book.setIsbn("978-0321356680");
// 模拟Repository行为
when(bookRepository.existsByIsbn(anyString())).thenReturn(false);
when(bookRepository.save(any(Book.class))).thenReturn(book);
// 执行测试
Book savedBook = bookService.addBook(book);
// 验证结果
assertNotNull(savedBook);
assertEquals("Effective Java", savedBook.getTitle());
verify(bookRepository).save(book);
}
@Test
void shouldThrowExceptionWhenIsbnExists() {
Book book = new Book();
book.setIsbn("978-0321356680");
when(bookRepository.existsByIsbn(book.getIsbn())).thenReturn(true);
assertThrows(BusinessException.class, () -> bookService.addBook(book));
}
}
4.2 集成测试
使用Testcontainers进行数据库集成测试:
java复制@Testcontainers
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class BookRepositoryIntegrationTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
}
@Autowired
private BookRepository bookRepository;
@Test
void shouldSaveAndRetrieveBook() {
Book book = new Book();
book.setTitle("Clean Code");
book.setAuthor("Robert C. Martin");
book.setIsbn("978-0132350884");
bookRepository.save(book);
Optional<Book> found = bookRepository.findByIsbn(book.getIsbn());
assertTrue(found.isPresent());
assertEquals("Clean Code", found.get().getTitle());
}
}
4.3 打包与部署
使用Maven Assembly插件创建可执行包:
xml复制<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>com.library.MainApp</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
打包命令:
bash复制mvn clean package assembly:single
生成的可执行jar位于target目录下,可以通过以下命令运行:
bash复制java -jar library-management-system-jar-with-dependencies.jar
5. 项目扩展与优化建议
5.1 性能优化
- 数据库索引优化:为常用查询字段添加索引
sql复制CREATE INDEX idx_book_title ON books(title);
CREATE INDEX idx_book_author ON books(author);
CREATE INDEX idx_book_status ON books(status);
- 连接池配置:使用HikariCP替代默认连接池
properties复制spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=30000
- 缓存策略:对热点数据使用Redis缓存
java复制@Cacheable(value = "books", key = "#isbn")
public Book findByIsbn(String isbn) {
return bookRepository.findByIsbn(isbn)
.orElseThrow(() -> new EntityNotFoundException("图书不存在"));
}
5.2 功能扩展
- 图书预约功能:允许读者预约已被借出的图书
java复制public class ReservationService {
public Reservation reserveBook(Long bookId, Long readerId) {
Book book = bookRepository.findById(bookId)
.orElseThrow(() -> new EntityNotFoundException("图书不存在"));
if (book.getStatus() == BookStatus.AVAILABLE) {
throw new BusinessException("图书可借阅,无需预约");
}
// 检查是否已有预约
if (reservationRepository.existsByBookIdAndReaderId(bookId, readerId)) {
throw new BusinessException("您已预约过该图书");
}
Reservation reservation = new Reservation();
reservation.setBook(book);
reservation.setReader(readerRepository.getById(readerId));
reservation.setReserveDate(LocalDate.now());
reservation.setExpiryDate(LocalDate.now().plusDays(7));
return reservationRepository.save(reservation);
}
}
- 数据导出功能:支持将图书数据导出为Excel
java复制public void exportBooksToExcel(OutputStream outputStream) throws IOException {
List<Book> books = bookRepository.findAll();
try (Workbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet("图书列表");
// 创建表头
Row headerRow = sheet.createRow(0);
headerRow.createCell(0).setCellValue("ID");
headerRow.createCell(1).setCellValue("书名");
headerRow.createCell(2).setCellValue("作者");
headerRow.createCell(3).setCellValue("ISBN");
headerRow.createCell(4).setCellValue("状态");
// 填充数据
int rowNum = 1;
for (Book book : books) {
Row row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue(book.getId());
row.createCell(1).setCellValue(book.getTitle());
row.createCell(2).setCellValue(book.getAuthor());
row.createCell(3).setCellValue(book.getIsbn());
row.createCell(4).setCellValue(book.getStatus().name());
}
workbook.write(outputStream);
}
}
- 多语言支持:使用ResourceBundle实现国际化
java复制public class I18nUtil {
private static ResourceBundle bundle;
static {
try {
bundle = ResourceBundle.getBundle("messages", Locale.getDefault());
} catch (MissingResourceException e) {
bundle = ResourceBundle.getBundle("messages", Locale.ENGLISH);
}
}
public static String getString(String key) {
try {
return bundle.getString(key);
} catch (MissingResourceException e) {
return key;
}
}
}
// 在界面中使用
JButton button = new JButton(I18nUtil.getString("button.search"));
5.3 架构升级建议
- 迁移到Spring Boot:将项目升级为Spring Boot应用,简化配置
java复制@SpringBootApplication
public class LibraryApplication {
public static void main(String[] args) {
SpringApplication.run(LibraryApplication.class, args);
}
}
- 前后端分离:使用Spring Web开发REST API,前端采用Vue.js或React
java复制@RestController
@RequestMapping("/api/books")
public class BookController {
private final BookService bookService;
@GetMapping
public ResponseEntity<Page<BookDTO>> getBooks(
@RequestParam(required = false) String keyword,
@PageableDefault Pageable pageable) {
return ResponseEntity.ok(bookService.searchBooks(keyword, pageable));
}
@PostMapping
public ResponseEntity<BookDTO> addBook(@Valid @RequestBody BookDTO bookDTO) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(bookService.addBook(bookDTO));
}
}
- 微服务化:将系统拆分为用户服务、图书服务、借阅服务等微服务
通过以上优化和扩展,可以将这个简单的课程设计项目升级为一个符合企业级标准的应用系统。在实际开发中,建议使用Git进行版本控制,并采用敏捷开发方法迭代推进项目。