在当今的软件开发领域,Spring Boot 与 RESTful API 的结合已成为构建高效、可扩展 Web 应用的标配。本文将通过一个完整的项目示例,从知识铺垫到部署上线,带你一步步掌握 Spring Boot + RESTful 的开发流程。
Spring Boot 是基于 Spring 框架的快速开发工具,它简化了 Spring 应用的初始搭建和开发过程。通过自动配置和起步依赖,开发者可以快速构建独立运行的、生产级别的 Spring 应用。
核心优势:
RESTful 是一种基于 HTTP 协议的软件架构风格,它强调资源的无状态操作和统一的接口设计。RESTful API 通过 HTTP 方法(GET、POST、PUT、DELETE 等)对资源进行操作,具有简洁、易扩展的特点。
核心原则:
我们将开发一个简单的图书管理系统,提供以下 RESTful API:
/api/books
/api/books/{id}
/api/books
/api/books/{id}
/api/books/{id}
使用 Spring Initializr 快速生成项目骨架(也可以建立maven项目手动配置pom文件):
Spring Web
、Spring Data JPA
、H2 Database
src/
├── main/
│ ├── java/com/example/bookstore/
│ │ ├── controller/ # 控制器层
│ │ ├── model/ # 实体类
│ │ ├── repository/ # 数据访问层
│ │ └── BookstoreApplication.java # 主启动类
│ └── resources/
│ ├── application.properties # 配置文件
│ └── data.sql # 初始化数据(可选)
└── test/ # 测试代码
在 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 控制台
创建 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 Setters
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 Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }
}
创建 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 方法,除特殊查询方式外,不用单独写
}
创建 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 {
@Autowired
private BookRepository bookRepository;
// 获取所有图书
@GetMapping
public 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());
}
// 添加图书
@PostMapping
public 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());
}
}
在 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);
使用 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 {
@Mock
private BookRepository bookRepository;
@InjectMocks
private BookController bookController;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void 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();
}
@Test
void 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);
}
@Test
void 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);
}
@Test
void 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);
}
@Test
void 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);
}
@Test
void 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());
}
@Test
void 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);
}
@Test
void 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());
}
}
运行 BookstoreApplication
的 main
方法,启动 Spring Boot 应用。
mvn clean package
生成 JAR 文件。java -jar target/bookstore-0.0.1-SNAPSHOT.jar
启动应用。通过本文,我们完成了以下任务: