前言
在 Java 后端生态里,Spring Boot 已经从"快速启动框架"演变成了事实上的默认工程底座。它不只是帮你省掉 XML 配置,更重要的是把一套可运行、可扩展、可观测、可测试的工程约定打包好了。
很多人第一次接触 Spring Boot,会觉得它只是:
- 加一个
spring-boot-starter-web - 写一个
@RestController - 启动项目
- 然后接口就能跑了
这当然没错,但这只是表面。真正把 Spring Boot 用好,重点在于理解这几个问题:
- Spring Boot 到底帮我们"自动"了什么。
- 一个真实项目该怎么分层、怎么配配置、怎么做异常处理。
- 如何接数据库、如何做参数校验、如何测试。
- 在生产环境里如何监控、排障、发布。
这篇文章尽量不只停留在"hello world",而是用一篇偏工程化的博文,把 Spring Boot 的关键知识串起来,并尽量给出可直接参考的代码。
1. Spring Boot 是什么
Spring Boot 是 Spring 生态中的快速开发框架,核心目标有三个:
- 简化配置
- 提升开发效率
- 提供可直接上线的工程基础设施
它的几个核心能力可以概括为:
Auto Configuration:自动装配,根据 classpath、配置项、Bean 条件自动初始化组件Starter:一组开箱即用的依赖集合Embedded Server:内嵌 Tomcat、Jetty 或 UndertowActuator:健康检查、指标、监控端点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.propertiesapplication.ymlapplication.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 项目,我建议按这个顺序提升:
- 先掌握基础 Web 开发:Controller、Service、DTO、VO
- 再掌握配置管理:
application.yml、多环境、配置绑定 - 再掌握数据库访问:JPA 或 MyBatis
- 再掌握异常处理、统一返回体、日志规范
- 最后补上测试、监控、部署、性能优化
真正的项目能力,不是"会写接口",而是:
- 接口结构稳定
- 配置清晰
- 异常可控
- 日志可追踪
- 测试可回归
- 线上可观测
这才是 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
对应的执行链路通常是:
- 客户端发起 HTTP 请求
- Controller 接收并解析参数
- 使用
@Valid校验 DTO - Service 执行业务逻辑
- Repository 访问数据库
- 将 Entity 转换为 VO
- 包装成统一响应体返回
这就是一个典型 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 就不再只是一个框架,而是一整套后端工程实践的起点。