Spring Boot 深入实践指南:从入门到工程化落地

前言

在 Java 后端生态里,Spring Boot 已经从"快速启动框架"演变成了事实上的默认工程底座。它不只是帮你省掉 XML 配置,更重要的是把一套可运行、可扩展、可观测、可测试的工程约定打包好了。

很多人第一次接触 Spring Boot,会觉得它只是:

  • 加一个 spring-boot-starter-web
  • 写一个 @RestController
  • 启动项目
  • 然后接口就能跑了

这当然没错,但这只是表面。真正把 Spring Boot 用好,重点在于理解这几个问题:

  1. Spring Boot 到底帮我们"自动"了什么。
  2. 一个真实项目该怎么分层、怎么配配置、怎么做异常处理。
  3. 如何接数据库、如何做参数校验、如何测试。
  4. 在生产环境里如何监控、排障、发布。

这篇文章尽量不只停留在"hello world",而是用一篇偏工程化的博文,把 Spring Boot 的关键知识串起来,并尽量给出可直接参考的代码。


1. Spring Boot 是什么

Spring Boot 是 Spring 生态中的快速开发框架,核心目标有三个:

  • 简化配置
  • 提升开发效率
  • 提供可直接上线的工程基础设施

它的几个核心能力可以概括为:

  • Auto Configuration:自动装配,根据 classpath、配置项、Bean 条件自动初始化组件
  • Starter:一组开箱即用的依赖集合
  • Embedded Server:内嵌 Tomcat、Jetty 或 Undertow
  • Actuator:健康检查、指标、监控端点
  • Externalized Configuration:统一的外部配置管理

你可以把 Spring Boot 理解成:

它不是替代 Spring,而是帮你以"约定优于配置"的方式更高效地使用 Spring。


2. 一个最小 Spring Boot 应用

2.1 Maven 依赖

xml 复制代码
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.2</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>1.0.0</version>
    <name>demo</name>
    <description>Spring Boot demo</description>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

2.2 启动类

java 复制代码
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

2.3 第一个接口

java 复制代码
package com.example.demo.web;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/hello")
public class HelloController {

    @GetMapping
    public String hello() {
        return "Hello, Spring Boot";
    }
}

启动后访问:

text 复制代码
GET http://localhost:8080/api/hello

返回:

text 复制代码
Hello, Spring Boot

这一步很简单,但背后已经发生了很多自动配置:

  • Spring MVC 被自动启用
  • Tomcat 被自动启动
  • DispatcherServlet 被自动注册
  • Jackson 被自动配置用于 JSON 序列化

3. @SpringBootApplication 到底做了什么

这个注解是一个组合注解,本质上包含:

java 复制代码
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan

3.1 @SpringBootConfiguration

它本质上就是 @Configuration,表示当前类是一个配置类。

3.2 @EnableAutoConfiguration

它会触发 Spring Boot 的自动配置机制。

Spring Boot 会基于:

  • 当前 classpath 中是否存在某个类
  • 当前是否已经定义某个 Bean
  • 当前是否有某些配置项

来决定是否自动注册某些组件。

比如:

  • 如果 classpath 有 spring-webmvc,就自动配置 Spring MVC
  • 如果 classpath 有 jackson-databind,就自动配置 Jackson
  • 如果 classpath 有 spring-boot-starter-data-jpa 和数据源,就自动配置 JPA

3.3 @ComponentScan

它会扫描当前启动类所在包及其子包中的组件,例如:

  • @Component
  • @Service
  • @Repository
  • @Controller
  • @RestController

因此,启动类通常应该放在项目的根包下。

错误示例:

text 复制代码
com.example.app.boot.DemoApplication
com.example.user.controller.UserController

如果启动类不在更高层级,可能扫不到 user.controller

推荐结构:

text 复制代码
com.example.app
├─ DemoApplication
├─ config
├─ controller
├─ service
├─ repository
└─ domain

4. 推荐的工程目录结构

一个中小型业务项目,通常建议这样组织:

text 复制代码
src/main/java/com/example/order
├─ OrderApplication.java
├─ config
│  ├─ JacksonConfig.java
│  └─ WebConfig.java
├─ common
│  ├─ exception
│  ├─ response
│  └─ util
├─ controller
│  └─ OrderController.java
├─ service
│  ├─ OrderService.java
│  └─ impl
│     └─ OrderServiceImpl.java
├─ repository
│  └─ OrderRepository.java
├─ domain
│  ├─ entity
│  ├─ dto
│  └─ vo
└─ client
   └─ PaymentClient.java

各层职责建议如下:

  • controller:接收 HTTP 请求,参数转换,返回响应
  • service:封装业务逻辑
  • repository:负责数据访问
  • entity:数据库实体
  • dto:请求/传输对象
  • vo:视图对象、返回对象
  • config:配置类
  • common:通用异常、返回体、工具类

这套分层不是唯一标准,但优点是职责清晰,后期扩展成本低。


5. 配置文件管理

Spring Boot 默认支持:

  • application.properties
  • application.yml
  • application.yaml

实际项目里,推荐使用 application.yml,可读性更好。

5.1 基础配置

yaml 复制代码
server:
  port: 8080

spring:
  application:
    name: order-service

logging:
  level:
    root: info
    com.example.order: debug

5.2 多环境配置

application.yml

yaml 复制代码
spring:
  profiles:
    active: dev

application-dev.yml

yaml 复制代码
server:
  port: 8080

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/order_dev
    username: root
    password: 123456

application-prod.yml

yaml 复制代码
server:
  port: 8080

spring:
  datasource:
    url: jdbc:mysql://prod-db:3306/order_prod
    username: app_user
    password: ${DB_PASSWORD}

启动时也可以覆盖:

bash 复制代码
java -jar app.jar --spring.profiles.active=prod

5.3 读取自定义配置

使用 @Value
java 复制代码
@Value("${spring.application.name}")
private String appName;
使用 @ConfigurationProperties
yaml 复制代码
app:
  security:
    token-expire-seconds: 3600
    allowed-origins:
      - http://localhost:3000
      - https://example.com
java 复制代码
package com.example.order.config;

import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "app.security")
public class SecurityProperties {

    private Long tokenExpireSeconds;
    private List<String> allowedOrigins;

    public Long getTokenExpireSeconds() {
        return tokenExpireSeconds;
    }

    public void setTokenExpireSeconds(Long tokenExpireSeconds) {
        this.tokenExpireSeconds = tokenExpireSeconds;
    }

    public List<String> getAllowedOrigins() {
        return allowedOrigins;
    }

    public void setAllowedOrigins(List<String> allowedOrigins) {
        this.allowedOrigins = allowedOrigins;
    }
}
java 复制代码
package com.example.order.config;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class PropertiesConfig {
}

相比 @Value@ConfigurationProperties 更适合读取结构化配置。


6. 依赖注入与 Bean 管理

Spring Boot 底层仍然是 Spring IoC 容器。

6.1 常见组件注解

java 复制代码
@Component
@Service
@Repository
@Controller
@RestController

6.2 推荐构造器注入

不推荐字段注入:

java 复制代码
@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;
}

推荐构造器注入:

java 复制代码
@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

优点:

  • 依赖是显式的
  • 方便测试
  • 支持 final
  • 对不可变对象更友好

6.3 Java 配置方式注册 Bean

java 复制代码
package com.example.order.config;

import java.time.Clock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean
    public Clock systemClock() {
        return Clock.systemUTC();
    }
}

注入使用:

java 复制代码
@Service
public class TimeService {

    private final Clock clock;

    public TimeService(Clock clock) {
        this.clock = clock;
    }
}

7. 构建一个更真实的 REST API

下面用一个订单模块做示例。

7.1 请求 DTO

java 复制代码
package com.example.order.domain.dto;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

public class CreateOrderRequest {

    @NotBlank(message = "customerName 不能为空")
    private String customerName;

    @NotBlank(message = "productCode 不能为空")
    private String productCode;

    @NotNull(message = "quantity 不能为空")
    @Min(value = 1, message = "quantity 必须大于 0")
    private Integer quantity;

    public String getCustomerName() {
        return customerName;
    }

    public void setCustomerName(String customerName) {
        this.customerName = customerName;
    }

    public String getProductCode() {
        return productCode;
    }

    public void setProductCode(String productCode) {
        this.productCode = productCode;
    }

    public Integer getQuantity() {
        return quantity;
    }

    public void setQuantity(Integer quantity) {
        this.quantity = quantity;
    }
}

7.2 返回 VO

java 复制代码
package com.example.order.domain.vo;

public class OrderVO {

    private Long id;
    private String customerName;
    private String productCode;
    private Integer quantity;
    private String status;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getCustomerName() {
        return customerName;
    }

    public void setCustomerName(String customerName) {
        this.customerName = customerName;
    }

    public String getProductCode() {
        return productCode;
    }

    public void setProductCode(String productCode) {
        this.productCode = productCode;
    }

    public Integer getQuantity() {
        return quantity;
    }

    public void setQuantity(Integer quantity) {
        this.quantity = quantity;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }
}

7.3 统一返回体

java 复制代码
package com.example.order.common.response;

public class ApiResponse<T> {

    private boolean success;
    private String code;
    private String message;
    private T data;

    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.success = true;
        response.code = "OK";
        response.message = "success";
        response.data = data;
        return response;
    }

    public static <T> ApiResponse<T> fail(String code, String message) {
        ApiResponse<T> response = new ApiResponse<>();
        response.success = false;
        response.code = code;
        response.message = message;
        return response;
    }

    public boolean isSuccess() {
        return success;
    }

    public String getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }

    public T getData() {
        return data;
    }
}

7.4 Service 层

java 复制代码
package com.example.order.service;

import com.example.order.domain.dto.CreateOrderRequest;
import com.example.order.domain.vo.OrderVO;

public interface OrderService {

    OrderVO createOrder(CreateOrderRequest request);

    OrderVO getOrderById(Long id);
}
java 复制代码
package com.example.order.service.impl;

import com.example.order.domain.dto.CreateOrderRequest;
import com.example.order.domain.vo.OrderVO;
import com.example.order.service.OrderService;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.stereotype.Service;

@Service
public class OrderServiceImpl implements OrderService {

    private final AtomicLong idGenerator = new AtomicLong(1);
    private final Map<Long, OrderVO> orderStore = new ConcurrentHashMap<>();

    @Override
    public OrderVO createOrder(CreateOrderRequest request) {
        OrderVO order = new OrderVO();
        order.setId(idGenerator.getAndIncrement());
        order.setCustomerName(request.getCustomerName());
        order.setProductCode(request.getProductCode());
        order.setQuantity(request.getQuantity());
        order.setStatus("CREATED");
        orderStore.put(order.getId(), order);
        return order;
    }

    @Override
    public OrderVO getOrderById(Long id) {
        return orderStore.get(id);
    }
}

7.5 Controller 层

java 复制代码
package com.example.order.controller;

import com.example.order.common.response.ApiResponse;
import com.example.order.domain.dto.CreateOrderRequest;
import com.example.order.domain.vo.OrderVO;
import com.example.order.service.OrderService;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @PostMapping
    public ApiResponse<OrderVO> createOrder(@Valid @RequestBody CreateOrderRequest request) {
        return ApiResponse.success(orderService.createOrder(request));
    }

    @GetMapping("/{id}")
    public ApiResponse<OrderVO> getOrder(@PathVariable Long id) {
        return ApiResponse.success(orderService.getOrderById(id));
    }
}

测试请求:

http 复制代码
POST /api/orders
Content-Type: application/json

{
  "customerName": "Alice",
  "productCode": "P1001",
  "quantity": 2
}

8. 参数校验与异常处理

真实项目中,参数校验和异常处理必须统一,否则接口行为会非常混乱。

8.1 @Valid 的使用

只要引入:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

即可结合:

  • @Valid
  • @NotNull
  • @NotBlank
  • @Min
  • @Max
  • @Size
  • @Email

进行参数校验。

8.2 自定义业务异常

java 复制代码
package com.example.order.common.exception;

public class BizException extends RuntimeException {

    private final String code;

    public BizException(String code, String message) {
        super(message);
        this.code = code;
    }

    public String getCode() {
        return code;
    }
}

8.3 全局异常处理器

java 复制代码
package com.example.order.common.exception;

import com.example.order.common.response.ApiResponse;
import java.util.stream.Collectors;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BizException.class)
    public ApiResponse<Void> handleBizException(BizException ex) {
        return ApiResponse.fail(ex.getCode(), ex.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ApiResponse<Void> handleValidationException(MethodArgumentNotValidException ex) {
        String message = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(error -> error.getField() + ": " + error.getDefaultMessage())
            .collect(Collectors.joining("; "));

        return ApiResponse.fail("PARAM_INVALID", message);
    }

    @ExceptionHandler(Exception.class)
    public ApiResponse<Void> handleException(Exception ex) {
        return ApiResponse.fail("INTERNAL_ERROR", ex.getMessage());
    }
}

8.4 在业务里抛异常

java 复制代码
@Override
public OrderVO getOrderById(Long id) {
    OrderVO order = orderStore.get(id);
    if (order == null) {
        throw new BizException("ORDER_NOT_FOUND", "订单不存在: " + id);
    }
    return order;
}

这样接口层就不用散落着各种 try-catch


9. 使用 Spring Data JPA 访问数据库

如果你的项目更偏业务型 CRUD,JPA 通常能显著提高开发效率。

9.1 依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

9.2 数据源配置

yaml 复制代码
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/order_db?useSSL=false&serverTimezone=UTC
    username: root
    password: 123456
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true

9.3 实体类

java 复制代码
package com.example.order.domain.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "t_order")
public class OrderEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String customerName;
    private String productCode;
    private Integer quantity;
    private String status;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getCustomerName() {
        return customerName;
    }

    public void setCustomerName(String customerName) {
        this.customerName = customerName;
    }

    public String getProductCode() {
        return productCode;
    }

    public void setProductCode(String productCode) {
        this.productCode = productCode;
    }

    public Integer getQuantity() {
        return quantity;
    }

    public void setQuantity(Integer quantity) {
        this.quantity = quantity;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }
}

9.4 Repository

java 复制代码
package com.example.order.repository;

import com.example.order.domain.entity.OrderEntity;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;

public interface OrderRepository extends JpaRepository<OrderEntity, Long> {

    List<OrderEntity> findByStatus(String status);
}

9.5 Service 使用 Repository

java 复制代码
package com.example.order.service.impl;

import com.example.order.common.exception.BizException;
import com.example.order.domain.dto.CreateOrderRequest;
import com.example.order.domain.entity.OrderEntity;
import com.example.order.domain.vo.OrderVO;
import com.example.order.repository.OrderRepository;
import com.example.order.service.OrderService;
import org.springframework.stereotype.Service;

@Service
public class OrderServiceImpl implements OrderService {

    private final OrderRepository orderRepository;

    public OrderServiceImpl(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Override
    public OrderVO createOrder(CreateOrderRequest request) {
        OrderEntity entity = new OrderEntity();
        entity.setCustomerName(request.getCustomerName());
        entity.setProductCode(request.getProductCode());
        entity.setQuantity(request.getQuantity());
        entity.setStatus("CREATED");

        OrderEntity saved = orderRepository.save(entity);
        return toVO(saved);
    }

    @Override
    public OrderVO getOrderById(Long id) {
        OrderEntity entity = orderRepository.findById(id)
            .orElseThrow(() -> new BizException("ORDER_NOT_FOUND", "订单不存在: " + id));
        return toVO(entity);
    }

    private OrderVO toVO(OrderEntity entity) {
        OrderVO vo = new OrderVO();
        vo.setId(entity.getId());
        vo.setCustomerName(entity.getCustomerName());
        vo.setProductCode(entity.getProductCode());
        vo.setQuantity(entity.getQuantity());
        vo.setStatus(entity.getStatus());
        return vo;
    }
}

9.6 什么时候不用 JPA

JPA 很方便,但并不是所有场景都适合。

这几类情况你可能要考虑 MyBatis、JdbcTemplate 或原生 SQL:

  • 复杂动态 SQL
  • 大量报表查询
  • 强依赖数据库特性的场景
  • 需要精细控制 SQL 性能

结论不是"JPA 好"或"JPA 不好",而是:

简单 CRUD 用 JPA 很高效,复杂查询要务实选型。


10. 事务管理

Spring Boot 中事务管理最常见的方式就是 @Transactional

10.1 基本用法

java 复制代码
package com.example.order.service.impl;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class PaymentService {

    @Transactional
    public void pay(Long orderId) {
        // 1. 扣减余额
        // 2. 创建支付记录
        // 3. 更新订单状态
        // 任一步异常,整体回滚
    }
}

10.2 常见误区

误区 1:同类方法内部调用事务失效
java 复制代码
@Service
public class OrderService {

    public void create() {
        saveOrder();
    }

    @Transactional
    public void saveOrder() {
        // 事务可能不生效
    }
}

原因是 Spring 事务通常基于代理,同类内部直接调用绕过了代理。

误区 2:捕获异常后不抛出,事务不回滚
java 复制代码
@Transactional
public void doBusiness() {
    try {
        // do something
        throw new RuntimeException("error");
    } catch (Exception ex) {
        // 吃掉异常
    }
}

这样事务不会回滚。

误区 3:默认只回滚运行时异常

如果你抛的是受检异常,默认可能不回滚:

java 复制代码
@Transactional(rollbackFor = Exception.class)
public void doBusiness() throws Exception {
    throw new Exception("checked exception");
}

11. 自定义配置与拦截器

11.1 配置 Jackson

java 复制代码
package com.example.order.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JacksonConfig {

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JavaTimeModule());
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        return objectMapper;
    }
}

11.2 编写拦截器

java 复制代码
package com.example.order.config;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

@Component
public class RequestLogInterceptor implements HandlerInterceptor {

    private static final Logger log = LoggerFactory.getLogger(RequestLogInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        log.info("Incoming request: {} {}", request.getMethod(), request.getRequestURI());
        return true;
    }
}

11.3 注册拦截器

java 复制代码
package com.example.order.config;

import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.context.annotation.Configuration;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    private final RequestLogInterceptor requestLogInterceptor;

    public WebConfig(RequestLogInterceptor requestLogInterceptor) {
        this.requestLogInterceptor = requestLogInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(requestLogInterceptor)
            .addPathPatterns("/api/**")
            .excludePathPatterns("/actuator/**");
    }
}

12. 统一日志与排障思路

日志不是为了"打印一下看看",而是为了排障和审计。

12.1 推荐日志写法

java 复制代码
private static final Logger log = LoggerFactory.getLogger(OrderServiceImpl.class);

log.info("Creating order, customerName={}, productCode={}, quantity={}",
    request.getCustomerName(), request.getProductCode(), request.getQuantity());

避免这样写:

java 复制代码
log.info("Creating order: " + request);

原因:

  • 失去参数化日志的性能优势
  • 结构化分析不方便
  • 容易误输出敏感信息

12.2 建议记录的关键日志

  • 请求入口日志
  • 关键业务节点日志
  • 外部系统调用日志
  • 异常日志
  • 重要状态变更日志

12.3 日志级别建议

  • ERROR:明确失败,需要处理
  • WARN:异常但系统可继续运行
  • INFO:关键业务流程节点
  • DEBUG:调试阶段细节信息

13. 测试:不要只会手工点接口

Spring Boot 的测试能力很强,但很多项目最后只剩 Postman 手测,这是明显不够的。

13.1 Service 单元测试

java 复制代码
package com.example.order.service;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

import com.example.order.domain.entity.OrderEntity;
import com.example.order.repository.OrderRepository;
import com.example.order.service.impl.OrderServiceImpl;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock
    private OrderRepository orderRepository;

    @InjectMocks
    private OrderServiceImpl orderService;

    @Test
    void shouldReturnOrderWhenOrderExists() {
        OrderEntity entity = new OrderEntity();
        entity.setId(1L);
        entity.setCustomerName("Alice");
        entity.setProductCode("P1001");
        entity.setQuantity(2);
        entity.setStatus("CREATED");

        when(orderRepository.findById(1L)).thenReturn(Optional.of(entity));

        assertEquals("Alice", orderService.getOrderById(1L).getCustomerName());
    }
}

13.2 Controller 测试

java 复制代码
package com.example.order.controller;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.example.order.domain.vo.OrderVO;
import com.example.order.service.OrderService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(OrderController.class)
class OrderControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private OrderService orderService;

    @Test
    void shouldCreateOrder() throws Exception {
        OrderVO orderVO = new OrderVO();
        orderVO.setId(100L);
        orderVO.setCustomerName("Alice");
        orderVO.setProductCode("P1001");
        orderVO.setQuantity(2);
        orderVO.setStatus("CREATED");

        when(orderService.createOrder(any())).thenReturn(orderVO);

        mockMvc.perform(post("/api/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {
                      \"customerName\": \"Alice\",
                      \"productCode\": \"P1001\",
                      \"quantity\": 2
                    }
                    """))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.success").value(true))
            .andExpect(jsonPath("$.data.id").value(100));
    }
}

13.3 集成测试

java 复制代码
@SpringBootTest
class ApplicationIntegrationTest {

    @Test
    void contextLoads() {
    }
}

更完整的集成测试还可以结合:

  • Testcontainers
  • H2
  • MockWebServer
  • WireMock

14. 监控与 Actuator

生产环境里,Spring Boot 的监控能力非常重要。

14.1 引入依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

14.2 开启端点

yaml 复制代码
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,env,loggers
  endpoint:
    health:
      show-details: always

14.3 常见端点

  • /actuator/health:健康检查
  • /actuator/info:应用信息
  • /actuator/metrics:指标列表
  • /actuator/env:环境变量和配置
  • /actuator/loggers:动态日志级别

14.4 健康检查示例

json 复制代码
{
  "status": "UP",
  "components": {
    "diskSpace": {
      "status": "UP"
    },
    "ping": {
      "status": "UP"
    }
  }
}

Kubernetes、负载均衡器、监控平台都很依赖这个能力。


15. 异步任务与定时任务

15.1 开启异步

java 复制代码
package com.example.order;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@EnableAsync
@SpringBootApplication
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}

15.2 异步方法

java 复制代码
package com.example.order.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class NotificationService {

    private static final Logger log = LoggerFactory.getLogger(NotificationService.class);

    @Async
    public void sendOrderCreatedMessage(Long orderId) {
        log.info("Sending notification for orderId={}", orderId);
    }
}

15.3 开启定时任务

java 复制代码
package com.example.order;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling
@SpringBootApplication
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}

15.4 定时任务示例

java 复制代码
package com.example.order.job;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class OrderCleanupJob {

    private static final Logger log = LoggerFactory.getLogger(OrderCleanupJob.class);

    @Scheduled(cron = "0 0 2 * * ?")
    public void cleanExpiredOrders() {
        log.info("Cleaning expired orders...");
    }
}

注意事项:

  • 定时任务要考虑幂等性
  • 多实例部署时要考虑重复执行问题
  • 涉及分布式场景时常配合分布式锁

16. Spring Boot 中常见的 Web 进阶能力

16.1 处理查询参数

java 复制代码
@GetMapping
public ApiResponse<String> query(@RequestParam String keyword,
                                 @RequestParam(defaultValue = "1") int page) {
    return ApiResponse.success("keyword=" + keyword + ", page=" + page);
}

16.2 处理路径参数

java 复制代码
@GetMapping("/{id}")
public ApiResponse<Long> getById(@PathVariable Long id) {
    return ApiResponse.success(id);
}

16.3 处理请求头

java 复制代码
@GetMapping("/headers")
public ApiResponse<String> readHeader(@RequestHeader("X-Trace-Id") String traceId) {
    return ApiResponse.success(traceId);
}

16.4 文件上传

java 复制代码
@PostMapping("/upload")
public ApiResponse<String> upload(@RequestParam("file") MultipartFile file) throws Exception {
    String filename = file.getOriginalFilename();
    long size = file.getSize();
    return ApiResponse.success("uploaded: " + filename + ", size=" + size);
}

17. 安全与配置隔离的基本原则

即使不展开写 Spring Security,也至少要遵守几条原则:

  • 不要把密码直接写死在源码里
  • 区分开发、测试、生产环境配置
  • 敏感信息优先用环境变量或密钥平台管理
  • 对外暴露接口要做鉴权
  • Actuator 端点不要全部裸露到公网

例如:

yaml 复制代码
spring:
  datasource:
    password: ${DB_PASSWORD}

如果后期接 Spring Security,最少也会涉及:

  • 认证
  • 授权
  • Token/JWT
  • 登录态管理
  • 接口白名单

18. 打包与部署

18.1 Maven 打包

bash 复制代码
mvn clean package

产物通常是:

text 复制代码
target/demo-1.0.0.jar

18.2 运行 Jar

bash 复制代码
java -jar target/demo-1.0.0.jar

18.3 常见运行参数

bash 复制代码
java -jar target/demo-1.0.0.jar \
  --spring.profiles.active=prod \
  --server.port=9090

18.4 Docker 化示例

dockerfile 复制代码
FROM eclipse-temurin:17-jre

WORKDIR /app
COPY target/demo-1.0.0.jar app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "/app/app.jar"]

构建和运行:

bash 复制代码
docker build -t demo-app:1.0.0 .
docker run -p 8080:8080 demo-app:1.0.0

19. 一个实际项目的常见 Starter 组合

一个典型业务后端,常见依赖组合可能是:

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

如果用 MyBatis,则替换数据访问层相关依赖。


20. 新手最容易踩的坑

20.1 包路径扫描不到

启动类不在根包,导致 Bean 没被扫描。

20.2 事务不生效

同类内部调用、异常被吞、回滚规则不对。

20.3 DTO/Entity/VO 混着用

前期看似省事,后期字段会越来越乱。

20.4 Controller 写太重

Controller 应该薄,业务逻辑尽量放 Service。

20.5 所有异常都直接返回 500

这会让前端和调用方很难处理业务错误。

20.6 日志乱打

过多无意义日志会掩盖真正有价值的信息。

20.7 生产环境直接开全量 Actuator

存在明显安全风险。


21. 一套比较实用的开发建议

如果你是刚开始做 Spring Boot 项目,我建议按这个顺序提升:

  1. 先掌握基础 Web 开发:Controller、Service、DTO、VO
  2. 再掌握配置管理:application.yml、多环境、配置绑定
  3. 再掌握数据库访问:JPA 或 MyBatis
  4. 再掌握异常处理、统一返回体、日志规范
  5. 最后补上测试、监控、部署、性能优化

真正的项目能力,不是"会写接口",而是:

  • 接口结构稳定
  • 配置清晰
  • 异常可控
  • 日志可追踪
  • 测试可回归
  • 线上可观测

这才是 Spring Boot 工程化的价值。


22. 总结

Spring Boot 之所以流行,并不是因为它"简单",而是因为它把一整套现代 Java 后端项目真正需要的能力整合得足够好。

它适合的,不只是教学示例,而是实际生产工程。

你可以把这篇文章的重点浓缩成下面几句话:

  • Spring Boot 的核心不是少写配置,而是自动配置和工程约定
  • 分层、校验、异常、日志、事务,这些基础能力比"多写几个接口"更重要
  • JPA、MyBatis、Actuator、定时任务、异步任务,本质上都是工程能力的补充
  • 真正成熟的 Spring Boot 项目,重点在可维护性、可测试性和可观测性

如果要继续深入,下一步很自然的方向通常是:

  • Spring Security
  • Spring Cloud
  • MyBatis 动态 SQL
  • Redis 缓存
  • 消息队列
  • 分布式事务
  • 容器化与 Kubernetes 部署

附:一个完整的示例请求流

为了把全文串起来,最后放一个简化的请求流。
#mermaid-svg-cCyY1dZ1ekkWtCAc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-cCyY1dZ1ekkWtCAc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-cCyY1dZ1ekkWtCAc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-cCyY1dZ1ekkWtCAc .error-icon{fill:#552222;}#mermaid-svg-cCyY1dZ1ekkWtCAc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cCyY1dZ1ekkWtCAc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-cCyY1dZ1ekkWtCAc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cCyY1dZ1ekkWtCAc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cCyY1dZ1ekkWtCAc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-cCyY1dZ1ekkWtCAc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cCyY1dZ1ekkWtCAc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cCyY1dZ1ekkWtCAc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cCyY1dZ1ekkWtCAc .marker.cross{stroke:#333333;}#mermaid-svg-cCyY1dZ1ekkWtCAc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cCyY1dZ1ekkWtCAc p{margin:0;}#mermaid-svg-cCyY1dZ1ekkWtCAc .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-cCyY1dZ1ekkWtCAc .cluster-label text{fill:#333;}#mermaid-svg-cCyY1dZ1ekkWtCAc .cluster-label span{color:#333;}#mermaid-svg-cCyY1dZ1ekkWtCAc .cluster-label span p{background-color:transparent;}#mermaid-svg-cCyY1dZ1ekkWtCAc .label text,#mermaid-svg-cCyY1dZ1ekkWtCAc span{fill:#333;color:#333;}#mermaid-svg-cCyY1dZ1ekkWtCAc .node rect,#mermaid-svg-cCyY1dZ1ekkWtCAc .node circle,#mermaid-svg-cCyY1dZ1ekkWtCAc .node ellipse,#mermaid-svg-cCyY1dZ1ekkWtCAc .node polygon,#mermaid-svg-cCyY1dZ1ekkWtCAc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-cCyY1dZ1ekkWtCAc .rough-node .label text,#mermaid-svg-cCyY1dZ1ekkWtCAc .node .label text,#mermaid-svg-cCyY1dZ1ekkWtCAc .image-shape .label,#mermaid-svg-cCyY1dZ1ekkWtCAc .icon-shape .label{text-anchor:middle;}#mermaid-svg-cCyY1dZ1ekkWtCAc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-cCyY1dZ1ekkWtCAc .rough-node .label,#mermaid-svg-cCyY1dZ1ekkWtCAc .node .label,#mermaid-svg-cCyY1dZ1ekkWtCAc .image-shape .label,#mermaid-svg-cCyY1dZ1ekkWtCAc .icon-shape .label{text-align:center;}#mermaid-svg-cCyY1dZ1ekkWtCAc .node.clickable{cursor:pointer;}#mermaid-svg-cCyY1dZ1ekkWtCAc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-cCyY1dZ1ekkWtCAc .arrowheadPath{fill:#333333;}#mermaid-svg-cCyY1dZ1ekkWtCAc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-cCyY1dZ1ekkWtCAc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-cCyY1dZ1ekkWtCAc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cCyY1dZ1ekkWtCAc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-cCyY1dZ1ekkWtCAc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cCyY1dZ1ekkWtCAc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-cCyY1dZ1ekkWtCAc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-cCyY1dZ1ekkWtCAc .cluster text{fill:#333;}#mermaid-svg-cCyY1dZ1ekkWtCAc .cluster span{color:#333;}#mermaid-svg-cCyY1dZ1ekkWtCAc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-cCyY1dZ1ekkWtCAc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-cCyY1dZ1ekkWtCAc rect.text{fill:none;stroke-width:0;}#mermaid-svg-cCyY1dZ1ekkWtCAc .icon-shape,#mermaid-svg-cCyY1dZ1ekkWtCAc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cCyY1dZ1ekkWtCAc .icon-shape p,#mermaid-svg-cCyY1dZ1ekkWtCAc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-cCyY1dZ1ekkWtCAc .icon-shape .label rect,#mermaid-svg-cCyY1dZ1ekkWtCAc .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cCyY1dZ1ekkWtCAc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-cCyY1dZ1ekkWtCAc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-cCyY1dZ1ekkWtCAc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Client
Controller
DTO Validation
Service
Repository
Database
VO Mapping
ApiResponse JSON

对应的执行链路通常是:

  1. 客户端发起 HTTP 请求
  2. Controller 接收并解析参数
  3. 使用 @Valid 校验 DTO
  4. Service 执行业务逻辑
  5. Repository 访问数据库
  6. 将 Entity 转换为 VO
  7. 包装成统一响应体返回

这就是一个典型 Spring Boot API 的主干路径。


附:一个更接近生产的 application.yml

yaml 复制代码
server:
  port: 8080
  shutdown: graceful

spring:
  application:
    name: order-service
  profiles:
    active: dev
  jackson:
    default-property-inclusion: non_null
  datasource:
    url: jdbc:mysql://localhost:3306/order_db?useSSL=false&serverTimezone=UTC
    username: root
    password: ${DB_PASSWORD:123456}
  jpa:
    hibernate:
      ddl-auto: none
    open-in-view: false
    properties:
      hibernate:
        format_sql: true

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: always

logging:
  level:
    root: info
    com.example.order: info
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"

app:
  security:
    token-expire-seconds: 3600
    allowed-origins:
      - http://localhost:3000

这类配置已经比较接近真实服务的基础骨架了。


结尾

如果你的目标只是"把接口跑起来",Spring Boot 一天就能上手。

如果你的目标是"把项目做得稳、做得久、做得能上线",那你真正需要吃透的,是这篇文章里反复出现的几个关键词:

  • 自动配置
  • 分层设计
  • 参数校验
  • 事务控制
  • 异常处理
  • 测试
  • 监控
  • 部署

当这些基础打稳后,Spring Boot 就不再只是一个框架,而是一整套后端工程实践的起点。

相关推荐
IT_陈寒4 小时前
Java Stream并行流的坑:我花了3小时才找到的线程安全问题
前端·人工智能·后端
橘子海全栈攻城狮4 小时前
【最新源码】鸟博士微信小程序 023
spring boot·后端·web安全·微信小程序·小程序
Hiter_John4 小时前
Golang的运算符
开发语言·后端·golang
皮皮林5514 小时前
Dubbo 的 SPI 和 JDK 的 SPI 有什么区别?
后端
金銀銅鐵5 小时前
用 Tkinter 实现一个罗马数字转整数的简单工具
后端·python
河阿里5 小时前
Spring Boot:整合Quartz集群部署指南
java·spring boot·后端
Hiter_John5 小时前
Golang的变量常量初始化
开发语言·后端·golang
砍材农夫5 小时前
物联网实战:Spring Boot MQTT | 模拟器Paho客户端拆解高性能
java·javascript·spring boot·后端·物联网·struts
逍遥德6 小时前
Java编程高频的“技术点”-03:“下划线命名”参数,后端用“驼峰命名“接收
java·后端·springboot