【SpringBoot】Spring Boot + RESTful 技术实战指南
在当今的软件开发领域,Spring Boot 与 RESTful API 的结合已成为构建高效、可扩展 Web 应用的标配。本文将通过一个完整的项目示例,从知识铺垫到部署上线,带你一步步掌握 Spring Boot + RESTful 的开发流程。
一、知识铺垫
1.1 Spring Boot 简介
Spring Boot 是基于 Spring 框架的快速开发工具,它简化了 Spring 应用的初始搭建和开发过程。通过自动配置和起步依赖,开发者可以快速构建独立运行的、生产级别的 Spring 应用。
核心优势:
- 自动配置:根据项目依赖自动配置 Spring 组件,减少手动配置。
- 起步依赖:提供预定义的依赖组合,简化依赖管理。
- 嵌入式服务器:内置 Tomcat、Jetty 等服务器,无需额外部署。
1.2 RESTful API 简介
RESTful 是一种基于 HTTP 协议的软件架构风格,它强调资源的无状态操作和统一的接口设计。RESTful API 通过 HTTP 方法(GET、POST、PUT、DELETE 等)对资源进行操作,具有简洁、易扩展的特点。
核心原则:
- 资源:将数据抽象为资源,通过 URI 唯一标识。
- 无状态:每个请求包含所有必要信息,服务器不存储客户端状态。
- 统一接口:使用标准的 HTTP 方法对资源进行操作。
二、技术介绍
2.1 技术栈选择
- Spring Boot:作为后端框架,提供快速开发和自动配置能力。
- Spring Data JPA:简化数据库操作,支持 ORM 映射。
- H2 数据库:内存数据库,便于开发和测试。
- JUnit 5:单元测试框架。
- Maven:项目构建和依赖管理工具。
2.2 项目需求
我们将开发一个简单的图书管理系统,提供以下 RESTful API:
- 获取所有图书:GET
/api/books
- 根据 ID 获取图书:GET
/api/books/{id}
- 添加图书:POST
/api/books
- 更新图书:PUT
/api/books/{id}
- 删除图书:DELETE
/api/books/{id}
三、开发环境搭建
3.1 创建 Spring Boot 项目
使用 Spring Initializr 快速生成项目骨架(也可以建立maven项目手动配置pom文件):
- Project:Maven Project
- Language:Java
- Spring Boot:这里使用最新稳定版
- Dependencies:添加
Spring Web
、Spring Data JPA
、H2 Database
3.2 项目结构
src/
├── main/
│ ├── java/com/example/bookstore/
│ │ ├── controller/ # 控制器层
│ │ ├── model/ # 实体类
│ │ ├── repository/ # 数据访问层
│ │ └── BookstoreApplication.java # 主启动类
│ └── resources/
│ ├── application.properties # 配置文件
│ └── data.sql # 初始化数据(可选)
└── test/ # 测试代码
3.3 配置文件
在 application.properties
中配置 H2 数据库:
# H2 数据库配置
spring.datasource.url=jdbc:h2:mem:bookstore
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true # 启用 H2 控制台
四、核心功能实现
4.1 实体类定义
创建 Book
实体类:
package com.example.bookstore.model;import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;@Entity
public class Book {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;private String title;private String author;private Double price;public Book() {}public Book(String title, String author, Double price) {this.title = title;this.author = author;this.price = price;}// Getters and Setterspublic 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 Double getPrice() { return price; }public void setPrice(Double price) { this.price = price; }
}
4.2 数据访问层
创建 BookRepository
接口,继承 JpaRepository
:
package com.example.bookstore.repository;import com.example.bookstore.model.Book;
import org.springframework.data.jpa.repository.JpaRepository;public interface BookRepository extends JpaRepository<Book, Long> {// 继承 JpaRepository 后,自动拥有基本的 CRUD 方法,除特殊查询方式外,不用单独写
}
4.3 控制器层
创建 BookController
,实现 RESTful API:
package com.example.bookstore.controller;import com.example.bookstore.model.Book;
import com.example.bookstore.repository.BookRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;import java.util.List;
import java.util.Optional;@RestController
@RequestMapping("/api/books")
public class BookController {@Autowiredprivate BookRepository bookRepository;// 获取所有图书@GetMappingpublic List<Book> getAllBooks() {return bookRepository.findAll();}// 根据 ID 获取图书@GetMapping("/{id}")public ResponseEntity<Book> getBookById(@PathVariable Long id) {Optional<Book> book = bookRepository.findById(id);return book.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());}// 添加图书@PostMappingpublic Book createBook(@RequestBody Book book) {return bookRepository.save(book);}// 更新图书@PutMapping("/{id}")public ResponseEntity<Book> updateBook(@PathVariable Long id, @RequestBody Book bookDetails) {return bookRepository.findById(id).map(book -> {book.setTitle(bookDetails.getTitle());book.setAuthor(bookDetails.getAuthor());book.setPrice(bookDetails.getPrice());Book updatedBook = bookRepository.save(book);return ResponseEntity.ok(updatedBook);}).orElseGet(() -> ResponseEntity.notFound().build());}// 删除图书@DeleteMapping("/{id}")public ResponseEntity<Void> deleteBook(@PathVariable Long id) {return bookRepository.findById(id).map(book -> {bookRepository.delete(book);return ResponseEntity.noContent().<Void>build();}).orElseGet(() -> ResponseEntity.notFound().build());}
}
4.4 初始化数据(可选)
在 src/main/resources/data.sql
中添加初始化数据:
INSERT INTO book (title, author, price) VALUES ('Spring in Action', 'Craig Walls', 49.99);
INSERT INTO book (title, author, price) VALUES ('Effective Java', 'Joshua Bloch', 45.00);
五、测试与部署
5.1 单元测试
使用 JUnit 5 编写单元测试,测试 BookController
:
package com.example.bookstore.controller;import com.example.bookstore.model.Book;
import com.example.bookstore.repository.BookRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;import java.util.Arrays;
import java.util.List;
import java.util.Optional;import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;class BookControllerTest {@Mockprivate BookRepository bookRepository;@InjectMocksprivate BookController bookController;@BeforeEachvoid setUp() {MockitoAnnotations.openMocks(this);}@Testvoid getAllBooks_ShouldReturnAllBooks() {// 准备测试数据Book book1 = new Book("Book 1", "Author 1", 10.0);Book book2 = new Book("Book 2", "Author 2", 20.0);List<Book> books = Arrays.asList(book1, book2);// 模拟行为when(bookRepository.findAll()).thenReturn(books);// 调用方法List<Book> result = bookController.getAllBooks();// 验证结果assertEquals(2, result.size());assertEquals("Book 1", result.get(0).getTitle());verify(bookRepository, times(1)).findAll();}@Testvoid getBookById_ShouldReturnBookWhenExists() {// 准备测试数据Book book = new Book("Book 1", "Author 1", 10.0);book.setId(1L);// 模拟行为when(bookRepository.findById(1L)).thenReturn(Optional.of(book));// 调用方法ResponseEntity<Book> response = bookController.getBookById(1L);// 验证结果assertEquals(HttpStatus.OK, response.getStatusCode());assertEquals("Book 1", response.getBody().getTitle());verify(bookRepository, times(1)).findById(1L);}@Testvoid getBookById_ShouldReturnNotFoundWhenBookDoesNotExist() {// 模拟行为when(bookRepository.findById(1L)).thenReturn(Optional.empty());// 调用方法ResponseEntity<Book> response = bookController.getBookById(1L);// 验证结果assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());verify(bookRepository, times(1)).findById(1L);}@Testvoid createBook_ShouldSaveBook() {// 准备测试数据Book book = new Book("New Book", "New Author", 30.0);Book savedBook = new Book("New Book", "New Author", 30.0);savedBook.setId(1L);// 模拟行为when(bookRepository.save(book)).thenReturn(savedBook);// 调用方法Book result = bookController.createBook(book);// 验证结果assertEquals(1L, result.getId());assertEquals("New Book", result.getTitle());verify(bookRepository, times(1)).save(book);}@Testvoid updateBook_ShouldUpdateBookWhenExists() {// 准备测试数据Book existingBook = new Book("Old Book", "Old Author", 10.0);existingBook.setId(1L);Book updatedBookDetails = new Book("Updated Book", "Updated Author", 20.0);Book updatedBook = new Book("Updated Book", "Updated Author", 20.0);updatedBook.setId(1L);// 模拟行为when(bookRepository.findById(1L)).thenReturn(Optional.of(existingBook));when(bookRepository.save(existingBook)).thenReturn(updatedBook);// 调用方法ResponseEntity<Book> response = bookController.updateBook(1L, updatedBookDetails);// 验证结果assertEquals(HttpStatus.OK, response.getStatusCode());assertEquals("Updated Book", response.getBody().getTitle());verify(bookRepository, times(1)).findById(1L);verify(bookRepository, times(1)).save(existingBook);}@Testvoid updateBook_ShouldReturnNotFoundWhenBookDoesNotExist() {// 准备测试数据Book updatedBookDetails = new Book("Updated Book", "Updated Author", 20.0);// 模拟行为when(bookRepository.findById(1L)).thenReturn(Optional.empty());// 调用方法ResponseEntity<Book> response = bookController.updateBook(1L, updatedBookDetails);// 验证结果assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());verify(bookRepository, times(1)).findById(1L);verify(bookRepository, never()).save(any());}@Testvoid deleteBook_ShouldDeleteBookWhenExists() {// 准备测试数据Book book = new Book("Book 1", "Author 1", 10.0);book.setId(1L);// 模拟行为when(bookRepository.findById(1L)).thenReturn(Optional.of(book));doNothing().when(bookRepository).delete(book);// 调用方法ResponseEntity<Void> response = bookController.deleteBook(1L);// 验证结果assertEquals(HttpStatus.NO_CONTENT, response.getStatusCode());verify(bookRepository, times(1)).findById(1L);verify(bookRepository, times(1)).delete(book);}@Testvoid deleteBook_ShouldReturnNotFoundWhenBookDoesNotExist() {// 模拟行为when(bookRepository.findById(1L)).thenReturn(Optional.empty());// 调用方法ResponseEntity<Void> response = bookController.deleteBook(1L);// 验证结果assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());verify(bookRepository, times(1)).findById(1L);verify(bookRepository, never()).delete(any());}
}
5.2 启动应用
运行 BookstoreApplication
的 main
方法,启动 Spring Boot 应用。
5.3 部署应用
- 打包:运行
mvn clean package
生成 JAR 文件。 - 运行:执行
java -jar target/bookstore-0.0.1-SNAPSHOT.jar
启动应用。 - 部署到云服务器:将 JAR 文件上传到云服务器(如阿里云、AWS),使用类似命令启动。
六、总结
通过本文,我们完成了以下任务:
- 知识铺垫:了解了 Spring Boot 和 RESTful API 的基本概念。
- 开发环境搭建:使用 Spring Initializr 创建项目,配置 H2 数据库。
- 核心功能实现:定义实体类、数据访问层和控制器层,实现完整的 CRUD 操作。
- 测试与部署:编写单元测试,启动应用并测试 API,最后打包部署。