小型图书管理系统案例(用于spring mvc 实践)
小型图书管理系统案例 (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; }@Overridepublic 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; }@Overridepublic 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; }@Overridepublic 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", // 扫描 ServiceexcludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class)) // 排除 WebConfig
public class AppConfig { // Root Context 配置类// 配置数据源 (H2 嵌入式数据库)@Beanpublic 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)@Beanpublic 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 事务管理器@Beanpublic PlatformTransactionManager transactionManager(EntityManagerFactory emf) {JpaTransactionManager transactionManager = new JpaTransactionManager();transactionManager.setEntityManagerFactory(emf);return transactionManager;}// Bean PostProcessor,将 JPA 异常转换为 Spring 的 DataAccessException// 使 Repository 层抛出的 JPA 异常被 Spring 统一处理@Beanpublic PersistenceExceptionTranslationPostProcessor exceptionTranslation() {return new PersistenceExceptionTranslationPostProcessor();}// *** 示例: 在 Root Context 中初始化一些数据 (实际应用中通常有数据迁移脚本) ***// 注意:这种方式简单,但不是处理初始化数据的标准企业实践// 需要在一个实现了 ApplicationRunner 或 CommandLineRunner 的 Bean 中执行初始化 (通常在 Spring Boot)// 或者使用 JPA 的 @EntityListeners 或 `@PostPersist` 等// 对于非 Spring Boot 应用,可以在一个 Bean 的 init 方法中执行// 简单的模拟数据插入 (仅在 hibernate.hbm2ddl.auto 设置为 create-drop 时有效)@Beanpublic 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@Autowiredpublic 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@GetMappingpublic 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/RepositorySort 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); // 将分页数据添加到 Modelmodel.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()); // 将图书对象添加到 Modelreturn "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()); // 直接设置 LocalDatemodel.addAttribute("bookDTO", bookDTO); // 将填充好的 DTO 添加到 Modelreturn "books/form"; // Thymeleaf 视图名 books/form.html} else {throw new BookNotFoundException(id);}}// 处理新增或编辑图书表单提交// POST /books// 使用 @ModelAttribute 绑定表单数据到 BookDTO// 使用 @Valid 进行数据校验// 使用 BindingResult 获取校验结果// 使用 RedirectAttributes 在重定向后传递消息@PostMappingpublic 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 转换为 EntityBook book = new Book();book.setId(bookDTO.getId()); // 如果是编辑,ID 不为 nullbook.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) { // 注入 HttpSessionif (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 获取 ApplicationContextprivate ApplicationContext applicationContext;@Overridepublic void setApplicationContext(ApplicationContext applicationContext) {this.applicationContext = applicationContext;}// 配置模板资源解析器 (对应模块四)@Beanpublic SpringResourceTemplateResolver templateResolver() {SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();templateResolver.setApplicationContext(this.applicationContext);templateResolver.setPrefix("/WEB-INF/templates/"); // Thymeleaf 模板文件存放路径templateResolver.setSuffix(".html"); // 模板后缀templateResolver.setTemplateMode(TemplateMode.HTML); // 模板模式为 HTMLtemplateResolver.setCharacterEncoding("UTF-8"); // 设置编码templateResolver.setCacheable(false); // 开发时建议关闭缓存,方便修改模板后查看效果return templateResolver;}// 配置模板引擎 (对应模块四)@Beanpublic SpringTemplateEngine templateEngine() {SpringTemplateEngine templateEngine = new SpringTemplateEngine();templateEngine.setTemplateResolver(templateResolver()); // 设置模板资源解析器templateEngine.setEnableSpringELCompiler(true); // 启用 Spring EL 表达式// 可以添加 Thymeleaf 的布局方言等,用于更复杂的模板布局// templateEngine.addDialect(new LayoutDialect());return templateEngine;}// 配置视图解析器 (对应模块四)@Beanpublic ViewResolver thymeleafViewResolver() {ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();viewResolver.setTemplateEngine(templateEngine()); // 设置模板引擎viewResolver.setCharacterEncoding("UTF-8"); // 设置编码// 可以设置 order 属性,如果存在多个 ViewResolver// viewResolver.setOrder(1);return viewResolver;}// 配置静态资源处理 (对应模块一)@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");// 例如,CSS 文件放在 src/main/webapp/resources/css 下,可以通过 /resources/css/style.css 访问}// 配置默认 Servlet 处理,转发对静态资源的请求到容器默认的 Servlet// 通常 @EnableWebMvc 会自动处理,但明确配置可以避免问题@Overridepublic void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {configurer.enable();}// 配置 Bean Validation Validator (对应本模块数据校验)@Bean@Overridepublic Validator getValidator() {LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();return validator;}// 配置拦截器 (对应模块六)@Beanpublic AuthInterceptor authInterceptor() {return new AuthInterceptor();}@Overridepublic 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)@Overrideprotected Class<?>[] getRootConfigClasses() {// 通常用于配置 Service, Repository, DataSource, TransactionManager 等return new Class<?>[]{AppConfig.class}; // 加载 AppConfig}// 配置 Servlet Context (Web 层 Bean)@Overrideprotected Class<?>[] getServletConfigClasses() {// 通常用于配置 Controller, ViewResolver, ResourceHandler, Interceptor 等return new Class<?>[]{WebMvcConfig.class}; // 加载 WebMvcConfig}// 配置 DispatcherServlet 的映射路径@Overrideprotected 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; // 引入 HttpSessionpublic class AuthInterceptor implements HandlerInterceptor {// 在 Controller 方法执行前调用@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 获取当前请求的路径String requestURI = request.getRequestURI();System.out.println("Intercepting request: " + requestURI);// 获取 SessionHttpSession 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 方法执行后,视图渲染前调用@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {// System.out.println("AuthInterceptor postHandle...");// 可以在这里修改 Model 或 View}// 在整个请求处理完成后调用 (包括视图渲染后)@Overridepublic 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) // 设置响应状态码为 404public ModelAndView handleBookNotFound(BookNotFoundException ex) {logger.warn("Book not found: " + ex.getMessage()); // 记录警告日志ModelAndView mav = new ModelAndView("error/404"); // 返回错误视图 error/404.htmlmav.addObject("message", ex.getMessage()); // 将错误信息添加到 Modelreturn mav;}// 处理所有其他未捕获的 Exception@ExceptionHandler(Exception.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 设置响应状态码为 500public ModelAndView handleAllExceptions(Exception ex) {logger.error("Internal Server Error: ", ex); // 记录错误日志ModelAndView mav = new ModelAndView("error/500"); // 返回错误视图 error/500.htmlmav.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高级主题与实用技术讲解