【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 WebSpring Data JPAH2 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 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; }
}

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 {

    @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());
    }
}

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 {

    @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());
    }
}

5.2 启动应用

运行 BookstoreApplicationmain 方法,启动 Spring Boot 应用。

5.3 部署应用

  • 打包:运行 mvn clean package 生成 JAR 文件。
  • 运行:执行 java -jar target/bookstore-0.0.1-SNAPSHOT.jar 启动应用。
  • 部署到云服务器:将 JAR 文件上传到云服务器(如阿里云、AWS),使用类似命令启动。

六、总结

通过本文,我们完成了以下任务:

  1. 知识铺垫:了解了 Spring Boot 和 RESTful API 的基本概念。
  2. 开发环境搭建:使用 Spring Initializr 创建项目,配置 H2 数据库。
  3. 核心功能实现:定义实体类、数据访问层和控制器层,实现完整的 CRUD 操作。
  4. 测试与部署:编写单元测试,启动应用并测试 API,最后打包部署。

你可能感兴趣的:(SpringBoot,spring,boot,restful,后端)