教程类 大模型 Web3开发 入门指南 # Spring Boot实战:从零构建一个RESTful API服务 零点119官方团队 2025-12-24 2025-12-24 Spring Boot实战:从零构建一个RESTful API服务 Spring Boot作为Java生态中最受欢迎的框架之一,以其”约定优于配置”的理念,极大地简化了Spring应用的初始搭建和开发过程。本文将带你深入实战,从零开始构建一个完整的RESTful API服务,涵盖核心概念、实际开发步骤和最佳实践。
一、环境准备与项目初始化 1.1 环境要求 JDK 11或更高版本 Maven 3.6+ 或 Gradle 6.8+ IDE(推荐IntelliJ IDEA或VS Code) 1.2 使用Spring Initializr快速创建项目 访问 start.spring.io ,选择以下配置:
Project : Maven ProjectLanguage : JavaSpring Boot : 3.1.0+Packaging : JarJava : 17添加依赖:
Spring Web Spring Data JPA H2 Database(开发环境) Lombok(简化代码) Validation 或者使用命令行创建:
1 2 3 curl https://start.spring.io/starter.zip -d dependencies=web,data-jpa,h2,lombok,validation \ -d type =maven-project -d language=java -d bootVersion=3.1.5 \ -d groupId=com.example -d artifactId=demo-api -o demo-api.zip
二、项目结构与核心配置 2.1 项目结构解析 1 2 3 4 5 6 7 8 9 src/main/java/com/example/demoapi/ ├── DemoApiApplication.java # 主启动类 ├── config/ # 配置类 ├── controller/ # 控制器层 ├── dto/ # 数据传输对象 ├── entity/ # 实体类 ├── repository/ # 数据访问层 ├── service/ # 业务逻辑层 └── exception/ # 异常处理
2.2 配置文件详解 application.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 server: port: 8080 servlet: context-path: /api spring: application: name: demo-api datasource: url: jdbc:h2:mem:testdb driver-class-name: org.h2.Driver username: sa password: h2: console: enabled: true path: /h2-console jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: format_sql: true dialect: org.hibernate.dialect.H2Dialect logging: level: com.example.demoapi: DEBUG org.springframework.web: INFO
✨ 三、实体层设计与JPA映射 3.1 创建基础实体 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package com.example.demoapi.entity;import jakarta.persistence.*;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import org.hibernate.annotations.CreationTimestamp;import org.hibernate.annotations.UpdateTimestamp;import java.time.LocalDateTime;@Data @NoArgsConstructor @AllArgsConstructor @Entity @Table(name = "products") public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true) private String sku; @Column(nullable = false) private String name; private String description; @Column(nullable = false) private BigDecimal price; @Column(nullable = false) private Integer stockQuantity; @Enumerated(EnumType.STRING) private ProductStatus status = ProductStatus.ACTIVE; @CreationTimestamp @Column(updatable = false) private LocalDateTime createdAt; @UpdateTimestamp private LocalDateTime updatedAt; public enum ProductStatus { ACTIVE, INACTIVE, DISCONTINUED } }
3.2 实体关系映射示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 @Entity @Table(name = "orders") @Data public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String orderNumber; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") private User user; @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true) private List<OrderItem> items = new ArrayList <>(); @Enumerated(EnumType.STRING) private OrderStatus status; private BigDecimal totalAmount; } @Entity @Table(name = "order_items") @Data public class OrderItem { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "order_id") private Order order; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "product_id") private Product product; private Integer quantity; private BigDecimal unitPrice; }
四、数据访问层实现 4.1 基础Repository 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.example.demoapi.repository;import com.example.demoapi.entity.Product;import org.springframework.data.jpa.repository.JpaRepository;import org.springframework.data.jpa.repository.JpaSpecificationExecutor;import org.springframework.data.jpa.repository.Query;import org.springframework.data.repository.query.Param;import org.springframework.stereotype.Repository;import java.math.BigDecimal;import java.util.List;import java.util.Optional;@Repository public interface ProductRepository extends JpaRepository <Product, Long>, JpaSpecificationExecutor<Product> { Optional<Product> findBySku (String sku) ; List<Product> findByStatus (Product.ProductStatus status) ; @Query("SELECT p FROM Product p WHERE p.price BETWEEN :minPrice AND :maxPrice") List<Product> findByPriceRange (@Param("minPrice") BigDecimal minPrice, @Param("maxPrice") BigDecimal maxPrice) ; @Query("SELECT p FROM Product p WHERE p.name LIKE %:keyword% OR p.description LIKE %:keyword%") List<Product> searchByKeyword (@Param("keyword") String keyword) ; boolean existsBySku (String sku) ; }
4.2 自定义查询方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public interface ProductRepositoryCustom { Page<Product> searchProducts (ProductSearchCriteria criteria, Pageable pageable) ; } @Repository @RequiredArgsConstructor public class ProductRepositoryImpl implements ProductRepositoryCustom { private final EntityManager entityManager; @Override public Page<Product> searchProducts (ProductSearchCriteria criteria, Pageable pageable) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<Product> query = cb.createQuery(Product.class); Root<Product> root = query.from(Product.class); List<Predicate> predicates = new ArrayList <>(); if (StringUtils.hasText(criteria.getKeyword())) { Predicate namePredicate = cb.like(root.get("name" ), "%" + criteria.getKeyword() + "%" ); Predicate descPredicate = cb.like(root.get("description" ), "%" + criteria.getKeyword() + "%" ); predicates.add(cb.or(namePredicate, descPredicate)); } if (criteria.getMinPrice() != null ) { predicates.add(cb.ge(root.get("price" ), criteria.getMinPrice())); } if (criteria.getMaxPrice() != null ) { predicates.add(cb.le(root.get("price" ), criteria.getMaxPrice())); } if (criteria.getStatus() != null ) { predicates.add(cb.equal(root.get("status" ), criteria.getStatus())); } query.where(predicates.toArray(new Predicate [0 ])); TypedQuery<Product> typedQuery = entityManager.createQuery(query); typedQuery.setFirstResult((int ) pageable.getOffset()); typedQuery.setMaxResults(pageable.getPageSize()); List<Product> result = typedQuery.getResultList(); CriteriaQuery<Long> countQuery = cb.createQuery(Long.class); Root<Product> countRoot = countQuery.from(Product.class); countQuery.select(cb.count(countRoot)).where(predicates.toArray(new Predicate [0 ])); Long total = entityManager.createQuery(countQuery).getSingleResult(); return new PageImpl <>(result, pageable, total); } }
🚀 五、业务逻辑层设计 5.1 Service接口与实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 public interface ProductService { ProductDTO createProduct (CreateProductRequest request) ; ProductDTO updateProduct (Long id, UpdateProductRequest request) ; ProductDTO getProductById (Long id) ; ProductDTO getProductBySku (String sku) ; Page<ProductDTO> searchProducts (ProductSearchCriteria criteria, Pageable pageable) ; void deleteProduct (Long id) ; void updateStock (Long id, Integer quantity) ; } @Service @RequiredArgsConstructor @Slf4j public class ProductServiceImpl implements ProductService { private final ProductRepository productRepository; private final ProductMapper productMapper; @Override @Transactional public ProductDTO createProduct (CreateProductRequest request) { log.info("Creating product with SKU: {}" , request.getSku()); if (productRepository.existsBySku(request.getSku())) { throw new BusinessException ("Product with SKU " + request.getSku() + " already exists" ); } Product product = productMapper.toEntity(request); product.setStatus(Product.ProductStatus.ACTIVE); Product savedProduct = productRepository.save(product); log.info("Product created successfully with ID: {}" , savedProduct.getId()); return productMapper.toDTO(savedProduct); } @Override @Transactional(readOnly = true) public ProductDTO getProductById (Long id) { return productRepository.findById(id) .map(productMapper::toDTO) .orElseThrow(() -> new ResourceNotFoundException ("Product not found with id: " + id)); } @Override @Transactional public void updateStock (Long id, Integer quantity) { Product product = productRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException ("Product not found with id: " + id)); if (product.getStockQuantity() + quantity < 0 ) { throw new BusinessException ("Insufficient stock" ); } product.setStockQuantity(product.getStockQuantity() + quantity); productRepository.save(product); log.info("Updated stock for product {}: {}" , id, product.getStockQuantity()); } }
5.2 DTO与Mapper 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @Data @NoArgsConstructor @AllArgsConstructor public class ProductDTO { private Long id; private String sku; private String name; private String description; private BigDecimal price; private Integer stockQuantity; private Product.ProductStatus status; private LocalDateTime createdAt; } @Data public class CreateProductRequest { @NotBlank(message = "SKU不能为空") @Size(min = 3, max = 50, message = "SKU长度必须在3-50个字符之间") private String sku; @NotBlank(message = "产品名称不能为空") private String name; private String description; @NotNull(message = "价格不能为空") @DecimalMin(value = "0.0", inclusive = false, message = "价格必须大于0") private BigDecimal price; @NotNull(message = "库存数量不能为空") @Min(value = 0, message = "库存数量不能小于0") private Integer stockQuantity; } @Mapper(componentModel = "spring") public interface ProductMapper { ProductDTO toDTO (Product product) ; Product toEntity (CreateProductRequest request) ; void updateEntity (UpdateProductRequest request, @MappingTarget Product product) ; }
🚀 六、控制器层与RESTful API 6.1 REST控制器实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 @RestController @RequestMapping("/api/v1/products") @RequiredArgsConstructor @Validated @Slf4j public class ProductController { private final ProductService productService; @PostMapping @ResponseStatus(HttpStatus.CREATED) public ApiResponse<ProductDTO> createProduct (@Valid @RequestBody CreateProductRequest request) { log.info("POST /api/v1/products - Creating product: {}" , request.getName()); ProductDTO product = productService.createProduct(request); return ApiResponse.success("Product created successfully" , product); } @GetMapping("/{id}") public ApiResponse<ProductDTO> getProduct (@PathVariable Long id) { log.info("GET /api/v1/products/{}" , id); ProductDTO product = productService.getProductById(id); return ApiResponse.success(product); } @GetMapping public ApiResponse<Page<ProductDTO>> searchProducts ( @RequestParam(required = false) String keyword, @RequestParam(required = false) BigDecimal minPrice, @RequestParam(required = false) BigDecimal maxPrice, @RequestParam(required = false) Product.ProductStatus status, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size, @RequestParam(defaultValue = "createdAt,desc") String[] sort) { log.info("GET /api/v1/products - Search products" ); ProductSearchCriteria criteria = new ProductSearchCriteria (); criteria.setKeyword(keyword); criteria.setMinPrice(minPrice); criteria.setMaxPrice(maxPrice); criteria.setStatus(status); Sort.Direction direction = sort[1 ].equalsIgnoreCase("desc" ) ? Sort.Direction.DESC : Sort.Direction.ASC; Pageable pageable = PageRequest.of(page, size, Sort.by(direction, sort[0 ])); Page<ProductDTO> products = productService.searchProducts(criteria, pageable); return ApiResponse.success(products); } @PutMapping("/{id}") public ApiResponse<ProductDTO> updateProduct ( @PathVariable Long id, @Valid @RequestBody UpdateProductRequest request) { log.info("PUT /api/v1/products/{}" , id); ProductDTO product = productService.updateProduct(id, request); return ApiResponse.success("Product updated successfully" , product); } @DeleteMapping("/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteProduct (@PathVariable Long id) { log.info("DELETE /api/v1/products/{}" , id); productService.deleteProduct(id); } @PatchMapping("/{id}/stock") public ApiResponse<Void> updateStock ( @PathVariable Long id, @RequestParam Integer quantity) { log.info("PATCH /api/v1/products/{}/stock?quantity={}" , id, quantity); productService.updateStock(id, quantity); return ApiResponse.success("Stock updated successfully" ); } }
6.2 统一响应格式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Data @NoArgsConstructor @AllArgsConstructor public class ApiResponse <T> { private boolean success; private String message; private T data; private LocalDateTime timestamp; public static <T> ApiResponse<T> success (T data) { return new ApiResponse <>(true , null , data, LocalDateTime.now()); } public static <T> ApiResponse<T> success (String message, T data) { return new ApiResponse <>(true , message, data, LocalDateTime.now()); } public static <T> ApiResponse<T> error (String message) { return new ApiResponse <>(false , message, null , LocalDateTime.now()); } }
七、全局异常处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 @RestControllerAdvice @Slf4j public class GlobalExceptionHandler { @ExceptionHandler(ResourceNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public ApiResponse<Void> handleResourceNotFoundException (ResourceNotFoundException ex) { log.warn("Resource not found: {}" , ex.getMessage()); return ApiResponse.error(ex.getMessage()); } @ExceptionHandler(BusinessException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ApiResponse<Void> handleBusinessException (BusinessException ex) { log.warn("Business exception: {}" , ex.getMessage()); return ApiResponse.error(ex.getMessage()); } @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ApiResponse<Map<String, String>> handleValidationExceptions ( MethodArgumentNotValidException ex) { log.warn("Validation error: {}" , ex.getMessage()); Map<String, String> errors = new HashMap <>(); ex.getBindingResult().getFieldErrors().forEach(error -> errors.put(error.getField(), error.getDefaultMessage())); return new ApiResponse <>(false , "Validation failed" , errors, LocalDateTime.now()); } @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ApiResponse<Void> handleGenericException (Exception ex) { log.error("Unexpected error: " , ex); return ApiResponse.error("An unexpected error occurred. Please try again later." ); } }
八、测试与验证 8.1 单元测试 @ExtendWith(MockitoExtension.class)
class ProductServiceTest {
@Mock
private ProductRepository productRepository;
@Mock
private ProductMapper productMapper;
@InjectMocks
private ProductServiceImpl productService;
@Test
void createProduct_shouldSuccess() {
// Given
CreateProductRequest request = new CreateProductRequest();
request.setSku("TEST-001");
request.setName("Test Product");
request.setPrice(new BigDecimal("99.99"));
request.setStockQuantity(100);
Product product = new Product();
product.setId(1L);
product.setSku("TEST-001");
when(productRepository.existsBySku("TEST-001")).thenReturn(false);
when(productMapper.toEntity(request)).thenReturn(product);
when(productRepository.save(any(Product.class))).thenReturn(product);
// When
ProductDTO result = productService.createProduct(request);
<div class="video-container">
[up主专用,视频内嵌代码贴在这]
</div>
<style>
.video-container {
position: relative;
width: 100%;
padding-top: 56.25%; /* 16:9 aspect ratio */
}
.video-container iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
零点119官方团队
一站式科技资源平台 | 学生/开发者/极客必备
本文由零点119官方团队原创,转载请注明出处。文章ID: 587ff122