本项目案例旨在基于先前模块学习的 Spring MVC 知识,构建一个贴近企业实际的简单 Web 应用:小型图书管理系统。通过实现图书的 CRUD 操作、列表展示(含分页概念)和简单用户认证,帮助初学者巩固和应用 Spring MVC 核心概念与技术。
pom.xml
配置使用 Maven 构建项目。创建一个新的 Maven Webapp 项目,并修改 pom.xml
文件,添加以下核心依赖:
<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.0modelVersion>
<groupId>com.yourcompanygroupId>
<artifactId>book-managementartifactId>
<version>1.0-SNAPSHOTversion>
<packaging>warpackaging>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<maven.compiler.source>1.8maven.compiler.source>
<maven.compiler.target>1.8maven.compiler.target>
<spring.version>5.3.20spring.version>
<thymeleaf.version>3.0.11.RELEASEthymeleaf.version>
<thymeleaf-spring5.version>3.0.11.RELEASEthymeleaf-spring5.version>
<spring-data-jpa.version>2.7.2spring-data-jpa.version>
<hibernate.version>5.6.1.Finalhibernate.version>
<h2.version>1.4.200h2.version>
<logback.version>1.2.11logback.version>
<slf4j.version>1.7.36slf4j>
<servlet.api.version>4.0.1servlet.api.version>
<validation-api.version>2.0.1.Finalvalidation-api.version>
<hibernate-validator.version>6.2.0.Finalhibernate-validator.version>
<lombok.version>1.18.24lombok.version>
<jackson.version>2.13.0jackson.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-webmvcartifactId>
<version>${spring.version}version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>${spring.version}version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-ormartifactId>
<version>${spring.version}version>
dependency>
<dependency>
<groupId>org.springframework.datagroupId>
<artifactId>spring-data-jpaartifactId>
<version>${spring-data-jpa.version}version>
dependency>
<dependency>
<groupId>org.hibernategroupId>
<artifactId>hibernate-coreartifactId>
<version>${hibernate.version}version>
dependency>
<dependency>
<groupId>org.hibernategroupId>
<artifactId>hibernate-entitymanagerartifactId>
<version>${hibernate.version}version>
dependency>
<dependency>
<groupId>com.h2databasegroupId>
<artifactId>h2artifactId>
<version>${h2.version}version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.thymeleafgroupId>
<artifactId>thymeleafartifactId>
<version>${thymeleaf.version}version>
dependency>
<dependency>
<groupId>org.thymeleafgroupId>
<artifactId>thymeleaf-spring5artifactId>
<version>${thymeleaf-spring5.version}version>
dependency>
<dependency>
<groupId>javax.servletgroupId>
<artifactId>javax.servlet-apiartifactId>
<version>${servlet.api.version}version>
<scope>providedscope>
dependency>
<dependency>
<groupId>javax.validationgroupId>
<artifactId>validation-apiartifactId>
<version>${validation-api.version}version>
dependency>
<dependency>
<groupId>org.hibernate.validatorgroupId>
<artifactId>hibernate-validatorartifactId>
<version>${hibernate-validator.version}version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-apiartifactId>
<version>${slf4j.version}version>
dependency>
<dependency>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-classicartifactId>
<version>${logback.version}version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-databindartifactId>
<version>${jackson.version}version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>${lombok.version}version>
<scope>providedscope>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-testartifactId>
<version>${spring.version}version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.junit.jupitergroupId>
<artifactId>junit-jupiter-apiartifactId>
<version>5.8.1version>
<scope>testscope>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-war-pluginartifactId>
<version>3.3.2version>
<configuration>
<failOnMissingWebXml>falsefailOnMissingWebXml>
configuration>
plugin>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<version>3.8.1version>
<configuration>
<source>${maven.compiler.source}source>
<target>${maven.compiler.target}target>
configuration>
plugin>
plugins>
build>
project>
说明:请根据实际情况调整依赖版本,并确保它们相互兼容。本项目使用 Java 8。
遵循标准的 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
下。
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() { ... }
}
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 +
'}';
}
}
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]'" + // 不输出密码
'}';
}
}
使用 Spring Data JPA 简化数据访问。只需要定义 Repository 接口,Spring Data JPA 会自动生成实现。
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 findByTitleContainingIgnoreCase(String title);
// List findByAuthorContainingIgnoreCase(String author);
// 提供了分页查询功能,findAll 方法重载支持 Pageable 参数
// Page findAll(Pageable pageable);
}
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);
}
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
}
}
Service 层负责协调 Repository 和处理业务逻辑。
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删除图书
}
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 提供的方法
}
}
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);
}
}
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);
}
控制器负责接收 HTTP 请求,调用 Service 层处理业务,并选择合适的视图或数据作为响应。
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 populateGenres() {
* return Arrays.asList("小说", "技术", "历史");
* }
* // 这样在所有由这个 Controller 处理的请求中,Model 都会有一个名为 "genres" 的属性
*/
}
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 参数表示已注销
}
}
结合 Bean Validation API 和 Hibernate Validator 实现数据校验。
pom.xml
中添加 validation-api
和 hibernate-validator
。BookDTO.java
中使用了 @NotBlank
, @Size
, @Pattern
, @PastOrPresent
等注解。saveBook
方法的 BookDTO
参数前添加 @Valid
注解,并在其后紧跟 BindingResult
参数。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;
}
form.html
) 中使用 th:errors
标签显示校验错误信息。
<form th:object="${bookDTO}" th:action="@{/books}" method="post">
<div>
<label for="title">标题:label>
<input type="text" id="title" th:field="*{title}"/>
<span th:if="${#fields.hasErrors('title')}" th:errors="*{title}" style="color: red;">Title Errorspan>
div>
<div>
<label for="author">作者:label>
<input type="text" id="author" th:field="*{author}"/>
<span th:if="${#fields.hasErrors('author')}" th:errors="*{author}" style="color: red;">Author Errorspan>
div>
form>
使用 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 路径
}
}
在 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>IDth>
<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>ISBNth>
<th>出版日期th>
<th>操作th>
tr>
thead>
<tbody>
<tr th:each="book : ${bookPage.content}">
<td th:text="${book.id}">1td>
<td th:text="${book.title}">书名td>
<td th:text="${book.author}">作者td>
<td th:text="${book.isbn}">ISBNtd>
<td th:text="${book.publicationDate}">出版日期td>
<td>
<a th:href="@{/books/{id}(id=${book.id})}">详情a> |
<a th:href="@{/books/edit/{id}(id=${book.id})}">编辑a> |
<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}">1span>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}">ISBNspan>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>
<form th:object="${bookDTO}" th:action="@{/books}" method="post">
<input type="hidden" th:field="*{id}"/>
<div>
<label for="title">标题:label>
<input type="text" id="title" th:field="*{title}" th:errorclass="is-invalid"/>
<span th:if="${#fields.hasErrors('title')}" th:errors="*{title}" class="error-message">Title Errorspan>
div>
<div>
<label for="author">作者:label>
<input type="text" id="author" th:field="*{author}" th:errorclass="is-invalid"/>
<span th:if="${#fields.hasErrors('author')}" th:errors="*{author}" class="error-message">Author Errorspan>
div>
<div>
<label for="isbn">ISBN:label>
<input type="text" id="isbn" th:field="*{isbn}" th:errorclass="is-invalid"/>
<span th:if="${#fields.hasErrors('isbn')}" th:errors="*{isbn}" class="error-message">ISBN Errorspan>
div>
<div>
<label for="publicationDate">出版日期:label>
<input type="date" id="publicationDate" th:field="*{publicationDate}" th:errorclass="is-invalid"/>
<span th:if="${#fields.hasErrors('publicationDate')}" th:errors="*{publicationDate}" class="error-message">Date Errorspan>
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>
<div th:if="${param.logout}" style="color: green;">
<p>您已成功注销。p>
div>
<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
)。
<dependency>
<groupId>nz.net.ultraq.thymeleafgroupId>
<artifactId>thymeleaf-layout-dialectartifactId>
<version>2.5.3version>
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 Companyp>
footer>
body>
html>
其他页面通过 和
来使用布局。
使用 Java 类代替 XML 文件进行 Spring 和 Spring MVC 的配置。
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 容器的加载。
AppConfig.java
(Root Context 配置)已在 JPA 配置部分给出代码。主要配置非 Web 层的 Bean,如 DataSource, JPA/Hibernate, Spring Data JPA, Service。
WebMvcConfig.java
(Servlet Context 配置)已在 Thymeleaf 和数据校验配置部分给出代码。主要配置 Web 层的 Bean,如 Controller 扫描、ViewResolver、资源处理、Validator、Interceptor。
实现一个简单的拦截器检查用户是否登录,对应模块六。
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...");
// 用于清理资源等
}
}
WebMvcConfig.java
中)已在 Thymeleaf 配置部分给出代码。在 WebMvcConfig
中定义 AuthInterceptor
Bean,并在 addInterceptors
方法中注册并配置拦截规则 (addPathPatterns
, excludePathPatterns
)。
使用 @ControllerAdvice
和 @ExceptionHandler
实现全局异常处理,对应模块六。
BookNotFoundException.java
(自定义异常)package com.yourcompany.bookmanagement.exception;
// 自定义异常,继承 RuntimeException
public class BookNotFoundException extends RuntimeException {
public BookNotFoundException(Long id) {
super("Book not found with ID: " + id);
}
}
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><a th:href="@{/}">返回首页a>p>
body>
html>
在项目根目录打开终端,执行 Maven 命令:
mvn clean package
构建成功后,会在项目的 target
目录下生成 book-management-1.0-SNAPSHOT.war
文件。
将生成的 .war
文件复制到 Tomcat (或其他 Servlet 容器) 的 webapps
目录下。启动 Tomcat,它会自动解压并部署 WAR 包。
部署成功后,可以通过浏览器访问应用。默认情况下,应用的 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
进行登录。
通过这个小型图书管理系统案例,我们实践了 Spring MVC 在企业应用中的典型用法,包括:
@RequestMapping
系列注解进行请求映射。@ModelAttribute
和 @RequestParam
获取请求参数。redirect:
) 和转发 (默认) 的页面跳转方式。进一步学习和扩展方向:
@RestController
和 @RequestBody
/@ResponseBody
),供前端应用或第三方系统调用。LocaleResolver
等组件)。希望这个案例能帮助你更好地理解和掌握 Spring MVC!
速通Spring MVC ,一篇就够
企业级Spring MVC高级主题与实用技术讲解