小型图书管理系统案例 (Spring MVC + Spring Data JPA + Thymeleaf)
本项目案例旨在基于先前模块学习的 Spring MVC 知识,构建一个贴近企业实际的简单 Web 应用:小型图书管理系统。通过实现图书的 CRUD 操作、列表展示(含分页概念)和简单用户认证,帮助初学者巩固和应用 Spring MVC 核心概念与技术。
1. 项目概述
- 项目主题: 小型图书管理系统 (Small Book Management System)
- 核心功能:
- 图书列表展示 (带分页概念)
- 图书详情查看
- 新增图书
- 编辑图书
- 删除图书
- 简单用户认证 (登录/注销)
- 技术栈:
- 构建工具:Maven
- Web 框架:Spring MVC 5.x
- ORM 框架:Spring Data JPA
- 数据库:H2 (内嵌数据库,便于学习)
- 模板引擎:Thymeleaf
- 数据校验:Bean Validation (JSR 380) + Hibernate Validator
- 日志:SLF4J + Logback
- 辅助库:Lombok (可选,简化 POJO 代码)
- Spring 配置方式: 完全基于 JavaConfig (对应模块一、四、六)
- 部署方式: WAR 包部署到 Servlet 容器 (如 Tomcat)
2. 环境搭建与项目结构
2.1 Maven pom.xml
配置
使用 Maven 构建项目。创建一个新的 Maven Webapp 项目,并修改 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>
<groupId>com.yourcompany</groupId>
<artifactId>book-management</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging> <!-- 打包方式为 WAR -->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<spring.version>5.3.20</spring.version> <!-- Spring 版本 -->
<thymeleaf.version>3.0.11.RELEASE</thymeleaf.version> <!-- Thymeleaf 版本 -->
<thymeleaf-spring5.version>3.0.11.RELEASE</thymeleaf-spring5.version> <!-- Thymeleaf Spring 集成 -->
<spring-data-jpa.version>2.7.2</spring-data-jpa.version> <!-- Spring Data JPA 版本 -->
<hibernate.version>5.6.1.Final</hibernate.version> <!-- Hibernate 版本 (JPA 实现) -->
<h2.version>1.4.200</h2.version> <!-- H2 数据库版本 -->
<logback.version>1.2.11</logback.version> <!-- Logback 版本 -->
<slf4j.version>1.7.36</slf4j> <!-- SLF4J 版本 -->
<servlet.api.version>4.0.1</servlet.api.version> <!-- Servlet API 版本 -->
<validation-api.version>2.0.1.Final</validation-api.version> <!-- Bean Validation API -->
<hibernate-validator.version>6.2.0.Final</hibernate-validator.version> <!-- Bean Validation 实现 -->
<lombok.version>1.18.24</lombok.version> <!-- Lombok (可选) -->
<jackson.version>2.13.0</jackson.version> <!-- Jackson (用于可能的 JSON 处理或调试) -->
</properties>
<dependencies>
<!-- Spring MVC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- Spring Context (包含 IoC/DI) -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- Spring ORM (用于 JPA 集成) -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>${spring-data-jpa.version}</version>
</dependency>
<!-- JPA Implementation (Hibernate) -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.version}</version>
</dependency>
<!-- Hibernate EntityManager (JPA 规范实现) -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>${hibernate.version}</version>
</dependency>
<!-- Database (H2 - for simplicity) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2.version}</version>
<scope>runtime</scope> <!-- 运行时需要 -->
</dependency>
<!-- Thymeleaf for Spring MVC -->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>${thymeleaf.version}</version>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>${thymeleaf-spring5.version}</version>
</dependency>
<!-- Servlet API (Provided by Tomcat/Servlet Container) -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${servlet.api.version}</version>
<scope>provided</scope>
</dependency>
<!-- JSTL (如果使用 JSP 需要) -->
<!-- Thymeleaf 不需要 JSTL,这里不添加 -->
<!-- Bean Validation API and Implementation -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>${validation-api.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator.version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
<scope>runtime</scope>
</dependency>
<!-- Jackson (for JSON processing, useful if you add REST APIs later or for debugging) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Lombok (Optional - Install Lombok plugin in IDE) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<!-- Test Dependencies (Optional) -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.2</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml> <!-- Servlet 3.0+ 可以不需要 web.xml -->
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
说明:请根据实际情况调整依赖版本,并确保它们相互兼容。本项目使用 Java 8。
2.2 项目目录结构
遵循标准的 Maven 项目结构,并在此基础上为 Spring MVC 和 Thymeleaf 组织代码和资源文件。
.
├── pom.xml
└── src
└── main
├── java
│ └── com
│ └── yourcompany
│ └── bookmanagement
│ ├── config # Spring JavaConfig 配置类 (模块一, 四, 六)
│ │ ├── AppConfig.java # Root Context 配置 (DataSource, JPA, Service, Repo)
│ │ └── WebMvcConfig.java # Servlet Context 配置 (Controller, ViewResolver, Resources, Interceptor, Validation)
│ ├── controller # 控制器层 (模块三, 五, 六)
│ │ ├── AuthController.java # 简单登录/注销
│ │ └── BookController.java # 图书 CRUD
│ ├── dto # 数据传输对象 (用于表单绑定, 校验)
│ │ └── BookDTO.java
│ ├── entity # 领域模型 (JPA 实体)
│ │ ├── Book.java
│ │ └── User.java
│ ├── exception # 自定义异常与全局异常处理 (模块六)
│ │ ├── BookNotFoundException.java
│ │ └── GlobalExceptionHandler.java
│ ├── interceptor # MVC 拦截器 (模块六)
│ │ └── AuthInterceptor.java
│ ├── repository # 持久化层 (Spring Data JPA Repository)
│ │ ├── BookRepository.java
│ │ └── UserRepository.java
│ └── service # 业务逻辑层 (模块一)
│ ├── BookService.java
│ └── impl
│ ├── BookServiceImpl.java
│ └── UserServiceImpl.java
├── resources # Spring 资源文件 (如 application.properties/yml - 本例用 JavaConfig 无需此文件, logback.xml 等)
│ └── logback.xml
└── webapp # Web 应用根目录
├── WEB-INF
│ └── templates # Thymeleaf 模板文件 (根据 WebMvcConfig 中的前缀配置)
│ ├── books
│ │ ├── list.html
│ │ ├── detail.html
│ │ └── form.html
│ └── auth
│ └── login.html
└── resources # 静态资源 (CSS, JS, Images)
└── css
└── style.css
说明:src/main/java
存放 Java 源代码,src/main/resources
存放配置和资源文件,src/main/webapp
存放 Web 相关文件,WEB-INF
下的内容不能通过浏览器直接访问,增加了安全性。Thymeleaf 模板建议放在 WEB-INF
下。
3. 领域模型与数据传输对象
3.1 Book.java
(JPA 实体)
这是应用的核心领域对象,映射数据库中的图书表。使用 JPA 注解进行数据库映射。
package com.yourcompany.bookmanagement.entity;
import javax.persistence.*; // JPA 注解
import java.time.LocalDate; // 使用新日期 API
import java.util.Objects;
// 使用 Lombok 注解简化 boilerplate code (可选)
// import lombok.Getter;
// import lombok.Setter;
// import lombok.NoArgsConstructor;
// import lombok.AllArgsConstructor;
@Entity // 标记为 JPA 实体
@Table(name = "books") // 映射到数据库表 "books"
// @Getter // Lombok 注解,自动生成所有字段的 Getter
// @Setter // Lombok 注解,自动生成所有字段的 Setter
// @NoArgsConstructor // Lombok 注解,生成无参构造器
// @AllArgsConstructor // Lombok 注解,生成全参构造器
public class Book {
@Id // 标记为主键
@GeneratedValue(strategy = GenerationType.IDENTITY) // 主键生成策略,IDENTITY 表示数据库自增长
private Long id;
@Column(nullable = false) // 映射到数据库列 "title",不能为空
private String title;
@Column(nullable = false) // 映射到数据库列 "author",不能为空
private String author;
@Column // 映射到数据库列 "isbn"
private String isbn;
@Column(name = "publication_date") // 映射到数据库列 "publication_date"
private LocalDate publicationDate; // 出版日期
// 手动添加构造器 (如果不用 Lombok 的 @NoArgsConstructor, @AllArgsConstructor)
public Book() {
}
public Book(String title, String author, String isbn, LocalDate publicationDate) {
this.title = title;
this.author = author;
this.isbn = isbn;
this.publicationDate = publicationDate;
}
// 手动添加 Getter 和 Setter (如果不用 Lombok)
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
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 getIsbn() { return isbn; }
public void setIsbn(String isbn) { this.isbn = isbn; }
public LocalDate getPublicationDate() { return publicationDate; }
public void setPublicationDate(LocalDate publicationDate) { this.publicationDate = publicationDate; }
@Override
public String toString() {
return "Book{" +
"id=" + id +
", title='" + title + '\'' +
", author='" + author + '\'' +
", isbn='" + isbn + '\'' +
", publicationDate=" + publicationDate +
'}';
}
// 实际应用中可能还需要 equals() 和 hashCode() 方法
// @Override
// public boolean equals(Object o) { ... }
// @Override
// public int hashCode() { ... }
}
3.2 BookDTO.java
(数据传输对象)
用于在 Controller 和视图之间传输数据,特别是用于接收表单输入和进行数据校验。
package com.yourcompany.bookmanagement.dto;
import javax.validation.constraints.*; // Bean Validation 注解
import java.time.LocalDate;
import java.util.Objects;
// 使用 Lombok 注解简化 boilerplate code (可选)
// import lombok.Getter;
// import lombok.Setter;
// import lombok.NoArgsConstructor;
// @Getter // Lombok
// @Setter // Lombok
// @NoArgsConstructor // Lombok
public class BookDTO {
private Long id; // 用于编辑时标识图书
@NotBlank(message = "图书标题不能为空") // 标题不能为空白字符串
@Size(max = 255, message = "图书标题长度不能超过255字符") // 标题最大长度
private String title;
@NotBlank(message = "图书作者不能为空") // 作者不能为空白字符串
@Size(max = 255, message = "图书作者长度不能超过255字符") // 作者最大长度
private String author;
@Pattern(regexp = "^(?:ISBN(?:-13)?:?)(?=[0-9]{13}$)[0-9]{3}-?[0-9]{1}-?[0-9]{3}-?[0-9]{5}-?[0-9]{1}$", message = "ISBN格式不正确") // 简单的 ISBN 格式校验
@Size(max = 20, message = "ISBN长度不能超过20字符")
private String isbn;
@PastOrPresent(message = "出版日期不能晚于今天") // 出版日期不能是未来日期
// 注意:对于 LocalDate 这种对象类型,如果字段不是必须的,不使用 @NotNull,否则即使字符串为空也会因为无法绑定为 null 而报错。
// 如果日期是必须的,则需要 @NotNull(message = "出版日期不能为空")
private LocalDate publicationDate;
// 手动添加构造器 (如果不用 Lombok 的 @NoArgsConstructor)
public BookDTO() {
}
// 手动添加 Getter 和 Setter (如果不用 Lombok)
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
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 getIsbn() { return isbn; }
public void setIsbn(String isbn) { this.isbn = isbn; }
public LocalDate getPublicationDate() { return publicationDate; }
public void setPublicationDate(LocalDate publicationDate) { this.publicationDate = publicationDate; }
@Override
public String toString() {
return "BookDTO{" +
"id=" + id +
", title='" + title + '\'' +
", author='" + author + '\'' +
", isbn='" + isbn + '\'' +
", publicationDate=" + publicationDate +
'}';
}
}
3.3 User.java
(JPA 实体, 用于简单认证)
表示用户实体,用于登录校验。
package com.yourcompany.bookmanagement.entity;
import javax.persistence.*;
import java.util.Objects;
// 使用 Lombok 注解简化 boilerplate code (可选)
// import lombok.Getter;
// import lombok.Setter;
// import lombok.NoArgsConstructor;
// import lombok.AllArgsConstructor;
@Entity
@Table(name = "users")
// @Getter // Lombok
// @Setter // Lombok
// @NoArgsConstructor // Lombok
// @AllArgsConstructor // Lombok
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true) // 用户名唯一且不能为空
private String username;
@Column(nullable = false) // 密码不能为空
private String password; // 实际应用中密码需要加密存储
// 手动添加构造器 (如果不用 Lombok)
public User() {
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
// 手动添加 Getter 和 Setter (如果不用 Lombok)
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='[PROTECTED]'" + // 不输出密码
'}';
}
}
4. 持久化层 (Spring Data JPA)
使用 Spring Data JPA 简化数据访问。只需要定义 Repository 接口,Spring Data JPA 会自动生成实现。
4.1 BookRepository.java
package com.yourcompany.bookmanagement.repository;
import com.yourcompany.bookmanagement.entity.Book;
import org.springframework.data.jpa.repository.JpaRepository; // 引入 JpaRepository
import org.springframework.stereotype.Repository; // 标记为 Repository 组件
// JpaRepository<实体类型, 主键类型>
@Repository // 标记为 Repository Bean
public interface BookRepository extends JpaRepository<Book, Long> {
// Spring Data JPA 会自动提供 CRUD 方法:save, findById, findAll, deleteById, count 等
// 也可以定义查询方法,Spring Data JPA 会根据方法名自动生成查询实现,例如:
// List<Book> findByTitleContainingIgnoreCase(String title);
// List<Book> findByAuthorContainingIgnoreCase(String author);
// 提供了分页查询功能,findAll 方法重载支持 Pageable 参数
// Page<Book> findAll(Pageable pageable);
}
4.2 UserRepository.java
package com.yourcompany.bookmanagement.repository;
import com.yourcompany.bookmanagement.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository // 标记为 Repository Bean
public interface UserRepository extends JpaRepository<User, Long> {
// 添加一个根据用户名查找用户的方法,用于登录
User findByUsername(String username);
}
4.3 JPA 配置 (在 AppConfig.java
中)
在 Root Context 的配置类中配置 DataSource, EntityManagerFactory, TransactionManager 并启用 JPA Repository 扫描。
package com.yourcompany.bookmanagement.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories; // 启用 JPA Repository
import org.springframework.jdbc.datasource.DriverManagerDataSource; // JDBC DataSource
import org.springframework.orm.jpa.JpaTransactionManager; // JPA 事务管理器
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; // EntityManagerFactory
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; // Hibernate JPA 实现
import org.springframework.transaction.PlatformTransactionManager; // 事务管理器接口
import org.springframework.transaction.annotation.EnableTransactionManagement; // 启用事务注解 @Transactional
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.Properties;
@Configuration // 标记为配置类
@EnableTransactionManagement // 启用 @Transactional 注解支持
@EnableJpaRepositories(basePackages = "com.yourcompany.bookmanagement.repository") // 扫描 Repository 接口
@ComponentScan(basePackages = "com.yourcompany.bookmanagement.service", // 扫描 Service
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class)) // 排除 WebConfig
public class AppConfig { // Root Context 配置类
// 配置数据源 (H2 嵌入式数据库)
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.h2.Driver");
dataSource.setUrl("jdbc:h2:mem:bookdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"); // 使用内存数据库
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
// 配置 JPA EntityManagerFactory (整合 Hibernate)
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dataSource());
em.setPackagesToScan("com.yourcompany.bookmanagement.entity"); // 扫描 JPA 实体所在的包
JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);
em.setJpaProperties(additionalProperties()); // 配置 JPA/Hibernate 属性
return em;
}
// 配置 JPA/Hibernate 属性
Properties additionalProperties() {
Properties properties = new Properties();
// properties.setProperty("hibernate.hbm2ddl.auto", "none"); // 数据表生成策略: none/create/create-drop/update/validate
// 首次运行时可以使用 "create" 或 "create-drop",后续开发或生产环境应使用 "none" 或 "validate"
properties.setProperty("hibernate.hbm2ddl.auto", "create-drop"); // 示例:每次启动时创建新表并插入初始化数据 (仅限演示)
properties.setProperty("hibernate.dialect", "org.hibernate.dialect.H2Dialect"); // H2 数据库方言
properties.setProperty("hibernate.show_sql", "true"); // 在控制台显示 SQL 语句
properties.setProperty("hibernate.format_sql", "true"); // 格式化 SQL 语句
// properties.setProperty("hibernate.use_sql_comments", "true");
return properties;
}
// 配置 JPA 事务管理器
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(emf);
return transactionManager;
}
// Bean PostProcessor,将 JPA 异常转换为 Spring 的 DataAccessException
// 使 Repository 层抛出的 JPA 异常被 Spring 统一处理
@Bean
public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
return new PersistenceExceptionTranslationPostProcessor();
}
// *** 示例: 在 Root Context 中初始化一些数据 (实际应用中通常有数据迁移脚本) ***
// 注意:这种方式简单,但不是处理初始化数据的标准企业实践
// 需要在一个实现了 ApplicationRunner 或 CommandLineRunner 的 Bean 中执行初始化 (通常在 Spring Boot)
// 或者使用 JPA 的 @EntityListeners 或 `@PostPersist` 等
// 对于非 Spring Boot 应用,可以在一个 Bean 的 init 方法中执行
// 简单的模拟数据插入 (仅在 hibernate.hbm2ddl.auto 设置为 create-drop 时有效)
@Bean
public Boolean initializeDatabase(BookRepository bookRepository, UserRepository userRepository) {
// 启动后延迟执行,确保 JPA EntityManagerFactory 已创建且表已生成
new Thread(() -> {
try {
Thread.sleep(2000); // 等待 JPA 初始化
if (bookRepository.count() == 0) { // 只在表为空时初始化
System.out.println(">>> Initializing Book Data...");
bookRepository.save(new Book("Spring MVC 入门", "张三", "978-7-121-XXXX-X", LocalDate.of(2022, 1, 1)));
bookRepository.save(new Book("Spring Data JPA 实践", "李四", "978-7-121-YYYY-Y", LocalDate.of(2021, 5, 15)));
System.out.println(">>> Book Data Initialized.");
}
if (userRepository.count() == 0) { // 只在表为空时初始化
System.out.println(">>> Initializing User Data...");
// 实际应用中密码需要加密
userRepository.save(new User("admin", "password")); // 简单的硬编码用户
System.out.println(">>> User Data Initialized.");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
return true; // 返回任意 Bean
}
}
5. 业务逻辑层
Service 层负责协调 Repository 和处理业务逻辑。
5.1 BookService.java
(接口)
package com.yourcompany.bookmanagement.service;
import com.yourcompany.bookmanagement.entity.Book;
import org.springframework.data.domain.Page; // 用于分页
import org.springframework.data.domain.Pageable; // 用于分页参数
import java.util.List;
import java.util.Optional;
public interface BookService {
List<Book> findAllBooks(); // 获取所有图书
Page<Book> findBooks(Pageable pageable); // 获取分页图书数据
Optional<Book> findBookById(Long id); // 根据ID查找图书
Book saveBook(Book book); // 保存/更新图书
void deleteBookById(Long id); // 根据ID删除图书
}
5.2 BookServiceImpl.java
(实现类)
使用 @Service
注解标记为 Service Bean,并通过 @Autowired
注入 BookRepository
。
package com.yourcompany.bookmanagement.service.impl;
import com.yourcompany.bookmanagement.entity.Book;
import com.yourcompany.bookmanagement.repository.BookRepository;
import com.yourcompany.bookmanagement.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; // 引入事务注解
import java.util.List;
import java.util.Optional;
@Service // 标记为 Service Bean
@Transactional // 在类级别应用事务,默认对所有 public 方法生效
public class BookServiceImpl implements BookService {
private final BookRepository bookRepository; // 注入 BookRepository
@Autowired // 构造器注入
public BookServiceImpl(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
@Override
@Transactional(readOnly = true) // 查询方法设置为只读事务
public List<Book> findAllBooks() {
return bookRepository.findAll(); // 调用 JPA Repository 提供的方法
}
@Override
@Transactional(readOnly = true) // 分页查询方法设置为只读事务
public Page<Book> findBooks(Pageable pageable) {
return bookRepository.findAll(pageable); // 调用 JPA Repository 的分页方法
}
@Override
@Transactional(readOnly = true) // 查询方法设置为只读事务
public Optional<Book> findBookById(Long id) {
return bookRepository.findById(id); // 调用 JPA Repository 提供的方法
}
@Override
// 对于保存操作,使用默认的可写事务
public Book saveBook(Book book) {
return bookRepository.save(book); // 调用 JPA Repository 提供的方法 (新增和更新都用 save)
}
@Override
// 对于删除操作,使用默认的可写事务
public void deleteBookById(Long id) {
bookRepository.deleteById(id); // 调用 JPA Repository 提供的方法
}
}
5.3 UserServiceImpl.java
(实现类, 简单认证)
实现简单的用户查找和登录校验(这里是硬编码校验)。
package com.yourcompany.bookmanagement.service.impl;
import com.yourcompany.bookmanagement.entity.User;
import com.yourcompany.bookmanagement.repository.UserRepository;
import com.yourcompany.bookmanagement.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class UserServiceImpl implements UserService {
private final UserRepository userRepository; // 注入 UserRepository
@Autowired
public UserServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
@Transactional(readOnly = true)
public User findByUsername(String username) {
return userRepository.findByUsername(username);
}
@Override
@Transactional(readOnly = true)
public boolean authenticate(String username, String password) {
User user = findByUsername(username);
// 简单校验:用户存在且密码匹配 (实际应用中密码需要加密比较)
return user != null && user.getPassword().equals(password);
}
}
5.4 UserService.java
(接口)
package com.yourcompany.bookmanagement.service;
import com.yourcompany.bookmanagement.entity.User;
public interface UserService {
User findByUsername(String username);
boolean authenticate(String username, String password);
}
6. 控制器层
控制器负责接收 HTTP 请求,调用 Service 层处理业务,并选择合适的视图或数据作为响应。
6.1 BookController.java
(图书 CRUD 控制器)
package com.yourcompany.bookmanagement.controller;
import com.yourcompany.bookmanagement.dto.BookDTO; // 引入 DTO
import com.yourcompany.bookmanagement.entity.Book; // 引入 Entity
import com.yourcompany.bookmanagement.exception.BookNotFoundException; // 引入自定义异常
import com.yourcompany.bookmanagement.service.BookService; // 引入 Service
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page; // 用于分页
import org.springframework.data.domain.PageRequest; // 用于创建 Pageable
import org.springframework.data.domain.Pageable; // 用于方法参数
import org.springframework.data.domain.Sort; // 用于排序
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; // 用于传递数据到视图
import org.springframework.validation.BindingResult; // 用于接收数据绑定和校验结果
import org.springframework.web.bind.annotation.*; // 常用注解
import org.springframework.web.servlet.mvc.support.RedirectAttributes; // 用于重定向时传递参数
import javax.validation.Valid; // 引入 @Valid 注解
import java.time.LocalDate; // 用于日期转换
import java.util.Optional;
import java.util.stream.Collectors; // 可能用于 Entity 转 DTO
@Controller // 标记为 Controller
@RequestMapping("/books") // 所有方法的基础路径
public class BookController {
private final BookService bookService; // 注入 BookService
@Autowired // 构造器注入
public BookController(BookService bookService) {
this.bookService = bookService;
}
// 显示图书列表 (含分页和排序概念)
// GET /books
@GetMapping
public String listBooks(
@RequestParam(defaultValue = "0") int page, // 当前页码,默认第0页
@RequestParam(defaultValue = "10") int size, // 每页记录数,默认10条
@RequestParam(defaultValue = "title") String sortBy, // 排序字段,默认按标题
@RequestParam(defaultValue = "asc") String sortOrder, // 排序顺序,默认升序
Model model) {
// 创建 Pageable 对象,用于传递分页和排序信息给 Service/Repository
Sort sort = Sort.by(Sort.Direction.fromString(sortOrder), sortBy);
Pageable pageable = PageRequest.of(page, size, sort);
Page<Book> bookPage = bookService.findBooks(pageable); // 调用 Service 获取分页数据
model.addAttribute("bookPage", bookPage); // 将分页数据添加到 Model
model.addAttribute("currentPage", page); // 当前页码
model.addAttribute("pageSize", size); // 每页大小
model.addAttribute("sortBy", sortBy); // 排序字段
model.addAttribute("sortOrder", sortOrder); // 排序顺序
// Thymeleaf 视图名会是 books/list.html (根据 ViewResolver 配置)
return "books/list";
}
// 显示图书详情
// GET /books/{id}
@GetMapping("/{id}")
public String showBookDetail(@PathVariable("id") Long id, Model model) {
Optional<Book> book = bookService.findBookById(id);
if (book.isPresent()) {
model.addAttribute("book", book.get()); // 将图书对象添加到 Model
return "books/detail"; // Thymeleaf 视图名 books/detail.html
} else {
// 抛出自定义异常,由全局异常处理器处理 (对应模块六)
throw new BookNotFoundException(id);
}
}
// 显示新增图书表单
// GET /books/new
@GetMapping("/new")
public String showAddBookForm(Model model) {
// 在 Model 中添加一个空的 BookDTO 对象,供表单绑定使用 (@ModelAttribute 的另一种用法)
model.addAttribute("bookDTO", new BookDTO());
return "books/form"; // Thymeleaf 视图名 books/form.html (新增和编辑使用同一个表单视图)
}
// 显示编辑图书表单
// GET /books/edit/{id}
@GetMapping("/edit/{id}")
public String showEditBookForm(@PathVariable("id") Long id, Model model) {
Optional<Book> book = bookService.findBookById(id);
if (book.isPresent()) {
Book existingBook = book.get();
// 将 Entity 对象转换为 DTO 对象,用于填充表单
BookDTO bookDTO = new BookDTO();
bookDTO.setId(existingBook.getId());
bookDTO.setTitle(existingBook.getTitle());
bookDTO.setAuthor(existingBook.getAuthor());
bookDTO.setIsbn(existingBook.getIsbn());
bookDTO.setPublicationDate(existingBook.getPublicationDate()); // 直接设置 LocalDate
model.addAttribute("bookDTO", bookDTO); // 将填充好的 DTO 添加到 Model
return "books/form"; // Thymeleaf 视图名 books/form.html
} else {
throw new BookNotFoundException(id);
}
}
// 处理新增或编辑图书表单提交
// POST /books
// 使用 @ModelAttribute 绑定表单数据到 BookDTO
// 使用 @Valid 进行数据校验
// 使用 BindingResult 获取校验结果
// 使用 RedirectAttributes 在重定向后传递消息
@PostMapping
public String saveBook(@ModelAttribute("bookDTO") @Valid BookDTO bookDTO, // @Valid 启用校验,BindingResult 紧随其后
BindingResult bindingResult, // 校验结果会存储在这里
RedirectAttributes redirectAttributes, // 用于重定向传参
Model model) {
// 检查数据校验结果
if (bindingResult.hasErrors()) {
System.out.println("Validation errors: " + bindingResult.getAllErrors());
// 如果有错误,返回到表单页面,错误信息会自动添加到 Model 中供 Thymeleaf th:errors 显示
return "books/form";
}
// 将 DTO 转换为 Entity
Book book = new Book();
book.setId(bookDTO.getId()); // 如果是编辑,ID 不为 null
book.setTitle(bookDTO.getTitle());
book.setAuthor(bookDTO.getAuthor());
book.setIsbn(bookDTO.getIsbn());
book.setPublicationDate(bookDTO.getPublicationDate());
// 调用 Service 保存图书
Book savedBook = bookService.saveBook(book);
// 使用 RedirectAttributes 在重定向后显示成功消息
redirectAttributes.addFlashAttribute("successMessage", "图书信息保存成功!");
// 重定向到图书详情页或列表页
// 重定向到详情页: return "redirect:/books/" + savedBook.getId();
// 重定向到列表页:
return "redirect:/books"; // 对应模块六的重定向
}
// 处理删除图书请求
// POST /books/delete/{id} 或 DELETE /books/{id} (POST 更兼容浏览器)
@PostMapping("/delete/{id}")
public String deleteBook(@PathVariable("id") Long id, RedirectAttributes redirectAttributes) {
// 检查图书是否存在 (可选,Service 层的 delete 方法可能抛异常)
Optional<Book> book = bookService.findBookById(id);
if (!book.isPresent()) {
throw new BookNotFoundException(id);
}
bookService.deleteBookById(id); // 调用 Service 删除图书
redirectAttributes.addFlashAttribute("successMessage", "图书删除成功!");
return "redirect:/books"; // 重定向到图书列表页
}
/*
* 示例:使用 @ModelAttribute 方法为 Model 预填充数据
* @ModelAttribute("genres")
* public List<String> populateGenres() {
* return Arrays.asList("小说", "技术", "历史");
* }
* // 这样在所有由这个 Controller 处理的请求中,Model 都会有一个名为 "genres" 的属性
*/
}
6.2 AuthController.java
(简单认证控制器)
处理登录页面的显示和登录逻辑。
package com.yourcompany.bookmanagement.controller;
import com.yourcompany.bookmanagement.service.UserService; // 引入 UserService
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import javax.servlet.http.HttpSession; // 引入 HttpSession
@Controller // 标记为 Controller
@RequestMapping("/") // 登录相关通常在根路径或 /auth 路径下
public class AuthController {
private final UserService userService; // 注入 UserService
@Autowired // 构造器注入
public AuthController(UserService userService) {
this.userService = userService;
}
// 显示登录页面
// GET /login
@GetMapping("/login")
public String showLoginForm(@RequestParam(value = "error", required = false) String error, Model model) {
if (error != null) {
model.addAttribute("errorMessage", "用户名或密码不正确。"); // 如果登录失败,显示错误消息
}
return "auth/login"; // Thymeleaf 视图名 auth/login.html
}
// 处理登录请求
// POST /login
@PostMapping("/login")
public String processLogin(@RequestParam String username,
@RequestParam String password,
HttpSession session) { // 注入 HttpSession
if (userService.authenticate(username, password)) {
// 认证成功,将用户信息存储到 Session (这里只存用户名)
session.setAttribute("loggedInUser", username);
// 重定向到图书列表页
return "redirect:/books";
} else {
// 认证失败,重定向回登录页,并附带错误参数
return "redirect:/login?error";
}
}
// 处理注销请求
// GET /logout 或 POST /logout
@GetMapping("/logout")
public String logout(HttpSession session) {
// 使当前 Session 无效
session.invalidate();
// 重定向到登录页
return "redirect:/login?logout"; // 可以附带 logout 参数表示已注销
}
}
7. 数据校验 (Bean Validation)
结合 Bean Validation API 和 Hibernate Validator 实现数据校验。
- 添加依赖: 已在
pom.xml
中添加validation-api
和hibernate-validator
。 - 在 DTO 中添加注解: 在
BookDTO.java
中使用了@NotBlank
,@Size
,@Pattern
,@PastOrPresent
等注解。 - 在 Controller 中启用校验: 在
saveBook
方法的BookDTO
参数前添加@Valid
注解,并在其后紧跟BindingResult
参数。 - 配置 Validator: 在
WebMvcConfig.java
中配置LocalValidatorFactoryBean
Bean。
// 在 WebMvcConfig.java 中添加
import org.springframework.context.annotation.Bean;
import org.springframework.validation.Validator; // 引入 Validator 接口
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; // Bean Validation Validator
// ... 其他导入和类定义
// 在 WebMvcConfig 类中
@Override
public Validator getValidator() {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
// 可以配置 ValidationProviderResolver, MessageSource 等
// validator.setValidationMessageSource(messageSource()); // 例如,配置国际化错误消息
return validator;
}
- 在 Thymeleaf 视图中显示错误: 在表单视图 (
form.html
) 中使用th:errors
标签显示校验错误信息。
<!-- 在 WEB-INF/templates/books/form.html 中 -->
<form th:object="${bookDTO}" th:action="@{/books}" method="post">
<!-- ...其他字段 -->
<div>
<label for="title">标题:</label>
<input type="text" id="title" th:field="*{title}"/>
<!-- 显示 title 字段的校验错误 -->
<span th:if="${#fields.hasErrors('title')}" th:errors="*{title}" style="color: red;">Title Error</span>
</div>
<div>
<label for="author">作者:</label>
<input type="text" id="author" th:field="*{author}"/>
<!-- 显示 author 字段的校验错误 -->
<span th:if="${#fields.hasErrors('author')}" th:errors="*{author}" style="color: red;">Author Error</span>
</div>
<!-- ...其他字段 -->
</form>
8. 视图层 (Thymeleaf)
使用 Thymeleaf 作为模板引擎渲染视图。
8.1 Thymeleaf 配置 (在 WebMvcConfig.java
中)
在 Servlet Context 的配置类中配置 Thymeleaf 相关的 Bean。
package com.yourcompany.bookmanagement.config;
import com.yourcompany.bookmanagement.interceptor.AuthInterceptor; // 引入认证拦截器
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.*; // 引入 WebMvcConfigurer 相关注解和类
import org.springframework.validation.Validator; // 引入 Validator
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; // 引入 Bean Validation 实现
import org.thymeleaf.spring5.SpringTemplateEngine; // Thymeleaf 模板引擎
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver; // Thymeleaf 资源解析器
import org.thymeleaf.spring5.view.ThymeleafViewResolver; // Thymeleaf 视图解析器
import org.thymeleaf.templatemode.TemplateMode; // 模板模式
@Configuration // 标记为配置类
@EnableWebMvc // 启用 Spring MVC 注解驱动功能 (对应模块一, 四, 五)
@ComponentScan(basePackages = "com.yourcompany.bookmanagement.controller") // 扫描 Controller
public class WebMvcConfig implements WebMvcConfigurer, ApplicationContextAware { // 实现 WebMvcConfigurer 扩展 MVC 配置,实现 ApplicationContextAware 获取 ApplicationContext
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
// 配置模板资源解析器 (对应模块四)
@Bean
public SpringResourceTemplateResolver templateResolver() {
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setApplicationContext(this.applicationContext);
templateResolver.setPrefix("/WEB-INF/templates/"); // Thymeleaf 模板文件存放路径
templateResolver.setSuffix(".html"); // 模板后缀
templateResolver.setTemplateMode(TemplateMode.HTML); // 模板模式为 HTML
templateResolver.setCharacterEncoding("UTF-8"); // 设置编码
templateResolver.setCacheable(false); // 开发时建议关闭缓存,方便修改模板后查看效果
return templateResolver;
}
// 配置模板引擎 (对应模块四)
@Bean
public SpringTemplateEngine templateEngine() {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver()); // 设置模板资源解析器
templateEngine.setEnableSpringELCompiler(true); // 启用 Spring EL 表达式
// 可以添加 Thymeleaf 的布局方言等,用于更复杂的模板布局
// templateEngine.addDialect(new LayoutDialect());
return templateEngine;
}
// 配置视图解析器 (对应模块四)
@Bean
public ViewResolver thymeleafViewResolver() {
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(templateEngine()); // 设置模板引擎
viewResolver.setCharacterEncoding("UTF-8"); // 设置编码
// 可以设置 order 属性,如果存在多个 ViewResolver
// viewResolver.setOrder(1);
return viewResolver;
}
// 配置静态资源处理 (对应模块一)
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
// 例如,CSS 文件放在 src/main/webapp/resources/css 下,可以通过 /resources/css/style.css 访问
}
// 配置默认 Servlet 处理,转发对静态资源的请求到容器默认的 Servlet
// 通常 @EnableWebMvc 会自动处理,但明确配置可以避免问题
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
// 配置 Bean Validation Validator (对应本模块数据校验)
@Bean
@Override
public Validator getValidator() {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
return validator;
}
// 配置拦截器 (对应模块六)
@Bean
public AuthInterceptor authInterceptor() {
return new AuthInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor()) // 添加认证拦截器实例
.addPathPatterns("/**") // 拦截所有路径
.excludePathPatterns("/login", "/logout", "/resources/**", "/webjars/**"); // 排除登录、注销、静态资源、WebJars 路径
}
}
8.2 视图模板 (.html)
在 src/main/webapp/WEB-INF/templates
目录下创建对应的 html 文件。
-
WEB-INF/templates/books/list.html
(图书列表)<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}"> <!-- 可选:使用布局模板 --> <head> <title>图书列表</title> </head> <body> <div layout:fragment="content"> <!-- 可选:布局模板内容片段 --> <h1>图书列表</h1> <!-- 显示保存/删除成功消息 --> <div th:if="${successMessage}" style="color: green; margin-bottom: 10px;"> <p th:text="${successMessage}"></p> </div> <table> <thead> <tr> <th>ID</th> <th><a th:href="@{/books(page=${currentPage}, size=${pageSize}, sortBy='title', sortOrder=${sortBy == 'title' ? (sortOrder == 'asc' ? 'desc' : 'asc') : 'asc'})}">标题</a></th> <th><a th:href="@{/books(page=${currentPage}, size=${pageSize}, sortBy='author', sortOrder=${sortBy == 'author' ? (sortOrder == 'asc' ? 'desc' : 'asc') : 'asc'})}">作者</a></th> <th>ISBN</th> <th>出版日期</th> <th>操作</th> </tr> </thead> <tbody> <!-- 使用 th:each 遍历 Model 中的 bookPage.content (当前页的图书列表) --> <tr th:each="book : ${bookPage.content}"> <td th:text="${book.id}">1</td> <td th:text="${book.title}">书名</td> <td th:text="${book.author}">作者</td> <td th:text="${book.isbn}">ISBN</td> <td th:text="${book.publicationDate}">出版日期</td> <td> <!-- th:href 生成 URL,使用 @{...} 语法 --> <a th:href="@{/books/{id}(id=${book.id})}">详情</a> | <a th:href="@{/books/edit/{id}(id=${book.id})}">编辑</a> | <!-- 删除操作使用 POST 请求 --> <form th:action="@{/books/delete/{id}(id=${book.id})}" method="post" style="display: inline;"> <button type="submit" onclick="return confirm('确定删除吗?');" style="color: blue; background: none; border: none; cursor: pointer; text-decoration: underline; padding: 0;">删除</button> </form> </td> </tr> </tbody> </table> <!-- 分页链接 --> <div> <span th:text="'共 ' + ${bookPage.totalElements} + ' 条记录'"></span> <span th:text="' | 共 ' + ${bookPage.totalPages} + ' 页'"></span> <span th:text="' | 当前第 ' + ${bookPage.number + 1} + ' 页'"></span> <!-- 导航链接 --> <span th:if="${bookPage.hasPrevious()}"> <a th:href="@{/books(page=${bookPage.number - 1}, size=${pageSize}, sortBy=${sortBy}, sortOrder=${sortOrder})}">上一页</a> </span> <span th:if="${bookPage.hasNext()}"> <a th:href="@{/books(page=${bookPage.number + 1}, size=${pageSize}, sortBy=${sortBy}, sortOrder=${sortOrder})}">下一页</a> </span> <!-- 可以添加首页、尾页、页码列表等更复杂的分页控件 --> </div> <p><a th:href="@{/books/new}">新增图书</a></p> <p><a th:href="@{/logout}">注销</a></p> </div> </body> </html>
-
WEB-INF/templates/books/detail.html
(图书详情)<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}"> <head> <title th:text="${book.title} + ' - 图书详情'">图书详情</title> </head> <body> <div layout:fragment="content"> <h1 th:text="${book.title}">图书详情</h1> <p><strong>ID:</strong> <span th:text="${book.id}">1</span></p> <p><strong>标题:</strong> <span th:text="${book.title}">书名</span></p> <p><strong>作者:</strong> <span th:text="${book.author}">作者</span></p> <p><strong>ISBN:</strong> <span th:text="${book.isbn}">ISBN</span></p> <p><strong>出版日期:</strong> <span th:text="${book.publicationDate}">出版日期</span></p> <p> <a th:href="@{/books/edit/{id}(id=${book.id})}">编辑</a> | <a th:href="@{/books}">返回列表</a> </p> <p><a th:href="@{/logout}">注销</a></p> </div> </body> </html>
-
WEB-INF/templates/books/form.html
(新增/编辑表单)<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}"> <head> <title th:text="${bookDTO.id == null ? '新增图书' : '编辑图书'}">图书表单</title> <style> /* 简单的错误样式 */ .error-message { color: red; font-size: 0.9em; } input.is-invalid, textarea.is-invalid { border-color: red; } </style> </head> <body> <div layout:fragment="content"> <h1 th:text="${bookDTO.id == null ? '新增图书' : '编辑图书'}">图书表单</h1> <!-- th:object 指定要绑定的对象,th:action 指定表单提交的 URL --> <form th:object="${bookDTO}" th:action="@{/books}" method="post"> <!-- 对于编辑操作,需要提交图书 ID --> <input type="hidden" th:field="*{id}"/> <div> <label for="title">标题:</label> <!-- th:field 绑定输入框到对象的属性 --> <!-- 通过 th:errorclass 根据是否有校验错误添加 CSS 类 --> <input type="text" id="title" th:field="*{title}" th:errorclass="is-invalid"/> <!-- 显示 title 字段的校验错误 --> <span th:if="${#fields.hasErrors('title')}" th:errors="*{title}" class="error-message">Title Error</span> </div> <div> <label for="author">作者:</label> <input type="text" id="author" th:field="*{author}" th:errorclass="is-invalid"/> <!-- 显示 author 字段的校验错误 --> <span th:if="${#fields.hasErrors('author')}" th:errors="*{author}" class="error-message">Author Error</span> </div> <div> <label for="isbn">ISBN:</label> <input type="text" id="isbn" th:field="*{isbn}" th:errorclass="is-invalid"/> <!-- 显示 isbn 字段的校验错误 --> <span th:if="${#fields.hasErrors('isbn')}" th:errors="*{isbn}" class="error-message">ISBN Error</span> </div> <div> <label for="publicationDate">出版日期:</label> <!-- 注意:HTML input type="date" 返回字符串 "YYYY-MM-DD",Spring MVC 会自动绑定到 LocalDate --> <input type="date" id="publicationDate" th:field="*{publicationDate}" th:errorclass="is-invalid"/> <!-- 显示 publicationDate 字段的校验错误 --> <span th:if="${#fields.hasErrors('publicationDate')}" th:errors="*{publicationDate}" class="error-message">Date Error</span> </div> <div> <button type="submit" th:text="${bookDTO.id == null ? '新增' : '保存'}">提交</button> <a th:href="@{/books}">取消</a> </div> </form> <p><a th:href="@{/logout}">注销</a></p> </div> </body> </html>
-
WEB-INF/templates/auth/login.html
(登录页面)<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>用户登录</title> <style> .error-message { color: red; } </style> </head> <body> <h1>用户登录</h1> <!-- 显示错误消息 --> <div th:if="${errorMessage}" class="error-message"> <p th:text="${errorMessage}"></p> </div> <!-- 显示注销成功消息 (如果从 /logout 重定向过来) --> <div th:if="${param.logout}" style="color: green;"> <p>您已成功注销。</p> </div> <!-- 登录表单,提交到 /login --> <form th:action="@{/login}" method="post"> <div> <label for="username">用户名:</label> <input type="text" id="username" name="username" required/> </div> <div> <label for="password">密码:</label> <input type="password" id="password" name="password" required/> </div> <div> <button type="submit">登录</button> </div> </form> </body> </html>
-
WEB-INF/templates/layout.html
(可选,布局模板)为了简化页面结构和维护,可以定义一个布局模板。使用 Thymeleaf Layout Dialect (需要添加到
pom.xml
和WebMvcConfig
)。<!-- pom.xml 添加 --> <dependency> <groupId>nz.net.ultraq.thymeleaf</groupId> <artifactId>thymeleaf-layout-dialect</artifactId> <version>2.5.3</version> <!-- 或更高兼容版本 --> </dependency>
// WebMvcConfig.java 添加 import nz.net.ultraq.thymeleaf.layoutdialect.LayoutDialect; // 在 templateEngine() Bean 方法中添加 templateEngine.addDialect(new LayoutDialect());
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> <head> <meta charset="UTF-8"> <title layout:title-pattern="$LAYOUT_TITLE | $CONTENT_TITLE">图书管理系统</title> <link rel="stylesheet" th:href="@{/resources/css/style.css}"> <!-- 其他头部内容 --> </head> <body> <header> <h1>图书管理系统</h1> <!-- 导航或其他头部内容 --> </header> <main layout:fragment="content"> <!-- 页面内容会在这里插入 --> <p>页面内容区域</p> </main> <footer> <p>© 2023 Your Company</p> </footer> </body> </html>
其他页面通过
<html layout:decorate="~{layout}">
和<div layout:fragment="content">...</div>
来使用布局。
9. Spring 配置 (JavaConfig)
使用 Java 类代替 XML 文件进行 Spring 和 Spring MVC 的配置。
9.1 MyWebAppInitializer.java
(Servlet 容器初始化)
替代 web.xml
配置 DispatcherServlet
,对应模块一。
package com.yourcompany.bookmanagement.config;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; // 引入抽象基类
// 继承 AbstractAnnotationConfigDispatcherServletInitializer
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
// 配置 Root Context (非 Web 层 Bean)
@Override
protected Class<?>[] getRootConfigClasses() {
// 通常用于配置 Service, Repository, DataSource, TransactionManager 等
return new Class<?>[]{AppConfig.class}; // 加载 AppConfig
}
// 配置 Servlet Context (Web 层 Bean)
@Override
protected Class<?>[] getServletConfigClasses() {
// 通常用于配置 Controller, ViewResolver, ResourceHandler, Interceptor 等
return new Class<?>[]{WebMvcConfig.class}; // 加载 WebMvcConfig
}
// 配置 DispatcherServlet 的映射路径
@Override
protected String[] getServletMappings() {
// "/" 表示 DispatcherServlet 拦截所有请求 (除容器默认处理的,如 .jsp)
return new String[]{"/"};
}
// 可选:配置 DispatcherServlet 名称
// @Override
// protected String getServletName() {
// return "dispatcher";
// }
}
说明:Servlet 容器启动时会自动查找实现了 ServletContainerInitializer
接口的类,而 AbstractAnnotationConfigDispatcherServletInitializer
间接实现了这个接口,从而完成了 DispatcherServlet 的注册和 Spring 容器的加载。
9.2 AppConfig.java
(Root Context 配置)
已在 JPA 配置部分给出代码。主要配置非 Web 层的 Bean,如 DataSource, JPA/Hibernate, Spring Data JPA, Service。
9.3 WebMvcConfig.java
(Servlet Context 配置)
已在 Thymeleaf 和数据校验配置部分给出代码。主要配置 Web 层的 Bean,如 Controller 扫描、ViewResolver、资源处理、Validator、Interceptor。
10. 拦截器 (简单认证)
实现一个简单的拦截器检查用户是否登录,对应模块六。
10.1 AuthInterceptor.java
package com.yourcompany.bookmanagement.interceptor;
import org.springframework.web.servlet.HandlerInterceptor; // 引入拦截器接口
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession; // 引入 HttpSession
public class AuthInterceptor implements HandlerInterceptor {
// 在 Controller 方法执行前调用
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取当前请求的路径
String requestURI = request.getRequestURI();
System.out.println("Intercepting request: " + requestURI);
// 获取 Session
HttpSession session = request.getSession();
// 检查 Session 中是否存在 loggedInUser 属性
Object user = session.getAttribute("loggedInUser");
if (user != null) {
// 用户已登录,继续执行后续流程 (到 Controller 方法)
System.out.println("User is logged in. Continue request.");
return true;
} else {
// 用户未登录
System.out.println("User is NOT logged in. Redirecting to login page.");
// 重定向到登录页面
// 注意:这里需要使用 sendRedirect,并且路径是相对于 contextPath 的
response.sendRedirect(request.getContextPath() + "/login");
return false; // 阻止当前请求继续处理
}
}
// 在 Controller 方法执行后,视图渲染前调用
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// System.out.println("AuthInterceptor postHandle...");
// 可以在这里修改 Model 或 View
}
// 在整个请求处理完成后调用 (包括视图渲染后)
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// System.out.println("AuthInterceptor afterCompletion...");
// 用于清理资源等
}
}
10.2 拦截器配置 (在 WebMvcConfig.java
中)
已在 Thymeleaf 配置部分给出代码。在 WebMvcConfig
中定义 AuthInterceptor
Bean,并在 addInterceptors
方法中注册并配置拦截规则 (addPathPatterns
, excludePathPatterns
)。
11. 统一异常处理
使用 @ControllerAdvice
和 @ExceptionHandler
实现全局异常处理,对应模块六。
11.1 BookNotFoundException.java
(自定义异常)
package com.yourcompany.bookmanagement.exception;
// 自定义异常,继承 RuntimeException
public class BookNotFoundException extends RuntimeException {
public BookNotFoundException(Long id) {
super("Book not found with ID: " + id);
}
}
11.2 GlobalExceptionHandler.java
(@ControllerAdvice)
package com.yourcompany.bookmanagement.exception;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus; // 引入 HTTP 状态码
import org.springframework.web.bind.annotation.ControllerAdvice; // 引入 @ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler; // 引入 @ExceptionHandler
import org.springframework.web.bind.annotation.ResponseStatus; // 引入 @ResponseStatus
import org.springframework.web.servlet.ModelAndView; // 用于返回错误视图
// @ControllerAdvice 应用于所有 Controller
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); // 记录日志
// 处理 BookNotFoundException 异常
@ExceptionHandler(BookNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND) // 设置响应状态码为 404
public ModelAndView handleBookNotFound(BookNotFoundException ex) {
logger.warn("Book not found: " + ex.getMessage()); // 记录警告日志
ModelAndView mav = new ModelAndView("error/404"); // 返回错误视图 error/404.html
mav.addObject("message", ex.getMessage()); // 将错误信息添加到 Model
return mav;
}
// 处理所有其他未捕获的 Exception
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 设置响应状态码为 500
public ModelAndView handleAllExceptions(Exception ex) {
logger.error("Internal Server Error: ", ex); // 记录错误日志
ModelAndView mav = new ModelAndView("error/500"); // 返回错误视图 error/500.html
mav.addObject("message", "Internal Server Error. Please try again later.");
// 在开发环境中,可以添加更详细的错误信息:
// mav.addObject("details", ex.getMessage());
return mav;
}
/*
* 可以添加更多针对特定异常类型的处理方法,例如:
* @ExceptionHandler(MethodArgumentNotValidException.class) // 处理 @RequestBody 参数校验失败
* @ResponseStatus(HttpStatus.BAD_REQUEST)
* @ResponseBody // 通常用于 REST API 返回 JSON
* public ErrorResponse handleValidationExceptions(MethodArgumentNotValidException ex) {
* // 构建并返回包含所有校验错误的响应体
* }
*/
}
需要创建对应的错误视图文件,例如 WEB-INF/templates/error/404.html
和 WEB-INF/templates/error/500.html
。
-
WEB-INF/templates/error/404.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>资源未找到 (404)</title> </head> <body> <h1>404 资源未找到</h1> <p th:text="${message != null ? message : '您请求的资源不存在。'}">您请求的资源不存在。</p> <p><a th:href="@{/}">返回首页</a></p> </body> </html>
-
WEB-INF/templates/error/500.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>内部服务器错误 (500)</title> </head> <body> <h1>500 内部服务器错误</h1> <p th:text="${message != null ? message : '服务器处理您的请求时发生错误,请稍后重试。'}">服务器处理您的请求时发生错误,请稍后重试。</p> <!-- 在开发时可以显示更多错误详情 --> <!-- <p th:if="${details != null}" th:text="'详情: ' + ${details}"></p> --> <p><a th:href="@{/}">返回首页</a></p> </body> </html>
12. 运行与部署
12.1 Maven 构建 WAR 包
在项目根目录打开终端,执行 Maven 命令:
mvn clean package
构建成功后,会在项目的 target
目录下生成 book-management-1.0-SNAPSHOT.war
文件。
12.2 部署到 Servlet 容器
将生成的 .war
文件复制到 Tomcat (或其他 Servlet 容器) 的 webapps
目录下。启动 Tomcat,它会自动解压并部署 WAR 包。
12.3 访问应用
部署成功后,可以通过浏览器访问应用。默认情况下,应用的 URL 结构为:
http://localhost:8080/book-management/
或者,如果部署为 ROOT 应用(将 war 包重命名为 ROOT.war
),则为:
http://localhost:8080/
- 登录页面:
http://localhost:8080/book-management/login
- 图书列表:
http://localhost:8080/book-management/books
(需要先登录)
使用用户名 admin
和密码 password
进行登录。
13. 总结与扩展
通过这个小型图书管理系统案例,我们实践了 Spring MVC 在企业应用中的典型用法,包括:
- 使用 Maven 管理项目和依赖。
- 采用 JavaConfig 进行 Spring 和 Spring MVC 的配置。
- 结合 Spring Data JPA 实现数据持久化。
- 使用 Service 层封装业务逻辑。
- 编写 Controller 处理 Web 请求,使用
@RequestMapping
系列注解进行请求映射。 - 通过
@ModelAttribute
和@RequestParam
获取请求参数。 - 利用 Bean Validation 进行数据校验,并在视图层展示错误。
- 使用 Thymeleaf 模板引擎渲染动态 HTML 页面,展示 Model 数据。
- 实现简单的用户认证拦截器,保护页面访问。
- 实现全局异常处理,提升应用健壮性。
- 理解并应用了重定向 (
redirect:
) 和转发 (默认) 的页面跳转方式。 - 实现了基本的图书 CRUD 和列表(含分页概念和排序)。
进一步学习和扩展方向:
- 完善分页功能: 在列表页面添加完整的页码导航、每页显示数量选择等。
- 搜索功能: 在 Repository 层添加自定义查询方法,在 Controller 层接收搜索参数,在 Service 层调用,并在列表页面展示搜索结果。
- 文件上传: 添加图书封面图片上传功能 (参考模块六文件上传)。
- Spring Security: 将简单的认证机制替换为更强大和安全的 Spring Security 框架,实现更复杂的权限控制 (如不同角色用户)。
- RESTful API: 除了基于视图的 Web 应用,可以为图书管理功能添加 RESTful API 接口 (使用
@RestController
和@RequestBody
/@ResponseBody
),供前端应用或第三方系统调用。 - 国际化 (i18n): 为应用添加多语言支持 (Spring MVC 提供了
LocaleResolver
等组件)。 - 缓存: 引入 Spring Cache 或其他缓存方案优化数据访问性能。
- 日志增强: 配置更详细和灵活的日志策略。
- 测试: 编写单元测试和集成测试,确保代码质量。
- Spring Boot: 在熟悉了原生 Spring MVC 后,强烈建议学习 Spring Boot,它能极大地简化 Spring 应用的开发和部署。本项目案例很容易迁移到 Spring Boot。
希望这个案例能帮助你更好地理解和掌握 Spring MVC!
快速回顾Spring MVC基础知识
速通Spring MVC ,一篇就够
企业级实用技术讲解
企业级Spring MVC高级主题与实用技术讲解