1. 为什么选择图书管理系统作为SpringMVC实战项目
图书管理系统作为经典的CRUD(增删改查)应用场景,几乎是每个Java开发者入门Web开发的必经之路。我十年前刚接触SpringMVC时,也是通过一个图书馆项目来理解MVC架构的精妙之处。这种系统看似简单,实则涵盖了企业级应用开发的核心要素:
- 业务复杂度适中:包含图书、读者、借阅记录等基础实体,能完整展示一对多、多对多关系
- 技术栈全覆盖:从前端JSP/Thymeleaf到后端Controller,从Service层事务管理到DAO层数据库操作
- 扩展性强:后续可轻松加入权限控制、数据校验、RESTful API等进阶功能
提示:建议在本地创建
book-management项目时使用Maven的webapp原型,这会自动生成标准的Java Web项目结构。我见过不少初学者手动创建目录导致WEB-INF位置错误的问题。
2. 环境搭建与基础配置
2.1 最小化依赖配置
在pom.xml中只需要以下核心依赖(Spring 5.3.x版本):
xml复制<dependencies>
<!-- Spring MVC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.18</version>
</dependency>
<!-- JSTL for JSP -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- 数据库相关 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.18</version>
</dependency>
</dependencies>
2.2 容易被忽略的web.xml配置
很多教程会直接贴配置代码,但不会解释为什么需要这些配置。以下是我的经验之谈:
xml复制<!-- 字符编码过滤器必须放在最前 -->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- DispatcherServlet配置 -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring-config.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
注意:
<url-pattern>/</url-pattern>和<url-pattern>/*</url-pattern>有本质区别。前者是默认Servlet的匹配规则,后者会覆盖所有请求包括静态资源。我曾因此浪费两小时调试CSS加载问题。
3. 领域模型设计与数据库层实现
3.1 实体关系建模
图书管理系统的核心实体应包括:
java复制public class Book {
private Long id;
private String isbn;
private String title;
private String author;
private Integer stock; // 库存量
// getters/setters
}
public class Reader {
private Long id;
private String name;
private String studentId;
// getters/setters
}
public class BorrowRecord {
private Long id;
private Book book;
private Reader reader;
private Date borrowDate;
private Date returnDate;
// getters/setters
}
3.2 Spring JDBC实战技巧
比起Hibernate,我建议初学者先用Spring JDBC理解SQL本质。以下是JdbcTemplate的典型用法:
java复制@Repository
public class BookDaoImpl implements BookDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public List<Book> findByTitle(String keyword) {
String sql = "SELECT * FROM books WHERE title LIKE ?";
// 使用RowMapper避免字段映射错误
return jdbcTemplate.query(sql,
(rs, rowNum) -> new Book(
rs.getLong("id"),
rs.getString("isbn"),
rs.getString("title"),
rs.getString("author"),
rs.getInt("stock")
), "%" + keyword + "%");
}
@Override
@Transactional // 重要:保证操作原子性
public boolean borrowBook(Long bookId, Long readerId) {
// 1. 检查库存
Integer stock = jdbcTemplate.queryForObject(
"SELECT stock FROM books WHERE id = ?",
Integer.class, bookId);
if(stock <= 0) return false;
// 2. 减少库存
jdbcTemplate.update(
"UPDATE books SET stock = stock - 1 WHERE id = ?",
bookId);
// 3. 创建借阅记录
jdbcTemplate.update(
"INSERT INTO borrow_records(book_id, reader_id, borrow_date) VALUES(?,?,?)",
bookId, readerId, new Date());
return true;
}
}
经验:
RowMapper比BeanPropertyRowMapper更可靠,后者依赖字段名完全匹配,在字段有下划线时会出问题。
4. Controller设计模式与视图技术
4.1 三种典型的Controller写法
传统写法(适合简单CRUD):
java复制@Controller
@RequestMapping("/books")
public class BookController {
@Autowired
private BookService bookService;
@GetMapping
public String list(Model model) {
model.addAttribute("books", bookService.findAll());
return "book/list";
}
@PostMapping
public String create(Book book) {
bookService.save(book);
return "redirect:/books";
}
}
RESTful风格(适合前后端分离):
java复制@RestController
@RequestMapping("/api/books")
public class BookApiController {
@GetMapping
public ResponseEntity<List<Book>> list() {
return ResponseEntity.ok(bookService.findAll());
}
@PostMapping
public ResponseEntity<?> create(@RequestBody Book book) {
bookService.save(book);
return ResponseEntity.created(URI.create("/books/" + book.getId())).build();
}
}
带表单验证的增强版:
java复制@PostMapping
public String create(@Valid Book book, BindingResult result, Model model) {
if(result.hasErrors()) {
model.addAttribute("categories", categoryService.findAll());
return "book/add";
}
bookService.save(book);
return "redirect:/books";
}
4.2 视图技术选型建议
虽然JSP仍被广泛使用,但我推荐尝试Thymeleaf:
html复制<!-- 在templates/book/list.html -->
<table class="table">
<tr th:each="book : ${books}">
<td th:text="${book.isbn}">ISBN</td>
<td th:text="${book.title}">书名</td>
<td>
<a th:href="@{/books/{id}/edit(id=${book.id})}">编辑</a>
<form th:action="@{/books/{id}(id=${book.id})}" method="post">
<input type="hidden" name="_method" value="delete"/>
<button type="submit">删除</button>
</form>
</td>
</tr>
</table>
避坑指南:如果使用JSP,确保在Spring配置中正确设置视图解析器。我曾遇到
InternalResourceViewResolver未配置prefix导致404的问题:
xml复制<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
</bean>
5. 常见问题排查与性能优化
5.1 中文乱码终极解决方案
乱码问题通常由三个环节导致:
- 数据库连接:在JDBC URL后追加
?useUnicode=true&characterEncoding=UTF-8 - Servlet过滤器:确保配置了前文提到的
CharacterEncodingFilter - JSP页面:在页面顶部添加
<%@ page contentType="text/html;charset=UTF-8" %>
5.2 N+1查询问题优化
在展示借阅记录时,避免这样写:
java复制// 错误示范:会导致N+1查询
List<BorrowRecord> records = borrowRecordDao.findAll();
records.forEach(record -> {
record.setBook(bookDao.findById(record.getBookId()));
record.setReader(readerDao.findById(record.getReaderId()));
});
应改用联合查询:
java复制String sql = "SELECT r.*, b.title as book_title, u.name as reader_name " +
"FROM borrow_records r " +
"JOIN books b ON r.book_id = b.id " +
"JOIN readers u ON r.reader_id = u.id";
5.3 事务管理实战经验
在Service层使用声明式事务时要注意:
java复制@Service
public class BookServiceImpl implements BookService {
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.READ_COMMITTED,
rollbackFor = Exception.class
)
public void complexOperation() {
// 多个DAO操作
}
}
关键点:默认情况下,Spring只对RuntimeException回滚。像SQLException这种检查异常需要显式配置
rollbackFor
6. 项目扩展方向建议
完成基础CRUD后,可以考虑以下增强功能:
- 权限控制:集成Spring Security,实现管理员/普通用户分级
- 数据校验:使用Hibernate Validator添加校验规则
java复制public class Book { @NotBlank @Size(max = 100) private String title; @Pattern(regexp = "\\d{3}-\\d{10}") private String isbn; } - 缓存优化:为热门查询添加Redis缓存
- 异步处理:使用@Async实现借阅到期提醒
- API文档:集成Swagger生成接口文档
我在实际项目中发现,图书管理系统的借阅超期计算是个很好的业务逻辑练习点。可以尝试实现以下规则:
- 本科生可借30天,研究生可借60天
- 寒暑假期间自动延长借期
- 同一本书最多续借一次
这些业务规则能很好地锻炼Service层的设计能力。建议先用单元测试验证边界条件:
java复制@Test
public void testCalculateDueDate() {
// 给定暑假期间的借阅
LocalDate borrowDate = LocalDate.of(2023, 7, 1);
// 当读者是研究生时
DueDateCalculator calculator = new DueDateCalculator(ReaderType.POSTGRADUATE);
// 那么到期日应该考虑假期延长
LocalDate dueDate = calculator.calculate(borrowDate);
assertEquals(LocalDate.of(2023, 9, 15), dueDate);
}
