Spring Boot 3.x 新特性详解:从基础到高级实战
1. Spring Boot 3.x 简介与核心特性
1.1 Spring Boot 3.x 新特性概览
Spring Boot 3.x 是建立在 Spring Framework 6.0 基础上的重大版本更新,主要新特性包括:
特性 | 说明 | 优势 |
JDK 17+ 基线 | 最低要求Java 17 | 利用现代Java特性如record、文本块等 |
Jakarta EE 9+ | 从javax迁移到jakarta命名空间 | 符合最新企业标准 |
改进的GraalVM支持 | 更好的原生镜像支持 | 更快的启动速度和更低的内存占用 |
增强的Micrometer集成 | 改进的指标收集 | 更好的可观测性 |
新的Problem Details API | 标准化的错误响应格式 | 更好的API错误处理 |
自动配置改进 | 更智能的条件配置 | 更精简的运行时代码 |
1.2 Spring Boot 3.x 架构变化
1.3 版本升级注意事项
从Spring Boot 2.x升级到3.x需要考虑:
- 包名变更:所有javax.*包名变更为jakarta.*
- // 旧版
import javax.servlet.http.HttpServletRequest;
// 新版
import jakarta.servlet.http.HttpServletRequest; - 最低Java版本:必须使用Java 17或更高版本
- 依赖变更:
- Spring Framework 6.0+
- Hibernate 6.1+
- Tomcat 10+ (支持Jakarta EE 9)
2. 环境准备与项目创建
2.1 开发环境要求
组件 | 最低版本 | 推荐版本 |
JDK | 17 | 17或21 |
Maven | 3.5+ | 3.8+ |
Gradle | 7.x | 8.x |
IDE | IntelliJ IDEA 2021.3+ | IDEA Ultimate |
2.2 项目初始化方式
方式1:使用Spring Initializr网站
- 访问 https://start.spring.io
- 选择:
- Project: Maven/Gradle
- Language: Java
- Spring Boot: 3.1.0
- 添加依赖:根据需要选择
- 生成并下载项目
方式2:使用命令行
# 使用curl
curl https://start.spring.io/starter.tgz -d dependencies=web,data-jpa \
-d javaVersion=17 -d type=maven-project -d bootVersion=3.1.0 \
-d groupId=com.example -d artifactId=demo -d name=demo | tar -xzvf -
# 或者使用Spring Boot CLI
spring init --build=maven --java-version=17 --dependencies=web,data-jpa demo
2.3 项目结构解析
典型的Spring Boot 3.x项目结构:
src/
├── main/
│ ├── java/
│ │ └── com/example/demo/
│ │ ├── DemoApplication.java # 主启动类
│ │ ├── config/ # 配置类目录
│ │ ├── controller/ # 控制器目录
│ │ ├── service/ # 服务层目录
│ │ ├── repository/ # 数据访问层目录
│ │ └── model/ # 实体类目录
│ └── resources/
│ ├── static/ # 静态资源
│ ├── templates/ # 模板文件
│ ├── application.yml # 配置文件
│ └── application-dev.yml # 环境特定配置
└── test/ # 测试代码
2.4 第一个Spring Boot 3.x应用
主启动类示例:
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@RestController
class GreetingController {
@GetMapping("/greet")
public String greet(@RequestParam(value = "name", defaultValue = "World") String name) {
return String.format("Hello, %s from Spring Boot 3!", name);
}
}
运行与测试:
- 运行DemoApplication的main方法
- 访问 http://localhost:8080/greet
- 访问 http://localhost:8080/greet?name=John
3. 自动配置原理深度解析
3.1 自动配置机制详解
Spring Boot自动配置通过@EnableAutoConfiguration实现,其工作原理:
- 检查classpath下的依赖
- 根据条件注解(如@ConditionalOnClass)决定是否配置
- 应用合理的默认配置
自动配置流程:
自动配置Spring Boot开发者自动配置Spring Boot开发者添加starter依赖扫描META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports提供自动配置类列表应用条件过滤最终生效的配置
3.2 常用条件注解
注解 | 说明 | 示例 |
@ConditionalOnClass | 类路径存在指定类时生效 | @ConditionalOnClass(DataSource.class) |
@ConditionalOnMissingBean | 容器中不存在指定Bean时生效 | @ConditionalOnMissingBean(DataSource.class) |
@ConditionalOnProperty | 配置属性满足条件时生效 | @ConditionalOnProperty(name="cache.enabled", havingValue="true") |
@ConditionalOnWebApplication | Web应用环境下生效 | @ConditionalOnWebApplication(type=Type.SERVLET) |
@ConditionalOnExpression | SpEL表达式为true时生效 | @ConditionalOnExpression("${feature.enabled:true}") |
3.3 自定义自动配置
步骤1:创建自动配置类
/**
* 这是一个自动配置类,用于自动配置 MyService 相关的 Bean。
* 它会在满足特定条件时生效,并根据配置文件中的属性来创建 MyService 实例。
*/
@Configuration // 表明这是一个配置类,Spring 会对其进行处理,相当于传统的 XML 配置文件,该类中可以定义 Bean
@AutoConfiguration // 开启自动配置功能,Spring Boot 会根据类路径中的依赖自动配置 Spring 应用程序
@ConditionalOnClass(MyService.class) //条件注解,当类路径中存在 MyService 类时,这个配置类才会生效。即只有当项目依赖中包含 MyService 类时,Spring 才会处理这个配置类
@EnableConfigurationProperties(MyProperties.class) //启用配置属性绑定功能,将配置文件中的属性绑定到 MyProperties 类的实例上。这样就可以在配置文件中设置 MyProperties 类中定义的属性值
public class MyAutoConfiguration {
/**
* 创建并返回一个 MyService 实例。
*
* @param properties 从配置文件中绑定的 MyProperties 实例,包含了 MyService 所需的配置信息
* @return 返回一个使用了配置属性的 MyService 实例
*/
@Bean //定义一个名为 myService 的 Bean。该 Bean 的类型为 MyService,用于在 Spring 应用中提供特定的服务。
@ConditionalOnMissingBean //条件注解,当 Spring 容器中不存在 MyService 类型的 Bean 时,才会创建这个 Bean。避免重复创建相同类型的 Bean
public MyService myService(MyProperties properties) {
// 使用从配置文件中获取的属性创建 MyService 实例
return new MyService(properties);
}
}
步骤2:定义配置属性类
@ConfigurationProperties("my.service")
public class MyProperties {
private String prefix = "[INFO]";
private boolean enabled = true;
// getters and setters
}
步骤3:注册自动配置
在
src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports中添加:
com.example.config.MyAutoConfiguration
3.4 自动配置调试
方法1:启用调试日志
在application.yml中添加:
logging:
level:
org.springframework.boot.autoconfigure: DEBUG
方法2:使用ConditionEvaluationReport
启动时添加--debug参数,或在应用中:
// 这是一个 Spring Boot 应用的主注解,组合了 @SpringBootConfiguration、@EnableAutoConfiguration 和 @ComponentScan 等注解,用于简化 Spring Boot 应用的配置,标识这是一个 Spring Boot 应用的入口类
@SpringBootApplication
public class DemoApplication {
// 应用程序的入口点,程序从这里开始执行
public static void main(String[] args) {
// 启动 Spring Boot 应用,传入应用主类和命令行参数
SpringApplication.run(DemoApplication.class, args);
}
// 定义一个 Bean,类型为 CommandLineRunner,在 Spring Boot 应用启动完成后执行
@Bean
public CommandLineRunner conditionReport(ApplicationContext context) {
return args -> {
// 获取条件评估报告,通过应用上下文的 Bean 工厂来获取
ConditionEvaluationReport report = ConditionEvaluationReport.get(
context.getBeanFactory());
// 遍历条件评估报告中按源分组的条件和结果,并打印输出
report.getConditionAndOutcomesBySource().forEach((k, v) -> {
System.out.println(k + " => " + v);
});
};
}
}
4. Web开发全攻略
4.1 RESTful API开发
基础控制器示例:
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping
public ResponseEntity<List<Product>> getAllProducts() {
return ResponseEntity.ok(productService.findAll());
}
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
return productService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<Product> createProduct(@Valid @RequestBody Product product) {
Product saved = productService.save(product);
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(saved.getId())
.toUri();
return ResponseEntity.created(location).body(saved);
}
@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(
@PathVariable Long id,
@Valid @RequestBody Product product) {
if (!productService.existsById(id)) {
return ResponseEntity.notFound().build();
}
product.setId(id);
return ResponseEntity.ok(productService.save(product));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.deleteById(id);
return ResponseEntity.noContent().build();
}
}
4.2 异常处理
全局异常处理器:
// @RestControllerAdvice 是一个特殊的 @Component 注解,用于定义全局异常处理类。
// 它可以捕获并处理所有 @RestController 注解标注的控制器中抛出的异常,
// 并以统一的方式返回错误响应给客户端。
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理 ProductNotFoundException 异常的方法。
* 当控制器中抛出 ProductNotFoundException 异常时,会自动调用此方法进行处理。
*
* @param ex 捕获到的 ProductNotFoundException 异常实例
* @return 返回一个 ProblemDetail 对象,包含异常的详细信息,用于返回给客户端
*/
@ExceptionHandler(ProductNotFoundException.class)
public ProblemDetail handleProductNotFound(ProductNotFoundException ex) {
// 创建一个 ProblemDetail 对象,根据 HTTP 状态码(这里是 404 Not Found)和异常消息来初始化。
// ProblemDetail 是 Spring 提供的用于封装错误信息的类,方便以标准的方式返回错误响应。
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
HttpStatus.NOT_FOUND, ex.getMessage());
// 设置 ProblemDetail 对象的标题,用于简要描述错误类型
problemDetail.setTitle("Product Not Found");
// 为 ProblemDetail 对象添加一个属性,记录当前时间戳,方便追踪错误发生的时间
problemDetail.setProperty("timestamp", Instant.now());
// 为 ProblemDetail 对象添加一个属性,记录错误代码,方便客户端识别和处理特定类型的错误
problemDetail.setProperty("errorCode", "PRODUCT_NOT_FOUND");
// 返回包含详细错误信息的 ProblemDetail 对象
return problemDetail;
}
/**
* 处理 MethodArgumentNotValidException 异常的方法。
* 当控制器中的方法参数验证失败时,会抛出此异常,此方法会对其进行处理。
*
* @param ex 捕获到的 MethodArgumentNotValidException 异常实例
* @return 返回一个 ProblemDetail 对象,包含验证失败的详细信息,用于返回给客户端
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail handleValidationException(MethodArgumentNotValidException ex) {
// 创建一个 ProblemDetail 对象,根据 HTTP 状态码(这里是 400 Bad Request)和验证失败的提示信息来初始化
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_REQUEST, "Validation failed");
// 设置 ProblemDetail 对象的标题,用于简要描述错误类型
problemDetail.setTitle("Validation Error");
// 为 ProblemDetail 对象添加一个属性,记录当前时间戳,方便追踪错误发生的时间
problemDetail.setProperty("timestamp", Instant.now());
// 从异常中获取绑定结果,绑定结果中包含了所有验证失败的字段错误信息
List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors();
// 使用 Java 8 的 Stream API 对字段错误信息进行处理,将每个字段错误转换为一个包含字段名和错误消息的 Map
List<Map<String, String>> errors = fieldErrors.stream()
.map(fieldError -> Map.of(
"field", fieldError.getField(),
"message", fieldError.getDefaultMessage()))
// 将转换后的 Map 收集到一个列表中
.collect(Collectors.toList());
// 为 ProblemDetail 对象添加一个属性,记录所有验证失败的字段错误信息,方便客户端查看具体的错误情况
problemDetail.setProperty("errors", errors);
// 返回包含详细验证错误信息的 ProblemDetail 对象
return problemDetail;
}
}
4.3 静态资源与模板引擎
静态资源配置:
spring:
mvc:
static-path-pattern: /static/**
web:
resources:
static-locations:
- classpath:/static/
- file:/opt/app/static/
Thymeleaf模板示例:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Product List</title>
</head>
<body>
<h1>Products</h1>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<tr th:each="product : ${products}">
<td th:text="${product.id}">1</td>
<td th:text="${product.name}">Sample</td>
<td th:text="${#numbers.formatDecimal(product.price, 1, 2)}">0.00</td>
</tr>
</tbody>
</table>
</body>
</html>
4.4 文件上传与下载
文件上传控制器:
@RestController
@RequestMapping("/api/v1/files")
public class FileController {
private final FileStorageService storageService;
public FileController(FileStorageService storageService) {
this.storageService = storageService;
}
@PostMapping("/upload")
public ResponseEntity<FileInfo> uploadFile(@RequestParam("file") MultipartFile file) {
try {
FileInfo fileInfo = storageService.store(file);
return ResponseEntity.ok(fileInfo);
} catch (IOException e) {
throw new FileStorageException("Failed to store file", e);
}
}
@GetMapping("/download/{filename:.+}")
public ResponseEntity<Resource> downloadFile(@PathVariable String filename) {
Resource resource = storageService.loadAsResource(filename);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + resource.getFilename() + "\"")
.body(resource);
}
}
4.5 跨域配置
全局CORS配置:
// @Configuration 注解表明这是一个 Spring 配置类,Spring 会对其进行处理,
// 可以在这个类中定义 Bean 以及进行一些配置操作,类似于传统的 XML 配置文件。
@Configuration
// 实现 WebMvcConfigurer 接口,用于自定义 Spring MVC 的配置,
// 这里主要是为了配置跨域资源共享(CORS)相关的设置。
public class CorsConfig implements WebMvcConfigurer {
/**
* 重写 addCorsMappings 方法,用于配置跨域资源共享(CORS)规则。
* 跨域资源共享允许浏览器在跨源请求时,访问不同域名下的资源。
*
* @param registry CorsRegistry 对象,用于注册跨域映射规则
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
// 添加一个跨域映射规则,匹配所有以 /api/ 开头的请求路径
registry.addMapping("/api/**")
// 指定允许访问该资源的源(域名),这里允许 https://example.com 和 https://www.example.com 这两个域名的请求
.allowedOrigins("https://example.com", "https://www.example.com")
// 指定允许的 HTTP 请求方法,这里允许 GET、POST、PUT、DELETE 和 OPTIONS 方法
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
// 指定允许的请求头,* 表示允许所有请求头
.allowedHeaders("*")
// 指定允许客户端访问的响应头,这里允许客户端访问 Authorization 和 Content-Disposition 这两个响应头
.exposedHeaders("Authorization", "Content-Disposition")
// 允许请求携带凭证(如 cookies、HTTP 认证等),设置为 true 表示允许
.allowCredentials(true)
// 设置预检请求(OPTIONS 请求)的缓存时间,单位为秒,这里设置为 3600 秒(即 1 小时),
// 表示在 1 小时内,浏览器可以使用缓存的预检请求结果,而不需要再次发送预检请求
.maxAge(3600);
}
}
方法级CORS配置:
// @RestController 是 Spring 框架中的一个注解,它是 @Controller 和 @ResponseBody 的组合。
// 表明该类是一个 RESTful 风格的控制器,用于处理 HTTP 请求并返回 JSON 或 XML 等格式的数据。
@RestController
// @RequestMapping 注解用于映射 HTTP 请求到控制器的处理方法。
// 这里将所有以 /api/v1/users 开头的请求映射到这个控制器类中。
@RequestMapping("/api/v1/users")
public class UserController {
/**
* @CrossOrigin 注解用于解决跨域资源共享(CORS)问题。
* 跨域请求是指浏览器从一个域名的网页去请求另一个域名的资源时,由于浏览器的同源策略会受到限制。
* 通过该注解可以指定允许跨域访问的相关信息。
*
* origins 属性指定允许访问该资源的源(域名),这里只允许 https://client.example.com 这个域名的请求进行跨域访问。
* methods 属性指定允许的 HTTP 请求方法,这里只允许 GET 和 POST 请求跨域。
* allowedHeaders 属性指定允许的请求头,* 表示允许所有请求头。
*/
@CrossOrigin(origins = "https://client.example.com",
methods = {RequestMethod.GET, RequestMethod.POST},
allowedHeaders = "*")
// @GetMapping 是 @RequestMapping(method = RequestMethod.GET) 的缩写,
// 用于映射 HTTP GET 请求到该方法,即当客户端发送一个 GET 请求到 /api/v1/users 时,会调用此方法。
@GetMapping
/**
* 获取所有用户的信息。
* 该方法返回一个包含 User 对象的列表,用于表示所有用户的信息。
* 方法体中的 // ... 表示具体的业务逻辑代码,例如从数据库中查询所有用户信息等,这里省略了具体实现。
*
* @return 包含所有用户信息的列表
*/
public List<User> getAllUsers() {
// ... 这里应该实现具体的业务逻辑,例如从数据库中查询所有用户信息
return null;
}
}
5. 数据访问与事务管理
5.1 JDBC与JPA配置
application.yml配置示例:
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 10
connection-timeout: 30000
jpa:
show-sql: true
hibernate:
ddl-auto: update
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.MySQL8Dialect
5.2 JPA实体与Repository
实体类示例:
@Entity
@Table(name = "products")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String name;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price;
@Column(length = 500)
private String description;
@Enumerated(EnumType.STRING)
@Column(length = 20)
private ProductStatus status;
@CreationTimestamp
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
@Version
private Integer version;
}
public enum ProductStatus {
ACTIVE, DISCONTINUED, OUT_OF_STOCK
}
Repository接口:
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByStatus(ProductStatus status);
@Query("SELECT p FROM Product p WHERE p.price BETWEEN :min AND :max")
List<Product> findByPriceRange(@Param("min") BigDecimal min,
@Param("max") BigDecimal max);
@Query(value = "SELECT * FROM products WHERE name LIKE %:keyword%",
nativeQuery = true)
Page<Product> searchByName(@Param("keyword") String keyword, Pageable pageable);
@Modifying
@Query("UPDATE Product p SET p.price = p.price * :factor WHERE p.status = :status")
int updatePriceByStatus(@Param("status") ProductStatus status,
@Param("factor") BigDecimal factor);
}
5.3 事务管理
声明式事务:
// @Service 注解表明这是一个 Spring 服务类,通常用于处理业务逻辑。
// Spring 会自动扫描并将其注册为一个 Bean,以便在其他组件中进行依赖注入。
@Service
// @RequiredArgsConstructor 是 Lombok 提供的注解,
// 它会自动生成一个包含所有 final 字段的构造函数,方便进行依赖注入。
@RequiredArgsConstructor
// @Transactional 注解将整个类的方法都纳入事务管理,
// 意味着该类中的所有公共方法都会在事务中执行。
@Transactional
public class OrderService {
// 注入订单仓库,用于与订单数据进行交互,如保存、查询订单等操作。
private final OrderRepository orderRepository;
// 注入产品仓库,用于与产品数据进行交互,如查询产品信息等操作。
private final ProductRepository productRepository;
// 注入库存服务,用于处理库存相关的业务逻辑,如检查库存、扣减库存等操作。
private final InventoryService inventoryService;
/**
* 创建订单的方法。
*
* @Transactional(rollbackFor = {InventoryException.class, PaymentException.class})
* 该注解表明这个方法在事务中执行,并且当抛出 InventoryException 或 PaymentException 异常时会进行回滚操作。
*
* @param request 包含订单请求信息的对象
* @return 创建好的订单对象
*/
@Transactional(rollbackFor = {InventoryException.class, PaymentException.class})
public Order createOrder(OrderRequest request) {
// 调用库存服务的检查库存方法,检查订单请求中的商品是否有足够的库存。
inventoryService.checkInventory(request.getItems());
// 调用 createOrderFromRequest 方法,根据订单请求信息创建订单对象。
Order order = createOrderFromRequest(request);
// 将创建好的订单对象保存到数据库中。
order = orderRepository.save(order);
// 调用库存服务的扣减库存方法,扣减订单中商品的库存。
inventoryService.reduceInventory(request.getItems());
// 假设存在 paymentService 来处理支付逻辑,调用其处理支付方法完成订单支付。
// 这里代码中未定义 paymentService,可能是代码省略或者有其他依赖注入。
paymentService.processPayment(order);
// 返回创建好的订单对象。
return order;
}
/**
* 根据订单 ID 获取包含详细信息的订单。
*
* @Transactional(readOnly = true) 该注解表明这个方法在只读事务中执行,
* 可以提高性能,因为只读事务不需要进行锁操作。
*
* @param orderId 订单的 ID
* @return 包含详细信息的订单对象,如果未找到则抛出 OrderNotFoundException 异常
*/
@Transactional(readOnly = true)
public Order getOrderWithDetails(Long orderId) {
// 调用订单仓库的 findByIdWithDetails 方法,根据订单 ID 查询包含详细信息的订单。
// 如果未找到订单,则抛出 OrderNotFoundException 异常。
return orderRepository.findByIdWithDetails(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
}
/**
* 取消订单的方法。
*
* @Transactional(propagation = Propagation.REQUIRES_NEW) 该注解表明这个方法会开启一个新的事务,
* 无论外部是否存在事务,都会独立执行。
*
* @param orderId 要取消的订单的 ID
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void cancelOrder(Long orderId) {
// 调用订单仓库的 findById 方法,根据订单 ID 查询订单。
// 如果未找到订单,则抛出 OrderNotFoundException 异常。
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
// 检查订单的状态,如果订单状态不是已取消状态,则进行取消操作。
if (order.getStatus() != OrderStatus.CANCELLED) {
// 将订单状态设置为已取消。
order.setStatus(OrderStatus.CANCELLED);
// 将更新后的订单保存到数据库中。
orderRepository.save(order);
// 调用库存服务的恢复库存方法,恢复订单中商品的库存。
inventoryService.restoreInventory(order.getItems());
}
}
}
5.4 多数据源配置
主数据源配置:
// @Configuration 注解表明这是一个 Spring 配置类,Spring 会对其进行处理,
// 可以在这个类中定义 Bean 以及进行一些配置操作,类似于传统的 XML 配置文件。
@Configuration
// @EnableTransactionManagement 注解用于启用 Spring 的声明式事务管理功能。
// 它允许在服务层或其他组件中使用 @Transactional 注解来管理事务。
@EnableTransactionManagement
// @EnableJpaRepositories 注解用于启用 Spring Data JPA 仓库的自动扫描和注册功能。
// 它会自动创建仓库接口的实现类,并将其注册为 Spring Bean。
@EnableJpaRepositories(
// 指定要扫描的仓库接口所在的基础包路径,这里是 com.example.repository.primary 包及其子包。
basePackages = "com.example.repository.primary",
// 指定要使用的 EntityManagerFactory 的引用名称,这里使用 primaryEntityManagerFactory。
entityManagerFactoryRef = "primaryEntityManagerFactory",
// 指定要使用的事务管理器的引用名称,这里使用 primaryTransactionManager。
transactionManagerRef = "primaryTransactionManager"
)
/**
* 主数据源配置类,用于配置主数据源、实体管理器工厂和事务管理器。
*/
public class PrimaryDataSourceConfig {
/**
* 创建主数据源的 Bean。
*
* @Bean 注解将该方法返回的对象注册为 Spring Bean。
* @Primary 注解表示当有多个相同类型的 Bean 时,优先使用这个 Bean。
* @ConfigurationProperties 注解将配置文件中以 spring.datasource.primary 开头的属性绑定到数据源对象上。
*
* @return 返回一个根据配置文件创建的数据源对象。
*/
@Bean
@Primary
@ConfigurationProperties("spring.datasource.primary")
public DataSource primaryDataSource() {
// 使用 DataSourceBuilder 创建数据源对象,并根据配置文件中的属性进行初始化。
return DataSourceBuilder.create().build();
}
/**
* 创建主实体管理器工厂的 Bean。
*
* @Bean 注解将该方法返回的对象注册为 Spring Bean。
* @Primary 注解表示当有多个相同类型的 Bean 时,优先使用这个 Bean。
*
* @param builder 用于构建 EntityManagerFactory 的构建器对象。
* @param dataSource 主数据源对象,通过 @Qualifier 注解指定使用名为 primaryDataSource 的 Bean。
* @return 返回一个配置好的 LocalContainerEntityManagerFactoryBean 对象。
*/
@Bean
@Primary
public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory(
EntityManagerFactoryBuilder builder,
@Qualifier("primaryDataSource") DataSource dataSource) {
return builder
// 设置数据源为前面创建的主数据源。
.dataSource(dataSource)
// 指定要扫描的实体类所在的包路径,这里是 com.example.model.primary 包及其子包。
.packages("com.example.model.primary")
// 设置持久化单元的名称为 primary。
.persistenceUnit("primary")
// 设置 Hibernate 的相关属性,如 hbm2ddl.auto 用于自动更新数据库表结构,dialect 用于指定数据库方言。
.properties(Map.of(
"hibernate.hbm2ddl.auto", "update",
"hibernate.dialect", "org.hibernate.dialect.MySQL8Dialect"
))
// 构建并返回 LocalContainerEntityManagerFactoryBean 对象。
.build();
}
/**
* 创建主事务管理器的 Bean。
*
* @Bean 注解将该方法返回的对象注册为 Spring Bean。
* @Primary 注解表示当有多个相同类型的 Bean 时,优先使用这个 Bean。
*
* @param entityManagerFactory 主实体管理器工厂对象,通过 @Qualifier 注解指定使用名为 primaryEntityManagerFactory 的 Bean。
* @return 返回一个 JpaTransactionManager 对象,用于管理事务。
*/
@Bean
@Primary
public PlatformTransactionManager primaryTransactionManager(
@Qualifier("primaryEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
// 创建 JpaTransactionManager 对象,并将主实体管理器工厂作为参数传入,用于管理事务。
return new JpaTransactionManager(entityManagerFactory);
}
}
从数据源配置:
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
basePackages = "com.example.repository.secondary",
entityManagerFactoryRef = "secondaryEntityManagerFactory",
transactionManagerRef = "secondaryTransactionManager"
)
public class SecondaryDataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public LocalContainerEntityManagerFactoryBean secondaryEntityManagerFactory(
EntityManagerFactoryBuilder builder,
@Qualifier("secondaryDataSource") DataSource dataSource) {
return builder
.dataSource(dataSource)
.packages("com.example.model.secondary")
.persistenceUnit("secondary")
.properties(Map.of(
"hibernate.hbm2ddl.auto", "update",
"hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect"
))
.build();
}
@Bean
public PlatformTransactionManager secondaryTransactionManager(
@Qualifier("secondaryEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}
6. 安全控制与认证授权
6.1 Spring Security基础配置
依赖添加:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
基础安全配置:
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final UserDetailsService userDetailsService;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(ex -> ex
.authenticationEntryPoint(new JwtAuthenticationEntryPoint())
.accessDeniedHandler(new JwtAccessDeniedHandler())
)
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable);
return http.build();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
6.2 JWT认证实现
JWT工具类:
@Component
@RequiredArgsConstructor
public class JwtTokenProvider {
private final JwtProperties jwtProperties;
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getExpirationTime()))
.signWith(SignatureAlgorithm.HS512, jwtProperties.getSecretKey())
.compact();
}
public String getUsernameFromToken(String token) {
return Jwts.parser()
.setSigningKey(jwtProperties.getSecretKey())
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(jwtProperties.getSecretKey()).parseClaimsJws(token);
return true;
} catch (SignatureException ex) {
log.error("Invalid JWT signature");
} catch (MalformedJwtException ex) {
log.error("Invalid JWT token");
} catch (ExpiredJwtException ex) {
log.error("Expired JWT token");
} catch (UnsupportedJwtException ex) {
log.error("Unsupported JWT token");
} catch (IllegalArgumentException ex) {
log.error("JWT claims string is empty");
}
return false;
}
}
JWT过滤器:
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
String username = tokenProvider.getUsernameFromToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
log.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
6.3 OAuth2集成
OAuth2客户端配置:
spring:
security:
oauth2:
client:
registration:
google:
client-id: your-google-client-id
client-secret: your-google-client-secret
scope: email, profile
github:
client-id: your-github-client-id
client-secret: your-github-client-secret
scope: user:email
provider:
google:
issuer-uri: https://accounts.google.com
github:
issuer-uri: https://github.com
user-name-attribute: name
OAuth2登录配置:
@Configuration
@EnableWebSecurity
public class OAuth2SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login**", "/error**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService)
)
.successHandler(oAuth2AuthenticationSuccessHandler)
.failureHandler(oAuth2AuthenticationFailureHandler)
)
.logout(logout -> logout
.logoutSuccessUrl("/").permitAll()
);
return http.build();
}
@Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> customOAuth2UserService() {
DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
return request -> {
OAuth2User oAuth2User = delegate.loadUser(request);
Map<String, Object> attributes = oAuth2User.getAttributes();
String registrationId = request.getClientRegistration().getRegistrationId();
// 根据不同提供商处理用户信息
OAuth2UserInfo userInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(
registrationId, attributes);
// 保存或更新用户信息到数据库
User user = saveOrUpdateUser(userInfo);
return new CustomOAuth2User(user, attributes);
};
}
}
7. 响应式编程支持
7.1 WebFlux基础
响应式控制器:
@RestController
@RequestMapping("/api/reactive/products")
public class ReactiveProductController {
private final ReactiveProductService productService;
public ReactiveProductController(ReactiveProductService productService) {
this.productService = productService;
}
@GetMapping
public Flux<Product> getAllProducts() {
return productService.findAll();
}
@GetMapping("/{id}")
public Mono<ResponseEntity<Product>> getProductById(@PathVariable String id) {
return productService.findById(id)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<Product> createProduct(@Valid @RequestBody Mono<Product> productMono) {
return productMono.flatMap(productService::save);
}
@PutMapping("/{id}")
public Mono<ResponseEntity<Product>> updateProduct(
@PathVariable String id,
@Valid @RequestBody Mono<Product> productMono) {
return productMono
.flatMap(product -> productService.update(id, product))
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public Mono<ResponseEntity<Void>> deleteProduct(@PathVariable String id) {
return productService.deleteById(id)
.then(Mono.just(ResponseEntity.noContent().build()));
}
@GetMapping("/stream")
@Produces(MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Product> streamProducts() {
return productService.streamAll();
}
}
7.2 响应式数据访问
响应式Repository:
public interface ReactiveProductRepository extends ReactiveCrudRepository<Product, String> {
Flux<Product> findByStatus(ProductStatus status);
@Query("{ 'price': { $gte: ?0, $lte: ?1 } }")
Flux<Product> findByPriceBetween(BigDecimal minPrice, BigDecimal maxPrice);
Flux<Product> findByNameContaining(String name);
@Tailable
Flux<Product> findWithTailableCursorBy();
}
响应式服务层:
@Service
@RequiredArgsConstructor
public class ReactiveProductService {
private final ReactiveProductRepository productRepository;
public Flux<Product> findAll() {
return productRepository.findAll()
.delayElements(Duration.ofMillis(100)); // 模拟延迟
}
public Mono<Product> findById(String id) {
return productRepository.findById(id)
.switchIfEmpty(Mono.error(new ProductNotFoundException(id)));
}
public Mono<Product> save(Product product) {
return productRepository.save(product);
}
public Mono<Product> update(String id, Product product) {
return productRepository.existsById(id)
.flatMap(exists -> exists ?
Mono.just(product).flatMap(productRepository::save) :
Mono.error(new ProductNotFoundException(id)));
}
public Mono<Void> deleteById(String id) {
return productRepository.deleteById(id);
}
public Flux<Product> streamAll() {
return productRepository.findWithTailableCursorBy();
}
}
7.3 WebClient使用
WebClient配置:
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient(WebClient.Builder builder) {
return builder
.baseUrl("https://api.example.com")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.filter(logRequest())
.filter(logResponse())
.build();
}
private ExchangeFilterFunction logRequest() {
return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
log.info("Request: {} {}", clientRequest.method(), clientRequest.url());
clientRequest.headers().forEach((name, values) ->
values.forEach(value -> log.info("{}={}", name, value)));
return Mono.just(clientRequest);
});
}
private ExchangeFilterFunction logResponse() {
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
log.info("Response status: {}", clientResponse.statusCode());
clientResponse.headers().asHttpHeaders().forEach((name, values) ->
values.forEach(value -> log.info("{}={}", name, value)));
return Mono.just(clientResponse);
});
}
}
WebClient使用示例:
@Service
@RequiredArgsConstructor
public class ExternalApiService {
private final WebClient webClient;
public Mono<Product> fetchProduct(String productId) {
return webClient.get()
.uri("/products/{id}", productId)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response ->
Mono.error(new ProductNotFoundException("Product not found with id: " + productId)))
.onStatus(HttpStatus::is5xxServerError, response ->
Mono.error(new ServiceUnavailableException("External service unavailable")))
.bodyToMono(Product.class)
.timeout(Duration.ofSeconds(5))
.retryWhen(Retry.backoff(3, Duration.ofMillis(100))
.doOnError(e -> log.error("Error fetching product", e));
}
public Flux<Product> searchProducts(String keyword) {
return webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/products/search")
.queryParam("q", keyword)
.build())
.retrieve()
.bodyToFlux(Product.class);
}
public Mono<Product> createProduct(Product product) {
return webClient.post()
.uri("/products")
.bodyValue(product)
.retrieve()
.bodyToMono(Product.class);
}
}
8. 监控与管理端点
8.1 Actuator配置
基础配置:
management:
endpoint:
health:
show-details: always
show-components: always
info:
enabled: true
metrics:
enabled: true
endpoints:
web:
exposure:
include: health, info, metrics, prometheus
base-path: /manage
health:
db:
enabled: true
diskspace:
enabled: true
redis:
enabled: true
metrics:
export:
prometheus:
enabled: true
tags:
application: ${spring.application.name}
自定义健康指标:
@Component
public class CustomHealthIndicator implements HealthIndicator {
private final ExternalServiceClient serviceClient;
public CustomHealthIndicator(ExternalServiceClient serviceClient) {
this.serviceClient = serviceClient;
}
@Override
public Health health() {
try {
String status = serviceClient.getStatus();
if ("UP".equals(status)) {
return Health.up()
.withDetail("responseTime", serviceClient.getResponseTime())
.build();
} else {
return Health.down()
.withDetail("reason", "Service returned status: " + status)
.build();
}
} catch (Exception e) {
return Health.down(e)
.withDetail("error", e.getMessage())
.build();
}
}
}
8.2 自定义指标
计数器示例:
@Service
public class OrderService {
private final Counter orderCounter;
private final DistributionSummary orderAmountSummary;
private final Timer orderProcessingTimer;
public OrderService(MeterRegistry meterRegistry) {
this.orderCounter = meterRegistry.counter("orders.count");
this.orderAmountSummary = DistributionSummary
.builder("orders.amount")
.description("Order amount distribution")
.baseUnit("USD")
.register(meterRegistry);
this.orderProcessingTimer = Timer
.builder("orders.processing.time")
.description("Time taken to process orders")
.register(meterRegistry);
}
@Transactional
public Order createOrder(OrderRequest request) {
return orderProcessingTimer.record(() -> {
Order order = processOrder(request);
// 增加计数器
orderCounter.increment();
// 记录订单金额
orderAmountSummary.record(order.getTotalAmount().doubleValue());
return order;
});
}
}
自定义指标端点:
@Endpoint(id = "custommetrics")
@Component
public class CustomMetricsEndpoint {
private final Map<String, Double> metrics = new ConcurrentHashMap<>();
@ReadOperation
public Map<String, Double> metrics() {
return metrics;
}
@WriteOperation
public void updateMetric(@Selector String name, double value) {
metrics.put(name, value);
}
@DeleteOperation
public void removeMetric(@Selector String name) {
metrics.remove(name);
}
}
8.3 链路追踪
Sleuth与Zipkin配置:
spring:
sleuth:
sampler:
probability: 1.0 # 采样率
propagation:
type: B3,W3C # 支持多种追踪头
zipkin:
base-url: http://localhost:9411
sender:
type: web
service:
name: order-service
自定义Span:
@Service
@RequiredArgsConstructor
public class PaymentService {
private final Tracer tracer;
public PaymentResult processPayment(Order order) {
// 创建自定义Span
Span paymentSpan = tracer.nextSpan().name("process-payment").start();
try (SpanInScope ws = tracer.withSpan(paymentSpan)) {
paymentSpan.tag("order.id", order.getId().toString());
paymentSpan.tag("payment.amount", order.getTotalAmount().toString());
// 业务逻辑
PaymentResult result = doPayment(order);
paymentSpan.event("Payment processed");
return result;
} catch (Exception e) {
paymentSpan.error(e);
throw e;
} finally {
paymentSpan.end();
}
}
}
9. 测试策略与最佳实践
9.1 单元测试
服务层测试:
@ExtendWith(MockitoExtension.class)
class ProductServiceTest {
@Mock
private ProductRepository productRepository;
@InjectMocks
private ProductService productService;
@Test
void whenValidId_thenProductShouldBeFound() {
// Given
Long productId = 1L;
Product mockProduct = new Product(productId, "Test Product", BigDecimal.valueOf(10.99));
when(productRepository.findById(productId)).thenReturn(Optional.of(mockProduct));
// When
Product found = productService.getProductById(productId);
// Then
assertThat(found.getId()).isEqualTo(productId);
assertThat(found.getName()).isEqualTo("Test Product");
}
@Test
void whenInvalidId_thenThrowException() {
// Given
Long productId = 999L;
when(productRepository.findById(productId)).thenReturn(Optional.empty());
// When & Then
assertThatThrownBy(() -> productService.getProductById(productId))
.isInstanceOf(ProductNotFoundException.class)
.hasMessageContaining("Product not found with id: " + productId);
}
@Test
void whenCreateProduct_thenProductShouldBeSaved() {
// Given
Product newProduct = new Product(null, "New Product", BigDecimal.valueOf(15.99));
Product savedProduct = new Product(1L, "New Product", BigDecimal.valueOf(15.99));
when(productRepository.save(any(Product.class))).thenReturn(savedProduct);
// When
Product result = productService.createProduct(newProduct);
// Then
assertThat(result.getId()).isNotNull();
verify(productRepository, times(1)).save(any(Product.class));
}
}
9.2 集成测试
Web层集成测试:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@Transactional
class ProductControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private ProductRepository productRepository;
@Test
void whenGetAllProducts_thenReturnProductList() throws Exception {
// Given
Product product1 = new Product(null, "Product 1", BigDecimal.valueOf(10.99));
Product product2 = new Product(null, "Product 2", BigDecimal.valueOf(20.99));
productRepository.saveAll(List.of(product1, product2));
// When & Then
mockMvc.perform(get("/api/products")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("#34;, hasSize(2)))
.andExpect(jsonPath("$[0].name", is("Product 1")))
.andExpect(jsonPath("$[1].name", is("Product 2")));
}
@Test
void whenValidInput_thenCreateProduct() throws Exception {
// Given
Product product = new Product(null, "New Product", BigDecimal.valueOf(15.99));
// When & Then
mockMvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(product)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.name", is("New Product")));
// Verify database
List<Product> found = productRepository.findAll();
assertThat(found).extracting(Product::getName).containsOnly("New Product");
}
@Test
void whenInvalidInput_thenReturnBadRequest() throws Exception {
// Given
Product invalidProduct = new Product(null, "", BigDecimal.valueOf(-1));
// When & Then
mockMvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(invalidProduct)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.title", is("Validation Error")))
.andExpect(jsonPath("$.errors[0].field", is("name")))
.andExpect(jsonPath("$.errors[1].field", is("price")));
}
}
9.3 测试切片
WebMvc测试切片:
@WebMvcTest(ProductController.class)
@Import({ProductService.class, SecurityConfig.class})
class ProductControllerSliceTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private ProductRepository productRepository;
@Test
@WithMockUser(username = "admin", roles = {"ADMIN"})
void whenAdminAccess_thenShouldReturnOk() throws Exception {
when(productRepository.findAll()).thenReturn(List.of(
new Product(1L, "Test", BigDecimal.TEN)));
mockMvc.perform(get("/api/products"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(username = "user", roles = {"USER"})
void whenUserAccess_thenShouldReturnForbidden() throws Exception {
mockMvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"Test\",\"price\":10.99}"))
.andExpect(status().isForbidden());
}
}
DataJpa测试切片:
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class ProductRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private ProductRepository productRepository;
@Test
void whenFindByStatus_thenReturnProducts() {
// Given
Product activeProduct = new Product(null, "Active", BigDecimal.TEN, ProductStatus.ACTIVE);
Product discontinuedProduct = new Product(null, "Discontinued", BigDecimal.ONE, ProductStatus.DISCONTINUED);
entityManager.persist(activeProduct);
entityManager.persist(discontinuedProduct);
entityManager.flush();
// When
List<Product> found = productRepository.findByStatus(ProductStatus.ACTIVE);
// Then
assertThat(found).hasSize(1);
assertThat(found.get(0).isEqualTo(activeProduct);
}
}
10. 部署与性能优化
10.1 打包与部署
构建可执行JAR:
mvn clean package
# 生成的jar位于target目录下
java -jar target/demo-0.0.1-SNAPSHOT.jar
Docker化部署:
# 使用多阶段构建
FROM eclipse-temurin:17-jdk-jammy as builder
WORKDIR /workspace/app
COPY . .
RUN ./mvnw clean package -DskipTests
FROM eclipse-temurin:17-jre-jammy
WORKDIR /app
COPY --from=builder /workspace/app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
构建与运行Docker镜像:
docker build -t demo-app .
docker run -p 8080:8080 -e "SPRING_PROFILES_ACTIVE=prod" demo-app
10.2 性能优化
JVM调优参数:
java -jar app.jar \
-XX:+UseG1GC \
-Xms512m \
-Xmx1024m \
-XX:MaxGCPauseMillis=200 \
-XX:InitiatingHeapOccupancyPercent=35 \
-XX:+AlwaysPreTouch \
-Djava.security.egd=file:/dev/./urandom
Spring Boot性能配置:
server:
compression:
enabled: true
mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json
min-response-size: 1024
tomcat:
max-threads: 200
min-spare-threads: 10
connection-timeout: 5000
accept-count: 100
spring:
mvc:
async:
request-timeout: 30000
jpa:
properties:
hibernate:
jdbc:
batch_size: 30
fetch_size: 100
10.3 GraalVM原生镜像
原生镜像构建:
- 添加Native Build Tools插件:
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>0.9.27</version>
</plugin>
</plugins>
</build>
- 构建原生镜像:
mvn -Pnative native:compile
- 运行原生应用:
./target/demo
原生镜像配置提示:
创建
src/main/resources/META-INF/native-image目录,添加配置:
- reflect-config.json - 反射配置
- resource-config.json - 资源文件配置
- proxy-config.json - 动态代理配置
示例reflect-config.json:
[
{
"name": "com.example.model.Product",
"allDeclaredFields": true,
"allDeclaredMethods": true,
"allDeclaredConstructors": true
}
]
总结
Spring Boot 3.x带来了许多激动人心的新特性和改进,从对现代Java特性的支持到更好的原生镜像兼容性。通过本指南,您应该已经掌握了从基础到高级的Spring Boot 3.x开发知识,包括:
- 自动配置原理与自定义
- Web开发最佳实践
- 数据访问与事务管理
- 安全认证与授权
- 响应式编程模型
- 应用监控与管理
- 测试策略
- 部署与优化
千万别三连,不然我会以为自己是网红(但你可以试试)。
头条对markdown的文章显示不太友好,想了解更多的可以关注微信公众号:“Eric的技术杂货库”,后期会有更多的干货以及资料下载。